1 /*
2     SPDX-FileCopyrightText: 2017 Nicolas Carion
3     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
4 */
5 
6 #include "timelinemodel.hpp"
7 #include "assets/model/assetparametermodel.hpp"
8 #include "bin/projectclip.h"
9 #include "bin/projectitemmodel.h"
10 #include "clipmodel.hpp"
11 #include "compositionmodel.hpp"
12 #include "core.h"
13 #include "doc/docundostack.hpp"
14 #include "effects/effectsrepository.hpp"
15 #include "bin/model/subtitlemodel.hpp"
16 #include "effects/effectstack/model/effectstackmodel.hpp"
17 #include "groupsmodel.hpp"
18 #include "kdenlivesettings.h"
19 #include "snapmodel.hpp"
20 #include "timelinefunctions.hpp"
21 // TODO
22 //#include "mainwindow.h"
23 //#include "timeline2/view/timelinewidget.h"
24 //#include "timeline2/view/timelinecontroller.h"
25 #include "timeline2/model/timelinefunctions.hpp"
26 
27 #include "monitor/monitormanager.h"
28 
29 #include <QDebug>
30 #include <QThread>
31 #include <QModelIndex>
32 #include <klocalizedstring.h>
33 #include <mlt++/MltConsumer.h>
34 #include <mlt++/MltField.h>
35 #include <mlt++/MltProfile.h>
36 #include <mlt++/MltTractor.h>
37 #include <mlt++/MltTransition.h>
38 #include <queue>
39 #include <set>
40 
41 #include "macros.hpp"
42 
43 #ifdef CRASH_AUTO_TEST
44 #include "logger.hpp"
45 #pragma GCC diagnostic push
46 #pragma GCC diagnostic ignored "-Wunused-parameter"
47 #pragma GCC diagnostic ignored "-Wsign-conversion"
48 #pragma GCC diagnostic ignored "-Wfloat-equal"
49 #pragma GCC diagnostic ignored "-Wshadow"
50 #pragma GCC diagnostic ignored "-Wpedantic"
51 #include <rttr/registration>
52 #pragma GCC diagnostic pop
53 RTTR_REGISTRATION
54 {
55     using namespace rttr;
56     registration::class_<TimelineModel>("TimelineModel")
57         .method("setTrackLockedState", &TimelineModel::setTrackLockedState)(parameter_names("trackId", "lock"))
58         .method("requestClipMove", select_overload<bool(int, int, int, bool, bool, bool, bool, bool)>(&TimelineModel::requestClipMove))(
59             parameter_names("clipId", "trackId", "position", "moveMirrorTracks", "updateView", "logUndo", "invalidateTimeline", "revertMove"))
60         .method("requestCompositionMove", select_overload<bool(int, int, int, bool, bool)>(&TimelineModel::requestCompositionMove))(
61             parameter_names("compoId", "trackId", "position", "updateView", "logUndo"))
62         .method("requestClipInsertion", select_overload<bool(const QString &, int, int, int &, bool, bool, bool)>(&TimelineModel::requestClipInsertion))(
63             parameter_names("binClipId", "trackId", "position", "id", "logUndo", "refreshView", "useTargets"))
64         .method("requestItemDeletion", select_overload<bool(int, bool)>(&TimelineModel::requestItemDeletion))(parameter_names("clipId", "logUndo"))
65         .method("requestGroupMove", select_overload<bool(int, int, int, int, bool, bool, bool, bool)>(&TimelineModel::requestGroupMove))(
66             parameter_names("itemId", "groupId", "delta_track", "delta_pos", "moveMirrorTracks", "updateView", "logUndo", "revertMove"))
67         .method("requestGroupDeletion", select_overload<bool(int, bool)>(&TimelineModel::requestGroupDeletion))(parameter_names("clipId", "logUndo"))
68         .method("requestItemResize", select_overload<int(int, int, bool, bool, int, bool)>(&TimelineModel::requestItemResize))(
69             parameter_names("itemId", "size", "right", "logUndo", "snapDistance", "allowSingleResize"))
70         .method("requestClipsGroup", select_overload<int(const std::unordered_set<int> &, bool, GroupType)>(&TimelineModel::requestClipsGroup))(
71             parameter_names("itemIds", "logUndo", "type"))
72         .method("requestClipUngroup", select_overload<bool(int, bool)>(&TimelineModel::requestClipUngroup))(parameter_names("itemId", "logUndo"))
73         .method("requestClipsUngroup", &TimelineModel::requestClipsUngroup)(parameter_names("itemIds", "logUndo"))
74         .method("requestTrackInsertion", select_overload<bool(int, int &, const QString &, bool)>(&TimelineModel::requestTrackInsertion))(
75             parameter_names("pos", "id", "trackName", "audioTrack"))
76         .method("requestTrackDeletion", select_overload<bool(int)>(&TimelineModel::requestTrackDeletion))(parameter_names("trackId"))
77         .method("requestClearSelection", select_overload<bool(bool)>(&TimelineModel::requestClearSelection))(parameter_names("onDeletion"))
78         .method("requestAddToSelection", &TimelineModel::requestAddToSelection)(parameter_names("itemId", "clear"))
79         .method("requestRemoveFromSelection", &TimelineModel::requestRemoveFromSelection)(parameter_names("itemId"))
80         .method("requestSetSelection", select_overload<bool(const std::unordered_set<int> &)>(&TimelineModel::requestSetSelection))(parameter_names("itemIds"))
81         .method("requestFakeClipMove", select_overload<bool(int, int, int, bool, bool, bool)>(&TimelineModel::requestFakeClipMove))(
82             parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline"))
83         .method("requestFakeGroupMove", select_overload<bool(int, int, int, int, bool, bool)>(&TimelineModel::requestFakeGroupMove))(
84             parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo"))
85         .method("suggestClipMove", &TimelineModel::suggestClipMove)(parameter_names("clipId", "trackId", "position", "cursorPosition", "snapDistance", "moveMirrorTracks"))
86         .method("suggestCompositionMove",
87                 &TimelineModel::suggestCompositionMove)(parameter_names("compoId", "trackId", "position", "cursorPosition", "snapDistance"))
88         // .method("addSnap", &TimelineModel::addSnap)(parameter_names("pos"))
89         // .method("removeSnap", &TimelineModel::addSnap)(parameter_names("pos"))
90         // .method("requestCompositionInsertion", select_overload<bool(const QString &, int, int, int, std::unique_ptr<Mlt::Properties>, int &, bool)>(
91         //                                            &TimelineModel::requestCompositionInsertion))(
92         //     parameter_names("transitionId", "trackId", "position", "length", "transProps", "id", "logUndo"))
93         .method("requestClipTimeWarp", select_overload<bool(int, double,bool,bool)>(&TimelineModel::requestClipTimeWarp))(parameter_names("clipId", "speed","pitchCompensate","changeDuration"));
94 }
95 #else
96 #define TRACE_CONSTR(...)
97 #define TRACE_STATIC(...)
98 #define TRACE_RES(...)
99 #define TRACE(...)
100 #endif
101 
102 int TimelineModel::next_id = 0;
103 int TimelineModel::seekDuration = 30000;
104 
TimelineModel(Mlt::Profile * profile,std::weak_ptr<DocUndoStack> undo_stack)105 TimelineModel::TimelineModel(Mlt::Profile *profile, std::weak_ptr<DocUndoStack> undo_stack)
106     : QAbstractItemModel_shared_from_this()
107     , m_blockRefresh(false)
108     , m_tractor(new Mlt::Tractor(*profile))
109     , m_masterStack(nullptr)
110     , m_snaps(new SnapModel())
111     , m_undoStack(std::move(undo_stack))
112     , m_profile(profile)
113     , m_blackClip(new Mlt::Producer(*profile, "color:black"))
114     , m_lock(QReadWriteLock::Recursive)
115     , m_timelineEffectsEnabled(true)
116     , m_id(getNextId())
117     , m_overlayTrackCount(-1)
118     , m_videoTarget(-1)
119     , m_editMode(TimelineMode::NormalEdit)
120     , m_closing(false)
121 {
122     // Create black background track
123     m_blackClip->set("id", "black_track");
124     m_blackClip->set("mlt_type", "producer");
125     m_blackClip->set("aspect_ratio", 1);
126     m_blackClip->set("length", INT_MAX);
127     m_blackClip->set("mlt_image_format", "rgba");
128     m_blackClip->set("set.test_audio", 0);
129     m_blackClip->set_in_and_out(0, TimelineModel::seekDuration);
130     m_tractor->insert_track(*m_blackClip, 0);
131 
132     TRACE_CONSTR(this);
133 }
134 
prepareClose()135 void TimelineModel::prepareClose()
136 {
137     requestClearSelection(true);
138     QWriteLocker locker(&m_lock);
139     // Unlock all tracks to allow deleting clip from tracks
140     m_closing = true;
141     auto it = m_allTracks.begin();
142     while (it != m_allTracks.end()) {
143         (*it)->unlock();
144         ++it;
145     }
146     m_subtitleModel.reset();
147     //m_subtitleModel->removeAllSubtitles();
148 }
149 
~TimelineModel()150 TimelineModel::~TimelineModel()
151 {
152     std::vector<int> all_ids;
153     for (auto tracks : m_iteratorTable) {
154         all_ids.push_back(tracks.first);
155     }
156     for (auto tracks : all_ids) {
157         deregisterTrack_lambda(tracks)();
158     }
159     for (const auto &clip : m_allClips) {
160         clip.second->deregisterClipToBin();
161     }
162 }
163 
getTracksCount() const164 int TimelineModel::getTracksCount() const
165 {
166     READ_LOCK();
167     int count = m_tractor->count();
168     if (m_overlayTrackCount > -1) {
169         count -= m_overlayTrackCount;
170     }
171     Q_ASSERT(count >= 0);
172     // don't count the black background track
173     Q_ASSERT(count - 1 == static_cast<int>(m_allTracks.size()));
174     return count - 1;
175 }
176 
getAVtracksCount() const177 QPair<int, int> TimelineModel::getAVtracksCount() const
178 {
179     QPair <int, int> tracks{0, 0};
180     auto it = m_allTracks.cbegin();
181     while (it != m_allTracks.cend()) {
182         if ((*it)->isAudioTrack()) {
183             tracks.second++;
184         } else {
185             tracks.first++;
186         }
187         ++it;
188     }
189     if (m_overlayTrackCount > -1) {
190         tracks.first -= m_overlayTrackCount;
191     }
192     return tracks;
193 }
194 
getTracksIds(bool audio) const195 QList<int> TimelineModel::getTracksIds(bool audio) const
196 {
197     QList <int> trackIds;
198     auto it = m_allTracks.cbegin();
199     while (it != m_allTracks.cend()) {
200         if ((*it)->isAudioTrack() == audio) {
201             trackIds.insert(0, (*it)->getId());
202         }
203         ++it;
204     }
205     return trackIds;
206 }
207 
getTrackIndexFromPosition(int pos) const208 int TimelineModel::getTrackIndexFromPosition(int pos) const
209 {
210     Q_ASSERT(pos >= 0 && pos < int(m_allTracks.size()));
211     READ_LOCK();
212     auto it = m_allTracks.cbegin();
213     while (pos > 0) {
214         it++;
215         pos--;
216     }
217     return (*it)->getId();
218 }
219 
getClipsCount() const220 int TimelineModel::getClipsCount() const
221 {
222     READ_LOCK();
223     int size = int(m_allClips.size());
224     return size;
225 }
226 
getCompositionsCount() const227 int TimelineModel::getCompositionsCount() const
228 {
229     READ_LOCK();
230     int size = int(m_allCompositions.size());
231     return size;
232 }
233 
getClipTrackId(int clipId) const234 int TimelineModel::getClipTrackId(int clipId) const
235 {
236     READ_LOCK();
237     Q_ASSERT(m_allClips.count(clipId) > 0);
238     const auto clip = m_allClips.at(clipId);
239     return clip->getCurrentTrackId();
240 }
241 
getCompositionTrackId(int compoId) const242 int TimelineModel::getCompositionTrackId(int compoId) const
243 {
244     Q_ASSERT(m_allCompositions.count(compoId) > 0);
245     const auto trans = m_allCompositions.at(compoId);
246     return trans->getCurrentTrackId();
247 }
248 
getItemTrackId(int itemId) const249 int TimelineModel::getItemTrackId(int itemId) const
250 {
251     READ_LOCK();
252     Q_ASSERT(isItem(itemId));
253     if (isClip(itemId)) {
254         return getClipTrackId(itemId);
255     }
256     if (isComposition(itemId)) {
257         return getCompositionTrackId(itemId);
258     }
259     return -1;
260 }
261 
hasClipEndMix(int clipId) const262 bool TimelineModel::hasClipEndMix(int clipId) const {
263     if (!isClip(clipId)) return false;
264     int tid = getClipTrackId(clipId);
265     if (tid < 0) return false;
266 
267     return getTrackById_const(tid)->hasEndMix(clipId);
268 }
269 
getClipPosition(int clipId) const270 int TimelineModel::getClipPosition(int clipId) const
271 {
272     READ_LOCK();
273     Q_ASSERT(m_allClips.count(clipId) > 0);
274     const auto clip = m_allClips.at(clipId);
275     int pos = clip->getPosition();
276     return pos;
277 }
278 
getClipSpeed(int clipId) const279 double TimelineModel::getClipSpeed(int clipId) const
280 {
281     READ_LOCK();
282     Q_ASSERT(m_allClips.count(clipId) > 0);
283     return m_allClips.at(clipId)->getSpeed();
284 }
285 
getClipSplitPartner(int clipId) const286 int TimelineModel::getClipSplitPartner(int clipId) const
287 {
288     READ_LOCK();
289     Q_ASSERT(m_allClips.count(clipId) > 0);
290     return m_groups->getSplitPartner(clipId);
291 }
292 
getClipIn(int clipId) const293 int TimelineModel::getClipIn(int clipId) const
294 {
295     READ_LOCK();
296     Q_ASSERT(m_allClips.count(clipId) > 0);
297     const auto clip = m_allClips.at(clipId);
298     return clip->getIn();
299 }
300 
getClipInDuration(int clipId) const301 QPoint TimelineModel::getClipInDuration(int clipId) const
302 {
303     READ_LOCK();
304     Q_ASSERT(m_allClips.count(clipId) > 0);
305     const auto clip = m_allClips.at(clipId);
306     return {clip->getIn(), clip->getPlaytime()};
307 }
308 
getClipState(int clipId) const309 PlaylistState::ClipState TimelineModel::getClipState(int clipId) const
310 {
311     READ_LOCK();
312     Q_ASSERT(m_allClips.count(clipId) > 0);
313     const auto clip = m_allClips.at(clipId);
314     return clip->clipState();
315 }
316 
getClipBinId(int clipId) const317 const QString TimelineModel::getClipBinId(int clipId) const
318 {
319     READ_LOCK();
320     Q_ASSERT(m_allClips.count(clipId) > 0);
321     const auto clip = m_allClips.at(clipId);
322     QString id = clip->binId();
323     return id;
324 }
325 
getClipPlaytime(int clipId) const326 int TimelineModel::getClipPlaytime(int clipId) const
327 {
328     READ_LOCK();
329     Q_ASSERT(isClip(clipId));
330     const auto clip = m_allClips.at(clipId);
331     int playtime = clip->getPlaytime();
332     return playtime;
333 }
334 
getClipFrameSize(int clipId) const335 QSize TimelineModel::getClipFrameSize(int clipId) const
336 {
337     READ_LOCK();
338     Q_ASSERT(isClip(clipId));
339     const auto clip = m_allClips.at(clipId);
340     return clip->getFrameSize();
341 }
342 
getTrackClipsCount(int trackId) const343 int TimelineModel::getTrackClipsCount(int trackId) const
344 {
345     READ_LOCK();
346     Q_ASSERT(isTrack(trackId));
347     int count = getTrackById_const(trackId)->getClipsCount();
348     return count;
349 }
350 
getClipByStartPosition(int trackId,int position) const351 int TimelineModel::getClipByStartPosition(int trackId, int position) const
352 {
353     READ_LOCK();
354     Q_ASSERT(isTrack(trackId));
355     return getTrackById_const(trackId)->getClipByStartPosition(position);
356 }
357 
getClipByPosition(int trackId,int position) const358 int TimelineModel::getClipByPosition(int trackId, int position) const
359 {
360     READ_LOCK();
361     Q_ASSERT(isTrack(trackId));
362     return getTrackById_const(trackId)->getClipByPosition(position);
363 }
364 
getCompositionByPosition(int trackId,int position) const365 int TimelineModel::getCompositionByPosition(int trackId, int position) const
366 {
367     READ_LOCK();
368     Q_ASSERT(isTrack(trackId));
369     return getTrackById_const(trackId)->getCompositionByPosition(position);
370 }
371 
getSubtitleByStartPosition(int position) const372 int TimelineModel::getSubtitleByStartPosition(int position) const
373 {
374     READ_LOCK();
375     GenTime startTime(position, pCore->getCurrentFps());
376     auto findResult = std::find_if(std::begin(m_allSubtitles), std::end(m_allSubtitles), [&](const std::pair<int, GenTime> &pair) {
377         return pair.second == startTime;
378     });
379     if (findResult != std::end(m_allSubtitles)) {
380         return findResult->first;
381     }
382     return -1;
383 }
384 
getSubtitleByPosition(int position) const385 int TimelineModel::getSubtitleByPosition(int position) const
386 {
387     READ_LOCK();
388     GenTime startTime(position, pCore->getCurrentFps());
389     if (m_subtitleModel) {
390         std::unordered_set<int> sids = m_subtitleModel->getItemsInRange(position, position);
391         if (!sids.empty()) {
392             return *sids.begin();
393         }
394     }
395     return -1;
396 }
397 
getTrackPosition(int trackId) const398 int TimelineModel::getTrackPosition(int trackId) const
399 {
400     READ_LOCK();
401     Q_ASSERT(isTrack(trackId));
402     auto it = m_allTracks.cbegin();
403     int pos = int(std::distance(it, static_cast<decltype(it)>(m_iteratorTable.at(trackId))));
404     return pos;
405 }
406 
getTrackMltIndex(int trackId) const407 int TimelineModel::getTrackMltIndex(int trackId) const
408 {
409     READ_LOCK();
410     // Because of the black track that we insert in first position, the mlt index is the position + 1
411     return getTrackPosition(trackId) + 1;
412 }
413 
getTrackSortValue(int trackId,int separated) const414 int TimelineModel::getTrackSortValue(int trackId, int separated) const
415 {
416     if (separated == 1) {
417         // This will be A2, A1, V1, V2
418         return getTrackPosition(trackId) + 1;
419     }
420     if (separated == 2) {
421         // This will be A1, A2, V1, V2
422         // Count audio/video tracks
423         auto it = m_allTracks.cbegin();
424         int aCount = 0;
425         int vCount = 0;
426         int refPos = 0;
427         bool isVideo = true;
428         while (it != m_allTracks.cend()) {
429             if ((*it)->isAudioTrack()) {
430                 if ((*it)->getId() == trackId) {
431                     refPos = aCount;
432                     isVideo = false;
433                 }
434                 aCount++;
435             } else {
436                 // video track
437                 if ((*it)->getId() == trackId) {
438                     refPos = vCount;
439                 }
440                 vCount++;
441             }
442             ++it;
443         }
444         return isVideo ? aCount + refPos + 1 : aCount - refPos;
445     }
446     // This will be A1, V1, A2, V2
447     auto it = m_allTracks.cend();
448     int aCount = 0;
449     int vCount = 0;
450     bool isAudio = false;
451     int trackPos = 0;
452     while (it != m_allTracks.begin()) {
453         --it;
454         bool audioTrack = (*it)->isAudioTrack();
455         if (audioTrack) {
456             aCount++;
457         } else {
458             vCount++;
459         }
460         if (trackId == (*it)->getId()) {
461             isAudio = audioTrack;
462             trackPos = audioTrack ? aCount : vCount;
463         }
464     }
465     if (isAudio) {
466         if (aCount > vCount) {
467             if (trackPos - 1 > aCount - vCount) {
468                 // We have more audio tracks than video tracks
469                 return (aCount - vCount + 1) + 2 * (trackPos - (aCount - vCount +1));
470             }
471             return trackPos;
472         }
473         return 2 * trackPos;
474     }
475     return 2 * (vCount + 1 - trackPos) + 1;
476 }
477 
getLowerTracksId(int trackId,TrackType type) const478 QList<int> TimelineModel::getLowerTracksId(int trackId, TrackType type) const
479 {
480     READ_LOCK();
481     Q_ASSERT(isTrack(trackId));
482     QList<int> results;
483     auto it = m_iteratorTable.at(trackId);
484     while (it != m_allTracks.cbegin()) {
485         --it;
486         if (type == TrackType::AnyTrack) {
487             results << (*it)->getId();
488             continue;
489         }
490         bool audioTrack = (*it)->isAudioTrack();
491         if (type == TrackType::AudioTrack && audioTrack) {
492             results << (*it)->getId();
493         } else if (type == TrackType::VideoTrack && !audioTrack) {
494             results << (*it)->getId();
495         }
496     }
497     return results;
498 }
499 
getPreviousVideoTrackIndex(int trackId) const500 int TimelineModel::getPreviousVideoTrackIndex(int trackId) const
501 {
502     READ_LOCK();
503     Q_ASSERT(isTrack(trackId));
504     auto it = m_iteratorTable.at(trackId);
505     while (it != m_allTracks.cbegin()) {
506         --it;
507         if (!(*it)->isAudioTrack()) {
508             return (*it)->getId();
509         }
510     }
511     return 0;
512 }
513 
getPreviousVideoTrackPos(int trackId) const514 int TimelineModel::getPreviousVideoTrackPos(int trackId) const
515 {
516     READ_LOCK();
517     Q_ASSERT(isTrack(trackId));
518     auto it = m_iteratorTable.at(trackId);
519     while (it != m_allTracks.cbegin()) {
520         --it;
521         if (!(*it)->isAudioTrack()) {
522             return getTrackMltIndex((*it)->getId());
523         }
524     }
525     return 0;
526 }
527 
getMirrorVideoTrackId(int trackId) const528 int TimelineModel::getMirrorVideoTrackId(int trackId) const
529 {
530     READ_LOCK();
531     Q_ASSERT(isTrack(trackId));
532     auto it = m_iteratorTable.at(trackId);
533     if (!(*it)->isAudioTrack()) {
534         // we expected an audio track...
535         return -1;
536     }
537     int count = 0;
538     while (it != m_allTracks.cend()) {
539         if ((*it)->isAudioTrack()) {
540             count++;
541         } else {
542             count--;
543             if (count == 0) {
544                 return (*it)->getId();
545             }
546         }
547         ++it;
548     }
549     return -1;
550 }
551 
getMirrorTrackId(int trackId) const552 int TimelineModel::getMirrorTrackId(int trackId) const
553 {
554     if (isAudioTrack(trackId)) {
555         return getMirrorVideoTrackId(trackId);
556     }
557     return getMirrorAudioTrackId(trackId);
558 }
559 
getMirrorAudioTrackId(int trackId) const560 int TimelineModel::getMirrorAudioTrackId(int trackId) const
561 {
562     READ_LOCK();
563     Q_ASSERT(isTrack(trackId));
564     auto it = m_iteratorTable.at(trackId);
565     if ((*it)->isAudioTrack()) {
566         // we expected a video track...
567         qWarning() << "requesting mirror audio track for audio track";
568         return -1;
569     }
570     int count = 0;
571     while (it != m_allTracks.cbegin()) {
572         if (!(*it)->isAudioTrack()) {
573             count++;
574         } else {
575             count--;
576             if (count == 0) {
577                 return (*it)->getId();
578             }
579         }
580         --it;
581     }
582     if ((*it)->isAudioTrack() && count == 1) {
583         return (*it)->getId();
584     }
585     return -1;
586 }
587 
setEditMode(TimelineMode::EditMode mode)588 void TimelineModel::setEditMode(TimelineMode::EditMode mode)
589 {
590     m_editMode = mode;
591 }
592 
editMode() const593 TimelineMode::EditMode TimelineModel::editMode() const
594 {
595     return m_editMode;
596 }
597 
normalEdit() const598 bool TimelineModel::normalEdit() const
599 {
600     return m_editMode == TimelineMode::NormalEdit;
601 }
602 
requestFakeClipMove(int clipId,int trackId,int position,bool updateView,bool invalidateTimeline,Fun & undo,Fun & redo)603 bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo)
604 {
605     Q_UNUSED(updateView);
606     Q_UNUSED(invalidateTimeline);
607     Q_UNUSED(undo);
608     Q_UNUSED(redo);
609     Q_ASSERT(isClip(clipId));
610     m_allClips[clipId]->setFakePosition(position);
611     bool trackChanged = false;
612     if (trackId > -1) {
613         if (trackId != m_allClips[clipId]->getFakeTrackId()) {
614             if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) {
615                 m_allClips[clipId]->setFakeTrackId(trackId);
616                 trackChanged = true;
617             }
618         }
619     }
620     QModelIndex modelIndex = makeClipIndexFromID(clipId);
621     if (modelIndex.isValid()) {
622         QVector<int> roles{FakePositionRole};
623         if (trackChanged) {
624             roles << FakeTrackIdRole;
625         }
626         notifyChange(modelIndex, modelIndex, roles);
627         return true;
628     }
629     return false;
630 }
631 
requestClipMove(int clipId,int trackId,int position,bool moveMirrorTracks,bool updateView,bool invalidateTimeline,bool finalMove,Fun & undo,Fun & redo,bool revertMove,bool groupMove,QMap<int,int> moving_clips,std::pair<MixInfo,MixInfo> mixData)632 bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool moveMirrorTracks, bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo, bool revertMove, bool groupMove, QMap <int, int> moving_clips, std::pair<MixInfo,MixInfo>mixData)
633 {
634     Q_UNUSED(moveMirrorTracks)
635     if (trackId == -1) {
636         qWarning() << "clip is not on a track";
637         return false;
638     }
639     Q_ASSERT(isClip(clipId));
640     if (m_allClips[clipId]->clipState() == PlaylistState::Disabled) {
641         if (getTrackById_const(trackId)->trackType() == PlaylistState::AudioOnly && !m_allClips[clipId]->canBeAudio()) {
642             qWarning() << "clip type mismatch 1";
643             return false;
644         }
645         if (getTrackById_const(trackId)->trackType() == PlaylistState::VideoOnly && !m_allClips[clipId]->canBeVideo()) {
646             qWarning() << "clip type mismatch 2";
647             return false;
648         }
649     } else if (getTrackById_const(trackId)->trackType() != m_allClips[clipId]->clipState()) {
650         // Move not allowed (audio / video mismatch)
651         qWarning() << "clip type mismatch 3";
652         return false;
653     }
654     std::function<bool(void)> local_undo = []() { return true; };
655     std::function<bool(void)> local_redo = []() { return true; };
656     bool ok = true;
657     int old_trackId = getClipTrackId(clipId);
658     int previous_track = moving_clips.value(clipId, -1);
659     if (old_trackId == -1) {
660         //old_trackId = previous_track;
661     }
662     bool notifyViewOnly = false;
663     Fun update_model = []() { return true; };
664     if (old_trackId == trackId) {
665         // Move on same track, simply inform the view
666         updateView = false;
667         notifyViewOnly = true;
668         update_model = [clipId, this, trackId, invalidateTimeline]() {
669             QModelIndex modelIndex = makeClipIndexFromID(clipId);
670             notifyChange(modelIndex, modelIndex, StartRole);
671             if (invalidateTimeline && !getTrackById_const(trackId)->isAudioTrack()) {
672                 int in = getClipPosition(clipId);
673                 emit invalidateZone(in, in + getClipPlaytime(clipId));
674             }
675             return true;
676         };
677     }
678     Fun sync_mix = []() { return true; };
679     Fun simple_move_mix = []() { return true; };
680     Fun simple_restore_mix = []() { return true; };
681     QList<int> allowedClipMixes;
682     if (!groupMove && old_trackId > -1) {
683         mixData = getTrackById_const(old_trackId)->getMixInfo(clipId);
684     }
685     if (old_trackId == trackId && !finalMove && !revertMove) {
686         if (mixData.first.firstClipId > -1 && !moving_clips.contains(mixData.first.firstClipId)) {
687             // Mix at clip start, don't allow moving left
688             if (position < (mixData.first.firstClipInOut.second - mixData.first.mixOffset) && (position + m_allClips[clipId]->getPlaytime() >= mixData.first.firstClipInOut.first))  {
689                 qDebug()<<"==== ABORTING GROUP MOVE ON START MIX";
690                 return false;
691             }
692         }
693         if (mixData.second.firstClipId > -1 && !moving_clips.contains(mixData.second.secondClipId)) {
694             // Mix at clip end, don't allow moving right
695             if (position + getClipPlaytime(clipId) > mixData.second.secondClipInOut.first && position < mixData.second.secondClipInOut.second) {
696                 qDebug()<<"==== ABORTING GROUP MOVE ON END MIX: "<<position;
697                 return false;
698             }
699         }
700     }
701     bool hadMix = mixData.first.firstClipId > -1 || mixData.second.firstClipId > -1;
702     if (!finalMove && !revertMove) {
703         QVector <int>exceptions = {clipId};
704         if (mixData.first.firstClipId > -1) {
705             exceptions << mixData.first.firstClipId;
706         }
707         if (mixData.second.secondClipId > -1) {
708             exceptions << mixData.second.secondClipId;
709         }
710         if (!getTrackById_const(trackId)->isAvailableWithExceptions(position, getClipPlaytime(clipId), exceptions)) {
711             // No space for clip insert operation, abort
712             qWarning() << "No free space for clip move";
713             return false;
714         }
715     }
716     if (old_trackId == -1 && isTrack(previous_track) && hadMix && previous_track != trackId) {
717         // Clip is moved to another track
718         bool mixGroupMove = false;
719         if (mixData.first.firstClipId > 0) {
720             allowedClipMixes << mixData.first.firstClipId;
721             if (moving_clips.contains(mixData.first.firstClipId)) {
722                 allowedClipMixes << mixData.first.firstClipId;
723             } else if (finalMove) {
724                 position += (mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first - mixData.first.mixOffset);
725                 removeMixWithUndo(clipId, local_undo, local_redo);
726             }
727         }
728         if (mixData.second.firstClipId > 0) {
729             allowedClipMixes << mixData.second.secondClipId;
730             if (moving_clips.contains(mixData.second.secondClipId)) {
731                 allowedClipMixes << mixData.second.secondClipId;
732             } else if (finalMove) {
733                 removeMixWithUndo(mixData.second.secondClipId, local_undo, local_redo);
734             }
735         }
736         if (m_groups->isInGroup(clipId) && mixData.first.firstClipId > 0) {
737             int parentGroup = m_groups->getRootId(clipId);
738             if (parentGroup > -1) {
739                 std::unordered_set<int> sub = m_groups->getLeaves(parentGroup);
740                 if (sub.count(mixData.first.firstClipId) > 0 && sub.count(mixData.first.secondClipId) > 0) {
741                     mixGroupMove = true;
742                 }
743             }
744         }
745         if (mixGroupMove) {
746             // We are moving a group on another track, delete and re-add
747             // Get mix properties
748             std::pair<QString,QVector<QPair<QString, QVariant>>> mixParams = getTrackById_const(previous_track)->getMixParams(mixData.first.secondClipId);
749             simple_move_mix = [this, previous_track, trackId, finalMove, mixData, mixParams]() {
750                 // Insert mix on new track
751                 bool result = getTrackById_const(trackId)->createMix(mixData.first, mixParams, finalMove);
752                 // Remove mix on old track
753                 getTrackById_const(previous_track)->removeMix(mixData.first);
754                 return result;
755             };
756             simple_restore_mix = [this, previous_track, trackId, finalMove, mixData, mixParams]() {
757                 bool result = getTrackById_const(previous_track)->createMix(mixData.first, mixParams, finalMove);
758                 // Remove mix on old track
759                 getTrackById_const(trackId)->removeMix(mixData.first);
760 
761                 return result;
762             };
763         }
764     } else if (finalMove && !groupMove && isTrack(old_trackId) && hadMix) {
765         // Clip has a mix
766         if (mixData.first.firstClipId > -1) {
767             if (old_trackId == trackId) {
768                 int mixCut = m_allClips[clipId]->getMixCutPosition();
769                 // We are moving a clip on same track
770                 if (position > mixData.first.secondClipInOut.first - mixCut || position < mixData.first.firstClipInOut.first) {
771                     position += m_allClips[clipId]->getMixDuration() - mixCut;
772                     removeMixWithUndo(clipId, local_undo, local_redo);
773                 }
774             } else {
775                 // Clip moved to another track, delete mix
776                 position += (m_allClips[clipId]->getMixDuration() - m_allClips[clipId]->getMixCutPosition());
777                 removeMixWithUndo(clipId, local_undo, local_redo);
778             }
779         }
780         if (mixData.second.firstClipId > -1) {
781             // We have a mix at clip end
782             if (old_trackId == trackId) {
783                 int mixEnd = m_allClips[mixData.second.secondClipId]->getPosition() + m_allClips[mixData.second.secondClipId]->getMixDuration();
784                 if (position > mixEnd || position < m_allClips[mixData.second.secondClipId]->getPosition()) {
785                     // Moved outside mix zone
786                     removeMixWithUndo(mixData.second.secondClipId, local_undo, local_redo);
787 
788                 }
789             } else {
790                 // Clip moved to another track, delete mix
791                 // Mix will be deleted by syncronizeMixes operation, only
792                 // re-add it on undo
793                 removeMixWithUndo(mixData.second.secondClipId, local_undo, local_redo);
794             }
795         }
796     } else if (finalMove && groupMove && isTrack(old_trackId) && hadMix && old_trackId == trackId) {
797         // Group move on same track with mix
798         if (mixData.first.firstClipId > -1) {
799             // Mix on clip start, check if mix is still in range
800             if (!moving_clips.contains(mixData.first.firstClipId)) {
801                 int mixCut = m_allClips[clipId]->getMixCutPosition();
802                 // We are moving a clip on same track
803                 if (position > mixData.first.secondClipInOut.first - mixCut) {
804                     // Mix will be deleted, recreate on undo
805                     position += m_allClips[mixData.first.secondClipId]->getMixDuration() - m_allClips[mixData.first.secondClipId]->getMixCutPosition();
806                     removeMixWithUndo(mixData.first.secondClipId, local_undo, local_redo);
807                 }
808             } else {
809                 allowedClipMixes << mixData.first.firstClipId;
810             }
811         }
812         if (mixData.second.firstClipId > -1) {
813             // Mix on clip end, check if mix is still in range
814             if (!moving_clips.contains(mixData.second.secondClipId)) {
815                 int mixEnd = m_allClips[mixData.second.secondClipId]->getPosition() + m_allClips[mixData.second.secondClipId]->getMixDuration();
816                 if (mixEnd > position + m_allClips[clipId]->getPlaytime()) {
817                     // Mix will be deleted, recreate on undo
818                     removeMixWithUndo(mixData.second.secondClipId, local_undo, local_redo);
819                 }
820             } else {
821                 allowedClipMixes << mixData.second.secondClipId;
822             }
823         }
824     } else {
825     }
826 
827     if (old_trackId != -1) {
828         if (notifyViewOnly) {
829             PUSH_LAMBDA(update_model, local_undo);
830         }
831         ok = getTrackById(old_trackId)->requestClipDeletion(clipId, updateView, finalMove, local_undo, local_redo, groupMove, false, allowedClipMixes);
832         if (!ok) {
833             bool undone = local_undo();
834             Q_ASSERT(undone);
835             qWarning() << "clip deletion failed";
836             return false;
837         } else {
838         }
839     }
840     ok = ok && getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, finalMove, local_undo, local_redo, groupMove, allowedClipMixes);
841 
842     if (!ok) {
843         qWarning() << "clip insertion failed";
844         bool undone = local_undo();
845         Q_ASSERT(undone);
846         return false;
847     }
848 
849     sync_mix();
850     update_model();
851     simple_move_mix();
852 
853     if (finalMove) {
854         PUSH_LAMBDA(simple_restore_mix, undo);
855         PUSH_LAMBDA(simple_move_mix, local_redo);
856         //PUSH_LAMBDA(sync_mix, local_redo);
857     }
858     if (notifyViewOnly) {
859         PUSH_LAMBDA(update_model, local_redo);
860     }
861     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
862     return true;
863 }
864 
mixClip(int idToMove,const QString & mixId,int delta)865 bool TimelineModel::mixClip(int idToMove, const QString &mixId, int delta)
866 {
867     int selectedTrack = -1;
868 
869     std::unordered_set<int> initialSelection = getCurrentSelection();
870     if (idToMove == -1 && initialSelection.empty()) {
871         pCore->displayMessage(i18n("Select a clip to apply the mix"), ErrorMessage, 500);
872         return false;
873     }
874     std::pair<int, int> clipsToMix;
875     std::pair<int, int> mixDurations;
876     int mixPosition = 0;
877     int previousClip = -1;
878     int noSpaceInClip = 0;
879     int leftMax = 0;
880     int rightMax = 0;
881     if (idToMove != -1) {
882         initialSelection = {idToMove};
883         idToMove = -1;
884     }
885     for (int s : initialSelection) {
886         if (!isClip(s)) {
887             continue;
888         }
889         selectedTrack = getClipTrackId(s);
890         if (selectedTrack == -1 || !isTrack(selectedTrack)) {
891             continue;
892         }
893         mixPosition = getItemPosition(s);
894         int clipDuration =  getItemPlaytime(s);
895         // Check if we have a clip before and/or after
896         int nextClip = -1;
897         previousClip = -1;
898         // Check if clip already has a mix
899         if (delta > -1 && getTrackById_const(selectedTrack)->hasStartMix(s)) {
900             if (getTrackById_const(selectedTrack)->hasEndMix(s)) {
901                 continue;
902             }
903             nextClip = getTrackById_const(selectedTrack)->getClipByPosition(mixPosition + clipDuration + 1);
904         } else if (delta < 1 && getTrackById_const(selectedTrack)->hasEndMix(s)) {
905             previousClip = getTrackById_const(selectedTrack)->getClipByPosition(mixPosition - 1);
906             if (previousClip > -1 && getTrackById_const(selectedTrack)->hasEndMix(previousClip)) {
907                 // Could happen if 2 clips before are mixed to full length
908                 previousClip = -1;
909             }
910         } else {
911             if (delta < 1) {
912                 previousClip = getTrackById_const(selectedTrack)->getClipByPosition(mixPosition - 1);
913             }
914             if (delta > -1) {
915                 nextClip = getTrackById_const(selectedTrack)->getClipByPosition(mixPosition + clipDuration + 1);
916             }
917         }
918         if (previousClip > -1 && nextClip > -1) {
919             // We have a clip before and a clip after, check timeline cursor position to decide where to mix
920             int cursor = pCore->getTimelinePosition();
921             if (cursor < mixPosition + clipDuration / 2) {
922                 nextClip = -1;
923             } else {
924                 previousClip = -1;
925             }
926         }
927         if (nextClip == -1) {
928             if (previousClip == -1) {
929             // No clip to mix, abort
930                 continue;
931             }
932             // Make sure we have enough space in clip to resize
933             int maxLengthLeft = m_allClips[previousClip]->getMaxDuration();
934             int maxLengthRight = m_allClips[s]->getMaxDuration();
935             // leftMax is the maximum frames we have to expand first clip on the right
936             leftMax = maxLengthLeft > -1 ? (maxLengthLeft - 1 - m_allClips[previousClip]->getOut()) : m_allClips[s]->getPlaytime();
937             // rightMax is the maximum frames we have to expand second clip on the left
938             rightMax = maxLengthRight > -1 ? (m_allClips[s]->getIn()) : m_allClips[previousClip]->getPlaytime();
939             if (getTrackById_const(selectedTrack)->hasStartMix(previousClip)) {
940                 int spaceBeforeMix = m_allClips[s]->getPosition() - (m_allClips[previousClip]->getPosition() + m_allClips[previousClip]->getMixDuration());
941                 rightMax = rightMax == -1 ? spaceBeforeMix : qMin(rightMax, spaceBeforeMix);
942             }
943             if (getTrackById_const(selectedTrack)->hasEndMix(s)) {
944                 MixInfo mixData = getTrackById_const(selectedTrack)->getMixInfo(s).second;
945                 if (mixData.secondClipId > -1) {
946                     int spaceAfterMix = m_allClips[s]->getPlaytime() - m_allClips[mixData.secondClipId]->getMixDuration();
947                     leftMax = leftMax == -1 ? spaceAfterMix : qMin(leftMax, spaceAfterMix);
948                 }
949             }
950             if (leftMax > -1 && rightMax > -1 && (leftMax + rightMax < 3)) {
951                 noSpaceInClip = 1;
952                 continue;
953             }
954             // Create Mix at start of selected clip
955             clipsToMix.first = previousClip;
956             clipsToMix.second = s;
957             idToMove = s;
958             break;
959         } else {
960             // Mix at end of selected clip
961             // Make sure we have enough space in clip to resize
962             int maxLengthLeft = m_allClips[s]->getMaxDuration();
963             int maxLengthRight = m_allClips[nextClip]->getMaxDuration();
964             // leftMax is the maximum frames we have to expand first clip on the right
965             leftMax = maxLengthLeft > -1 ? (maxLengthLeft - 1 - m_allClips[s]->getOut()) : m_allClips[nextClip]->getPlaytime();
966             // rightMax is the maximum frames we have to expand second clip on the left
967             rightMax = maxLengthRight > -1 ? (m_allClips[nextClip]->getIn()) : m_allClips[s]->getPlaytime();
968             if (getTrackById_const(selectedTrack)->hasStartMix(s)) {
969                 int spaceBeforeMix = m_allClips[nextClip]->getPosition() - (m_allClips[s]->getPosition() + m_allClips[s]->getMixDuration());
970                 rightMax = rightMax == -1 ? spaceBeforeMix : qMin(rightMax, spaceBeforeMix);
971             }
972             if (getTrackById_const(selectedTrack)->hasEndMix(nextClip)) {
973                 MixInfo mixData = getTrackById_const(selectedTrack)->getMixInfo(nextClip).second;
974                 if (mixData.secondClipId > -1) {
975                     int spaceAfterMix = m_allClips[nextClip]->getPlaytime() - m_allClips[mixData.secondClipId]->getMixDuration();
976                     leftMax = leftMax == -1 ? spaceAfterMix : qMin(leftMax, spaceAfterMix);
977                 }
978             }
979             if (leftMax > -1 && rightMax > -1 && (leftMax + rightMax < 3)) {
980                 noSpaceInClip = 2;
981                 continue;
982             }
983             mixPosition += clipDuration;
984             clipsToMix.first = s;
985             clipsToMix.second = nextClip;
986             idToMove = s;
987             break;
988         }
989     }
990     if (noSpaceInClip > 0) {
991         pCore->displayMessage(i18n("Not enough frames at clip %1 to apply the mix", noSpaceInClip == 1 ? i18n("start") : i18n("end")), ErrorMessage, 500);
992         return false;
993     }
994     if (idToMove == -1 || !isClip(idToMove)) {
995         pCore->displayMessage(i18n("Select a clip to apply the mix"), ErrorMessage, 500);
996         return false;
997     }
998 
999     std::function<bool(void)> undo = []() { return true; };
1000     std::function<bool(void)> redo = []() { return true; };
1001     int mixDuration = pCore->getDurationFromString(KdenliveSettings::mix_duration());
1002     if (leftMax > -1) {
1003         if (rightMax > -1) {
1004             // Both clips have limited durations
1005             mixDurations.first = qMin(mixDuration / 2, leftMax);
1006             mixDurations.second = qMin(mixDuration - mixDuration / 2, rightMax);
1007             int offset = mixDuration - (mixDurations.first + mixDurations.second);
1008             if (offset > 0) {
1009                 if (leftMax > mixDurations.first) {
1010                     mixDurations.first = qMin(leftMax, mixDurations.first + offset);
1011                 } else if (rightMax > mixDurations.second) {
1012                     mixDurations.second = qMin(rightMax, mixDurations.second + offset);
1013                 }
1014             }
1015         } else {
1016             mixDurations.first = qMin(mixDuration - mixDuration / 2, leftMax);
1017             mixDurations.second = mixDuration - mixDurations.second;
1018         }
1019     } else {
1020         if (rightMax > -1) {
1021             mixDurations.second = qMin(mixDuration - mixDuration / 2, rightMax);
1022             mixDurations.first = mixDuration - mixDurations.second;
1023         } else {
1024             mixDurations.first = mixDuration / 2;
1025             mixDurations.second = mixDuration - mixDurations.first;
1026         }
1027     }
1028     bool result = requestClipMix(mixId, clipsToMix, mixDurations, selectedTrack, mixPosition, true, true, true, undo,
1029  redo, false);
1030     if (result) {
1031         // Check if this is an AV split group
1032         if (m_groups->isInGroup(idToMove)) {
1033             int parentGroup = m_groups->getRootId(idToMove);
1034             if (parentGroup > -1 && m_groups->getType(parentGroup) == GroupType::AVSplit) {
1035                 std::unordered_set<int> sub = m_groups->getLeaves(parentGroup);
1036                 // Perform mix on split clip
1037                 for (int current_id : sub) {
1038                     if (idToMove == current_id) {
1039                         continue;
1040                     }
1041                     int splitTrack = m_allClips[current_id]->getCurrentTrackId();
1042                     int splitId;
1043                     if (previousClip == -1) {
1044                         splitId = getTrackById_const(splitTrack)->getClipByPosition(mixPosition + 1);
1045                         clipsToMix.first = current_id;
1046                         clipsToMix.second = splitId;
1047                     } else {
1048                         splitId = getTrackById_const(splitTrack)->getClipByPosition(mixPosition - 1);
1049                         clipsToMix.first = splitId;
1050                         clipsToMix.second = current_id;
1051                     }
1052                     if (splitId > -1 && !getTrackById_const(splitTrack)->hasStartMix(clipsToMix.second) && clipsToMix.first != clipsToMix.second) {
1053                         result = requestClipMix(mixId, clipsToMix, mixDurations, splitTrack, mixPosition, true, true, true, undo, redo, false);
1054                     }
1055                 }
1056             }
1057         }
1058         pCore->pushUndo(undo, redo, i18n("Create mix"));
1059         // Reselect clips
1060         if (!initialSelection.empty()) {
1061             requestSetSelection(initialSelection);
1062         }
1063         return result;
1064     } else {
1065         qWarning() << "mix failed";
1066         undo();
1067         if (!initialSelection.empty()) {
1068             requestSetSelection(initialSelection);
1069         }
1070         return false;
1071     }
1072 }
1073 
requestClipMix(const QString & mixId,std::pair<int,int> clipIds,std::pair<int,int> mixDurations,int trackId,int position,bool updateView,bool invalidateTimeline,bool finalMove,Fun & undo,Fun & redo,bool groupMove)1074 bool TimelineModel::requestClipMix(const QString &mixId, std::pair<int, int> clipIds, std::pair<int, int> mixDurations, int trackId, int position, bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo, bool groupMove)
1075 {
1076     if (trackId == -1) {
1077         return false;
1078     }
1079     Q_ASSERT(isClip(clipIds.first));
1080     std::function<bool(void)> local_undo = []() { return true; };
1081     std::function<bool(void)> local_redo = []() { return true; };
1082     bool ok = true;
1083     bool notifyViewOnly = false;
1084     Fun update_model = []() { return true; };
1085     // Move on same track, simply inform the view
1086     updateView = false;
1087     notifyViewOnly = true;
1088     update_model = [clipIds, this, trackId, position, invalidateTimeline, mixDurations]() {
1089         QModelIndex modelIndex = makeClipIndexFromID(clipIds.second);
1090         notifyChange(modelIndex, modelIndex, {StartRole,DurationRole});
1091         QModelIndex modelIndex2 = makeClipIndexFromID(clipIds.first);
1092         notifyChange(modelIndex2, modelIndex2, DurationRole);
1093         if (invalidateTimeline && !getTrackById_const(trackId)->isAudioTrack()) {
1094             emit invalidateZone(position - mixDurations.second, position + mixDurations.first);
1095         }
1096         return true;
1097     };
1098     if (notifyViewOnly) {
1099         PUSH_LAMBDA(update_model, local_undo);
1100     }
1101     ok = getTrackById(trackId)->requestClipMix(mixId, clipIds, mixDurations, updateView, finalMove, local_undo, local_redo, groupMove);
1102     if (!ok) {
1103         qWarning() << "mix failed, reverting";
1104         bool undone = local_undo();
1105         Q_ASSERT(undone);
1106         return false;
1107     }
1108     update_model();
1109     if (notifyViewOnly) {
1110         PUSH_LAMBDA(update_model, local_redo);
1111     }
1112     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
1113     return ok;
1114 
1115 }
1116 
requestFakeClipMove(int clipId,int trackId,int position,bool updateView,bool logUndo,bool invalidateTimeline)1117 bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
1118 {
1119     QWriteLocker locker(&m_lock);
1120     TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline)
1121     Q_ASSERT(m_allClips.count(clipId) > 0);
1122     if (m_groups->isInGroup(clipId)) {
1123         // element is in a group.
1124         int groupId = m_groups->getRootId(clipId);
1125         int current_trackId = getClipTrackId(clipId);
1126         int track_pos1 = getTrackPosition(trackId);
1127         int track_pos2 = getTrackPosition(current_trackId);
1128         int delta_track = track_pos1 - track_pos2;
1129         int delta_pos = position - m_allClips[clipId]->getPosition();
1130         bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
1131         TRACE_RES(res);
1132         return res;
1133     }
1134     std::function<bool(void)> undo = []() { return true; };
1135     std::function<bool(void)> redo = []() { return true; };
1136     bool res = requestFakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo);
1137     if (res && logUndo) {
1138         PUSH_UNDO(undo, redo, i18n("Move clip"));
1139     }
1140     TRACE_RES(res);
1141     return res;
1142 }
1143 
requestClipMove(int clipId,int trackId,int position,bool moveMirrorTracks,bool updateView,bool logUndo,bool invalidateTimeline,bool revertMove)1144 bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool moveMirrorTracks, bool updateView, bool logUndo, bool invalidateTimeline, bool revertMove)
1145 {
1146     QWriteLocker locker(&m_lock);
1147     TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline);
1148     Q_ASSERT(m_allClips.count(clipId) > 0);
1149     if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
1150         TRACE_RES(true);
1151         return true;
1152     }
1153     if (m_groups->isInGroup(clipId)) {
1154         // element is in a group.
1155         int groupId = m_groups->getRootId(clipId);
1156         int current_trackId = getClipTrackId(clipId);
1157         int track_pos1 = getTrackPosition(trackId);
1158         int track_pos2 = getTrackPosition(current_trackId);
1159         int delta_track = track_pos1 - track_pos2;
1160         int delta_pos = position - m_allClips[clipId]->getPosition();
1161         return requestGroupMove(clipId, groupId, delta_track, delta_pos, moveMirrorTracks, updateView, logUndo, revertMove);
1162     }
1163     std::function<bool(void)> undo = []() { return true; };
1164     std::function<bool(void)> redo = []() { return true; };
1165     bool res = requestClipMove(clipId, trackId, position, moveMirrorTracks, updateView, invalidateTimeline, logUndo, undo, redo, revertMove);
1166     if (res && logUndo) {
1167         PUSH_UNDO(undo, redo, i18n("Move clip"));
1168     }
1169     TRACE_RES(res);
1170     return res;
1171 }
1172 
cutSubtitle(int position,Fun & undo,Fun & redo)1173 int TimelineModel::cutSubtitle(int position, Fun &undo, Fun &redo)
1174 {
1175     if (m_subtitleModel) {
1176         return m_subtitleModel->cutSubtitle(position, undo, redo);
1177     }
1178     return -1;
1179 }
1180 
requestSubtitleMove(int clipId,int position,bool updateView,bool logUndo,bool invalidateTimeline)1181 bool TimelineModel::requestSubtitleMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline)
1182 {
1183     QWriteLocker locker(&m_lock);
1184     Q_ASSERT(m_allSubtitles.count(clipId) > 0);
1185     GenTime oldPos = m_allSubtitles.at(clipId);
1186     GenTime newPos(position, pCore->getCurrentFps());
1187     if (oldPos == newPos) {
1188         return true;
1189     }
1190     if (m_groups->isInGroup(clipId)) {
1191         // element is in a group.
1192         int groupId = m_groups->getRootId(clipId);
1193         int delta_pos = position - oldPos.frames(pCore->getCurrentFps());
1194         return requestGroupMove(clipId, groupId, 0, delta_pos, false, updateView, logUndo);
1195     }
1196     std::function<bool(void)> undo = []() { return true; };
1197     std::function<bool(void)> redo = []() { return true; };
1198     bool res = requestSubtitleMove(clipId, position, updateView, logUndo, logUndo, invalidateTimeline, undo, redo);
1199     if (res && logUndo) {
1200         PUSH_UNDO(undo, redo, i18n("Move subtitle"));
1201     }
1202     return res;
1203 }
1204 
requestSubtitleMove(int clipId,int position,bool updateView,bool first,bool last,bool invalidateTimeline,Fun & undo,Fun & redo)1205 bool TimelineModel::requestSubtitleMove(int clipId, int position, bool updateView, bool first, bool last, bool invalidateTimeline, Fun &undo, Fun &redo)
1206 {
1207     Q_UNUSED(invalidateTimeline)
1208     QWriteLocker locker(&m_lock);
1209     GenTime oldPos = m_allSubtitles.at(clipId);
1210     GenTime newPos(position, pCore->getCurrentFps());
1211     Fun local_redo = [this, clipId, newPos, last, updateView]() {
1212         return m_subtitleModel->moveSubtitle(clipId, newPos, last, updateView);
1213     };
1214     Fun local_undo = [this, oldPos, clipId, first, updateView]() {
1215         return m_subtitleModel->moveSubtitle(clipId, oldPos, first, updateView);
1216     };
1217     bool res = local_redo();
1218     if (res) {
1219         UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
1220     } else {
1221         local_undo();
1222     }
1223     return res;
1224 }
1225 
getSubtitleModel()1226 std::shared_ptr<SubtitleModel> TimelineModel::getSubtitleModel()
1227 {
1228     return m_subtitleModel;
1229 }
1230 
requestClipMoveAttempt(int clipId,int trackId,int position)1231 bool TimelineModel::requestClipMoveAttempt(int clipId, int trackId, int position)
1232 {
1233     QWriteLocker locker(&m_lock);
1234     Q_ASSERT(m_allClips.count(clipId) > 0);
1235     if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) {
1236         return true;
1237     }
1238     std::function<bool(void)> undo = []() { return true; };
1239     std::function<bool(void)> redo = []() { return true; };
1240     bool res = true;
1241     if (m_groups->isInGroup(clipId)) {
1242         // element is in a group.
1243         int groupId = m_groups->getRootId(clipId);
1244         int current_trackId = getClipTrackId(clipId);
1245         int track_pos1 = getTrackPosition(trackId);
1246         int track_pos2 = getTrackPosition(current_trackId);
1247         int delta_track = track_pos1 - track_pos2;
1248         int delta_pos = position - m_allClips[clipId]->getPosition();
1249         res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false, false);
1250     } else {
1251         res = requestClipMove(clipId, trackId, position, true, false, false, false, undo, redo);
1252     }
1253     if (res) {
1254         undo();
1255     }
1256     return res;
1257 }
1258 
suggestItemMove(int itemId,int trackId,int position,int cursorPosition,int snapDistance)1259 QVariantList TimelineModel::suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance)
1260 {
1261     if (isClip(itemId)) {
1262         return suggestClipMove(itemId, trackId, position, cursorPosition, snapDistance);
1263     }
1264     if (isComposition(itemId)) {
1265         return suggestCompositionMove(itemId, trackId, position, cursorPosition, snapDistance);
1266     }
1267     if (isSubTitle(itemId)) {
1268         return {suggestSubtitleMove(itemId, position, cursorPosition, snapDistance), -1};
1269     }
1270     return QVariantList();
1271 }
1272 
adjustFrame(int frame,int trackId)1273 int TimelineModel::adjustFrame(int frame, int trackId)
1274 {
1275     if (m_editMode == TimelineMode::InsertEdit && isTrack(trackId)) {
1276         frame = qMin(frame, getTrackById_const(trackId)->trackDuration());
1277     }
1278     return frame;
1279 }
1280 
1281 
suggestSubtitleMove(int subId,int position,int cursorPosition,int snapDistance)1282 int TimelineModel::suggestSubtitleMove(int subId, int position, int cursorPosition, int snapDistance)
1283 {
1284     QWriteLocker locker(&m_lock);
1285     Q_ASSERT(isSubTitle(subId));
1286     int currentPos = getSubtitlePosition(subId);
1287     int offset = 0;
1288     if (currentPos == position || m_subtitleModel->isLocked()) {
1289         return position;
1290     }
1291     int newPos = position;
1292     if (snapDistance > 0) {
1293         std::vector<int> ignored_pts;
1294         // For snapping, we must ignore all in/outs of the clips of the group being moved
1295         std::unordered_set<int> all_items = {subId};
1296         if (m_groups->isInGroup(subId)) {
1297             int groupId = m_groups->getRootId(subId);
1298             all_items = m_groups->getLeaves(groupId);
1299         }
1300         for (int current_clipId : all_items) {
1301             if (getItemTrackId(current_clipId) != -1) {
1302                 if (isClip(current_clipId)) {
1303                     m_allClips[current_clipId]->allSnaps(ignored_pts, offset);
1304                 } else if (isComposition(current_clipId)) {
1305                     // Composition
1306                     int in = getItemPosition(current_clipId) - offset;
1307                     ignored_pts.push_back(in);
1308                     ignored_pts.push_back(in + getItemPlaytime(current_clipId));
1309                 }
1310             } else if (isSubTitle(current_clipId)) {
1311                 int in = getItemPosition(current_clipId) - offset;
1312                 ignored_pts.push_back(in);
1313                 ignored_pts.push_back(in + getItemPlaytime(current_clipId));
1314             }
1315         }
1316         int snapped = getBestSnapPos(currentPos, position - currentPos, ignored_pts, cursorPosition, snapDistance);
1317         if (snapped >= 0) {
1318             newPos = snapped;
1319         }
1320     }
1321     //m_subtitleModel->moveSubtitle(GenTime(currentPos, pCore->getCurrentFps()), GenTime(position, pCore->getCurrentFps()));
1322     if (requestSubtitleMove(subId, newPos, true, false)) {
1323         return newPos;
1324     }
1325     return position;
1326 }
1327 
suggestClipMove(int clipId,int trackId,int position,int cursorPosition,int snapDistance,bool moveMirrorTracks)1328 QVariantList TimelineModel::suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance, bool moveMirrorTracks)
1329 {
1330     QWriteLocker locker(&m_lock);
1331     TRACE(clipId, trackId, position, cursorPosition, snapDistance);
1332     Q_ASSERT(isClip(clipId));
1333     Q_ASSERT(isTrack(trackId));
1334     int currentPos = m_editMode == TimelineMode::NormalEdit ? getClipPosition(clipId) : m_allClips[clipId]->getFakePosition();
1335     int offset = m_editMode == TimelineMode::NormalEdit ? 0 : getClipPosition(clipId) - currentPos;
1336     int sourceTrackId = (m_editMode != TimelineMode::NormalEdit) ? m_allClips[clipId]->getFakeTrackId() : getClipTrackId(clipId);
1337     if (sourceTrackId > -1 && getTrackById_const(trackId)->isAudioTrack() != getTrackById_const(sourceTrackId)->isAudioTrack()) {
1338         // Trying move on incompatible track type, stay on same track
1339         trackId = sourceTrackId;
1340     }
1341     if (currentPos == position && sourceTrackId == trackId) {
1342         TRACE_RES(position);
1343         return {position, trackId};
1344     }
1345     if (m_editMode == TimelineMode::InsertEdit) {
1346         int maxPos = getTrackById_const(trackId)->trackDuration();
1347         if (m_allClips[clipId]->getCurrentTrackId() == trackId) {
1348             maxPos -= m_allClips[clipId]->getPlaytime();
1349         }
1350         position = qMin(position, maxPos);
1351     }
1352     bool after = position > currentPos;
1353     if (snapDistance > 0) {
1354         std::vector<int> ignored_pts;
1355         // For snapping, we must ignore all in/outs of the clips of the group being moved
1356         std::unordered_set<int> all_items = {clipId};
1357         if (m_groups->isInGroup(clipId)) {
1358             int groupId = m_groups->getRootId(clipId);
1359             all_items = m_groups->getLeaves(groupId);
1360         }
1361         for (int current_clipId : all_items) {
1362             if (getItemTrackId(current_clipId) != -1) {
1363                 if (isClip(current_clipId)) {
1364                     m_allClips[current_clipId]->allSnaps(ignored_pts, offset);
1365                 } else if (isComposition(current_clipId)) {
1366                     // Composition
1367                     int in = getItemPosition(current_clipId) - offset;
1368                     ignored_pts.push_back(in);
1369                     ignored_pts.push_back(in + getItemPlaytime(current_clipId));
1370                 }
1371             } else if (isSubTitle(current_clipId)) {
1372                 // TODO: Subtitle
1373                 /*int in = getItemPosition(current_clipId) - offset;
1374                 ignored_pts.push_back(in);
1375                 ignored_pts.push_back(in + getItemPlaytime(current_clipId));*/
1376             }
1377         }
1378         int snapped = getBestSnapPos(currentPos, position - currentPos, ignored_pts, cursorPosition, snapDistance);
1379         if (snapped >= 0) {
1380             position = snapped;
1381         }
1382     }
1383     bool isInGroup = m_groups->isInGroup(clipId);
1384     if (sourceTrackId == trackId) {
1385         // Same track move, check if there is a mix and limit move
1386         std::pair<MixInfo, MixInfo> mixData = getTrackById_const(trackId)->getMixInfo(clipId);
1387         if (mixData.first.firstClipId > -1) {
1388             // Clip has start mix
1389             int clipDuration = m_allClips[clipId]->getPlaytime();
1390             // ensure we don't move into clip
1391             bool allowMove = false;
1392             if (isInGroup) {
1393                 // Check if in same group as clip mix
1394                 int groupId = m_groups->getRootId(clipId);
1395                 if (groupId == m_groups->getRootId(mixData.first.firstClipId)) {
1396                     allowMove = true;
1397                 }
1398             }
1399             if (!allowMove && position + clipDuration > mixData.first.firstClipInOut.first && position < mixData.first.firstClipInOut.second) {
1400                 // Abort move
1401                 return {currentPos, sourceTrackId};
1402             }
1403         }
1404         if (mixData.second.firstClipId > -1) {
1405             // Clip has end mix
1406             int clipDuration = m_allClips[clipId]->getPlaytime();
1407             bool allowMove = false;
1408             if (isInGroup) {
1409                 // Check if in same group as clip mix
1410                 int groupId = m_groups->getRootId(clipId);
1411                 if (groupId == m_groups->getRootId(mixData.second.secondClipId)) {
1412                     allowMove = true;
1413                 }
1414             }
1415             if (!allowMove && position + clipDuration > mixData.second.secondClipInOut.first && position < mixData.second.secondClipInOut.second) {
1416                 // Abort move
1417                 return {currentPos, sourceTrackId};
1418             }
1419         }
1420     }
1421     // we check if move is possible
1422     bool possible = (m_editMode == TimelineMode::NormalEdit) ? requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false)
1423                                                            : requestFakeClipMove(clipId, trackId, position, true, false, false);
1424 
1425     if (possible) {
1426         TRACE_RES(position);
1427         if (m_editMode != TimelineMode::NormalEdit) {
1428             trackId = m_allClips[clipId]->getFakeTrackId();
1429         }
1430         return {position, trackId};
1431     }
1432     if (sourceTrackId == -1) {
1433         // not clear what to do here, if the current move doesn't work. We could try to find empty space, but it might end up being far away...
1434         TRACE_RES(currentPos);
1435         return {currentPos, -1};
1436     }
1437     // Find best possible move
1438     if (!isInGroup) {
1439         // Try same track move
1440         if (trackId != sourceTrackId && sourceTrackId != -1) {
1441             trackId = sourceTrackId;
1442             possible = requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false);
1443             if (!possible) {
1444                 qWarning() << "can't move clip" << clipId << "on track" << trackId << "at" << position;
1445             } else {
1446                 TRACE_RES(position);
1447                 return {position, trackId};
1448             }
1449         }
1450 
1451         int blank_length = getTrackById_const(trackId)->getBlankSizeNearClip(clipId, after);
1452         if (blank_length < INT_MAX) {
1453             if (after) {
1454                 position = currentPos + blank_length;
1455             } else {
1456                 position = currentPos - blank_length;
1457             }
1458         } else {
1459             TRACE_RES(currentPos);
1460             return {currentPos, sourceTrackId};
1461         }
1462         possible = requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false);
1463         TRACE_RES(possible ? position : currentPos);
1464         if (possible) {
1465             return {position, trackId};
1466         }
1467         return {currentPos, sourceTrackId};
1468     }
1469     if (trackId != sourceTrackId) {
1470         // Try same track move
1471         possible = requestClipMove(clipId, sourceTrackId, position, moveMirrorTracks, true, false, false);
1472         if (possible) {
1473             return {position, sourceTrackId};
1474         }
1475         return {currentPos, sourceTrackId};
1476     }
1477     // find best pos for groups
1478     int groupId = m_groups->getRootId(clipId);
1479     std::unordered_set<int> all_items = m_groups->getLeaves(groupId);
1480     QMap<int, int> trackPosition;
1481 
1482     // First pass, sort clips by track and keep only the first / last depending on move direction
1483     for (int current_clipId : all_items) {
1484         int clipTrack = getItemTrackId(current_clipId);
1485         if (clipTrack == -1) {
1486             continue;
1487         }
1488         int in = getItemPosition(current_clipId);
1489         if (trackPosition.contains(clipTrack)) {
1490             if (after) {
1491                 // keep only last clip position for track
1492                 int out = in + getItemPlaytime(current_clipId);
1493                 if (trackPosition.value(clipTrack) < out) {
1494                     trackPosition.insert(clipTrack, out);
1495                 }
1496             } else {
1497                 // keep only first clip position for track
1498                 if (trackPosition.value(clipTrack) > in) {
1499                     trackPosition.insert(clipTrack, in);
1500                 }
1501             }
1502         } else {
1503             trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) - 1 : in);
1504         }
1505     }
1506 
1507     // Now check space on each track
1508     QMapIterator<int, int> i(trackPosition);
1509     int blank_length = 0;
1510     while (i.hasNext()) {
1511         i.next();
1512         int track_space;
1513         if (!after) {
1514             // Check space before the position
1515             track_space = i.value() - getTrackById_const(i.key())->getBlankStart(i.value() - 1);
1516             if (blank_length == 0 || blank_length > track_space) {
1517                 blank_length = track_space;
1518             }
1519         } else {
1520             // Check space after the position
1521             track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value();
1522             if (blank_length == 0 || blank_length > track_space) {
1523                 blank_length = track_space;
1524             }
1525         }
1526     }
1527     if (snapDistance > 0) {
1528         if (blank_length > 10 * snapDistance) {
1529             blank_length = 0;
1530         }
1531     } else if (blank_length / m_profile->fps() > 5) {
1532         blank_length = 0;
1533     }
1534     if (blank_length != 0) {
1535         int updatedPos = currentPos + (after ? blank_length : -blank_length);
1536         possible = requestClipMove(clipId, trackId, updatedPos, moveMirrorTracks, true, false, false);
1537         if (possible) {
1538             TRACE_RES(updatedPos);
1539             return {updatedPos, trackId};
1540         }
1541     }
1542     TRACE_RES(currentPos);
1543     return {currentPos, sourceTrackId};
1544 }
1545 
suggestCompositionMove(int compoId,int trackId,int position,int cursorPosition,int snapDistance)1546 QVariantList TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance)
1547 {
1548     QWriteLocker locker(&m_lock);
1549     TRACE(compoId, trackId, position, cursorPosition, snapDistance);
1550     Q_ASSERT(isComposition(compoId));
1551     Q_ASSERT(isTrack(trackId));
1552     int currentPos = getCompositionPosition(compoId);
1553     int currentTrack = getCompositionTrackId(compoId);
1554     if (getTrackById_const(trackId)->isAudioTrack()) {
1555         // Trying move on incompatible track type, stay on same track
1556         trackId = currentTrack;
1557     }
1558     if (currentPos == position && currentTrack == trackId) {
1559         TRACE_RES(position);
1560         return {position, trackId};
1561     }
1562 
1563     if (snapDistance > 0) {
1564         // For snapping, we must ignore all in/outs of the clips of the group being moved
1565         std::vector<int> ignored_pts;
1566         if (m_groups->isInGroup(compoId)) {
1567             int groupId = m_groups->getRootId(compoId);
1568             auto all_items = m_groups->getLeaves(groupId);
1569             for (int current_compoId : all_items) {
1570                 // TODO: fix for composition
1571                 int in = getItemPosition(current_compoId);
1572                 ignored_pts.push_back(in);
1573                 ignored_pts.push_back(in + getItemPlaytime(current_compoId));
1574             }
1575         } else {
1576             int in = currentPos;
1577             int out = in + getCompositionPlaytime(compoId);
1578             ignored_pts.push_back(in);
1579             ignored_pts.push_back(out);
1580         }
1581         int snapped = getBestSnapPos(currentPos, position - currentPos, ignored_pts, cursorPosition, snapDistance);
1582         if (snapped >= 0) {
1583             position = snapped;
1584         }
1585     }
1586     // we check if move is possible
1587     bool possible = requestCompositionMove(compoId, trackId, position, true, false);
1588     if (possible) {
1589         TRACE_RES(position);
1590         return {position, trackId};
1591     }
1592     TRACE_RES(currentPos);
1593     return {currentPos, currentTrack};
1594 }
1595 
requestClipCreation(const QString & binClipId,int & id,PlaylistState::ClipState state,int audioStream,double speed,bool warp_pitch,Fun & undo,Fun & redo)1596 bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, int audioStream, double speed, bool warp_pitch, Fun &undo, Fun &redo)
1597 {
1598     QString bid = binClipId;
1599     if (binClipId.contains(QLatin1Char('/'))) {
1600         bid = binClipId.section(QLatin1Char('/'), 0, 0);
1601     }
1602     if (!pCore->projectItemModel()->hasClip(bid)) {
1603         qWarning() << "master clip not found";
1604         return false;
1605     }
1606     std::shared_ptr<ProjectClip> master = pCore->projectItemModel()->getClipByBinID(bid);
1607     if (!master->statusReady() || !master->isCompatible(state)) {
1608         qWarning() << "clip not ready or not compatible" << state << master->statusReady();
1609         return false;
1610     }
1611     int clipId = TimelineModel::getNextId();
1612     id = clipId;
1613     Fun local_undo = deregisterClip_lambda(clipId);
1614     ClipModel::construct(shared_from_this(), bid, clipId, state, audioStream, speed, warp_pitch);
1615     auto clip = m_allClips[clipId];
1616     Fun local_redo = [clip, this, state, audioStream, speed, warp_pitch]() {
1617         // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is
1618         // sufficient to register it.
1619         registerClip(clip, true);
1620         clip->refreshProducerFromBin(-1, state, audioStream, speed, warp_pitch);
1621         return true;
1622     };
1623 
1624     if (binClipId.contains(QLatin1Char('/'))) {
1625         int in = binClipId.section(QLatin1Char('/'), 1, 1).toInt();
1626         int out = binClipId.section(QLatin1Char('/'), 2, 2).toInt();
1627         int initLength = m_allClips[clipId]->getPlaytime();
1628         bool res = true;
1629         if (in != 0) {
1630             initLength -= in;
1631             res = requestItemResize(clipId, initLength, false, true, local_undo, local_redo);
1632         }
1633         int updatedDuration = out - in + 1;
1634         res = res && requestItemResize(clipId, updatedDuration, true, true, local_undo, local_redo);
1635         if (!res) {
1636             bool undone = local_undo();
1637             Q_ASSERT(undone);
1638             return false;
1639         }
1640     }
1641     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
1642     return true;
1643 }
1644 
requestClipInsertion(const QString & binClipId,int trackId,int position,int & id,bool logUndo,bool refreshView,bool useTargets)1645 bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets)
1646 {
1647     QWriteLocker locker(&m_lock);
1648     TRACE(binClipId, trackId, position, id, logUndo, refreshView, useTargets);
1649     Fun undo = []() { return true; };
1650     Fun redo = []() { return true; };
1651     QVector<int> allowedTracks;
1652     if (useTargets) {
1653         auto it = m_allTracks.cbegin();
1654         while (it != m_allTracks.cend()) {
1655             int target_track = (*it)->getId();
1656             if (getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
1657                 allowedTracks << target_track;
1658             }
1659             ++it;
1660         }
1661     }
1662     if (useTargets && allowedTracks.isEmpty()) {
1663         pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage, 500);
1664         return false;
1665     }
1666     bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, useTargets, undo, redo, allowedTracks);
1667     if (result && logUndo) {
1668         PUSH_UNDO(undo, redo, i18n("Insert Clip"));
1669     }
1670     TRACE_RES(result);
1671     return result;
1672 }
1673 
requestClipInsertion(const QString & binClipId,int trackId,int position,int & id,bool logUndo,bool refreshView,bool useTargets,Fun & undo,Fun & redo,QVector<int> allowedTracks)1674 bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets,
1675                                          Fun &undo, Fun &redo, QVector<int> allowedTracks)
1676 {
1677     Fun local_undo = []() { return true; };
1678     Fun local_redo = []() { return true; };
1679     bool res = false;
1680     ClipType::ProducerType type = ClipType::Unknown;
1681     // binClipId id is in the form: A2/10/50
1682     // A2 means audio only insertion for bin clip with id 2
1683     // 10 is in point
1684     // 50 is out point
1685     QString binIdWithInOut = binClipId;
1686     // bid is the A2 part
1687     QString bid = binClipId.section(QLatin1Char('/'), 0, 0);
1688     // dropType indicates if we want a normal drop (disabled), audio only or video only drop
1689     PlaylistState::ClipState dropType = PlaylistState::Disabled;
1690     if (bid.startsWith(QLatin1Char('A'))) {
1691         dropType = PlaylistState::AudioOnly;
1692         bid.remove(0, 1);
1693         binIdWithInOut.remove(0, 1);
1694     } else if (bid.startsWith(QLatin1Char('V'))) {
1695         dropType = PlaylistState::VideoOnly;
1696         bid.remove(0, 1);
1697         binIdWithInOut.remove(0, 1);
1698     }
1699     if (!pCore->projectItemModel()->hasClip(bid)) {
1700         qWarning() << "no clip found in bin for" << bid;
1701         return false;
1702     }
1703 
1704     bool audioDrop = false;
1705     if (!useTargets) {
1706         audioDrop = getTrackById_const(trackId)->isAudioTrack();
1707         if (audioDrop) {
1708             if (dropType == PlaylistState::VideoOnly) {
1709                 return false;
1710             }
1711         } else if (dropType == PlaylistState::AudioOnly) {
1712             return false;
1713         }
1714     }
1715 
1716     std::shared_ptr<ProjectClip> master = pCore->projectItemModel()->getClipByBinID(bid);
1717     type = master->clipType();
1718     if (useTargets && m_audioTarget.isEmpty() && m_videoTarget == -1) {
1719         useTargets = false;
1720     }
1721     if ((dropType == PlaylistState::Disabled || dropType == PlaylistState::AudioOnly) && (type == ClipType::AV || type == ClipType::Playlist)) {
1722         bool useAudioTarget = false;
1723         if (useTargets && !m_audioTarget.isEmpty() && m_videoTarget == -1) {
1724             // If audio target is set but no video target, only insert audio
1725             useAudioTarget = true;
1726         } else if (useTargets && (getTrackById_const(trackId)->isLocked() || !allowedTracks.contains(trackId))) {
1727             // Video target set but locked
1728             useAudioTarget = true;
1729         }
1730         if (useAudioTarget) {
1731             // Find first possible audio target
1732             QList <int> audioTargetTracks = m_audioTarget.keys();
1733             trackId = -1;
1734             for (int tid : qAsConst(audioTargetTracks)) {
1735                 if (tid > -1 && !getTrackById_const(tid)->isLocked() && allowedTracks.contains(tid)) {
1736                     trackId = tid;
1737                     break;
1738                 }
1739             }
1740         }
1741         if (trackId == -1) {
1742             if (!allowedTracks.isEmpty()) {
1743                 // No active tracks, aborting
1744                 return true;
1745             }
1746             pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage);
1747             return false;
1748         }
1749         int audioStream = -1;
1750         QList <int> keys = m_binAudioTargets.keys();
1751         if (!useTargets) {
1752             // Drag and drop, calculate target tracks
1753             if (audioDrop) {
1754                 if (keys.count() > 1) {
1755                     // Dropping a clip with several audio streams
1756                     int tracksBelow = getLowerTracksId(trackId, TrackType::AudioTrack).count();
1757                     if (tracksBelow < keys.count() - 1) {
1758                         // We don't have enough audio tracks below, check above
1759                         QList <int> audioTrackIds = getTracksIds(true);
1760                         if (audioTrackIds.count() < keys.count()) {
1761                             // Not enough audio tracks
1762                             pCore->displayMessage(i18n("Not enough audio tracks for all streams (%1)", keys.count()), ErrorMessage);
1763                             return false;
1764                         }
1765                         trackId = audioTrackIds.at(audioTrackIds.count() - keys.count());
1766                     }
1767                 }
1768                 if (keys.isEmpty()) {
1769                     pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage);
1770                     return false;
1771                 }
1772                 audioStream = keys.first();
1773             } else {
1774                 // Dropping video, ensure we have enough audio tracks for its streams
1775                 int mirror = getMirrorTrackId(trackId);
1776                 QList <int> audioTids = {};
1777                 if (mirror > -1) {
1778                    audioTids = getLowerTracksId(mirror, TrackType::AudioTrack);
1779                 }
1780                 if (audioTids.count() < keys.count() - 1 || (mirror == -1 && !keys.isEmpty())) {
1781                     // Check if project has enough audio tracks
1782                     if (keys.count() > getTracksIds(true).count()) {
1783                         // Not enough audio tracks in the project
1784                         pCore->displayMessage(i18n("Not enough audio tracks for all streams (%1)", keys.count()), ErrorMessage);
1785                         return false;
1786                     } else if (!useTargets) {
1787                         // Check if all audio tracks are locked. In that case allow inserting video only
1788                         QList<int> audioTracks = getTracksIds(true);
1789                         bool hasUnlockedAudio = false;
1790                         for (int id : qAsConst(audioTracks)) {
1791                             if (!getTrackById_const(id)->isLocked()) {
1792                                 hasUnlockedAudio = true;
1793                                 break;
1794                             }
1795                         }
1796                         if (hasUnlockedAudio) {
1797                             pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage);
1798                             return false;
1799                         } else {
1800                             keys.clear();
1801                         }
1802                     }
1803                 }
1804             }
1805         } else if (audioDrop) {
1806             // Drag & drop, use our first audio target
1807             audioStream = m_audioTarget.first();
1808         } else {
1809             // Using target tracks
1810             if (m_audioTarget.contains(trackId)) {
1811                 audioStream = m_audioTarget.value(trackId);
1812             }
1813         }
1814 
1815         res = requestClipCreation(binIdWithInOut, id, getTrackById_const(trackId)->trackType(), audioStream, 1.0, false, local_undo, local_redo);
1816         res = res && requestClipMove(id, trackId, position, true, refreshView, logUndo, logUndo, local_undo, local_redo);
1817         // Get mirror track
1818         int mirror = dropType == PlaylistState::Disabled ? getMirrorTrackId(trackId) : -1;
1819         if (mirror > -1 && getTrackById_const(mirror)->isLocked() && !useTargets) {
1820             mirror = -1;
1821         }
1822         QList <int> target_track;
1823         if (audioDrop) {
1824             if (m_videoTarget > -1 && !getTrackById_const(m_videoTarget)->isLocked() && dropType != PlaylistState::AudioOnly) {
1825                 target_track << m_videoTarget;
1826             }
1827         } else if (useTargets) {
1828             QList <int> targetIds = m_audioTarget.keys();
1829             targetIds.removeAll(trackId);
1830             for (int &ix : targetIds) {
1831                 if (!getTrackById_const(ix)->isLocked() && allowedTracks.contains(ix)) {
1832                     target_track << ix;
1833                 }
1834             }
1835         }
1836 
1837         bool canMirrorDrop = !useTargets && ((mirror > -1 && (audioDrop || !keys.isEmpty())) || keys.count() > 1);
1838         QMap<int, int> dropTargets;
1839         if (res && (canMirrorDrop || !target_track.isEmpty()) && master->hasAudioAndVideo()) {
1840             int streamsCount = 0;
1841             if (!useTargets) {
1842                 target_track.clear();
1843                 QList <int> audioTids;
1844                 if (!audioDrop) {
1845                     // insert audio mirror track
1846                     if (mirror > -1) {
1847                         target_track << mirror;
1848                         audioTids = getLowerTracksId(mirror, TrackType::AudioTrack);
1849                     }
1850                 } else {
1851                     audioTids = getLowerTracksId(trackId, TrackType::AudioTrack);
1852                 }
1853                 // First audio stream already inserted in target_track or in timeline
1854                 streamsCount = m_binAudioTargets.count() - 1;
1855                 while (streamsCount > 0 && !audioTids.isEmpty()) {
1856                     target_track << audioTids.takeFirst();
1857                     streamsCount--;
1858                 }
1859                 QList <int> aTargets = m_binAudioTargets.keys();
1860                 if (audioDrop) {
1861                     aTargets.removeAll(audioStream);
1862                 }
1863                 std::sort(aTargets.begin(), aTargets.end());
1864                 for (int i = 0; i < target_track.count() && i < aTargets.count() ; ++i) {
1865                     dropTargets.insert(target_track.at(i), aTargets.at(i));
1866                 }
1867                 if (audioDrop && mirror > -1) {
1868                     target_track << mirror;
1869                 }
1870             }
1871             if (target_track.isEmpty() && useTargets) {
1872                 // No available track for splitting, abort
1873                 pCore->displayMessage(i18n("No available track for split operation"), ErrorMessage);
1874                 res = false;
1875             }
1876             if (!res) {
1877                 bool undone = local_undo();
1878                 Q_ASSERT(undone);
1879                 id = -1;
1880                 return false;
1881             }
1882             // Process all mirror insertions
1883             std::function<bool(void)> audio_undo = []() { return true; };
1884             std::function<bool(void)> audio_redo = []() { return true; };
1885             std::unordered_set<int> createdMirrors = {id};
1886             int mirrorAudioStream = -1;
1887             for (int &target_ix : target_track) {
1888                 bool currentDropIsAudio = !audioDrop;
1889                 if (!useTargets && m_binAudioTargets.count() > 1 && dropTargets.contains(target_ix)) {
1890                     // Audio clip dropped first but has other streams
1891                     currentDropIsAudio = true;
1892                     mirrorAudioStream = dropTargets.value(target_ix);
1893                     if (mirrorAudioStream == audioStream) {
1894                         continue;
1895                     }
1896                 }
1897                 else if (currentDropIsAudio) {
1898                     if (!useTargets) {
1899                         mirrorAudioStream = dropTargets.value(target_ix);
1900                     } else {
1901                         mirrorAudioStream = m_audioTarget.value(target_ix);
1902                     }
1903                 }
1904                 int newId;
1905                 res = requestClipCreation(binIdWithInOut, newId, currentDropIsAudio ? PlaylistState::AudioOnly : PlaylistState::VideoOnly, currentDropIsAudio ? mirrorAudioStream : -1, 1.0, false, audio_undo, audio_redo);
1906                 if (res) {
1907                     res = requestClipMove(newId, target_ix, position, true, true, true, true, audio_undo, audio_redo);
1908                     // use lazy evaluation to group only if move was successful
1909                     if (!res) {
1910                         pCore->displayMessage(i18n("Audio split failed: no viable track"), ErrorMessage);
1911                         bool undone = audio_undo();
1912                         Q_ASSERT(undone);
1913                         break;
1914                     } else {
1915                         createdMirrors.insert(newId);
1916                     }
1917                 } else {
1918                     pCore->displayMessage(i18n("Audio split failed: impossible to create audio clip"), ErrorMessage);
1919                     bool undone = audio_undo();
1920                     Q_ASSERT(undone);
1921                     break;
1922                 }
1923             }
1924             if (res) {
1925                 requestClipsGroup(createdMirrors, audio_undo, audio_redo, GroupType::AVSplit);
1926                 UPDATE_UNDO_REDO(audio_redo, audio_undo, local_undo, local_redo);
1927             }
1928         }
1929     } else {
1930         std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(bid);
1931         if (dropType == PlaylistState::Disabled) {
1932             dropType = getTrackById_const(trackId)->trackType();
1933         } else if (dropType != getTrackById_const(trackId)->trackType()) {
1934             return false;
1935         }
1936         QString normalisedBinId = binClipId;
1937         if (normalisedBinId.startsWith(QLatin1Char('A')) || normalisedBinId.startsWith(QLatin1Char('V'))) {
1938             normalisedBinId.remove(0, 1);
1939         }
1940         res = requestClipCreation(normalisedBinId, id, dropType, binClip->getProducerIntProperty(QStringLiteral("audio_index")), 1.0, false, local_undo, local_redo);
1941         res = res && requestClipMove(id, trackId, position, true, refreshView, logUndo, logUndo, local_undo, local_redo);
1942     }
1943     if (!res) {
1944         bool undone = local_undo();
1945         Q_ASSERT(undone);
1946         id = -1;
1947         return false;
1948     }
1949     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
1950     return true;
1951 }
1952 
requestItemDeletion(int itemId,Fun & undo,Fun & redo,bool logUndo)1953 bool TimelineModel::requestItemDeletion(int itemId, Fun &undo, Fun &redo, bool logUndo)
1954 {
1955     Q_UNUSED(logUndo)
1956     QWriteLocker locker(&m_lock);
1957     if (m_groups->isInGroup(itemId)) {
1958         return requestGroupDeletion(itemId, undo, redo);
1959     }
1960     if (isClip(itemId)) {
1961         return requestClipDeletion(itemId, undo, redo);
1962     }
1963     if (isComposition(itemId)) {
1964         return requestCompositionDeletion(itemId, undo, redo);
1965     }
1966     if (isSubTitle(itemId)) {
1967         return requestSubtitleDeletion(itemId, undo, redo, true, true);
1968     }
1969     Q_ASSERT(false);
1970     return false;
1971 }
1972 
requestItemDeletion(int itemId,bool logUndo)1973 bool TimelineModel::requestItemDeletion(int itemId, bool logUndo)
1974 {
1975     QWriteLocker locker(&m_lock);
1976     TRACE(itemId, logUndo);
1977     Q_ASSERT(isItem(itemId));
1978     QString actionLabel;
1979     if (m_groups->isInGroup(itemId)) {
1980         actionLabel = i18n("Remove group");
1981     } else {
1982         if (isClip(itemId)) {
1983             actionLabel = i18n("Delete Clip");
1984         } else if (isComposition(itemId)) {
1985             actionLabel = i18n("Delete Composition");
1986         } else if (isSubTitle(itemId)) {
1987             actionLabel = i18n("Delete Subtitle");
1988         }
1989     }
1990     Fun undo = []() { return true; };
1991     Fun redo = []() { return true; };
1992     bool res = requestItemDeletion(itemId, undo, redo, logUndo);
1993     if (res && logUndo) {
1994         PUSH_UNDO(undo, redo, actionLabel);
1995     }
1996     TRACE_RES(res);
1997     return res;
1998 }
1999 
requestClipDeletion(int clipId,Fun & undo,Fun & redo)2000 bool TimelineModel::requestClipDeletion(int clipId, Fun &undo, Fun &redo)
2001 {
2002     int trackId = getClipTrackId(clipId);
2003     if (trackId != -1) {
2004         bool res = true;
2005         if (getTrackById_const(trackId)->hasStartMix(clipId)) {
2006             MixInfo mixData = getTrackById_const(trackId)->getMixInfo(clipId).first;
2007             if (isClip(mixData.firstClipId)) {
2008                 res = getTrackById(trackId)->requestRemoveMix({mixData.firstClipId, clipId}, undo, redo);
2009             }
2010         }
2011         if (getTrackById_const(trackId)->hasEndMix(clipId)) {
2012             MixInfo mixData = getTrackById_const(trackId)->getMixInfo(clipId).second;
2013             if (isClip(mixData.secondClipId)) {
2014                 res = getTrackById(trackId)->requestRemoveMix({clipId, mixData.secondClipId}, undo, redo);
2015             }
2016         }
2017         res = res && getTrackById(trackId)->requestClipDeletion(clipId, true, !m_closing, undo, redo, false, true);
2018         if (!res) {
2019             undo();
2020             return false;
2021         }
2022     }
2023     auto operation = deregisterClip_lambda(clipId);
2024     auto clip = m_allClips[clipId];
2025     Fun reverse = [this, clip]() {
2026         // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is
2027         // sufficient to register it.
2028         registerClip(clip, true);
2029         return true;
2030     };
2031     if (operation()) {
2032         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
2033         return true;
2034     }
2035     undo();
2036     return false;
2037 }
2038 
requestSubtitleDeletion(int clipId,Fun & undo,Fun & redo,bool first,bool last)2039 bool TimelineModel::requestSubtitleDeletion(int clipId, Fun &undo, Fun &redo, bool first, bool last)
2040 {
2041     GenTime startTime = m_allSubtitles.at(clipId);
2042     SubtitledTime sub = m_subtitleModel->getSubtitle(startTime);
2043     Fun operation = [this, clipId, last] () {
2044         return m_subtitleModel->removeSubtitle(clipId, false, last);
2045     };
2046     GenTime start = sub.start();
2047     GenTime end = sub.end();
2048     QString text = sub.subtitle();
2049     Fun reverse = [this, clipId, start, end, text, first]() {
2050         return m_subtitleModel->addSubtitle(clipId, start, end, text, false, first);
2051     };
2052     if (operation()) {
2053         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
2054         return true;
2055     }
2056     return false;
2057 }
2058 
requestCompositionDeletion(int compositionId,Fun & undo,Fun & redo)2059 bool TimelineModel::requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo)
2060 {
2061     int trackId = getCompositionTrackId(compositionId);
2062     if (trackId != -1) {
2063         bool res = getTrackById(trackId)->requestCompositionDeletion(compositionId, true, true, undo, redo, true);
2064         if (!res) {
2065             undo();
2066             return false;
2067         } else {
2068             Fun unplant_op = [this, compositionId]() {
2069                 unplantComposition(compositionId);
2070                 return true;
2071             };
2072             unplant_op();
2073             PUSH_LAMBDA(unplant_op, redo);
2074         }
2075     }
2076     Fun operation = deregisterComposition_lambda(compositionId);
2077     auto composition = m_allCompositions[compositionId];
2078     int new_in = composition->getPosition();
2079     int new_out = new_in + composition->getPlaytime();
2080     Fun reverse = [this, composition, compositionId, trackId, new_in, new_out]() {
2081         // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it
2082         // back it is sufficient to register it.
2083         registerComposition(composition);
2084         composition->setCurrentTrackId(trackId, true);
2085         replantCompositions(compositionId, false);
2086         checkRefresh(new_in, new_out);
2087         return true;
2088     };
2089     if (operation()) {
2090         Fun update_monitor = [this, new_in, new_out]() {
2091             checkRefresh(new_in, new_out);
2092             return true;
2093         };
2094         update_monitor();
2095         PUSH_LAMBDA(update_monitor, operation);
2096         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
2097         return true;
2098     }
2099     undo();
2100     return false;
2101 }
2102 
getItemsInRange(int trackId,int start,int end,bool listCompositions)2103 std::unordered_set<int> TimelineModel::getItemsInRange(int trackId, int start, int end, bool listCompositions)
2104 {
2105     Q_UNUSED(listCompositions)
2106 
2107     std::unordered_set<int> allClips;
2108     if (trackId < 0) {
2109         // Subtitles
2110         if (m_subtitleModel) {
2111             std::unordered_set<int> subs = m_subtitleModel->getItemsInRange(start, end);
2112             allClips.insert(subs.begin(), subs.end());
2113         }
2114     }
2115     if (trackId == -1) {
2116         for (const auto &track : m_allTracks) {
2117             if (track->isLocked()) {
2118                 continue;
2119             }
2120             std::unordered_set<int> clipTracks = getItemsInRange(track->getId(), start, end, listCompositions);
2121             allClips.insert(clipTracks.begin(), clipTracks.end());
2122         }
2123     } else if (trackId >= 0) {
2124         std::unordered_set<int> clipTracks = getTrackById(trackId)->getClipsInRange(start, end);
2125         allClips.insert(clipTracks.begin(), clipTracks.end());
2126         if (listCompositions) {
2127             std::unordered_set<int> compoTracks = getTrackById(trackId)->getCompositionsInRange(start, end);
2128             allClips.insert(compoTracks.begin(), compoTracks.end());
2129         }
2130     }
2131     return allClips;
2132 }
2133 
requestFakeGroupMove(int clipId,int groupId,int delta_track,int delta_pos,bool updateView,bool logUndo)2134 bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo)
2135 {
2136     TRACE(clipId, groupId, delta_track, delta_pos, updateView, logUndo);
2137     std::function<bool(void)> undo = []() { return true; };
2138     std::function<bool(void)> redo = []() { return true; };
2139     bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo);
2140     if (res && logUndo) {
2141         PUSH_UNDO(undo, redo, i18n("Move group"));
2142     }
2143     TRACE_RES(res);
2144     return res;
2145 }
2146 
requestFakeGroupMove(int clipId,int groupId,int delta_track,int delta_pos,bool updateView,bool finalMove,Fun & undo,Fun & redo,bool allowViewRefresh)2147 bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo,
2148                                          bool allowViewRefresh)
2149 {
2150     Q_UNUSED(updateView);
2151     Q_UNUSED(finalMove);
2152     Q_UNUSED(undo);
2153     Q_UNUSED(redo);
2154     Q_UNUSED(allowViewRefresh);
2155     QWriteLocker locker(&m_lock);
2156     Q_ASSERT(m_allGroups.count(groupId) > 0);
2157     bool ok = true;
2158     auto all_items = m_groups->getLeaves(groupId);
2159     Q_ASSERT(all_items.size() > 1);
2160     Fun local_undo = []() { return true; };
2161     Fun local_redo = []() { return true; };
2162 
2163     // 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.
2164     // This way, we ensure that no conflict will arise with clips inside the group being moved
2165 
2166     // Check if there is a track move
2167 
2168     // First, remove clips
2169     bool hasAudio = false;
2170     bool hasVideo = false;
2171     std::unordered_map<int, int> old_track_ids, old_position, old_forced_track;
2172     for (int item : all_items) {
2173         int old_trackId = getItemTrackId(item);
2174         old_track_ids[item] = old_trackId;
2175         if (old_trackId != -1) {
2176             if (isClip(item)) {
2177                 old_position[item] = m_allClips[item]->getPosition();
2178                 if (!hasAudio && getTrackById_const(old_trackId)->isAudioTrack()) {
2179                     hasAudio = true;
2180                 } else if (!hasVideo && !getTrackById_const(old_trackId)->isAudioTrack()) {
2181                     hasVideo = true;
2182                 }
2183             } else {
2184                 hasVideo = true;
2185                 old_position[item] = m_allCompositions[item]->getPosition();
2186                 old_forced_track[item] = m_allCompositions[item]->getForcedTrack();
2187             }
2188         }
2189     }
2190 
2191     // Second step, calculate delta
2192     int audio_delta, video_delta;
2193     audio_delta = video_delta = delta_track;
2194 
2195     if (getTrackById(old_track_ids[clipId])->isAudioTrack()) {
2196         // Master clip is audio, so reverse delta for video clips
2197         if (hasAudio) {
2198             video_delta = -delta_track;
2199         } else {
2200             video_delta = 0;
2201         }
2202     } else {
2203         if (hasVideo) {
2204             audio_delta = -delta_track;
2205         } else {
2206             audio_delta = 0;
2207         }
2208     }
2209     bool trackChanged = false;
2210 
2211     // Reverse sort. We need to insert from left to right to avoid confusing the view
2212     for (int item : all_items) {
2213         int current_track_id = old_track_ids[item];
2214         int current_track_position = getTrackPosition(current_track_id);
2215         int d = getTrackById_const(current_track_id)->isAudioTrack() ? audio_delta : video_delta;
2216         int target_track_position = current_track_position + d;
2217         if (target_track_position >= 0 && target_track_position < getTracksCount()) {
2218             auto it = m_allTracks.cbegin();
2219             std::advance(it, target_track_position);
2220             int target_track = (*it)->getId();
2221             int target_position = old_position[item] + delta_pos;
2222             if (isClip(item)) {
2223                 m_allClips[item]->setFakePosition(target_position);
2224                 if (m_allClips[item]->getFakeTrackId() != target_track) {
2225                     trackChanged = true;
2226                 }
2227                 m_allClips[item]->setFakeTrackId(target_track);
2228             } else {
2229             }
2230         } else {
2231             ok = false;
2232         }
2233         if (!ok) {
2234             bool undone = local_undo();
2235             Q_ASSERT(undone);
2236             return false;
2237         }
2238     }
2239     QModelIndex modelIndex;
2240     QVector<int> roles{FakePositionRole};
2241     if (trackChanged) {
2242         roles << FakeTrackIdRole;
2243     }
2244     for (int item : all_items) {
2245         if (isClip(item)) {
2246             modelIndex = makeClipIndexFromID(item);
2247         } else {
2248             modelIndex = makeCompositionIndexFromID(item);
2249         }
2250         notifyChange(modelIndex, modelIndex, roles);
2251     }
2252     return true;
2253 }
2254 
requestGroupMove(int itemId,int groupId,int delta_track,int delta_pos,bool moveMirrorTracks,bool updateView,bool logUndo,bool revertMove)2255 bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool moveMirrorTracks, bool updateView, bool logUndo, bool revertMove)
2256 {
2257     QWriteLocker locker(&m_lock);
2258     TRACE(itemId, groupId, delta_track, delta_pos, updateView, logUndo);
2259     std::function<bool(void)> undo = []() { return true; };
2260     std::function<bool(void)> redo = []() { return true; };
2261     bool res = requestGroupMove(itemId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo, revertMove, moveMirrorTracks);
2262     if (res && logUndo) {
2263         PUSH_UNDO(undo, redo, i18n("Move group"));
2264     }
2265     TRACE_RES(res);
2266     return res;
2267 }
2268 
requestGroupMove(int itemId,int groupId,int delta_track,int delta_pos,bool updateView,bool finalMove,Fun & undo,Fun & redo,bool revertMove,bool moveMirrorTracks,bool allowViewRefresh,QVector<int> allowedTracks)2269 bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool revertMove, bool moveMirrorTracks,
2270                                      bool allowViewRefresh, QVector<int> allowedTracks)
2271 {
2272     QWriteLocker locker(&m_lock);
2273     Q_ASSERT(m_allGroups.count(groupId) > 0);
2274     Q_ASSERT(isItem(itemId));
2275     if (getGroupElements(groupId).count(itemId) == 0) {
2276         // this group doesn't contain the clip, abort
2277         return false;
2278     }
2279     bool ok = true;
2280     auto all_items = m_groups->getLeaves(groupId);
2281     Q_ASSERT(all_items.size() > 1);
2282     Fun local_undo = []() { return true; };
2283     Fun local_redo = []() { return true; };
2284     std::vector< std::pair<int, int> > sorted_clips;
2285     std::vector<int> sorted_clips_ids;
2286     std::vector< std::pair<int, std::pair<int, int> > > sorted_compositions;
2287     std::vector< std::pair<int, GenTime> > sorted_subtitles;
2288     int lowerTrack = -1;
2289     int upperTrack = -1;
2290     QVector <int> tracksWithMix;
2291 
2292     // Separate clips from compositions to sort and check source tracks
2293     QMap<std::pair<int, int>, int> mixesToDelete;
2294     // Mixes might be deleted while moving clips to another track, so store them before attempting a move
2295     QMap<int, std::pair<MixInfo, MixInfo>> mixDataArray;
2296     for (int affectedItemId : all_items) {
2297         if (delta_track != 0 && !isSubTitle(affectedItemId)) {
2298             // Check if an upper / lower move is possible
2299             const int trackPos = getTrackPosition(getItemTrackId(affectedItemId));
2300             if (lowerTrack == -1 || lowerTrack > trackPos) {
2301                 lowerTrack = trackPos;
2302             }
2303             if (upperTrack == -1 || upperTrack < trackPos) {
2304                 upperTrack = trackPos;
2305             }
2306         }
2307         if (isClip(affectedItemId)) {
2308             sorted_clips.emplace_back(affectedItemId, m_allClips[affectedItemId]->getPosition());
2309             sorted_clips_ids.push_back(affectedItemId);
2310             int current_track_id = getClipTrackId(affectedItemId);
2311             // Check if we have a mix in the group
2312             if (getTrackById_const(current_track_id)->hasMix(affectedItemId)) {
2313                 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(current_track_id)->getMixInfo(affectedItemId);
2314                 mixDataArray.insert(affectedItemId, mixData);
2315                 if (delta_track != 0) {
2316                     if (mixData.first.firstClipId > -1 && all_items.find(mixData.first.firstClipId) == all_items.end()) {
2317                         // First part of the mix is not moving, delete start mix
2318                         mixesToDelete.insert({mixData.first.firstClipId,affectedItemId}, current_track_id);
2319                     }
2320                     if (mixData.second.firstClipId > -1 && all_items.find(mixData.second.secondClipId) == all_items.end()) {
2321                         // First part of the mix is not moving, delete start mix
2322                         mixesToDelete.insert({affectedItemId, mixData.second.secondClipId}, current_track_id);
2323                     }
2324                 } else if (!tracksWithMix.contains(current_track_id)) {
2325                     // There is a mix, prepare for update
2326                     tracksWithMix << current_track_id;
2327                 }
2328             }
2329         } else if (isComposition(affectedItemId)) {
2330             sorted_compositions.push_back({affectedItemId, {m_allCompositions[affectedItemId]->getPosition(), getTrackMltIndex(m_allCompositions[affectedItemId]->getCurrentTrackId())}});
2331         } else if (isSubTitle(affectedItemId)) {
2332             sorted_subtitles.emplace_back(affectedItemId, m_allSubtitles.at(affectedItemId));
2333         }
2334     }
2335 
2336     if (!sorted_subtitles.empty() && m_subtitleModel->isLocked()) {
2337         // Group with a locked subtitle, abort
2338         return false;
2339     }
2340 
2341     // Sort clips first
2342     std::sort(sorted_clips.begin(), sorted_clips.end(), [delta_pos](const std::pair<int, int> &clipId1, const std::pair<int, int> &clipId2) {
2343         return delta_pos > 0 ? clipId2.second < clipId1.second : clipId1.second < clipId2.second;
2344     });
2345 
2346     // Sort subtitles
2347     std::sort(sorted_subtitles.begin(), sorted_subtitles.end(), [delta_pos](const std::pair<int, GenTime> &clipId1, const std::pair<int, GenTime> &clipId2) {
2348         return delta_pos > 0 ? clipId2.second < clipId1.second : clipId1.second < clipId2.second;
2349     });
2350 
2351     // Sort compositions. We need to delete in the move direction from top to bottom
2352     std::sort(sorted_compositions.begin(), sorted_compositions.end(), [delta_track, delta_pos](const std::pair<int, std::pair<int, int > > &clipId1, const std::pair<int, std::pair<int, int > > &clipId2) {
2353         const int p1 = delta_track < 0
2354                      ? clipId1.second.second : delta_track > 0 ? -clipId1.second.second : clipId1.second.first;
2355         const int p2 = delta_track < 0
2356                      ? clipId2.second.second : delta_track > 0 ? -clipId2.second.second : clipId2.second.first;
2357         return delta_track == 0 ? (delta_pos > 0 ? p2 < p1 : p1 < p2) : p1 < p2;
2358     });
2359 
2360     // 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.
2361     // This way, we ensure that no conflict will arise with clips inside the group being moved
2362 
2363     Fun update_model = [this, finalMove]() {
2364         if (finalMove) {
2365             updateDuration();
2366         }
2367         return true;
2368     };
2369     // Move subtitles
2370     if (!sorted_subtitles.empty()) {
2371         std::vector<std::pair<int, GenTime>>::iterator ptr;
2372         auto last = std::prev(sorted_subtitles.end());
2373         for (ptr = sorted_subtitles.begin(); ptr < sorted_subtitles.end(); ptr++) {
2374             requestSubtitleMove((*ptr).first, (*ptr).second.frames(pCore->getCurrentFps()) + delta_pos, updateView, ptr == sorted_subtitles.begin(), ptr == last, finalMove, local_undo, local_redo);
2375         }
2376     }
2377 
2378     // Check if there is a track move
2379     // Second step, reinsert clips at correct positions
2380     int audio_delta, video_delta;
2381     audio_delta = video_delta = delta_track;
2382     bool masterIsAudio = delta_track != 0 ? getTrackById_const(getItemTrackId(itemId))->isAudioTrack() : false;
2383     if (delta_track < 0) {
2384         if (!masterIsAudio) {
2385             // Case 1, dragging a video clip down
2386             bool lowerTrackIsAudio = getTrackById_const(getTrackIndexFromPosition(lowerTrack))->isAudioTrack();
2387             int lowerPos = lowerTrackIsAudio ? lowerTrack - delta_track : lowerTrack + delta_track;
2388             if (lowerPos < 0) {
2389                 // No space below
2390                 delta_track = 0;
2391             } else if (!lowerTrackIsAudio) {
2392                 // Moving a group of video clips
2393                 if (getTrackById_const(getTrackIndexFromPosition(lowerPos))->isAudioTrack()) {
2394                     // Moving to a non matching track (video on audio track)
2395                     delta_track = 0;
2396                 }
2397             }
2398         } else if (lowerTrack + delta_track < 0) {
2399             // Case 2, dragging an audio clip down
2400             delta_track = 0;
2401         }
2402     } else if (delta_track > 0) {
2403         if (!masterIsAudio) {
2404             // Case 1, dragging a video clip up
2405             int upperPos = upperTrack + delta_track;
2406             if (upperPos >= getTracksCount()) {
2407                 // Moving above top track, not allowed
2408                 delta_track = 0;
2409             } else if (getTrackById_const(getTrackIndexFromPosition(upperPos))->isAudioTrack()) {
2410                 // Trying to move to a non matching track (video clip on audio track)
2411                 delta_track = 0;
2412             }
2413         } else {
2414             bool upperTrackIsAudio = getTrackById_const(getTrackIndexFromPosition(upperTrack))->isAudioTrack();
2415             if (!upperTrackIsAudio) {
2416                 // Dragging an audio clip up, check that upper video clip has an available video track
2417                 int targetPos = upperTrack - delta_track;
2418                 if (moveMirrorTracks && (targetPos <0 || getTrackById_const(getTrackIndexFromPosition(targetPos))->isAudioTrack())) {
2419                     delta_track = 0;
2420                 }
2421             } else {
2422                 int targetPos = upperTrack + delta_track;
2423                 if (targetPos >= getTracksCount() || !getTrackById_const(getTrackIndexFromPosition(targetPos))->isAudioTrack()) {
2424                     // Trying to drag audio above topmost track or on video track
2425                     delta_track = 0;
2426                 }
2427             }
2428         }
2429     }
2430     if (delta_track == 0 && updateView) {
2431         updateView = false;
2432         allowViewRefresh = false;
2433         update_model = [sorted_clips, sorted_compositions, finalMove, this]() {
2434             QModelIndex modelIndex;
2435             QVector<int> roles{StartRole};
2436             for (const std::pair<int, int> &item : sorted_clips) {
2437                 modelIndex = makeClipIndexFromID(item.first);
2438                 notifyChange(modelIndex, modelIndex, roles);
2439             }
2440             for (const std::pair<int, std::pair<int, int>> &item : sorted_compositions) {
2441                 modelIndex = makeCompositionIndexFromID(item.first);
2442                 notifyChange(modelIndex, modelIndex, roles);
2443             }
2444             if (finalMove) {
2445                 updateDuration();
2446             }
2447             return true;
2448         };
2449     }
2450 
2451     std::unordered_map<int, int> old_track_ids, old_position, old_forced_track;
2452     QMap<int, int> oldTrackIds;
2453     // Check for mixes
2454     for (const std::pair<int, int> &item : sorted_clips) {
2455         // Keep track of old track for mixes
2456         oldTrackIds.insert(item.first, getClipTrackId(item.first));
2457     }
2458     // First delete mixes that have to
2459     if (finalMove && !mixesToDelete.isEmpty()) {
2460         QMapIterator<std::pair<int, int>, int> i(mixesToDelete);
2461         while (i.hasNext()) {
2462             i.next();
2463             // Delete mix
2464             getTrackById(i.value())->requestRemoveMix(i.key(), local_undo, local_redo);
2465         }
2466     }
2467 
2468     // First, remove clips
2469     if (delta_track != 0) {
2470         // We delete our clips only if changing track
2471         for (const std::pair<int, int> &item : sorted_clips) {
2472             int old_trackId = getClipTrackId(item.first);
2473             old_track_ids[item.first] = old_trackId;
2474             if (old_trackId != -1) {
2475                 bool updateThisView = allowViewRefresh;
2476                 ok = ok && getTrackById(old_trackId)->requestClipDeletion(item.first, updateThisView, finalMove, local_undo, local_redo, true, false);
2477                 old_position[item.first] = item.second;
2478                 if (!ok) {
2479                     bool undone = local_undo();
2480                     Q_ASSERT(undone);
2481                     return false;
2482                 }
2483             }
2484         }
2485         for (const std::pair<int, std::pair<int, int>> &item : sorted_compositions) {
2486             int old_trackId = getCompositionTrackId(item.first);
2487             if (old_trackId != -1) {
2488                 old_track_ids[item.first] = old_trackId;
2489                 old_position[item.first] = item.second.first;
2490                 old_forced_track[item.first] = m_allCompositions[item.first]->getForcedTrack();
2491             }
2492         }
2493         if (masterIsAudio) {
2494             // Master clip is audio, so reverse delta for video clips
2495             video_delta = -delta_track;
2496         } else {
2497             audio_delta = -delta_track;
2498         }
2499     }
2500 
2501     Fun sync_mix = [this, tracksWithMix, finalMove]() {
2502         if (!finalMove) {
2503             return true;
2504         }
2505         for (int tid : tracksWithMix) {
2506             getTrackById_const(tid)->syncronizeMixes(finalMove);
2507         }
2508         return true;
2509     };
2510     // We need to insert depending on the move direction to avoid confusing the view
2511     // std::reverse(std::begin(sorted_clips), std::end(sorted_clips));
2512     bool updateThisView = allowViewRefresh;
2513     if (delta_track == 0) {
2514         // Special case, we are moving on same track, avoid too many calculations
2515         // First pass, check for collisions and suggest better delta
2516         QVector <int> processedTracks;
2517         for (const std::pair<int, int> &item : sorted_clips) {
2518             int current_track_id = getClipTrackId(item.first);
2519             if (processedTracks.contains(current_track_id)) {
2520                 // We only check the first clip for each track since they are sorted depending on the move direction
2521                 continue;
2522             }
2523             processedTracks << current_track_id;
2524             if (!allowedTracks.isEmpty() && !allowedTracks.contains(current_track_id)) {
2525                 continue;
2526             }
2527             int current_in = item.second;
2528             int playtime = getClipPlaytime(item.first);
2529             int target_position = current_in + delta_pos;
2530             int subPlaylist = -1;
2531             if (delta_pos < 0) {
2532                 if (getTrackById_const(current_track_id)->hasStartMix(item.first)) {
2533                     subPlaylist = m_allClips[item.first]->getSubPlaylistIndex();
2534                 }
2535                 if (!getTrackById_const(current_track_id)->isAvailable(target_position, qMin(qAbs(delta_pos), playtime), subPlaylist)) {
2536                     if (!getTrackById_const(current_track_id)->isBlankAt(current_in - 1)) {
2537                         // No move possible, abort
2538                         bool undone = local_undo();
2539                         Q_ASSERT(undone);
2540                         return false;
2541                     }
2542                     int newStart = getTrackById_const(current_track_id)->getBlankStart(current_in - 1, subPlaylist);
2543                     delta_pos = qMax(delta_pos, newStart - current_in);
2544                 }
2545             } else {
2546                 int moveEnd = target_position + playtime;
2547                 int moveStart = qMax(current_in + playtime, target_position);
2548                 if (getTrackById_const(current_track_id)->hasEndMix(item.first)) {
2549                     subPlaylist = m_allClips[item.first]->getSubPlaylistIndex();
2550                 }
2551                 if (!getTrackById_const(current_track_id)->isAvailable(moveStart, moveEnd - moveStart, subPlaylist)) {
2552                     int newStart = getTrackById_const(current_track_id)->getBlankEnd(current_in + playtime, subPlaylist);
2553                     if (newStart == current_in + playtime) {
2554                         // No move possible, abort
2555                         bool undone = local_undo();
2556                         Q_ASSERT(undone);
2557                         return false;
2558                     }
2559                     delta_pos = qMin(delta_pos, newStart - (current_in + playtime));
2560                 }
2561             }
2562         }
2563         PUSH_LAMBDA(sync_mix, local_undo);
2564         for (const std::pair<int, int> &item : sorted_clips) {
2565             int current_track_id = getClipTrackId(item.first);
2566             if (!allowedTracks.isEmpty() && !allowedTracks.contains(current_track_id)) {
2567                 continue;
2568             }
2569             int current_in = item.second;
2570             int target_position = current_in + delta_pos;
2571             ok = requestClipMove(item.first, current_track_id, target_position, moveMirrorTracks, updateThisView, finalMove, finalMove, local_undo, local_redo, revertMove, true, oldTrackIds, mixDataArray.contains(item.first) ? mixDataArray.value(item.first) : std::pair<MixInfo,MixInfo>());
2572             if (!ok) {
2573                 qWarning() << "failed moving clip on track " << current_track_id;
2574                 break;
2575             }
2576         }
2577         if (ok) {
2578             sync_mix();
2579             PUSH_LAMBDA(sync_mix, local_redo);
2580 
2581             for (const std::pair<int, std::pair<int, int>> &item : sorted_compositions) {
2582                 int current_track_id = getItemTrackId(item.first);
2583                 if (!allowedTracks.isEmpty() && !allowedTracks.contains(current_track_id)) {
2584                     continue;
2585                 }
2586                 int current_in = item.second.first;
2587                 int target_position = current_in + delta_pos;
2588                 ok = requestCompositionMove(item.first, current_track_id, m_allCompositions[item.first]->getForcedTrack(), target_position, updateThisView, finalMove, local_undo, local_redo);
2589                 if (!ok) {
2590                     break;
2591                 }
2592             }
2593         }
2594         if (!ok) {
2595             bool undone = local_undo();
2596             Q_ASSERT(undone);
2597             return false;
2598         }
2599     } else {
2600         // Track changed
2601         PUSH_LAMBDA(sync_mix, local_undo);
2602         for (const std::pair<int, int> &item : sorted_clips) {
2603             int current_track_id = old_track_ids[item.first];
2604             int current_track_position = getTrackPosition(current_track_id);
2605             int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta;
2606             if (!moveMirrorTracks && item.first != itemId) {
2607                 d = 0;
2608             }
2609             int target_track_position = current_track_position + d;
2610             if (target_track_position >= 0 && target_track_position < getTracksCount()) {
2611                 auto it = m_allTracks.cbegin();
2612                 std::advance(it, target_track_position);
2613                 int target_track = (*it)->getId();
2614                 int target_position = old_position[item.first] + delta_pos;
2615                 ok = ok && requestClipMove(item.first, target_track, target_position, moveMirrorTracks, updateThisView, finalMove, finalMove, local_undo, local_redo, revertMove, true, oldTrackIds, mixDataArray.contains(item.first) ? mixDataArray.value(item.first) : std::pair<MixInfo,MixInfo>());
2616             } else {
2617                 ok = false;
2618             }
2619             if (!ok) {
2620                 bool undone = local_undo();
2621                 Q_ASSERT(undone);
2622                 return false;
2623             }
2624         }
2625         sync_mix();
2626         PUSH_LAMBDA(sync_mix, local_redo);
2627         for (const std::pair<int, std::pair<int, int> > &item : sorted_compositions) {
2628             int current_track_id = old_track_ids[item.first];
2629             int current_track_position = getTrackPosition(current_track_id);
2630             int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta;
2631             int target_track_position = current_track_position + d;
2632 
2633             if (target_track_position >= 0 && target_track_position < getTracksCount()) {
2634                 auto it = m_allTracks.cbegin();
2635                 std::advance(it, target_track_position);
2636                 int target_track = (*it)->getId();
2637                 int target_position = old_position[item.first] + delta_pos;
2638                 ok = ok &&
2639                      requestCompositionMove(item.first, target_track, old_forced_track[item.first], target_position, updateThisView, finalMove, local_undo, local_redo);
2640             } else {
2641                 qWarning() << "aborting move tried on track" << target_track_position;
2642                 ok = false;
2643             }
2644             if (!ok) {
2645                 bool undone = local_undo();
2646                 Q_ASSERT(undone);
2647                 return false;
2648             }
2649         }
2650     }
2651     update_model();
2652     PUSH_LAMBDA(update_model, local_redo);
2653     PUSH_LAMBDA(update_model, local_undo);
2654     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
2655     return true;
2656 }
2657 
requestGroupDeletion(int clipId,bool logUndo)2658 bool TimelineModel::requestGroupDeletion(int clipId, bool logUndo)
2659 {
2660     QWriteLocker locker(&m_lock);
2661     TRACE(clipId, logUndo);
2662     if (!m_groups->isInGroup(clipId)) {
2663         TRACE_RES(false);
2664         return false;
2665     }
2666     bool res = requestItemDeletion(clipId, logUndo);
2667     TRACE_RES(res);
2668     return res;
2669 }
2670 
requestGroupDeletion(int clipId,Fun & undo,Fun & redo)2671 bool TimelineModel::requestGroupDeletion(int clipId, Fun &undo, Fun &redo)
2672 {
2673     // we do a breadth first exploration of the group tree, ungroup (delete) every inner node, and then delete all the leaves.
2674     std::queue<int> group_queue;
2675     group_queue.push(m_groups->getRootId(clipId));
2676     std::unordered_set<int> all_items;
2677     std::unordered_set<int> all_compositions;
2678     // Subtitles MUST BE SORTED BY REVERSE timeline id to preserve the qml model index on undo!!
2679     std::set<int> all_subtitles;
2680     while (!group_queue.empty()) {
2681         int current_group = group_queue.front();
2682         bool isSelection = m_currentSelection == current_group;
2683 
2684         group_queue.pop();
2685         Q_ASSERT(isGroup(current_group));
2686         auto children = m_groups->getDirectChildren(current_group);
2687         int one_child = -1; // we need the id on any of the indices of the elements of the group
2688         for (int c : children) {
2689             if (isClip(c)) {
2690                 all_items.insert(c);
2691                 one_child = c;
2692             } else if (isComposition(c)) {
2693                 all_compositions.insert(c);
2694                 one_child = c;
2695             } else if (isSubTitle(c)) {
2696                 all_subtitles.insert(c);
2697                 one_child = c;
2698             } else {
2699                 Q_ASSERT(isGroup(c));
2700                 one_child = c;
2701                 group_queue.push(c);
2702             }
2703         }
2704         if (one_child != -1) {
2705             if (m_groups->getType(current_group) == GroupType::Selection) {
2706                 Q_ASSERT(isSelection);
2707                 // in the case of a selection group, we delete the group but don't log it in the undo object
2708                 Fun tmp_undo = []() { return true; };
2709                 Fun tmp_redo = []() { return true; };
2710                 m_groups->ungroupItem(one_child, tmp_undo, tmp_redo);
2711             } else {
2712                 bool res = m_groups->ungroupItem(one_child, undo, redo);
2713                 if (!res) {
2714                     undo();
2715                     return false;
2716                 }
2717             }
2718         }
2719         if (isSelection) {
2720             requestClearSelection(true);
2721         }
2722     }
2723     for (int clip : all_items) {
2724         bool res = requestClipDeletion(clip, undo, redo);
2725         if (!res) {
2726             // Undo is processed in requestClipDeletion
2727             return false;
2728         }
2729     }
2730     for (int compo : all_compositions) {
2731         bool res = requestCompositionDeletion(compo, undo, redo);
2732         if (!res) {
2733             undo();
2734             return false;
2735         }
2736     }
2737     std::set<int>::reverse_iterator rit;
2738     for (rit = all_subtitles.rbegin(); rit != all_subtitles.rend(); ++rit) {
2739         bool res = requestSubtitleDeletion(*rit, undo, redo, rit == all_subtitles.rbegin(), rit == std::prev(all_subtitles.rend()));
2740         if (!res) {
2741             undo();
2742             return false;
2743         }
2744     }
2745     return true;
2746 }
2747 
getGroupData(int itemId)2748 const QVariantList TimelineModel::getGroupData(int itemId)
2749 {
2750     QWriteLocker locker(&m_lock);
2751     if (!m_groups->isInGroup(itemId)) {
2752         return {itemId, getItemPosition(itemId), getItemPlaytime(itemId)};
2753     }
2754     int groupId = m_groups->getRootId(itemId);
2755     QVariantList result;
2756     std::unordered_set<int> items = m_groups->getLeaves(groupId);
2757     for (int id : items) {
2758         result << id << getItemPosition(id) << getItemPlaytime(id);
2759     }
2760     return result;
2761 }
2762 
processGroupResize(QVariantList startPos,QVariantList endPos,bool right)2763 void TimelineModel::processGroupResize(QVariantList startPos, QVariantList endPos, bool right)
2764 {
2765     Q_ASSERT(startPos.size() == endPos.size());
2766     QMap<int, QPair<int, int>> startData;
2767     QMap<int, QPair<int, int>> endData;
2768     while (!startPos.isEmpty()) {
2769         int id = startPos.takeFirst().toInt();
2770         int in = startPos.takeFirst().toInt();
2771         int duration = startPos.takeFirst().toInt();
2772         startData.insert(id, {in, duration});
2773         id = endPos.takeFirst().toInt();
2774         in = endPos.takeFirst().toInt();
2775         duration = endPos.takeFirst().toInt();
2776         endData.insert(id, {in, duration});
2777     }
2778     QMapIterator<int, QPair<int, int>> i(startData);
2779     QList<int> changedItems;
2780     Fun undo = []() { return true; };
2781     Fun redo = []() { return true; };
2782     bool result = true;
2783     QVector <int> mixTracks;
2784     QList <int> ids = startData.keys();
2785     for (auto &id : ids) {
2786         if (isClip(id)) {
2787             int tid = m_allClips[id]->getCurrentTrackId();
2788             if (tid > -1 && getTrackById(tid)->hasMix(id)) {
2789                 if (!mixTracks.contains(tid)) {
2790                     mixTracks << tid;
2791                 }
2792                 if (right) {
2793                     if (getTrackById_const(tid)->hasEndMix(id)) {
2794                         std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(id);
2795                         QPair<int, int> endPos = endData.value(id);
2796                         if (endPos.first + endPos.second <= mixData.second.secondClipInOut.first) {
2797                             Fun sync_mix_undo = [this, tid, mixData]() {
2798                                 getTrackById_const(tid)->createMix(mixData.second, getTrackById_const(tid)->isAudioTrack());
2799                                 return true;
2800                             };
2801                             PUSH_LAMBDA(sync_mix_undo, undo);
2802                         }
2803                     }
2804                 } else if (getTrackById_const(tid)->hasStartMix(id)) {
2805                     std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(id);
2806                     QPair<int, int> endPos = endData.value(id);
2807                     if (endPos.first >= mixData.first.firstClipInOut.second) {
2808                         Fun sync_mix_undo = [this, tid, mixData]() {
2809                             getTrackById_const(tid)->createMix(mixData.first, getTrackById_const(tid)->isAudioTrack());
2810                             return true;
2811                         };
2812                         PUSH_LAMBDA(sync_mix_undo, undo);
2813                     }
2814                 }
2815             }
2816         }
2817     }
2818     Fun sync_mix = []() { return true; };
2819     if (!mixTracks.isEmpty()) {
2820         sync_mix = [this, mixTracks]() {
2821             for (auto &tid : mixTracks) {
2822                 getTrackById_const(tid)->syncronizeMixes(true);
2823             }
2824             return true;
2825         };
2826         PUSH_LAMBDA(sync_mix, undo);
2827     }
2828     while (i.hasNext()) {
2829         i.next();
2830         QPair<int, int> startItemPos = i.value();
2831         QPair<int, int> endItemPos = endData.value(i.key());
2832         if (startItemPos.first != endItemPos.first || startItemPos.second != endItemPos.second) {
2833             // Revert individual items to original position
2834             requestItemResize(i.key(), startItemPos.second, right, false, 0, true);
2835             changedItems << i.key();
2836         }
2837     }
2838     for (int id : qAsConst(changedItems)) {
2839         QPair<int, int> endItemPos = endData.value(id);
2840         int duration = endItemPos.second;
2841         result = result & requestItemResize(id, duration, right, true, undo, redo, false);
2842         if (!result) {
2843             break;
2844         }
2845     }
2846     if (result) {
2847         sync_mix();
2848         PUSH_LAMBDA(sync_mix, redo);
2849         PUSH_UNDO(undo, redo, i18n("Resize group"));
2850     } else {
2851         undo();
2852     }
2853 }
2854 
getBoundaries(int itemId)2855 const std::vector<int> TimelineModel::getBoundaries(int itemId)
2856 {
2857     std::vector<int> boundaries;
2858     std::unordered_set<int> items;
2859     if (m_groups->isInGroup(itemId)) {
2860         int groupId = m_groups->getRootId(itemId);
2861         items = m_groups->getLeaves(groupId);
2862     } else {
2863         items.insert(itemId);
2864     }
2865     for (int id : items) {
2866         if (isItem(id)) {
2867             int pos = getItemPosition(id);
2868             boundaries.push_back(pos);
2869             pos += getItemPlaytime(id);
2870             boundaries.push_back(pos);
2871         }
2872     }
2873     return boundaries;
2874 }
2875 
requestClipResizeAndTimeWarp(int itemId,int size,bool right,int snapDistance,bool allowSingleResize,double speed)2876 int TimelineModel::requestClipResizeAndTimeWarp(int itemId, int size, bool right, int snapDistance, bool allowSingleResize, double speed)
2877 {
2878     Q_UNUSED(snapDistance)
2879     QWriteLocker locker(&m_lock);
2880     TRACE(itemId, size, right, true, snapDistance, allowSingleResize);
2881     Q_ASSERT(isClip(itemId));
2882     if (size <= 0) {
2883         TRACE_RES(-1);
2884         return -1;
2885     }
2886     int in = getItemPosition(itemId);
2887     int out = in + getItemPlaytime(itemId);
2888     //size = requestItemResizeInfo(itemId, in, out, size, right, snapDistance);
2889     Fun undo = []() { return true; };
2890     Fun redo = []() { return true; };
2891     std::unordered_set<int> all_items;
2892     if (!allowSingleResize && m_groups->isInGroup(itemId)) {
2893         int groupId = m_groups->getRootId(itemId);
2894         std::unordered_set<int> items;
2895         if (m_groups->getType(groupId) == GroupType::AVSplit) {
2896             // Only resize group elements if it is an avsplit
2897             items = m_groups->getLeaves(groupId);
2898         } else {
2899             all_items.insert(itemId);
2900         }
2901         for (int id : items) {
2902             if (id == itemId) {
2903                 all_items.insert(id);
2904                 continue;
2905             }
2906             int start = getItemPosition(id);
2907             int end = in + getItemPlaytime(id);
2908             if (right) {
2909                 if (out == end) {
2910                     all_items.insert(id);
2911                 }
2912             } else if (start == in) {
2913                 all_items.insert(id);
2914             }
2915         }
2916     } else {
2917         all_items.insert(itemId);
2918     }
2919     bool result = true;
2920     for (int id : all_items) {
2921         int tid = getItemTrackId(id);
2922         if (tid > -1 && getTrackById_const(tid)->isLocked()) {
2923             continue;
2924         }
2925         // First delete clip, then timewarp, resize and reinsert
2926         int pos = getItemPosition(id);
2927         if (!right) {
2928             pos += getItemPlaytime(id) - size;
2929         }
2930         result = getTrackById(tid)->requestClipDeletion(id, true, true, undo, redo, false, true);
2931         bool pitchCompensate = m_allClips[id]->getIntProperty(QStringLiteral("warp_pitch"));
2932         result = result && requestClipTimeWarp(id, speed, pitchCompensate, true, undo, redo);
2933         result = result && requestItemResize(id, size, true, true, undo, redo);
2934         result = result && getTrackById(tid)->requestClipInsertion(id, pos, true, true, undo, redo);
2935         if (!result) {
2936             break;
2937         }
2938     }
2939     if (!result) {
2940         bool undone = undo();
2941         Q_ASSERT(undone);
2942         TRACE_RES(-1);
2943         return -1;
2944     }
2945     if (result) {
2946         PUSH_UNDO(undo, redo, i18n("Resize clip speed"));
2947     }
2948     int res = result ? size : -1;
2949     TRACE_RES(res);
2950     return res;
2951 }
2952 
requestItemResizeInfo(int itemId,int in,int out,int size,bool right,int snapDistance)2953 int TimelineModel::requestItemResizeInfo(int itemId, int in, int out, int size, bool right, int snapDistance)
2954 {
2955     int trackId = getItemTrackId(itemId);
2956     bool checkMix = trackId != -1;
2957     Fun temp_undo = []() { return true; };
2958     Fun temp_redo = []() { return true; };
2959     bool skipSnaps = snapDistance <= 0;
2960     bool sizeUpdated = false;
2961     if (checkMix && right && size > out - in && isClip(itemId)) {
2962         int playlist = -1;
2963         if (getTrackById_const(trackId)->hasEndMix(itemId)) {
2964             playlist = m_allClips[itemId]->getSubPlaylistIndex();
2965         }
2966         int targetPos = in + size - 1;
2967         if (!getTrackById_const(trackId)->isBlankAt(targetPos, playlist)) {
2968             int updatedSize = getTrackById_const(trackId)->getBlankEnd(out, playlist) - in + 1;
2969             if (!skipSnaps && size - updatedSize > snapDistance) {
2970                 skipSnaps = true;
2971             }
2972             size = updatedSize;
2973             sizeUpdated = true;
2974         }
2975     } else if (checkMix && !right && size > (out - in) && isClip(itemId)) {
2976         int targetPos = out - size;
2977         int playlist = -1;
2978         if (getTrackById_const(trackId)->hasStartMix(itemId)) {
2979             playlist = m_allClips[itemId]->getSubPlaylistIndex();
2980         }
2981         if (!getTrackById_const(trackId)->isBlankAt(targetPos, playlist)) {
2982             int updatedSize = out - getTrackById_const(trackId)->getBlankStart(in - 1, playlist);
2983             if (!skipSnaps && size - updatedSize > snapDistance) {
2984                 skipSnaps = true;
2985             }
2986             size = updatedSize;
2987             sizeUpdated = true;
2988         }
2989     }
2990     int proposed_size = size;
2991     if (!skipSnaps) {
2992         int timelinePos = pCore->getTimelinePosition();
2993         m_snaps->addPoint(timelinePos);
2994         proposed_size = m_snaps->proposeSize(in, out, getBoundaries(itemId), size, right, snapDistance);
2995         m_snaps->removePoint(timelinePos);
2996     }
2997     if (proposed_size > 0 && (!skipSnaps || sizeUpdated)) {
2998         // only test move if proposed_size is valid
2999         bool success = false;
3000         if (isClip(itemId)) {
3001             bool hasMix = getTrackById_const(trackId)->hasMix(itemId);
3002             success = m_allClips[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false, hasMix);
3003         } else if (isComposition(itemId)) {
3004             success = m_allCompositions[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false);
3005         } else if (isSubTitle(itemId)) {
3006             //TODO: don't allow subtitle overlap?
3007             success = true;
3008         }
3009         // undo temp move
3010         temp_undo();
3011         if (success) {
3012             size = proposed_size;
3013         }
3014     }
3015     return size;
3016 }
3017 
trackIsBlankAt(int tid,int pos,int playlist) const3018 bool TimelineModel::trackIsBlankAt(int tid, int pos, int playlist) const
3019 {
3020     if (pos > getTrackById_const(tid)->trackDuration() - 1) {
3021         return true;
3022     }
3023     return getTrackById_const(tid)->isBlankAt(pos, playlist);
3024 }
3025 
trackIsAvailable(int tid,int pos,int duration,int playlist) const3026 bool TimelineModel::trackIsAvailable(int tid, int pos, int duration, int playlist) const
3027 {
3028     return getTrackById_const(tid)->isAvailable(pos, duration, playlist);
3029 }
3030 
getClipStartAt(int tid,int pos,int playlist) const3031 int TimelineModel::getClipStartAt(int tid, int pos, int playlist) const
3032 {
3033     return getTrackById_const(tid)->getClipStart(pos, playlist);
3034 }
3035 
getClipEndAt(int tid,int pos,int playlist) const3036 int TimelineModel::getClipEndAt(int tid, int pos, int playlist) const
3037 {
3038     return getTrackById_const(tid)->getClipEnd(pos, playlist);
3039 }
3040 
requestItemSpeedChange(int itemId,int size,bool right,int snapDistance)3041 int TimelineModel::requestItemSpeedChange(int itemId, int size, bool right, int snapDistance)
3042 {
3043     Q_ASSERT(isClip(itemId));
3044     QWriteLocker locker(&m_lock);
3045     TRACE(itemId, size, right, snapDistance);
3046     Q_ASSERT(isItem(itemId));
3047     if (size <= 0) {
3048         TRACE_RES(-1);
3049         return -1;
3050     }
3051     int in = getItemPosition(itemId);
3052     int out = in + getItemPlaytime(itemId);
3053 
3054     if (right && size > out - in) {
3055         int targetPos = in + size - 1;
3056         int trackId = getItemTrackId(itemId);
3057         if (!getTrackById_const(trackId)->isBlankAt(targetPos) || !getItemsInRange(trackId, out + 1, targetPos, false).empty()) {
3058             size = getTrackById_const(trackId)->getBlankEnd(out + 1) - in;
3059         }
3060     } else if (!right && size > (out - in)) {
3061         int targetPos = out - size;
3062         int trackId = getItemTrackId(itemId);
3063         if (!getTrackById_const(trackId)->isBlankAt(targetPos) || !getItemsInRange(trackId, targetPos, in - 1, false).empty()) {
3064             size = out - getTrackById_const(trackId)->getBlankStart(in - 1);
3065         }
3066     }
3067     int timelinePos = pCore->getTimelinePosition();
3068     m_snaps->addPoint(timelinePos);
3069     int proposed_size = m_snaps->proposeSize(in, out, getBoundaries(itemId), size, right, snapDistance);
3070     m_snaps->removePoint(timelinePos);
3071     return proposed_size > 0 ? proposed_size : size;
3072 }
3073 
removeMixWithUndo(int cid,Fun & undo,Fun & redo)3074 bool TimelineModel::removeMixWithUndo(int cid, Fun &undo, Fun &redo)
3075 {
3076     int tid = getItemTrackId(cid);
3077     if (isTrack(tid)) {
3078         MixInfo mixData = getTrackById_const(tid)->getMixInfo(cid).first;
3079         if (mixData.firstClipId > -1 && mixData.secondClipId > -1) {
3080             bool res = getTrackById(tid)->requestRemoveMix({mixData.firstClipId,mixData.secondClipId}, undo, redo);
3081             return res;
3082         }
3083     }
3084     return true;
3085 }
3086 
removeMix(int cid)3087 bool TimelineModel::removeMix(int cid)
3088 {
3089     Fun undo = []() { return true; };
3090     Fun redo = []() { return true; };
3091     bool res = removeMixWithUndo(cid, undo, redo);
3092     if (res) {
3093         PUSH_UNDO(undo, redo, i18n("Remove mix"));
3094     } else {
3095         pCore->displayMessage(i18n("Removing mix failed"), ErrorMessage, 500);
3096     }
3097     return res;
3098 }
3099 
requestItemResize(int itemId,int size,bool right,bool logUndo,int snapDistance,bool allowSingleResize)3100 int TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, int snapDistance, bool allowSingleResize)
3101 {
3102     QWriteLocker locker(&m_lock);
3103     TRACE(itemId, size, right, logUndo, snapDistance, allowSingleResize)
3104     Q_ASSERT(isItem(itemId));
3105     if (size <= 0) {
3106         TRACE_RES(-1)
3107         return -1;
3108     }
3109     int in = 0;
3110     int offset = getItemPlaytime(itemId);
3111     int tid = getItemTrackId(itemId);
3112     int out = offset;
3113     qDebug()<<"======= REQUESTING NEW CLIP SIZE: "<<size<<", ON TID: "<<tid;
3114     if (tid != -1 || !isClip(itemId)) {
3115         in = qMax(0, getItemPosition(itemId));
3116         out += in;
3117         size = requestItemResizeInfo(itemId, in, out, size, right, snapDistance);
3118     }
3119     qDebug()<<"======= ADJUSTED NEW CLIP SIZE: "<<size<<" FROM "<<offset;
3120     offset -= size;
3121     Fun undo = []() { return true; };
3122     Fun redo = []() { return true; };
3123     Fun sync_mix = []() { return true; };
3124     Fun adjust_mix = []() { return true; };
3125     Fun sync_end_mix = []() { return true; };
3126     Fun sync_end_mix_undo = []() { return true; };
3127     PUSH_LAMBDA(sync_mix, undo);
3128     std::unordered_set<int> all_items;
3129     QList <int> tracksWithMixes;
3130     all_items.insert(itemId);
3131     if (logUndo && isClip(itemId)) {
3132         if (tid > -1) {
3133             if (right) {
3134                 if (getTrackById_const(tid)->hasEndMix(itemId)) {
3135                     tracksWithMixes << tid;
3136                     std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId);
3137                     if (in + size < mixData.second.secondClipInOut.first + m_allClips[mixData.second.secondClipId]->getMixDuration() - m_allClips[mixData.second.secondClipId]->getMixCutPosition()) {
3138                         // Clip resized outside of mix zone, mix will be deleted
3139                         bool res = removeMixWithUndo(mixData.second.secondClipId, undo, redo);
3140                         if (res) {
3141                             size = m_allClips[itemId]->getPlaytime();
3142                         } else {
3143                             return -1;
3144                         }
3145                     } else {
3146                         // Mix was resized, update cut position
3147                         int currentMixDuration = m_allClips[mixData.second.secondClipId]->getMixDuration();
3148                         int currentMixCut = m_allClips[mixData.second.secondClipId]->getMixCutPosition();
3149                         adjust_mix = [this, tid, mixData, currentMixCut, itemId]() {
3150                             MixInfo secondMixData = getTrackById_const(tid)->getMixInfo(itemId).second;
3151                             int offset = mixData.second.firstClipInOut.second - secondMixData.firstClipInOut.second;
3152                             getTrackById_const(tid)->setMixDuration(secondMixData.secondClipId, secondMixData.firstClipInOut.second - secondMixData.secondClipInOut.first, currentMixCut - offset);
3153                             QModelIndex ix = makeClipIndexFromID(secondMixData.secondClipId);
3154                             emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3155                             return true;
3156                         };
3157                         Fun adjust_mix_undo = [this, tid, mixData, currentMixCut, currentMixDuration]() {
3158                             getTrackById_const(tid)->setMixDuration(mixData.second.secondClipId, currentMixDuration, currentMixCut);
3159                             QModelIndex ix = makeClipIndexFromID(mixData.second.secondClipId);
3160                             emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3161                             return true;
3162                         };
3163                         PUSH_LAMBDA(adjust_mix_undo, undo);
3164                     }
3165                 }
3166                 if (getTrackById_const(tid)->hasStartMix(itemId)) {
3167                     // Resize mix if necessary
3168                     std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId);
3169                     if (in + size <= mixData.first.firstClipInOut.second) {
3170                         // Resized smaller than mix, adjust
3171                         int updatedSize = in + size - mixData.first.firstClipInOut.first;
3172                         // Mix was resized, update cut position
3173                         int currentMixDuration = m_allClips[itemId]->getMixDuration();
3174                         int currentMixCut = m_allClips[itemId]->getMixCutPosition();
3175                         Fun adjust_mix1 = [this, tid, currentMixDuration, currentMixCut, itemId, offset = mixData.first.firstClipInOut.second - (in + size)]() {
3176                             getTrackById_const(tid)->setMixDuration(itemId, currentMixDuration - offset, currentMixCut - offset);
3177                             QModelIndex ix = makeClipIndexFromID(itemId);
3178                             emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3179                             return true;
3180                         };
3181                         Fun adjust_mix_undo = [this, tid, itemId, currentMixCut, currentMixDuration]() {
3182                             getTrackById_const(tid)->setMixDuration(itemId, currentMixDuration, currentMixCut);
3183                             QModelIndex ix = makeClipIndexFromID(itemId);
3184                             emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3185                             return true;
3186                         };
3187                         PUSH_LAMBDA(adjust_mix1, adjust_mix);
3188                         PUSH_LAMBDA(adjust_mix_undo, undo);
3189                         requestItemResize(mixData.first.firstClipId, updatedSize, true, logUndo, undo, redo);
3190                     }
3191                 }
3192             } else {
3193                 // Resized left side
3194                 if (getTrackById_const(tid)->hasStartMix(itemId)) {
3195                     tracksWithMixes << tid;
3196                     std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId);
3197                     if (out - size >= mixData.first.firstClipInOut.second) {
3198                         // Moved outside mix, delete
3199                         Fun sync_mix_undo = [this, tid, mixData]() {
3200                             getTrackById_const(tid)->createMix(mixData.first, getTrackById_const(tid)->isAudioTrack());
3201                             getTrackById_const(tid)->syncronizeMixes(true);
3202                             return true;
3203                         };
3204                         bool switchPlaylist = getTrackById_const(tid)->hasEndMix(itemId) == false && m_allClips[itemId]->getSubPlaylistIndex() == 1;
3205                         if (switchPlaylist) {
3206                             sync_end_mix = [this, tid, mixData]() {
3207                                 return getTrackById_const(tid)->switchPlaylist(mixData.first.secondClipId, m_allClips[mixData.first.secondClipId]->getPosition(), 1, 0);
3208                             };
3209                             sync_end_mix_undo = [this, tid, mixData]() {
3210                                 return getTrackById_const(tid)->switchPlaylist(mixData.first.secondClipId, m_allClips[mixData.first.secondClipId]->getPosition(), 0, 1);
3211                             };
3212                         }
3213                         PUSH_LAMBDA(sync_mix_undo, undo);
3214                     }
3215                 }
3216                 if (getTrackById_const(tid)->hasEndMix(itemId)) {
3217                     // Resize mix if necessary
3218                     std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId);
3219                     if (out - size >= mixData.second.secondClipInOut.first) {
3220                         // Resized smaller than mix, adjust
3221                         int updatedClipSize = mixData.second.secondClipInOut.second - (out - size);
3222                         int updatedMixDuration = mixData.second.firstClipInOut.second - (out - size);
3223                         // Mix was resized, update cut position
3224                         int currentMixDuration = m_allClips[mixData.second.secondClipId]->getMixDuration();
3225                         int currentMixCut = m_allClips[mixData.second.secondClipId]->getMixCutPosition();
3226                         Fun adjust_mix1 = [this, tid, currentMixDuration, currentMixCut, secondId = mixData.second.secondClipId, updatedMixDuration]() {
3227                             getTrackById_const(tid)->setMixDuration(secondId, updatedMixDuration, currentMixCut);
3228                             QModelIndex ix = makeClipIndexFromID(secondId);
3229                             emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3230                             return true;
3231                         };
3232                         Fun adjust_mix_undo = [this, tid, secondId = mixData.second.secondClipId, currentMixCut, currentMixDuration]() {
3233                             getTrackById_const(tid)->setMixDuration(secondId, currentMixDuration, currentMixCut);
3234                             QModelIndex ix = makeClipIndexFromID(secondId);
3235                             emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3236                             return true;
3237                         };
3238                         PUSH_LAMBDA(adjust_mix1, adjust_mix);
3239                         PUSH_LAMBDA(adjust_mix_undo, undo);
3240                         requestItemResize(mixData.second.secondClipId, updatedClipSize, false, logUndo, undo, redo);
3241                     }
3242                 }
3243             }
3244         }
3245     }
3246     if (!allowSingleResize && m_groups->isInGroup(itemId)) {
3247         int groupId = m_groups->getRootId(itemId);
3248         std::unordered_set<int> items = m_groups->getLeaves(groupId);
3249         /*if (m_groups->getType(groupId) == GroupType::AVSplit) {
3250             // Only resize group elements if it is an avsplit
3251             items = m_groups->getLeaves(groupId);
3252         }*/
3253         for (int id : items) {
3254             if (id == itemId) {
3255                 continue;
3256             }
3257             int start = getItemPosition(id);
3258             int end = start + getItemPlaytime(id);
3259             bool resizeMix = false;
3260             if (right) {
3261                 if (out == end) {
3262                     all_items.insert(id);
3263                     resizeMix = true;
3264                 }
3265             } else if (start == in) {
3266                 all_items.insert(id);
3267                 resizeMix = true;
3268             }
3269             if (logUndo && resizeMix && isClip(id)) {
3270                 int tid = getItemTrackId(id);
3271                 if (tid > -1) {
3272                     if (right) {
3273                         if (getTrackById_const(tid)->hasEndMix(id)) {
3274                             if (!tracksWithMixes.contains(tid)) {
3275                                 tracksWithMixes << tid;
3276                             }
3277                             std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(id);
3278                             if (end - offset <= mixData.second.secondClipInOut.first + m_allClips[mixData.second.secondClipId]->getMixDuration() - m_allClips[mixData.second.secondClipId]->getMixCutPosition()) {
3279                                 // Resized outside mix
3280                                 removeMixWithUndo(mixData.second.secondClipId, undo, redo);
3281                                 Fun sync_mix_undo = [this, tid, mixData]() {
3282                                     getTrackById_const(tid)->createMix(mixData.second, getTrackById_const(tid)->isAudioTrack());
3283                                     getTrackById_const(tid)->syncronizeMixes(true);
3284                                     return true;
3285                                 };
3286                                 bool switchPlaylist = getTrackById_const(tid)->hasEndMix(mixData.second.secondClipId) == false && m_allClips[mixData.second.secondClipId]->getSubPlaylistIndex() == 1;
3287                                 if (switchPlaylist) {
3288                                     Fun sync_end_mix2 = [this, tid, mixData]() {
3289                                         return getTrackById_const(tid)->switchPlaylist(mixData.second.secondClipId, mixData.second.secondClipInOut.first, 1, 0);
3290                                     };
3291                                     Fun sync_end_mix_undo2 = [this, tid, mixData]() {
3292                                         return getTrackById_const(tid)->switchPlaylist(mixData.second.secondClipId, m_allClips[mixData.second.secondClipId]->getPosition(), 0, 1);
3293                                     };
3294                                     PUSH_LAMBDA(sync_end_mix2, sync_end_mix);
3295                                     PUSH_LAMBDA(sync_end_mix_undo2, sync_end_mix_undo);
3296                                 }
3297                                 PUSH_LAMBDA(sync_mix_undo, undo);
3298                             } else {
3299                                 // Mix was resized, update cut position
3300                                 int currentMixDuration = m_allClips[mixData.second.secondClipId]->getMixDuration();
3301                                 int currentMixCut = m_allClips[mixData.second.secondClipId]->getMixCutPosition();
3302                                 Fun adjust_mix2 = [this, tid, mixData, currentMixCut, id]() {
3303                                     MixInfo secondMixData = getTrackById_const(tid)->getMixInfo(id).second;
3304                                     int offset = mixData.second.firstClipInOut.second - secondMixData.firstClipInOut.second;
3305                                     getTrackById_const(tid)->setMixDuration(secondMixData.secondClipId, secondMixData.firstClipInOut.second - secondMixData.secondClipInOut.first, currentMixCut - offset);
3306                                     QModelIndex ix = makeClipIndexFromID(secondMixData.secondClipId);
3307                                     emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3308                                     return true;
3309                                 };
3310                                 Fun adjust_mix_undo = [this, tid, mixData, currentMixCut, currentMixDuration]() {
3311                                     getTrackById_const(tid)->setMixDuration(mixData.second.secondClipId, currentMixDuration, currentMixCut);
3312                                     QModelIndex ix = makeClipIndexFromID(mixData.second.secondClipId);
3313                                     emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3314                                     return true;
3315                                 };
3316                                 PUSH_LAMBDA(adjust_mix2, adjust_mix);
3317                                 PUSH_LAMBDA(adjust_mix_undo, undo);
3318                             }
3319                         }
3320                     } else if (getTrackById_const(tid)->hasStartMix(id)) {
3321                         if (!tracksWithMixes.contains(tid)) {
3322                             tracksWithMixes << tid;
3323                         }
3324                         std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(id);
3325                         if (start + offset >= mixData.first.firstClipInOut.second) {
3326                             // Moved outside mix, remove
3327                             Fun sync_mix_undo = [this, tid, mixData]() {
3328                                 getTrackById_const(tid)->createMix(mixData.first, getTrackById_const(tid)->isAudioTrack());
3329                                 getTrackById_const(tid)->syncronizeMixes(true);
3330                                 return true;
3331                             };
3332                             bool switchPlaylist = getTrackById_const(tid)->hasEndMix(id) == false && m_allClips[id]->getSubPlaylistIndex() == 1;
3333                             if (switchPlaylist) {
3334                                 Fun sync_end_mix2 = [this, tid, mixData]() {
3335                                     return getTrackById_const(tid)->switchPlaylist(mixData.first.secondClipId, m_allClips[mixData.first.secondClipId]->getPosition(), 1, 0);
3336                                 };
3337                                 Fun sync_end_mix_undo2 = [this, tid, mixData]() {
3338                                     return getTrackById_const(tid)->switchPlaylist(mixData.first.secondClipId, m_allClips[mixData.first.secondClipId]->getPosition(), 0, 1);
3339                                 };
3340                                 PUSH_LAMBDA(sync_end_mix2, sync_end_mix);
3341                                 PUSH_LAMBDA(sync_end_mix_undo2, sync_end_mix_undo);
3342                             }
3343                             PUSH_LAMBDA(sync_mix_undo, undo);
3344                         }
3345                     }
3346                 }
3347             }
3348         }
3349     }
3350     if (logUndo && !tracksWithMixes.isEmpty()) {
3351         sync_mix = [this, tracksWithMixes]() {
3352             for (auto &t : tracksWithMixes) {
3353                 getTrackById_const(t)->syncronizeMixes(true);
3354             }
3355             return true;
3356         };
3357     }
3358     bool result = true;
3359     int finalPos = right ? in + size : out - size;
3360     int finalSize;
3361     int resizedCount = 0;
3362     for (int id : all_items) {
3363         int tid = getItemTrackId(id);
3364         if (tid > -1 && getTrackById_const(tid)->isLocked()) {
3365             continue;
3366         }
3367         if (tid == -2 && m_subtitleModel && m_subtitleModel->isLocked()) {
3368             continue;
3369         }
3370         if (right) {
3371             finalSize = finalPos - qMax(0, getItemPosition(id));
3372         } else {
3373             finalSize = qMax(0, getItemPosition(id)) + getItemPlaytime(id) - finalPos;
3374         }
3375         result = result && requestItemResize(id, finalSize, right, logUndo, undo, redo);
3376         if (id == itemId) {
3377             size = finalSize;
3378         }
3379         resizedCount++;
3380     }
3381     if (!result || resizedCount == 0) {
3382         qDebug() << "resize aborted" << result;
3383         bool undone = undo();
3384         Q_ASSERT(undone);
3385         TRACE_RES(-1)
3386         return -1;
3387     }
3388     if (result && logUndo) {
3389         if (isClip(itemId)) {
3390             sync_end_mix();
3391             sync_mix();
3392             adjust_mix();
3393             PUSH_LAMBDA(sync_end_mix, redo);
3394             PUSH_LAMBDA(adjust_mix, redo);
3395             PUSH_LAMBDA(sync_mix, redo);
3396             PUSH_LAMBDA(undo, sync_end_mix_undo);
3397             PUSH_UNDO(sync_end_mix_undo, redo, i18n("Resize clip"))
3398         } else if (isComposition(itemId)) {
3399             PUSH_UNDO(undo, redo, i18n("Resize composition"))
3400         } else if (isSubTitle(itemId)) {
3401             PUSH_UNDO(undo, redo, i18n("Resize subtitle"))
3402         }
3403     }
3404     int res = result ? size : -1;
3405     TRACE_RES(res)
3406     return res;
3407 }
3408 
requestItemResize(int itemId,int & size,bool right,bool logUndo,Fun & undo,Fun & redo,bool blockUndo)3409 bool TimelineModel::requestItemResize(int itemId, int &size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo)
3410 {
3411     Q_UNUSED(blockUndo)
3412     Fun local_undo = []() { return true; };
3413     Fun local_redo = []() { return true; };
3414     bool result = false;
3415     if (isClip(itemId)) {
3416         bool hasMix = false;
3417         int tid = m_allClips[itemId]->getCurrentTrackId();
3418         if (tid > -1) {
3419             std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId);
3420             if (right && mixData.second.firstClipId > -1) {
3421                 hasMix = true;
3422                 size = qMin(size, mixData.second.secondClipInOut.second - mixData.second.firstClipInOut.first);
3423             } else if (!right && mixData.first.firstClipId > -1) {
3424                 hasMix = true;
3425                 // We have a mix at clip start, limit size to previous clip start
3426                 size = qMin(size, mixData.first.secondClipInOut.second - mixData.first.firstClipInOut.first);
3427                 int currentMixDuration = mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first;
3428                 int mixDuration = mixData.first.firstClipInOut.second - (mixData.first.secondClipInOut.second - size);
3429                 Fun local_update = [this, itemId, tid, mixData, mixDuration] {
3430                     getTrackById_const(tid)->setMixDuration(itemId, qMax(1, mixDuration), mixData.first.mixOffset);
3431                     QModelIndex ix = makeClipIndexFromID(itemId);
3432                     emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3433                     return true;
3434                 };
3435                 Fun local_update_undo = [this, itemId, tid, mixData, currentMixDuration] {
3436                     if (getTrackById_const(tid)->hasStartMix(itemId)) {
3437                         getTrackById_const(tid)->setMixDuration(itemId, currentMixDuration, mixData.first.mixOffset);
3438                         QModelIndex ix = makeClipIndexFromID(itemId);
3439                         emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3440                     }
3441                     return true;
3442                 };
3443                 local_update();
3444                 if (logUndo) {
3445                     UPDATE_UNDO_REDO(local_update, local_update_undo, local_undo, local_redo);
3446                 }
3447             } else {
3448                 hasMix = mixData.second.firstClipId > -1 || mixData.first.firstClipId > -1;
3449             }
3450         }
3451         result = m_allClips[itemId]->requestResize(size, right, local_undo, local_redo, logUndo, hasMix);
3452     } else if (isComposition(itemId)) {
3453         result = m_allCompositions[itemId]->requestResize(size, right, local_undo, local_redo, logUndo);
3454     } else if (isSubTitle(itemId)) {
3455         result = m_subtitleModel->requestResize(itemId, size, right, local_undo, local_redo, logUndo);
3456     }
3457     if (result) {
3458         UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
3459     }
3460     return result;
3461 }
3462 
requestItemRippleResize(const std::shared_ptr<TimelineItemModel> & timeline,int itemId,int size,bool right,bool logUndo,int snapDistance,bool allowSingleResize)3463 int TimelineModel::requestItemRippleResize(const std::shared_ptr<TimelineItemModel> &timeline, int itemId, int size, bool right, bool logUndo, int snapDistance, bool allowSingleResize) {
3464     QWriteLocker locker(&m_lock);
3465     TRACE(itemId, size, right, logUndo, snapDistance, allowSingleResize)
3466     Q_ASSERT(isItem(itemId));
3467     if (size <= 0) {
3468         TRACE_RES(-1)
3469         return -1;
3470     }
3471     int in = 0;
3472     int offset = getItemPlaytime(itemId);
3473     int tid = getItemTrackId(itemId);
3474     int out = offset;
3475     qDebug()<<"======= REQUESTING NEW CLIP SIZE (RIPPLE): "<<size;
3476     if (tid != -1 || !isClip(itemId)) {
3477         in = qMax(0, getItemPosition(itemId));
3478         out += in;
3479         //size = requestItemResizeInfo(itemId, in, out, size, right, snapDistance); //TODO: implement snapping
3480     }
3481     qDebug()<<"======= ADJUSTED NEW CLIP SIZE (RIPPLE): "<<size;
3482     offset -= size;
3483     Fun undo = []() { return true; };
3484     Fun redo = []() { return true; };
3485     Fun sync_mix = []() { return true; };
3486     Fun adjust_mix = []() { return true; };
3487     Fun sync_end_mix = []() { return true; };
3488     Fun sync_end_mix_undo = []() { return true; };
3489     PUSH_LAMBDA(sync_mix, undo);
3490     std::unordered_set<int> all_items;
3491     QList <int> tracksWithMixes;
3492     all_items.insert(itemId);
3493     if (logUndo && isClip(itemId)) {
3494         /*if (tid > -1) {
3495             if (right) {
3496                 if (getTrackById_const(tid)->hasEndMix(itemId)) {
3497                     tracksWithMixes << tid;
3498                     std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId);
3499                     if (in + size <= mixData.second.secondClipInOut.first + m_allClips[mixData.second.secondClipId]->getMixDuration() - m_allClips[mixData.second.secondClipId]->getMixCutPosition()) {
3500                         // Clip resized outside of mix zone, mix will be deleted
3501                         bool res = removeMixWithUndo(mixData.second.secondClipId, undo, redo);
3502                         if (res) {
3503                             size = m_allClips[itemId]->getPlaytime();
3504                         } else {
3505                             return -1;
3506                         }
3507                     } else {
3508                         // Mix was resized, update cut position
3509                         int currentMixDuration = m_allClips[mixData.second.secondClipId]->getMixDuration();
3510                         int currentMixCut = m_allClips[mixData.second.secondClipId]->getMixCutPosition();
3511                         adjust_mix = [this, tid, mixData, currentMixCut, itemId]() {
3512                             MixInfo secondMixData = getTrackById_const(tid)->getMixInfo(itemId).second;
3513                             int offset = mixData.second.firstClipInOut.second - secondMixData.firstClipInOut.second;
3514                             getTrackById_const(tid)->setMixDuration(secondMixData.secondClipId, secondMixData.firstClipInOut.second - secondMixData.secondClipInOut.first, currentMixCut - offset);
3515                             QModelIndex ix = makeClipIndexFromID(secondMixData.secondClipId);
3516                             emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3517                             return true;
3518                         };
3519                         Fun adjust_mix_undo = [this, tid, mixData, currentMixCut, currentMixDuration]() {
3520                             getTrackById_const(tid)->setMixDuration(mixData.second.secondClipId, currentMixDuration, currentMixCut);
3521                             QModelIndex ix = makeClipIndexFromID(mixData.second.secondClipId);
3522                             emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3523                             return true;
3524                         };
3525                         PUSH_LAMBDA(adjust_mix_undo, undo);
3526                     }
3527                 }
3528             } else if (getTrackById_const(tid)->hasStartMix(itemId)) {
3529                 tracksWithMixes << tid;
3530                 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId);
3531                 if (out - size >= mixData.first.firstClipInOut.second) {
3532                     // Moved outside mix, delete
3533                     Fun sync_mix_undo = [this, tid, mixData]() {
3534                         getTrackById_const(tid)->createMix(mixData.first, getTrackById_const(tid)->isAudioTrack());
3535                         getTrackById_const(tid)->syncronizeMixes(true);
3536                         return true;
3537                     };
3538                     bool switchPlaylist = getTrackById_const(tid)->hasEndMix(itemId) == false && m_allClips[itemId]->getSubPlaylistIndex() == 1;
3539                     if (switchPlaylist) {
3540                         sync_end_mix = [this, tid, mixData]() {
3541                             return getTrackById_const(tid)->switchPlaylist(mixData.first.secondClipId, m_allClips[mixData.first.secondClipId]->getPosition(), 1, 0);
3542                         };
3543                         sync_end_mix_undo = [this, tid, mixData]() {
3544                             return getTrackById_const(tid)->switchPlaylist(mixData.first.secondClipId, m_allClips[mixData.first.secondClipId]->getPosition(), 0, 1);
3545                         };
3546                     }
3547                     PUSH_LAMBDA(sync_mix_undo, undo);
3548 
3549                 }
3550             }
3551         }*/
3552     }
3553     if (!allowSingleResize && m_groups->isInGroup(itemId)) {
3554         int groupId = m_groups->getRootId(itemId);
3555         std::unordered_set<int> items = m_groups->getLeaves(groupId);
3556         /*if (m_groups->getType(groupId) == GroupType::AVSplit) {
3557             // Only resize group elements if it is an avsplit
3558             items = m_groups->getLeaves(groupId);
3559         }*/
3560         for (int id : items) {
3561             if (id == itemId) {
3562                 continue;
3563             }
3564             int start = getItemPosition(id);
3565             int end = start + getItemPlaytime(id);
3566             bool resizeMix = false;
3567             if (right) {
3568                 if (out == end) {
3569                     all_items.insert(id);
3570                     resizeMix = true;
3571                 }
3572             } else if (start == in) {
3573                 all_items.insert(id);
3574                 resizeMix = true;
3575             }
3576             if (logUndo && resizeMix && isClip(id)) {
3577                 int tid = getItemTrackId(id);
3578                 if (tid > -1) {
3579                     if (right) {
3580                         if (getTrackById_const(tid)->hasEndMix(id)) {
3581                             if (!tracksWithMixes.contains(tid)) {
3582                                 tracksWithMixes << tid;
3583                             }
3584                             std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(id);
3585                             if (end - offset <= mixData.second.secondClipInOut.first + m_allClips[mixData.second.secondClipId]->getMixDuration() - m_allClips[mixData.second.secondClipId]->getMixCutPosition()) {
3586                                 // Resized outside mix
3587                                 removeMixWithUndo(mixData.second.secondClipId, undo, redo);
3588                                 Fun sync_mix_undo = [this, tid, mixData]() {
3589                                     getTrackById_const(tid)->createMix(mixData.second, getTrackById_const(tid)->isAudioTrack());
3590                                     getTrackById_const(tid)->syncronizeMixes(true);
3591                                     return true;
3592                                 };
3593                                 bool switchPlaylist = getTrackById_const(tid)->hasEndMix(mixData.second.secondClipId) == false && m_allClips[mixData.second.secondClipId]->getSubPlaylistIndex() == 1;
3594                                 if (switchPlaylist) {
3595                                     Fun sync_end_mix2 = [this, tid, mixData]() {
3596                                         return getTrackById_const(tid)->switchPlaylist(mixData.second.secondClipId, mixData.second.secondClipInOut.first, 1, 0);
3597                                     };
3598                                     Fun sync_end_mix_undo2 = [this, tid, mixData]() {
3599                                         return getTrackById_const(tid)->switchPlaylist(mixData.second.secondClipId, m_allClips[mixData.second.secondClipId]->getPosition(), 0, 1);
3600                                     };
3601                                     PUSH_LAMBDA(sync_end_mix2, sync_end_mix);
3602                                     PUSH_LAMBDA(sync_end_mix_undo2, sync_end_mix_undo);
3603                                 }
3604                                 PUSH_LAMBDA(sync_mix_undo, undo);
3605                             } else {
3606                                 // Mix was resized, update cut position
3607                                 int currentMixDuration = m_allClips[mixData.second.secondClipId]->getMixDuration();
3608                                 int currentMixCut = m_allClips[mixData.second.secondClipId]->getMixCutPosition();
3609                                 Fun adjust_mix2 = [this, tid, mixData, currentMixCut, id]() {
3610                                     MixInfo secondMixData = getTrackById_const(tid)->getMixInfo(id).second;
3611                                     int offset = mixData.second.firstClipInOut.second - secondMixData.firstClipInOut.second;
3612                                     getTrackById_const(tid)->setMixDuration(secondMixData.secondClipId, secondMixData.firstClipInOut.second - secondMixData.secondClipInOut.first, currentMixCut - offset);
3613                                     QModelIndex ix = makeClipIndexFromID(secondMixData.secondClipId);
3614                                     emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3615                                     return true;
3616                                 };
3617                                 Fun adjust_mix_undo = [this, tid, mixData, currentMixCut, currentMixDuration]() {
3618                                     getTrackById_const(tid)->setMixDuration(mixData.second.secondClipId, currentMixDuration, currentMixCut);
3619                                     QModelIndex ix = makeClipIndexFromID(mixData.second.secondClipId);
3620                                     emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3621                                     return true;
3622                                 };
3623                                 PUSH_LAMBDA(adjust_mix2, adjust_mix);
3624                                 PUSH_LAMBDA(adjust_mix_undo, undo);
3625                             }
3626                         }
3627                     } else if (getTrackById_const(tid)->hasStartMix(id)) {
3628                         if (!tracksWithMixes.contains(tid)) {
3629                             tracksWithMixes << tid;
3630                         }
3631                         std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(id);
3632                         if (start + offset >= mixData.first.firstClipInOut.second) {
3633                             // Moved outside mix, remove
3634                             Fun sync_mix_undo = [this, tid, mixData]() {
3635                                 getTrackById_const(tid)->createMix(mixData.first, getTrackById_const(tid)->isAudioTrack());
3636                                 getTrackById_const(tid)->syncronizeMixes(true);
3637                                 return true;
3638                             };
3639                             bool switchPlaylist = getTrackById_const(tid)->hasEndMix(id) == false && m_allClips[id]->getSubPlaylistIndex() == 1;
3640                             if (switchPlaylist) {
3641                                 Fun sync_end_mix2 = [this, tid, mixData]() {
3642                                     return getTrackById_const(tid)->switchPlaylist(mixData.first.secondClipId, m_allClips[mixData.first.secondClipId]->getPosition(), 1, 0);
3643                                 };
3644                                 Fun sync_end_mix_undo2 = [this, tid, mixData]() {
3645                                     return getTrackById_const(tid)->switchPlaylist(mixData.first.secondClipId, m_allClips[mixData.first.secondClipId]->getPosition(), 0, 1);
3646                                 };
3647                                 PUSH_LAMBDA(sync_end_mix2, sync_end_mix);
3648                                 PUSH_LAMBDA(sync_end_mix_undo2, sync_end_mix_undo);
3649                             }
3650                             PUSH_LAMBDA(sync_mix_undo, undo);
3651                         }
3652                     }
3653                 }
3654             }
3655         }
3656     }
3657     if (logUndo && !tracksWithMixes.isEmpty()) {
3658         sync_mix = [this, tracksWithMixes]() {
3659             for (auto &t : tracksWithMixes) {
3660                 getTrackById_const(t)->syncronizeMixes(true);
3661             }
3662             return true;
3663         };
3664     }
3665     bool result = true;
3666     int finalPos = right ? in + size : out - size;
3667     int finalSize;
3668     int resizedCount = 0;
3669     for (int id : all_items) {
3670         int tid = getItemTrackId(id);
3671         if (tid > -1 && getTrackById_const(tid)->isLocked()) {
3672             continue;
3673         }
3674         if (tid == -2 && m_subtitleModel && m_subtitleModel->isLocked()) {
3675             continue;
3676         }
3677         if (right) {
3678             finalSize = finalPos - qMax(0, getItemPosition(id));
3679         } else {
3680             finalSize = qMax(0, getItemPosition(id)) + getItemPlaytime(id) - finalPos;
3681         }
3682         result = result && requestItemRippleResize(timeline, id, finalSize, right, logUndo, undo, redo);
3683         resizedCount++;
3684     }
3685     if (!result || resizedCount == 0) {
3686         qDebug() << "resize aborted" << result;
3687         bool undone = undo();
3688         Q_ASSERT(undone);
3689         TRACE_RES(-1)
3690         return -1;
3691     }
3692     if (result && logUndo) {
3693         if (isClip(itemId)) {
3694             sync_end_mix();
3695             sync_mix();
3696             adjust_mix();
3697             PUSH_LAMBDA(sync_end_mix, redo);
3698             PUSH_LAMBDA(adjust_mix, redo);
3699             PUSH_LAMBDA(sync_mix, redo);
3700             PUSH_LAMBDA(undo, sync_end_mix_undo);
3701             PUSH_UNDO(sync_end_mix_undo, redo, i18n("Ripple resize clip"))
3702         } else if (isComposition(itemId)) {
3703             PUSH_UNDO(undo, redo, i18n("Ripple resize composition"))
3704         } else if (isSubTitle(itemId)) {
3705             PUSH_UNDO(undo, redo, i18n("Ripple resize subtitle"))
3706         }
3707     }
3708     int res = result ? size : -1;
3709     TRACE_RES(res)
3710     return res;
3711 }
3712 
requestItemRippleResize(const std::shared_ptr<TimelineItemModel> & timeline,int itemId,int size,bool right,bool logUndo,Fun & undo,Fun & redo,bool blockUndo)3713 bool TimelineModel::requestItemRippleResize(const std::shared_ptr<TimelineItemModel> &timeline, int itemId, int size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo)
3714 {
3715     Q_UNUSED(blockUndo)
3716     Fun local_undo = []() { return true; };
3717     Fun local_redo = []() { return true; };
3718     bool result = false;
3719     if (isClip(itemId)) {
3720         bool hasMix = false;
3721         int tid = m_allClips[itemId]->getCurrentTrackId();
3722         if (tid > -1) {
3723             std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId);
3724             /*if (right && mixData.second.firstClipId > -1) {
3725                 hasMix = true;
3726                 size = qMin(size, mixData.second.secondClipInOut.second - mixData.second.firstClipInOut.first);
3727             } else if (!right && mixData.first.firstClipId > -1) {
3728                 hasMix = true;
3729                 // We have a mix at clip start, limit size to previous clip start
3730                 size = qMin(size, mixData.first.secondClipInOut.second - mixData.first.firstClipInOut.first);
3731                 int currentMixDuration = mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first;
3732                 int mixDuration = mixData.first.firstClipInOut.second - (mixData.first.secondClipInOut.second - size);
3733                 Fun local_update = [this, itemId, tid, mixData, mixDuration] {
3734                     getTrackById_const(tid)->setMixDuration(itemId, qMax(1, mixDuration), mixData.first.mixOffset);
3735                     QModelIndex ix = makeClipIndexFromID(itemId);
3736                     emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3737                     return true;
3738                 };
3739                 Fun local_update_undo = [this, itemId, tid, mixData, currentMixDuration] {
3740                     getTrackById_const(tid)->setMixDuration(itemId, currentMixDuration, mixData.first.mixOffset);
3741                     QModelIndex ix = makeClipIndexFromID(itemId);
3742                     emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
3743                     return true;
3744                 };
3745                 local_update();
3746                 if (logUndo) {
3747                     UPDATE_UNDO_REDO(local_update, local_update_undo, local_undo, local_redo);
3748                 }
3749             } else {
3750                 hasMix = mixData.second.firstClipId > -1 || mixData.first.firstClipId > -1;
3751             }*/
3752         }
3753         bool affectAllTracks = false;
3754         size = m_allClips[itemId]->getMaxDuration() > 0 ? qBound(1, size, m_allClips[itemId]->getMaxDuration()) : qMax(1, size);
3755         int delta = size - m_allClips[itemId]->getPlaytime();
3756         qDebug() << "requestItemRippleResize logUndo: " << logUndo << " size: " << size << " playtime: " << m_allClips[itemId]->getPlaytime() <<" delta: " << delta;
3757         auto spacerOperation = [this, itemId, affectAllTracks, &local_undo, &local_redo, delta, right, timeline](int position) {
3758             int trackId = getItemTrackId(itemId);
3759             if (right && getTrackById_const(trackId)->isLastClip(getItemPosition(itemId))) {
3760                 return true;
3761             }
3762             int cid = TimelineFunctions::requestSpacerStartOperation(timeline, affectAllTracks ? -1 : trackId, position + 1, true, true);
3763             if (cid == -1) {
3764                 return false;
3765             }
3766             int endPos = getItemPosition(cid) + delta;
3767             // Start undoable command
3768             TimelineFunctions::requestSpacerEndOperation(timeline, cid, getItemPosition(cid), endPos, affectAllTracks ? -1 : trackId, !KdenliveSettings::lockedGuides(), local_undo, local_redo, false);
3769             return true;
3770         };
3771         if (delta > 0) {
3772             if (right) {
3773                 int position = getItemPosition(itemId) + getItemPlaytime(itemId);
3774                 if (!spacerOperation(position)) {
3775                     return false;
3776                 }
3777             } else {
3778                 int position = getItemPosition(itemId);
3779                 if (!spacerOperation(position)) {
3780                     return false;
3781                 }
3782             }
3783         }
3784 
3785         result = m_allClips[itemId]->requestResize(size, right, local_undo, local_redo, logUndo, hasMix);
3786         if (!result && delta > 0) {
3787             local_undo();
3788         }
3789         if (result && delta < 0) {
3790             if (right) {
3791                 int position = getItemPosition(itemId) + getItemPlaytime(itemId) - delta;
3792                 if (!spacerOperation(position)) {
3793                     return false;
3794                 }
3795             } else {
3796                 int position = getItemPosition(itemId) + delta;
3797                 if (!spacerOperation(position)) {
3798                     return false;
3799                 }
3800             }
3801         }
3802 
3803     } else if (isComposition(itemId)) {
3804         return false;
3805         // TODO? Does it make sense?
3806         // result = m_allCompositions[itemId]->requestResize(size, right, local_undo, local_redo, logUndo);
3807     } else if (isSubTitle(itemId)) {
3808         return false;
3809         // TODO?
3810         // result = m_subtitleModel->requestResize(itemId, size, right, local_undo, local_redo, logUndo);
3811     }
3812     if (result) {
3813         UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
3814     }
3815     return result;
3816 }
3817 
requestSlipSelection(int offset,bool logUndo)3818 int TimelineModel::requestSlipSelection(int offset, bool logUndo) {
3819     QWriteLocker locker(&m_lock);
3820     TRACE(offset, logUndo)
3821 
3822     Fun undo = []() { return true; };
3823     Fun redo = []() { return true; };
3824     bool result = true;
3825     int slipCount = 0;
3826     for (auto id: getCurrentSelection()) {
3827         int tid = getItemTrackId(id);
3828         if (tid > -1 && getTrackById_const(tid)->isLocked()) {
3829             continue;
3830         }
3831         if (!isClip(id)) {
3832             continue;
3833         }
3834         result = result && requestClipSlip(id, offset, logUndo, undo, redo);
3835         slipCount++;
3836     }
3837     if (!result || slipCount == 0) {
3838         bool undone = undo();
3839         Q_ASSERT(undone);
3840         TRACE_RES(-1)
3841         return -1;
3842     }
3843     if(result && logUndo) {
3844         PUSH_UNDO(undo, redo, i18ncp("Undo/Redo menu text", "Slip clip", "Slip clips", slipCount));
3845     }
3846     int res = result ? offset : 0;
3847     TRACE_RES(res)
3848     return res;
3849 }
3850 
requestClipSlip(int itemId,int offset,bool logUndo,bool allowSingleResize)3851 int TimelineModel::requestClipSlip(int itemId, int offset, bool logUndo, bool allowSingleResize)
3852 {
3853     QWriteLocker locker(&m_lock);
3854     TRACE(itemId, offset, logUndo, allowSingleResize)
3855     Q_ASSERT(isClip(itemId));
3856     Fun undo = []() { return true; };
3857     Fun redo = []() { return true; };
3858     std::unordered_set<int> all_items;
3859     all_items.insert(itemId);
3860     if (!allowSingleResize && m_groups->isInGroup(itemId)) {
3861         int groupId = m_groups->getRootId(itemId);
3862         all_items = m_groups->getLeaves(groupId);
3863     }
3864     bool result = true;
3865     int slipCount = 0;
3866     for (int id : all_items) {
3867         int tid = getItemTrackId(id);
3868         if (tid > -1 && getTrackById_const(tid)->isLocked()) {
3869             continue;
3870         }
3871         result = result && requestClipSlip(id, offset, logUndo, undo, redo);
3872         slipCount++;
3873     }
3874     if (!result || slipCount == 0) {
3875         bool undone = undo();
3876         Q_ASSERT(undone);
3877         TRACE_RES(-1)
3878         return -1;
3879     }
3880     if (result && logUndo) {
3881         PUSH_UNDO(undo, redo, i18n("Slip clip"))
3882     }
3883     int res = result ? offset : 0;
3884     TRACE_RES(res)
3885     return res;
3886 }
3887 
requestClipSlip(int itemId,int offset,bool logUndo,Fun & undo,Fun & redo,bool blockUndo)3888 bool TimelineModel::requestClipSlip(int itemId, int offset, bool logUndo, Fun &undo, Fun &redo, bool blockUndo)
3889 {
3890     Q_UNUSED(blockUndo)
3891     Fun local_undo = []() { return true; };
3892     Fun local_redo = []() { return true; };
3893     bool result = false;
3894     if (isClip(itemId)) {
3895         result = m_allClips[itemId]->requestSlip(offset, local_undo, local_redo, logUndo);
3896     }
3897     if (result) {
3898         UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
3899     }
3900     return result;
3901 }
3902 
requestClipsGroup(const std::unordered_set<int> & ids,bool logUndo,GroupType type)3903 int TimelineModel::requestClipsGroup(const std::unordered_set<int> &ids, bool logUndo, GroupType type)
3904 {
3905     QWriteLocker locker(&m_lock);
3906     TRACE(ids, logUndo, type);
3907     if (type == GroupType::Selection || type == GroupType::Leaf) {
3908         // Selections shouldn't be done here. Call requestSetSelection instead
3909         TRACE_RES(-1);
3910         return -1;
3911     }
3912     Fun undo = []() { return true; };
3913     Fun redo = []() { return true; };
3914     int result = requestClipsGroup(ids, undo, redo, type);
3915     if (result > -1 && logUndo) {
3916         PUSH_UNDO(undo, redo, i18n("Group clips"));
3917     }
3918     TRACE_RES(result);
3919     return result;
3920 }
3921 
requestClipsGroup(const std::unordered_set<int> & ids,Fun & undo,Fun & redo,GroupType type)3922 int TimelineModel::requestClipsGroup(const std::unordered_set<int> &ids, Fun &undo, Fun &redo, GroupType type)
3923 {
3924     QWriteLocker locker(&m_lock);
3925     if (type != GroupType::Selection) {
3926         requestClearSelection();
3927     }
3928     int clipsCount = 0;
3929     QList<int> tracks;
3930     for (int id : ids) {
3931         if (isClip(id)) {
3932             int trackId = getClipTrackId(id);
3933             if (trackId == -1) {
3934                 return -1;
3935             }
3936             tracks << trackId;
3937             clipsCount++;
3938         } else if (isComposition(id)) {
3939             if (getCompositionTrackId(id) == -1) {
3940                 return -1;
3941             }
3942         } else if (isSubTitle(id)) {
3943         } else if (!isGroup(id)) {
3944             return -1;
3945         }
3946     }
3947     if (type == GroupType::Selection && ids.size() == 1) {
3948         // only one element selected, no group created
3949         return -1;
3950     }
3951     if (ids.size() == 2 && clipsCount == 2 && type == GroupType::Normal) {
3952         // Check if we are grouping an AVSplit
3953         auto it = ids.begin();
3954         int firstId = *it;
3955         std::advance(it, 1);
3956         int secondId = *it;
3957         bool isAVGroup = false;
3958         if (getClipBinId(firstId) == getClipBinId(secondId)) {
3959             if (getClipState(firstId) == PlaylistState::AudioOnly) {
3960                 if (getClipState(secondId) == PlaylistState::VideoOnly) {
3961                     isAVGroup = true;
3962                 }
3963             } else if (getClipState(secondId) == PlaylistState::AudioOnly) {
3964                 isAVGroup = true;
3965             }
3966         }
3967         if (isAVGroup) {
3968             type = GroupType::AVSplit;
3969         }
3970     }
3971     int groupId = m_groups->groupItems(ids, undo, redo, type);
3972     if (type != GroupType::Selection) {
3973         // we make sure that the undo and the redo are going to unselect before doing anything else
3974         Fun unselect = [this]() { return requestClearSelection(); };
3975         PUSH_FRONT_LAMBDA(unselect, undo);
3976         PUSH_FRONT_LAMBDA(unselect, redo);
3977     }
3978     return groupId;
3979 }
3980 
requestClipsUngroup(const std::unordered_set<int> & itemIds,bool logUndo)3981 bool TimelineModel::requestClipsUngroup(const std::unordered_set<int> &itemIds, bool logUndo)
3982 {
3983     QWriteLocker locker(&m_lock);
3984     TRACE(itemIds, logUndo);
3985     Fun undo = []() { return true; };
3986     Fun redo = []() { return true; };
3987     bool result = true;
3988     requestClearSelection();
3989     std::unordered_set<int> roots;
3990     std::transform(itemIds.begin(), itemIds.end(), std::inserter(roots, roots.begin()), [&](int id) { return m_groups->getRootId(id); });
3991     for (int root : roots) {
3992         if (isGroup(root)) {
3993             result = result && requestClipUngroup(root, undo, redo);
3994         }
3995     }
3996     if (!result) {
3997         bool undone = undo();
3998         Q_ASSERT(undone);
3999     }
4000     if (result && logUndo) {
4001         PUSH_UNDO(undo, redo, i18n("Ungroup clips"));
4002     }
4003     TRACE_RES(result);
4004     return result;
4005 }
4006 
requestClipUngroup(int itemId,bool logUndo)4007 bool TimelineModel::requestClipUngroup(int itemId, bool logUndo)
4008 {
4009     QWriteLocker locker(&m_lock);
4010     TRACE(itemId, logUndo);
4011     requestClearSelection();
4012     Fun undo = []() { return true; };
4013     Fun redo = []() { return true; };
4014     bool result = true;
4015     result = requestClipUngroup(itemId, undo, redo);
4016     if (result && logUndo) {
4017         PUSH_UNDO(undo, redo, i18n("Ungroup clips"));
4018     }
4019     TRACE_RES(result);
4020     return result;
4021 }
4022 
requestClipUngroup(int itemId,Fun & undo,Fun & redo)4023 bool TimelineModel::requestClipUngroup(int itemId, Fun &undo, Fun &redo)
4024 {
4025     QWriteLocker locker(&m_lock);
4026     bool isSelection = m_groups->getType(m_groups->getRootId(itemId)) == GroupType::Selection;
4027     if (!isSelection) {
4028         requestClearSelection();
4029     }
4030     bool res = m_groups->ungroupItem(itemId, undo, redo);
4031     if (res && !isSelection) {
4032         // we make sure that the undo and the redo are going to unselect before doing anything else
4033         Fun unselect = [this]() { return requestClearSelection(); };
4034         PUSH_FRONT_LAMBDA(unselect, undo);
4035         PUSH_FRONT_LAMBDA(unselect, redo);
4036     }
4037     return res;
4038 }
4039 
requestTrackInsertion(int position,int & id,const QString & trackName,bool audioTrack)4040 bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack)
4041 {
4042     QWriteLocker locker(&m_lock);
4043     TRACE(position, id, trackName, audioTrack);
4044     Fun undo = []() { return true; };
4045     Fun redo = []() { return true; };
4046     bool result = requestTrackInsertion(position, id, trackName, audioTrack, undo, redo);
4047     if (result) {
4048         PUSH_UNDO(undo, redo, i18nc("@action", "Insert Track"));
4049     }
4050     TRACE_RES(result);
4051     return result;
4052 }
4053 
requestTrackInsertion(int position,int & id,const QString & trackName,bool audioTrack,Fun & undo,Fun & redo,bool addCompositing)4054 bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool addCompositing)
4055 {
4056     // TODO: make sure we disable overlayTrack before inserting a track
4057     if (position == -1) {
4058         position = int(m_allTracks.size());
4059     }
4060     if (position < 0 || position > int(m_allTracks.size())) {
4061         return false;
4062     }
4063     int previousId = -1;
4064     if (position < int(m_allTracks.size())) {
4065         previousId = getTrackIndexFromPosition(position);
4066     }
4067     int trackId = TimelineModel::getNextId();
4068     id = trackId;
4069     Fun local_undo = deregisterTrack_lambda(trackId);
4070     TrackModel::construct(shared_from_this(), trackId, position, trackName, audioTrack);
4071     // Adjust compositions that were affecting track at previous pos
4072     QList <std::shared_ptr<CompositionModel>> updatedCompositions;
4073     if (previousId > -1) {
4074         for (auto &compo : m_allCompositions) {
4075             if (position > 0 && compo.second->getATrack() == position && compo.second->getForcedTrack() == -1) {
4076                 updatedCompositions << compo.second;
4077             }
4078         }
4079     }
4080     Fun local_update = [position, updatedCompositions]() {
4081         for (auto &compo : updatedCompositions) {
4082             compo->setATrack(position + 1, -1);
4083         }
4084         return true;
4085     };
4086     Fun local_update_undo = [position, updatedCompositions]() {
4087         for (auto &compo : updatedCompositions) {
4088             compo->setATrack(position, -1);
4089         }
4090         return true;
4091     };
4092 
4093     Fun local_name_update = [position, audioTrack, this]() {
4094         if (KdenliveSettings::audiotracksbelow() == 0) {
4095             _resetView();
4096         } else {
4097             if (audioTrack) {
4098                 for (int i = 0; i <= position; i++) {
4099                     QModelIndex ix = makeTrackIndexFromID(getTrackIndexFromPosition(i));
4100                     emit dataChanged(ix, ix, {TimelineModel::TrackTagRole});
4101                 }
4102             } else {
4103                 for (int i = position; i < int(m_allTracks.size()); i++) {
4104                     QModelIndex ix = makeTrackIndexFromID(getTrackIndexFromPosition(i));
4105                     emit dataChanged(ix, ix, {TimelineModel::TrackTagRole});
4106                 }
4107             }
4108         }
4109         return true;
4110     };
4111 
4112     local_update();
4113     local_name_update();
4114     Fun rebuild_compositing = [this]() {
4115         buildTrackCompositing(true);
4116         return true;
4117     };
4118     if (addCompositing) {
4119         buildTrackCompositing(true);
4120     }
4121     auto track = getTrackById(trackId);
4122     Fun local_redo = [track, position, local_update, addCompositing, this]() {
4123         // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is
4124         // sufficient to register it.
4125         registerTrack(track, position, true);
4126         local_update();
4127         if (addCompositing) {
4128             buildTrackCompositing(true);
4129         }
4130         return true;
4131     };
4132     if (addCompositing) {
4133         PUSH_LAMBDA(local_update_undo, local_undo);
4134         PUSH_LAMBDA(rebuild_compositing, local_undo);
4135     }
4136     PUSH_LAMBDA(local_name_update, local_undo);
4137     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
4138     PUSH_LAMBDA(local_name_update, redo);
4139     return true;
4140 }
4141 
requestTrackDeletion(int trackId)4142 bool TimelineModel::requestTrackDeletion(int trackId)
4143 {
4144     // TODO: make sure we disable overlayTrack before deleting a track
4145     QWriteLocker locker(&m_lock);
4146     TRACE(trackId);
4147     Fun undo = []() { return true; };
4148     Fun redo = []() { return true; };
4149     bool result = requestTrackDeletion(trackId, undo, redo);
4150     if (result) {
4151         if (m_videoTarget == trackId) {
4152             m_videoTarget = -1;
4153         }
4154         if (m_audioTarget.contains(trackId)) {
4155             m_audioTarget.remove(trackId);
4156         }
4157         PUSH_UNDO(undo, redo, i18n("Delete Track"));
4158     }
4159     TRACE_RES(result);
4160     return result;
4161 }
4162 
requestTrackDeletion(int trackId,Fun & undo,Fun & redo)4163 bool TimelineModel::requestTrackDeletion(int trackId, Fun &undo, Fun &redo)
4164 {
4165     Q_ASSERT(isTrack(trackId));
4166     if (m_allTracks.size() < 2) {
4167         pCore->displayMessage(i18n("Cannot delete last track in timeline"), ErrorMessage, 500);
4168         return false;
4169     }
4170     // Discard running jobs
4171     pCore->taskManager.discardJobs({ObjectType::TimelineTrack,trackId});
4172 
4173     std::vector<int> clips_to_delete;
4174     for (const auto &it : getTrackById(trackId)->m_allClips) {
4175         clips_to_delete.push_back(it.first);
4176     }
4177     Fun local_undo = []() { return true; };
4178     Fun local_redo = []() { return true; };
4179     for (int clip : clips_to_delete) {
4180         bool res = true;
4181         while (res && m_groups->isInGroup(clip)) {
4182             res = requestClipUngroup(clip, local_undo, local_redo);
4183         }
4184         if (res) {
4185             res = requestClipDeletion(clip, local_undo, local_redo);
4186         }
4187         if (!res) {
4188             bool u = local_undo();
4189             Q_ASSERT(u);
4190             return false;
4191         }
4192     }
4193     std::vector<int> compositions_to_delete;
4194     for (const auto &it : getTrackById(trackId)->m_allCompositions) {
4195         compositions_to_delete.push_back(it.first);
4196     }
4197     for (int compo : compositions_to_delete) {
4198         bool res = true;
4199         while (res && m_groups->isInGroup(compo)) {
4200             res = requestClipUngroup(compo, local_undo, local_redo);
4201         }
4202         if (res) {
4203             res = requestCompositionDeletion(compo, local_undo, local_redo);
4204         }
4205         if (!res) {
4206             bool u = local_undo();
4207             Q_ASSERT(u);
4208             return false;
4209         }
4210     }
4211     int old_position = getTrackPosition(trackId);
4212     int previousTrack = getPreviousVideoTrackPos(trackId);
4213     auto operation = deregisterTrack_lambda(trackId);
4214     std::shared_ptr<TrackModel> track = getTrackById(trackId);
4215     bool audioTrack = track->isAudioTrack();
4216     QList <std::shared_ptr<CompositionModel>> updatedCompositions;
4217     for (auto &compo : m_allCompositions) {
4218         if (compo.second->getATrack() == old_position + 1 && compo.second->getForcedTrack() == -1) {
4219             updatedCompositions << compo.second;
4220         }
4221     }
4222     Fun reverse = [this, track, old_position, updatedCompositions]() {
4223         // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is
4224         // sufficient to register it.
4225         registerTrack(track, old_position);
4226         for (auto &compo : updatedCompositions) {
4227             compo->setATrack(old_position + 1, -1);
4228         }
4229         return true;
4230     };
4231     Fun local_update = [previousTrack, updatedCompositions]() {
4232         for (auto &compo : updatedCompositions) {
4233             compo->setATrack(previousTrack, -1);
4234         }
4235         return true;
4236     };
4237     Fun rebuild_compositing = [this]() {
4238         buildTrackCompositing(true);
4239         return true;
4240     };
4241     Fun local_name_update = [old_position, audioTrack, this]() {
4242         if (audioTrack) {
4243             for (int i = 0; i < qMin(old_position + 1, getTracksCount()); i++) {
4244                 QModelIndex ix = makeTrackIndexFromID(getTrackIndexFromPosition(i));
4245                 emit dataChanged(ix, ix, {TimelineModel::TrackTagRole});
4246             }
4247         } else {
4248             for (int i = old_position; i < getTracksCount(); i++) {
4249                 QModelIndex ix = makeTrackIndexFromID(getTrackIndexFromPosition(i));
4250                 emit dataChanged(ix, ix, {TimelineModel::TrackTagRole});
4251             }
4252         }
4253         return true;
4254     };
4255     if (operation()) {
4256         local_update();
4257         rebuild_compositing();
4258         local_name_update();
4259         PUSH_LAMBDA(rebuild_compositing, local_undo);
4260         PUSH_LAMBDA(local_name_update, local_undo);
4261         UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo);
4262         UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
4263         PUSH_LAMBDA(local_update, redo);
4264         PUSH_LAMBDA(rebuild_compositing, redo);
4265         PUSH_LAMBDA(local_name_update, redo);
4266         return true;
4267     }
4268     local_undo();
4269     return false;
4270 }
4271 
registerTrack(std::shared_ptr<TrackModel> track,int pos,bool doInsert)4272 void TimelineModel::registerTrack(std::shared_ptr<TrackModel> track, int pos, bool doInsert)
4273 {
4274     int id = track->getId();
4275     if (pos == -1) {
4276         pos = static_cast<int>(m_allTracks.size());
4277     }
4278     Q_ASSERT(pos >= 0);
4279     Q_ASSERT(pos <= static_cast<int>(m_allTracks.size()));
4280 
4281     // effective insertion (MLT operation), add 1 to account for black background track
4282     if (doInsert) {
4283         int error = m_tractor->insert_track(*track, pos + 1);
4284         Q_ASSERT(error == 0); // we might need better error handling...
4285     }
4286 
4287     // we now insert in the list
4288     auto posIt = m_allTracks.begin();
4289     std::advance(posIt, pos);
4290     beginInsertRows(QModelIndex(), pos, pos);
4291     auto it = m_allTracks.insert(posIt, std::move(track));
4292     // it now contains the iterator to the inserted element, we store it
4293     Q_ASSERT(m_iteratorTable.count(id) == 0); // check that id is not used (shouldn't happen)
4294     m_iteratorTable[id] = it;
4295     endInsertRows();
4296     int cache = int(QThread::idealThreadCount()) + int(m_allTracks.size() + 1) * 2;
4297     mlt_service_cache_set_size(nullptr, "producer_avformat", qMax(4, cache));
4298 }
4299 
registerClip(const std::shared_ptr<ClipModel> & clip,bool registerProducer)4300 void TimelineModel::registerClip(const std::shared_ptr<ClipModel> &clip, bool registerProducer)
4301 {
4302     int id = clip->getId();
4303     Q_ASSERT(m_allClips.count(id) == 0);
4304     m_allClips[id] = clip;
4305     qDebug()<<"::: REGISTERING CLIP TO BIN:::::\n::::::::::::::::::::::";
4306     clip->registerClipToBin(clip->getProducer(), registerProducer);
4307     m_groups->createGroupItem(id);
4308     clip->setTimelineEffectsEnabled(m_timelineEffectsEnabled);
4309 }
4310 
registerSubtitle(int id,GenTime startTime,bool temporary)4311 void TimelineModel::registerSubtitle(int id, GenTime startTime, bool temporary)
4312 {
4313     Q_ASSERT(m_allSubtitles.count(id) == 0);
4314     m_allSubtitles.emplace(id, startTime);
4315     if (!temporary) {
4316         m_groups->createGroupItem(id);
4317     }
4318 }
4319 
positionForIndex(int id)4320 int TimelineModel::positionForIndex(int id)
4321 {
4322     return int(std::distance(m_allSubtitles.begin(),m_allSubtitles.find(id)));
4323 }
4324 
deregisterSubtitle(int id,bool temporary)4325 void TimelineModel::deregisterSubtitle(int id, bool temporary)
4326 {
4327     Q_ASSERT(m_allSubtitles.count(id) > 0);
4328     if (!temporary && m_subtitleModel->isSelected(id)) {
4329         requestClearSelection(true);
4330     }
4331     m_allSubtitles.erase(id);
4332     if (!temporary) {
4333         m_groups->destructGroupItem(id);
4334     }
4335 }
4336 
registerGroup(int groupId)4337 void TimelineModel::registerGroup(int groupId)
4338 {
4339     Q_ASSERT(m_allGroups.count(groupId) == 0);
4340     m_allGroups.insert(groupId);
4341 }
4342 
deregisterTrack_lambda(int id)4343 Fun TimelineModel::deregisterTrack_lambda(int id)
4344 {
4345     return [this, id]() {
4346         if (!m_closing) {
4347             emit checkTrackDeletion(id);
4348         }
4349         auto it = m_iteratorTable[id];                        // iterator to the element
4350         int index = getTrackPosition(id);                     // compute index in list
4351         // send update to the model
4352         beginRemoveRows(QModelIndex(), index, index);
4353         // melt operation, add 1 to account for black background track
4354         m_tractor->remove_track(static_cast<int>(index + 1));
4355         // actual deletion of object
4356         m_allTracks.erase(it);
4357         // clean table
4358         m_iteratorTable.erase(id);
4359         // Finish operation
4360         endRemoveRows();
4361         if (!m_closing) {
4362             int cache = int(QThread::idealThreadCount()) + int(m_allTracks.size() + 1) * 2;
4363             mlt_service_cache_set_size(nullptr, "producer_avformat", qMax(4, cache));
4364         }
4365         return true;
4366     };
4367 }
4368 
deregisterClip_lambda(int clipId)4369 Fun TimelineModel::deregisterClip_lambda(int clipId)
4370 {
4371     return [this, clipId]() {
4372         // Clear effect stack
4373         clearAssetView(clipId);
4374         if (!m_closing) {
4375             emit checkItemDeletion(clipId);
4376         }
4377         Q_ASSERT(m_allClips.count(clipId) > 0);
4378         Q_ASSERT(getClipTrackId(clipId) == -1); // clip must be deleted from its track at this point
4379         Q_ASSERT(!m_groups->isInGroup(clipId)); // clip must be ungrouped at this point
4380         auto clip = m_allClips[clipId];
4381         m_allClips.erase(clipId);
4382         clip->deregisterClipToBin();
4383         m_groups->destructGroupItem(clipId);
4384         return true;
4385     };
4386 }
4387 
deregisterGroup(int id)4388 void TimelineModel::deregisterGroup(int id)
4389 {
4390     Q_ASSERT(m_allGroups.count(id) > 0);
4391     m_allGroups.erase(id);
4392 }
4393 
getTrackById(int trackId)4394 std::shared_ptr<TrackModel> TimelineModel::getTrackById(int trackId)
4395 {
4396     Q_ASSERT(m_iteratorTable.count(trackId) > 0);
4397     return *m_iteratorTable[trackId];
4398 }
4399 
getTrackById_const(int trackId) const4400 const std::shared_ptr<TrackModel> TimelineModel::getTrackById_const(int trackId) const
4401 {
4402     Q_ASSERT(m_iteratorTable.count(trackId) > 0);
4403     return *m_iteratorTable.at(trackId);
4404 }
4405 
addTrackEffect(int trackId,const QString & effectId)4406 bool TimelineModel::addTrackEffect(int trackId, const QString &effectId)
4407 {
4408     if(trackId == -1) {
4409         if(m_masterStack== nullptr || m_masterStack->appendEffect(effectId) == false) {
4410             QString effectName = EffectsRepository::get()->getName(effectId);
4411             pCore->displayMessage(i18n("Cannot add effect %1 to master track", effectName), InformationMessage, 500);
4412             return false;
4413         }
4414     } else {
4415         Q_ASSERT(m_iteratorTable.count(trackId) > 0);
4416         if ((*m_iteratorTable.at(trackId))->addEffect(effectId) == false) {
4417             QString effectName = EffectsRepository::get()->getName(effectId);
4418             pCore->displayMessage(i18n("Cannot add effect %1 to selected track", effectName), InformationMessage, 500);
4419             return false;
4420         }
4421     }
4422     return true;
4423 }
4424 
copyTrackEffect(int trackId,const QString & sourceId)4425 bool TimelineModel::copyTrackEffect(int trackId, const QString &sourceId)
4426 {
4427     QStringList source = sourceId.split(QLatin1Char('-'));
4428     Q_ASSERT(source.count() == 3);
4429     int itemType = source.at(0).toInt();
4430     int itemId = source.at(1).toInt();
4431     int itemRow = source.at(2).toInt();
4432     std::shared_ptr<EffectStackModel> effectStack = pCore->getItemEffectStack(itemType, itemId);
4433 
4434     if(trackId == -1) {
4435         QWriteLocker locker(&m_lock);
4436         if(m_masterStack== nullptr || m_masterStack->copyEffect(effectStack->getEffectStackRow(itemRow), PlaylistState::Disabled) == false) { //We use "disabled" in a hacky way to accept video and audio on master
4437             pCore->displayMessage(i18n("Cannot paste effect to master track"), InformationMessage, 500);
4438             return false;
4439         }
4440     } else {
4441         Q_ASSERT(m_iteratorTable.count(trackId) > 0);
4442         if ((*m_iteratorTable.at(trackId))->copyEffect(effectStack, itemRow) == false) {
4443             pCore->displayMessage(i18n("Cannot paste effect to selected track"), InformationMessage, 500);
4444             return false;
4445         }
4446     }
4447     return true;
4448 }
4449 
getClipPtr(int clipId) const4450 std::shared_ptr<ClipModel> TimelineModel::getClipPtr(int clipId) const
4451 {
4452     Q_ASSERT(m_allClips.count(clipId) > 0);
4453     return m_allClips.at(clipId);
4454 }
4455 
addClipEffect(int clipId,const QString & effectId,bool notify)4456 bool TimelineModel::addClipEffect(int clipId, const QString &effectId, bool notify)
4457 {
4458     Q_ASSERT(m_allClips.count(clipId) > 0);
4459     // Check if we are applying an audio effect on an audio clip
4460     bool isAudio = EffectsRepository::get()->isAudioEffect(effectId);
4461     bool audioClip = m_allClips.at(clipId)->isAudioOnly();
4462     if (isAudio != audioClip) {
4463         // Check if we have a split partner
4464         clipId = getClipSplitPartner(clipId);
4465     }
4466     bool result = clipId > -1 && m_allClips.at(clipId)->addEffect(effectId);
4467     if (!result && notify) {
4468         QString effectName = EffectsRepository::get()->getName(effectId);
4469         pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), ErrorMessage, 500);
4470     }
4471     return result;
4472 }
4473 
removeFade(int clipId,bool fromStart)4474 bool TimelineModel::removeFade(int clipId, bool fromStart)
4475 {
4476     Q_ASSERT(m_allClips.count(clipId) > 0);
4477     return m_allClips.at(clipId)->removeFade(fromStart);
4478 }
4479 
getClipEffectStack(int itemId)4480 std::shared_ptr<EffectStackModel> TimelineModel::getClipEffectStack(int itemId)
4481 {
4482     Q_ASSERT(m_allClips.count(itemId));
4483     return m_allClips.at(itemId)->m_effectStack;
4484 }
4485 
copyClipEffect(int clipId,const QString & sourceId)4486 bool TimelineModel::copyClipEffect(int clipId, const QString &sourceId)
4487 {
4488     QStringList source = sourceId.split(QLatin1Char('-'));
4489     Q_ASSERT(m_allClips.count(clipId) && source.count() == 3);
4490     int itemType = source.at(0).toInt();
4491     int itemId = source.at(1).toInt();
4492     int itemRow = source.at(2).toInt();
4493     std::shared_ptr<EffectStackModel> effectStack = pCore->getItemEffectStack(itemType, itemId);
4494     return m_allClips.at(clipId)->copyEffect(effectStack, itemRow);
4495 }
4496 
adjustEffectLength(int clipId,const QString & effectId,int duration,int initialDuration)4497 bool TimelineModel::adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration)
4498 {
4499     Q_ASSERT(m_allClips.count(clipId));
4500     Fun undo = []() { return true; };
4501     Fun redo = []() { return true; };
4502     bool res = m_allClips.at(clipId)->adjustEffectLength(effectId, duration, initialDuration, undo, redo);
4503     if (res && initialDuration > 0) {
4504         PUSH_UNDO(undo, redo, i18n("Adjust Fade"));
4505     }
4506     return res;
4507 }
4508 
getCompositionPtr(int compoId) const4509 std::shared_ptr<CompositionModel> TimelineModel::getCompositionPtr(int compoId) const
4510 {
4511     Q_ASSERT(m_allCompositions.count(compoId) > 0);
4512     return m_allCompositions.at(compoId);
4513 }
4514 
getNextId()4515 int TimelineModel::getNextId()
4516 {
4517     return TimelineModel::next_id++;
4518 }
4519 
isClip(int id) const4520 bool TimelineModel::isClip(int id) const
4521 {
4522     return m_allClips.count(id) > 0;
4523 }
4524 
isComposition(int id) const4525 bool TimelineModel::isComposition(int id) const
4526 {
4527     return m_allCompositions.count(id) > 0;
4528 }
4529 
isSubTitle(int id) const4530 bool TimelineModel::isSubTitle(int id) const
4531 {
4532     return m_allSubtitles.count(id) > 0;
4533 }
4534 
isItem(int id) const4535 bool TimelineModel::isItem(int id) const
4536 {
4537     return isClip(id) || isComposition(id) || isSubTitle(id);
4538 }
4539 
isTrack(int id) const4540 bool TimelineModel::isTrack(int id) const
4541 {
4542     return m_iteratorTable.count(id) > 0;
4543 }
4544 
isGroup(int id) const4545 bool TimelineModel::isGroup(int id) const
4546 {
4547     return m_allGroups.count(id) > 0;
4548 }
4549 
updateDuration()4550 void TimelineModel::updateDuration()
4551 {
4552     if (m_closing) {
4553         return;
4554     }
4555     int current = m_blackClip->get_playtime() - TimelineModel::seekDuration;
4556     int duration = 0;
4557     for (const auto &tck : m_iteratorTable) {
4558         auto track = (*tck.second);
4559         duration = qMax(duration, track->trackDuration());
4560     }
4561     if (m_subtitleModel) {
4562         duration = qMax(duration, m_subtitleModel->trackDuration());
4563     }
4564     if (duration != current) {
4565         // update black track length
4566         m_blackClip->set("out", duration + TimelineModel::seekDuration);
4567         emit durationUpdated();
4568         if (m_masterStack) {
4569             emit m_masterStack->dataChanged(QModelIndex(), QModelIndex(), {});
4570         }
4571     }
4572 }
4573 
duration() const4574 int TimelineModel::duration() const
4575 {
4576     return m_tractor->get_playtime() - TimelineModel::seekDuration;
4577 }
4578 
getGroupElements(int clipId)4579 std::unordered_set<int> TimelineModel::getGroupElements(int clipId)
4580 {
4581     int groupId = m_groups->getRootId(clipId);
4582     return m_groups->getLeaves(groupId);
4583 }
4584 
getProfile()4585 Mlt::Profile *TimelineModel::getProfile()
4586 {
4587     return m_profile;
4588 }
4589 
requestReset(Fun & undo,Fun & redo)4590 bool TimelineModel::requestReset(Fun &undo, Fun &redo)
4591 {
4592     std::vector<int> all_ids;
4593     for (const auto &track : m_iteratorTable) {
4594         all_ids.push_back(track.first);
4595     }
4596     bool ok = true;
4597     for (int trackId : all_ids) {
4598         ok = ok && requestTrackDeletion(trackId, undo, redo);
4599     }
4600     return ok;
4601 }
4602 
setUndoStack(std::weak_ptr<DocUndoStack> undo_stack)4603 void TimelineModel::setUndoStack(std::weak_ptr<DocUndoStack> undo_stack)
4604 {
4605     m_undoStack = std::move(undo_stack);
4606 }
4607 
suggestSnapPoint(int pos,int snapDistance)4608 int TimelineModel::suggestSnapPoint(int pos, int snapDistance)
4609 {
4610     int cursorPosition = pCore->getTimelinePosition();
4611     m_snaps->addPoint(cursorPosition);
4612     int snapped = m_snaps->getClosestPoint(pos);
4613     m_snaps->removePoint(cursorPosition);
4614     return (qAbs(snapped - pos) < snapDistance ? snapped : pos);
4615 }
4616 
getBestSnapPos(int referencePos,int diff,std::vector<int> pts,int cursorPosition,int snapDistance)4617 int TimelineModel::getBestSnapPos(int referencePos, int diff, std::vector<int> pts, int cursorPosition, int snapDistance)
4618 {
4619     if (!pts.empty()) {
4620         if (m_editMode == TimelineMode::NormalEdit) {
4621             m_snaps->ignore(pts);
4622         }
4623     } else {
4624         return -1;
4625     }
4626     // Sort and remove duplicates
4627     std::sort(pts.begin(), pts.end());
4628     pts.erase( std::unique(pts.begin(), pts.end()), pts.end());
4629     m_snaps->addPoint(cursorPosition);
4630     int closest = -1;
4631     int lowestDiff = snapDistance + 1;
4632     for (int point : pts) {
4633         int snapped = m_snaps->getClosestPoint(point + diff);
4634         int currentDiff = qAbs(point + diff - snapped);
4635         if (currentDiff < lowestDiff) {
4636             lowestDiff = currentDiff;
4637             closest = snapped - (point - referencePos);
4638             if (lowestDiff < 2) {
4639                 break;
4640             }
4641         }
4642     }
4643     if (m_editMode == TimelineMode::NormalEdit) {
4644         m_snaps->unIgnore();
4645     }
4646     m_snaps->removePoint(cursorPosition);
4647     return closest;
4648 }
4649 
getNextSnapPos(int pos,std::vector<int> & snaps)4650 int TimelineModel::getNextSnapPos(int pos, std::vector<int> &snaps)
4651 {
4652     QVector<int>tracks;
4653     // Get active tracks
4654     auto it = m_allTracks.cbegin();
4655     while (it != m_allTracks.cend()) {
4656         if ((*it)->shouldReceiveTimelineOp()) {
4657             tracks << (*it)->getId();
4658         }
4659         ++it;
4660     }
4661     bool hasSubtitles = m_subtitleModel && !m_allSubtitles.empty();
4662     bool filterOutSubtitles = false;
4663     if (hasSubtitles) {
4664         // If subtitle track is locked or hidden, don't snap to it
4665         if (m_subtitleModel->isLocked() || !KdenliveSettings::showSubtitles()) {
4666             filterOutSubtitles = true;
4667         }
4668     }
4669     if ((tracks.isEmpty() || tracks.count() == int(m_allTracks.size())) && !filterOutSubtitles) {
4670         // No active track, use all possible snap points
4671         return m_snaps->getNextPoint(pos);
4672     }
4673     // Build snap points for selected tracks
4674     for (const auto &cp : m_allClips) {
4675         // Check if clip is on a target track
4676         if (tracks.contains(cp.second->getCurrentTrackId())) {
4677             auto clip = (cp.second);
4678             clip->allSnaps(snaps);
4679         }
4680     }
4681     // Subtitle snaps
4682     if (hasSubtitles && !filterOutSubtitles) {
4683         // Add subtitle snaps
4684         m_subtitleModel->allSnaps(snaps);
4685     }
4686     // sort snaps
4687     std::sort(snaps.begin(), snaps.end());
4688     for (auto i : snaps) {
4689         if (int(i) > pos) {
4690             return int(i);
4691         }
4692     }
4693     return pos;
4694 }
4695 
getPreviousSnapPos(int pos,std::vector<int> & snaps)4696 int TimelineModel::getPreviousSnapPos(int pos, std::vector<int> &snaps)
4697 {
4698     QVector<int>tracks;
4699     // Get active tracks
4700     auto it = m_allTracks.cbegin();
4701     while (it != m_allTracks.cend()) {
4702         if ((*it)->shouldReceiveTimelineOp()) {
4703             tracks << (*it)->getId();
4704         }
4705         ++it;
4706     }
4707     bool hasSubtitles = m_subtitleModel && !m_allSubtitles.empty();
4708     bool filterOutSubtitles = false;
4709     if (hasSubtitles) {
4710         // If subtitle track is locked or hidden, don't snap to it
4711         if (m_subtitleModel->isLocked() || !KdenliveSettings::showSubtitles()) {
4712             filterOutSubtitles = true;
4713         }
4714     }
4715     if ((tracks.isEmpty() || tracks.count() == int(m_allTracks.size())) && !filterOutSubtitles) {
4716         // No active track, use all possible snap points
4717         return m_snaps->getPreviousPoint(int(pos));
4718     }
4719     // Build snap points for selected tracks
4720     for (const auto &cp : m_allClips) {
4721         // Check if clip is on a target track
4722         if (tracks.contains(cp.second->getCurrentTrackId())) {
4723             auto clip = (cp.second);
4724             clip->allSnaps(snaps);
4725         }
4726     }
4727     // Subtitle snaps
4728     if (hasSubtitles && !filterOutSubtitles) {
4729         // Add subtitle snaps
4730         m_subtitleModel->allSnaps(snaps);
4731     }
4732     // sort snaps
4733     std::sort(snaps.begin(), snaps.end());
4734     // sort descending
4735     std::reverse(snaps.begin(),snaps.end());
4736     for (auto i : snaps) {
4737         if (int(i) < pos) {
4738             return int(i);
4739         }
4740     }
4741     return 0;
4742 }
4743 
addSnap(int pos)4744 void TimelineModel::addSnap(int pos)
4745 {
4746     TRACE(pos);
4747     return m_snaps->addPoint(pos);
4748 }
4749 
removeSnap(int pos)4750 void TimelineModel::removeSnap(int pos)
4751 {
4752     TRACE(pos);
4753     return m_snaps->removePoint(pos);
4754 }
4755 
registerComposition(const std::shared_ptr<CompositionModel> & composition)4756 void TimelineModel::registerComposition(const std::shared_ptr<CompositionModel> &composition)
4757 {
4758     int id = composition->getId();
4759     Q_ASSERT(m_allCompositions.count(id) == 0);
4760     m_allCompositions[id] = composition;
4761     m_groups->createGroupItem(id);
4762 }
4763 
requestCompositionInsertion(const QString & transitionId,int trackId,int position,int length,std::unique_ptr<Mlt::Properties> transProps,int & id,bool logUndo)4764 bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int position, int length, std::unique_ptr<Mlt::Properties> transProps,
4765                                                 int &id, bool logUndo)
4766 {
4767     QWriteLocker locker(&m_lock);
4768     // TRACE(transitionId, trackId, position, length, transProps.get(), id, logUndo);
4769     Fun undo = []() { return true; };
4770     Fun redo = []() { return true; };
4771     bool result = requestCompositionInsertion(transitionId, trackId, -1, position, length, std::move(transProps), id, undo, redo, logUndo);
4772     if (result && logUndo) {
4773         PUSH_UNDO(undo, redo, i18n("Insert Composition"));
4774     }
4775     // TRACE_RES(result);
4776     return result;
4777 }
4778 
requestCompositionInsertion(const QString & transitionId,int trackId,int compositionTrack,int position,int length,std::unique_ptr<Mlt::Properties> transProps,int & id,Fun & undo,Fun & redo,bool finalMove,QString originalDecimalPoint)4779 bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length,
4780                                                 std::unique_ptr<Mlt::Properties> transProps, int &id, Fun &undo, Fun &redo, bool finalMove, QString originalDecimalPoint)
4781 {
4782     int compositionId = TimelineModel::getNextId();
4783     id = compositionId;
4784     Fun local_undo = deregisterComposition_lambda(compositionId);
4785     CompositionModel::construct(shared_from_this(), transitionId, originalDecimalPoint, compositionId, std::move(transProps));
4786     auto composition = m_allCompositions[compositionId];
4787     Fun local_redo = [composition, this]() {
4788         // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it
4789         // back it is sufficient to register it.
4790         registerComposition(composition);
4791         return true;
4792     };
4793     bool res = requestCompositionMove(compositionId, trackId, compositionTrack, position, true, finalMove, local_undo, local_redo);
4794     if (res) {
4795         res = requestItemResize(compositionId, length, true, true, local_undo, local_redo, true);
4796     }
4797     if (!res) {
4798         bool undone = local_undo();
4799         Q_ASSERT(undone);
4800         id = -1;
4801         return false;
4802     }
4803     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
4804     return true;
4805 }
4806 
deregisterComposition_lambda(int compoId)4807 Fun TimelineModel::deregisterComposition_lambda(int compoId)
4808 {
4809     return [this, compoId]() {
4810         Q_ASSERT(m_allCompositions.count(compoId) > 0);
4811         Q_ASSERT(!m_groups->isInGroup(compoId)); // composition must be ungrouped at this point
4812         requestClearSelection(true);
4813         clearAssetView(compoId);
4814         m_allCompositions.erase(compoId);
4815         m_groups->destructGroupItem(compoId);
4816         return true;
4817     };
4818 }
4819 
getSubtitlePosition(int subId) const4820 int TimelineModel::getSubtitlePosition(int subId) const
4821 {
4822     Q_ASSERT(m_allSubtitles.count(subId) > 0);
4823     return m_allSubtitles.at(subId).frames(pCore->getCurrentFps());
4824 }
4825 
getCompositionPosition(int compoId) const4826 int TimelineModel::getCompositionPosition(int compoId) const
4827 {
4828     Q_ASSERT(m_allCompositions.count(compoId) > 0);
4829     const auto trans = m_allCompositions.at(compoId);
4830     return trans->getPosition();
4831 }
4832 
getCompositionPlaytime(int compoId) const4833 int TimelineModel::getCompositionPlaytime(int compoId) const
4834 {
4835     READ_LOCK();
4836     Q_ASSERT(m_allCompositions.count(compoId) > 0);
4837     const auto trans = m_allCompositions.at(compoId);
4838     int playtime = trans->getPlaytime();
4839     return playtime;
4840 }
4841 
getItemPosition(int itemId) const4842 int TimelineModel::getItemPosition(int itemId) const
4843 {
4844     if (isClip(itemId)) {
4845         return getClipPosition(itemId);
4846     }
4847     if (isComposition(itemId)) {
4848         return getCompositionPosition(itemId);
4849     }
4850     if (isSubTitle(itemId)) {
4851         return getSubtitlePosition(itemId);
4852     }
4853     return -1;
4854 }
4855 
getItemPlaytime(int itemId) const4856 int TimelineModel::getItemPlaytime(int itemId) const
4857 {
4858     if (isClip(itemId)) {
4859         return getClipPlaytime(itemId);
4860     }
4861     if (isComposition(itemId)) {
4862         return getCompositionPlaytime(itemId);
4863     }
4864     if (isSubTitle(itemId)) {
4865         return m_subtitleModel->getSubtitlePlaytime(itemId);
4866     }
4867     return -1;
4868 }
4869 
getTrackCompositionsCount(int trackId) const4870 int TimelineModel::getTrackCompositionsCount(int trackId) const
4871 {
4872     Q_ASSERT(isTrack(trackId));
4873     return getTrackById_const(trackId)->getCompositionsCount();
4874 }
4875 
requestCompositionMove(int compoId,int trackId,int position,bool updateView,bool logUndo)4876 bool TimelineModel::requestCompositionMove(int compoId, int trackId, int position, bool updateView, bool logUndo)
4877 {
4878     QWriteLocker locker(&m_lock);
4879     Q_ASSERT(isComposition(compoId));
4880     if (m_allCompositions[compoId]->getPosition() == position && getCompositionTrackId(compoId) == trackId) {
4881         return true;
4882     }
4883     if (m_groups->isInGroup(compoId)) {
4884         // element is in a group.
4885         int groupId = m_groups->getRootId(compoId);
4886         int current_trackId = getCompositionTrackId(compoId);
4887         int track_pos1 = getTrackPosition(trackId);
4888         int track_pos2 = getTrackPosition(current_trackId);
4889         int delta_track = track_pos1 - track_pos2;
4890         int delta_pos = position - m_allCompositions[compoId]->getPosition();
4891         return requestGroupMove(compoId, groupId, delta_track, delta_pos, true, updateView, logUndo);
4892     }
4893     std::function<bool(void)> undo = []() { return true; };
4894     std::function<bool(void)> redo = []() { return true; };
4895     int min = getCompositionPosition(compoId);
4896     int max = min + getCompositionPlaytime(compoId);
4897     int tk = getCompositionTrackId(compoId);
4898     bool res = requestCompositionMove(compoId, trackId, m_allCompositions[compoId]->getForcedTrack(), position, updateView, logUndo, undo, redo);
4899     if (tk > -1) {
4900         min = qMin(min, getCompositionPosition(compoId));
4901         max = qMax(max, getCompositionPosition(compoId));
4902     } else {
4903         min = getCompositionPosition(compoId);
4904         max = min + getCompositionPlaytime(compoId);
4905     }
4906 
4907     if (res && logUndo) {
4908         PUSH_UNDO(undo, redo, i18n("Move composition"));
4909         checkRefresh(min, max);
4910     }
4911     return res;
4912 }
4913 
isAudioTrack(int trackId) const4914 bool TimelineModel::isAudioTrack(int trackId) const
4915 {
4916     READ_LOCK();
4917     Q_ASSERT(isTrack(trackId));
4918     auto it = m_iteratorTable.at(trackId);
4919     return (*it)->isAudioTrack();
4920 }
4921 
requestCompositionMove(int compoId,int trackId,int compositionTrack,int position,bool updateView,bool finalMove,Fun & undo,Fun & redo)4922 bool TimelineModel::requestCompositionMove(int compoId, int trackId, int compositionTrack, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo)
4923 {
4924     QWriteLocker locker(&m_lock);
4925     Q_ASSERT(isComposition(compoId));
4926     Q_ASSERT(isTrack(trackId));
4927     if (compositionTrack == -1 || (compositionTrack > 0 && trackId == getTrackIndexFromPosition(compositionTrack - 1))) {
4928         compositionTrack = getPreviousVideoTrackPos(trackId);
4929     }
4930     if (compositionTrack == -1) {
4931         // it doesn't make sense to insert a composition on the last track
4932         qDebug() << "Move failed because of last track";
4933         return false;
4934     }
4935 
4936     Fun local_undo = []() { return true; };
4937     Fun local_redo = []() { return true; };
4938     bool ok = true;
4939     int old_trackId = getCompositionTrackId(compoId);
4940     bool notifyViewOnly = false;
4941     Fun update_model = []() { return true; };
4942     if (updateView && old_trackId == trackId) {
4943         // Move on same track, only send view update
4944         updateView = false;
4945         notifyViewOnly = true;
4946         update_model = [compoId, this]() {
4947             QModelIndex modelIndex = makeCompositionIndexFromID(compoId);
4948             notifyChange(modelIndex, modelIndex, StartRole);
4949             return true;
4950         };
4951     }
4952     if (old_trackId != -1) {
4953         Fun delete_operation = []() { return true; };
4954         Fun delete_reverse = []() { return true; };
4955         if (old_trackId != trackId) {
4956             delete_operation = [this, compoId]() {
4957                 bool res = unplantComposition(compoId);
4958                 if (res) m_allCompositions[compoId]->setATrack(-1, -1);
4959                 return res;
4960             };
4961             int oldAtrack = m_allCompositions[compoId]->getATrack();
4962             delete_reverse = [this, compoId, oldAtrack, updateView]() {
4963                 m_allCompositions[compoId]->setATrack(oldAtrack, oldAtrack <= 0 ? -1 : getTrackIndexFromPosition(oldAtrack - 1));
4964                 return replantCompositions(compoId, updateView);
4965             };
4966         }
4967         ok = delete_operation();
4968         if (!ok) qDebug() << "Move failed because of first delete operation";
4969 
4970         if (ok) {
4971             if (notifyViewOnly) {
4972                 PUSH_LAMBDA(update_model, local_undo);
4973             }
4974             UPDATE_UNDO_REDO(delete_operation, delete_reverse, local_undo, local_redo);
4975             ok = getTrackById(old_trackId)->requestCompositionDeletion(compoId, updateView, finalMove, local_undo, local_redo, false);
4976         }
4977         if (!ok) {
4978             qDebug() << "Move failed because of first deletion request";
4979             bool undone = local_undo();
4980             Q_ASSERT(undone);
4981             return false;
4982         }
4983     }
4984     ok = getTrackById(trackId)->requestCompositionInsertion(compoId, position, updateView, finalMove, local_undo, local_redo);
4985     if (!ok) qDebug() << "Move failed because of second insertion request";
4986     if (ok) {
4987         Fun insert_operation = []() { return true; };
4988         Fun insert_reverse = []() { return true; };
4989         if (old_trackId != trackId) {
4990             insert_operation = [this, compoId, compositionTrack, updateView]() {
4991                 m_allCompositions[compoId]->setATrack(compositionTrack, compositionTrack <= 0 ? -1 : getTrackIndexFromPosition(compositionTrack - 1));
4992                 return replantCompositions(compoId, updateView);
4993             };
4994             insert_reverse = [this, compoId]() {
4995                 bool res = unplantComposition(compoId);
4996                 if (res) m_allCompositions[compoId]->setATrack(-1, -1);
4997                 return res;
4998             };
4999         }
5000         ok = insert_operation();
5001         if (!ok) qDebug() << "Move failed because of second insert operation";
5002         if (ok) {
5003             if (notifyViewOnly) {
5004                 PUSH_LAMBDA(update_model, local_redo);
5005             }
5006             UPDATE_UNDO_REDO(insert_operation, insert_reverse, local_undo, local_redo);
5007         }
5008     }
5009     if (!ok) {
5010         bool undone = local_undo();
5011         Q_ASSERT(undone);
5012         return false;
5013     }
5014     update_model();
5015     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
5016     return true;
5017 }
5018 
replantCompositions(int currentCompo,bool updateView)5019 bool TimelineModel::replantCompositions(int currentCompo, bool updateView)
5020 {
5021     // We ensure that the compositions are planted in a decreasing order of a_track, and increasing order of b_track.
5022     // For that, there is no better option than to disconnect every composition and then reinsert everything in the correct order.
5023     std::vector<std::pair<int, int>> compos;
5024     for (const auto &compo : m_allCompositions) {
5025         int trackId = compo.second->getCurrentTrackId();
5026         if (trackId == -1 || compo.second->getATrack() == -1) {
5027             continue;
5028         }
5029         // Note: we need to retrieve the position of the track, that is its melt index.
5030         int trackPos = getTrackMltIndex(trackId);
5031         compos.emplace_back(trackPos, compo.first);
5032         if (compo.first != currentCompo) {
5033             unplantComposition(compo.first);
5034         }
5035     }
5036     // sort by decreasing b_track
5037     std::sort(compos.begin(), compos.end(), [&](const std::pair<int, int> &a, const std::pair<int, int> &b) {
5038         if (m_allCompositions[a.second]->getATrack() == m_allCompositions[b.second]->getATrack()) {
5039             return a.first < b.first;
5040         }
5041         return m_allCompositions[a.second]->getATrack() > m_allCompositions[b.second]->getATrack();
5042     });
5043     // replant
5044     QScopedPointer<Mlt::Field> field(m_tractor->field());
5045     field->lock();
5046 
5047     // Unplant track compositing
5048     mlt_service nextservice = mlt_service_get_producer(field->get_service());
5049     mlt_properties properties = MLT_SERVICE_PROPERTIES(nextservice);
5050     QString resource = mlt_properties_get(properties, "mlt_service");
5051 
5052     mlt_service_type mlt_type = mlt_service_identify(nextservice);
5053     QList<Mlt::Transition *> trackCompositions;
5054     while (mlt_type == mlt_service_transition_type) {
5055         Mlt::Transition transition(reinterpret_cast<mlt_transition>(nextservice));
5056         nextservice = mlt_service_producer(nextservice);
5057         int internal = transition.get_int("internal_added");
5058         if (internal > 0 && resource != QLatin1String("mix")) {
5059             trackCompositions << new Mlt::Transition(transition);
5060             field->disconnect_service(transition);
5061             transition.disconnect_all_producers();
5062         }
5063         if (nextservice == nullptr) {
5064             break;
5065         }
5066         mlt_type = mlt_service_identify(nextservice);
5067         properties = MLT_SERVICE_PROPERTIES(nextservice);
5068         resource = mlt_properties_get(properties, "mlt_service");
5069     }
5070     // Sort track compositing
5071     std::sort(trackCompositions.begin(), trackCompositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); });
5072 
5073     for (const auto &compo : compos) {
5074         int aTrack = m_allCompositions[compo.second]->getATrack();
5075         Q_ASSERT(aTrack != -1 && aTrack < m_tractor->count());
5076 
5077         Mlt::Transition &transition = *m_allCompositions[compo.second].get();
5078         transition.set_tracks(aTrack, compo.first);
5079         int ret = field->plant_transition(transition, aTrack, compo.first);
5080 
5081         mlt_service consumer = mlt_service_consumer(transition.get_service());
5082         Q_ASSERT(consumer != nullptr);
5083         if (ret != 0) {
5084             field->unlock();
5085             return false;
5086         }
5087     }
5088     // Replant last tracks compositing
5089     while (!trackCompositions.isEmpty()) {
5090         Mlt::Transition *firstTr = trackCompositions.takeFirst();
5091         field->plant_transition(*firstTr, firstTr->get_a_track(), firstTr->get_b_track());
5092     }
5093     field->unlock();
5094     if (updateView) {
5095         QModelIndex modelIndex = makeCompositionIndexFromID(currentCompo);
5096         notifyChange(modelIndex, modelIndex, ItemATrack);
5097     }
5098     return true;
5099 }
5100 
unplantComposition(int compoId)5101 bool TimelineModel::unplantComposition(int compoId)
5102 {
5103     Mlt::Transition &transition = *m_allCompositions[compoId].get();
5104     mlt_service consumer = mlt_service_consumer(transition.get_service());
5105     Q_ASSERT(consumer != nullptr);
5106     QScopedPointer<Mlt::Field> field(m_tractor->field());
5107     field->lock();
5108     field->disconnect_service(transition);
5109     int ret = transition.disconnect_all_producers();
5110 
5111     mlt_service nextservice = mlt_service_get_producer(transition.get_service());
5112     // mlt_service consumer = mlt_service_consumer(transition.get_service());
5113     Q_ASSERT(nextservice == nullptr);
5114     // Q_ASSERT(consumer == nullptr);
5115     field->unlock();
5116     return ret != 0;
5117 }
5118 
checkConsistency()5119 bool TimelineModel::checkConsistency()
5120 {
5121     // We store all in/outs of clips to check snap points
5122     std::map<int, int> snaps;
5123 
5124     for (const auto &tck : m_iteratorTable) {
5125         auto track = (*tck.second);
5126         // Check parent/children link for tracks
5127         if (auto ptr = track->m_parent.lock()) {
5128             if (ptr.get() != this) {
5129                 qWarning() << "Wrong parent for track" << tck.first;
5130                 return false;
5131             }
5132         } else {
5133             qWarning() << "NULL parent for track" << tck.first;
5134             return false;
5135         }
5136         // check consistency of track
5137         if (!track->checkConsistency()) {
5138             qWarning() << "Consistency check failed for track" << tck.first;
5139             return false;
5140         }
5141     }
5142 
5143     // Check parent/children link for clips
5144     for (const auto &cp : m_allClips) {
5145         auto clip = (cp.second);
5146         // Check parent/children link for tracks
5147         if (auto ptr = clip->m_parent.lock()) {
5148             if (ptr.get() != this) {
5149                 qWarning() << "Wrong parent for clip" << cp.first;
5150                 return false;
5151             }
5152         } else {
5153             qWarning() << "NULL parent for clip" << cp.first;
5154             return false;
5155         }
5156         if (getClipTrackId(cp.first) != -1) {
5157             snaps[clip->getPosition()] += 1;
5158             snaps[clip->getPosition() + clip->getPlaytime()] += 1;
5159             if (clip->getMixDuration() > 0) {
5160                 snaps[clip->getPosition() + clip->getMixDuration() - clip->getMixCutPosition()] += 1;
5161             }
5162         }
5163         if (!clip->checkConsistency()) {
5164             qWarning() << "Consistency check failed for clip" << cp.first;
5165             return false;
5166         }
5167     }
5168     for (const auto &cp : m_allCompositions) {
5169         auto clip = (cp.second);
5170         // Check parent/children link for tracks
5171         if (auto ptr = clip->m_parent.lock()) {
5172             if (ptr.get() != this) {
5173                 qWarning() << "Wrong parent for compo" << cp.first;
5174                 return false;
5175             }
5176         } else {
5177             qWarning() << "NULL parent for compo" << cp.first;
5178             return false;
5179         }
5180         if (getCompositionTrackId(cp.first) != -1) {
5181             snaps[clip->getPosition()] += 1;
5182             snaps[clip->getPosition() + clip->getPlaytime()] += 1;
5183         }
5184     }
5185 
5186 
5187 
5188     // Check snaps
5189     auto stored_snaps = m_snaps->_snaps();
5190     if (snaps.size() != stored_snaps.size()) {
5191         qWarning() << "Wrong number of snaps" << snaps.size() << stored_snaps.size();
5192         return false;
5193     }
5194     for (auto i = snaps.begin(), j = stored_snaps.begin(); i != snaps.end(); ++i, ++j) {
5195         if (*i != *j) {
5196             qWarning() << "Wrong snap info at point" << (*i).first;
5197             return false;
5198         }
5199     }
5200 
5201     // We check consistency with bin model
5202     auto binClips = pCore->projectItemModel()->getAllClipIds();
5203     // First step: all clips referenced by the bin model exist and are inserted
5204     for (const auto &binClip : binClips) {
5205         auto projClip = pCore->projectItemModel()->getClipByBinID(binClip);
5206         for (const auto &insertedClip : projClip->m_registeredClips) {
5207             if (auto ptr = insertedClip.second.lock()) {
5208                 if (ptr.get() == this) { // check we are talking of this timeline
5209                     if (!isClip(insertedClip.first)) {
5210                         qWarning() << "Bin model registers a bad clip ID" << insertedClip.first;
5211                         return false;
5212                     }
5213                 }
5214             } else {
5215                 qWarning() << "Bin model registers a clip in a NULL timeline" << insertedClip.first;
5216                 return false;
5217             }
5218         }
5219     }
5220 
5221     // Second step: all clips are referenced
5222     for (const auto &clip : m_allClips) {
5223         auto binId = clip.second->m_binClipId;
5224         auto projClip = pCore->projectItemModel()->getClipByBinID(binId);
5225         if (projClip->m_registeredClips.count(clip.first) == 0) {
5226             qWarning() << "Clip " << clip.first << "not registered in bin";
5227             return false;
5228         }
5229     }
5230 
5231     // We now check consistency of the compositions. For that, we list all compositions of the tractor, and see if we have a matching one in our
5232     // m_allCompositions
5233     std::unordered_set<int> remaining_compo;
5234     for (const auto &compo : m_allCompositions) {
5235         if (getCompositionTrackId(compo.first) != -1 && m_allCompositions[compo.first]->getATrack() != -1) {
5236             remaining_compo.insert(compo.first);
5237 
5238             // check validity of the consumer
5239             Mlt::Transition &transition = *m_allCompositions[compo.first].get();
5240             mlt_service consumer = mlt_service_consumer(transition.get_service());
5241             Q_ASSERT(consumer != nullptr);
5242         }
5243     }
5244     QScopedPointer<Mlt::Field> field(m_tractor->field());
5245     field->lock();
5246 
5247     mlt_service nextservice = mlt_service_get_producer(field->get_service());
5248     mlt_service_type mlt_type = mlt_service_identify(nextservice);
5249     while (nextservice != nullptr) {
5250         if (mlt_type == mlt_service_transition_type) {
5251             auto tr = mlt_transition(nextservice);
5252             if (mlt_properties_get_int( MLT_TRANSITION_PROPERTIES(tr), "internal_added") > 0) {
5253                 // Skip track compositing
5254                 nextservice = mlt_service_producer(nextservice);
5255                 continue;
5256             }
5257             int currentTrack = mlt_transition_get_b_track(tr);
5258             int currentATrack = mlt_transition_get_a_track(tr);
5259             if (currentTrack == currentATrack) {
5260                 // Skip invalid transitions created by MLT on track deletion
5261                 nextservice = mlt_service_producer(nextservice);
5262                 continue;
5263             }
5264 
5265             int currentIn = mlt_transition_get_in(tr);
5266             int currentOut = mlt_transition_get_out(tr);
5267 
5268             int foundId = -1;
5269             // we iterate to try to find a matching compo
5270             for (int compoId : remaining_compo) {
5271                 if (getTrackMltIndex(getCompositionTrackId(compoId)) == currentTrack && m_allCompositions[compoId]->getATrack() == currentATrack &&
5272                     m_allCompositions[compoId]->getIn() == currentIn && m_allCompositions[compoId]->getOut() == currentOut) {
5273                     foundId = compoId;
5274                     break;
5275                 }
5276             }
5277             if (foundId == -1) {
5278                 qWarning() << "No matching composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / "
5279                          << currentATrack <<", SERVICE: "<<mlt_properties_get( MLT_TRANSITION_PROPERTIES(tr), "mlt_service")<<"\nID: "<<mlt_properties_get( MLT_TRANSITION_PROPERTIES(tr), "id");
5280                 field->unlock();
5281                 return false;
5282             }
5283             remaining_compo.erase(foundId);
5284         }
5285         nextservice = mlt_service_producer(nextservice);
5286         if (nextservice == nullptr) {
5287             break;
5288         }
5289         mlt_type = mlt_service_identify(nextservice);
5290     }
5291     field->unlock();
5292 
5293     if (!remaining_compo.empty()) {
5294         qWarning() << "Compositions have not been found:";
5295         for (int compoId : remaining_compo) {
5296             qWarning() << compoId;
5297         }
5298         return false;
5299     }
5300 
5301     // We check consistency of groups
5302     if (!m_groups->checkConsistency(true, true)) {
5303         qWarning() << "error in group consistency";
5304         return false;
5305     }
5306 
5307     // Check that the selection is in a valid state:
5308     if (m_currentSelection != -1 && !isClip(m_currentSelection) && !isComposition(m_currentSelection) && !isSubTitle(m_currentSelection) && !isGroup(m_currentSelection)) {
5309         qWarning() << "Selection is in inconsistent state";
5310         return false;
5311     }
5312     return true;
5313 }
5314 
setTimelineEffectsEnabled(bool enabled)5315 void TimelineModel::setTimelineEffectsEnabled(bool enabled)
5316 {
5317     m_timelineEffectsEnabled = enabled;
5318     // propagate info to clips
5319     for (const auto &clip : m_allClips) {
5320         clip.second->setTimelineEffectsEnabled(enabled);
5321     }
5322 
5323     // TODO if we support track effects, they should be disabled here too
5324 }
5325 
producer()5326 std::shared_ptr<Mlt::Producer> TimelineModel::producer()
5327 {
5328     return std::make_shared<Mlt::Producer>(tractor());
5329 }
5330 
checkRefresh(int start,int end)5331 void TimelineModel::checkRefresh(int start, int end)
5332 {
5333     if (m_blockRefresh) {
5334         return;
5335     }
5336     int currentPos = tractor()->position();
5337     if (currentPos >= start && currentPos < end) {
5338         emit requestMonitorRefresh();
5339     }
5340 }
5341 
clearAssetView(int itemId)5342 void TimelineModel::clearAssetView(int itemId)
5343 {
5344     emit requestClearAssetView(itemId);
5345 }
5346 
getCompositionParameterModel(int compoId) const5347 std::shared_ptr<AssetParameterModel> TimelineModel::getCompositionParameterModel(int compoId) const
5348 {
5349     READ_LOCK();
5350     Q_ASSERT(isComposition(compoId));
5351     return std::static_pointer_cast<AssetParameterModel>(m_allCompositions.at(compoId));
5352 }
5353 
getClipEffectStackModel(int clipId) const5354 std::shared_ptr<EffectStackModel> TimelineModel::getClipEffectStackModel(int clipId) const
5355 {
5356     READ_LOCK();
5357     Q_ASSERT(isClip(clipId));
5358     return std::static_pointer_cast<EffectStackModel>(m_allClips.at(clipId)->m_effectStack);
5359 }
5360 
getClipMixStackModel(int clipId) const5361 std::shared_ptr<EffectStackModel> TimelineModel::getClipMixStackModel(int clipId) const
5362 {
5363     READ_LOCK();
5364     Q_ASSERT(isClip(clipId));
5365     return std::static_pointer_cast<EffectStackModel>(m_allClips.at(clipId)->m_effectStack);
5366 }
5367 
getTrackEffectStackModel(int trackId)5368 std::shared_ptr<EffectStackModel> TimelineModel::getTrackEffectStackModel(int trackId)
5369 {
5370     READ_LOCK();
5371     Q_ASSERT(isTrack(trackId));
5372     return getTrackById(trackId)->m_effectStack;
5373 }
5374 
getMasterEffectStackModel()5375 std::shared_ptr<EffectStackModel> TimelineModel::getMasterEffectStackModel()
5376 {
5377     READ_LOCK();
5378     if (m_masterStack == nullptr) {
5379         m_masterService.reset(new Mlt::Service(*m_tractor.get()));
5380         m_masterStack = EffectStackModel::construct(m_masterService, {ObjectType::Master, 0}, m_undoStack);
5381         connect(m_masterStack.get(), &EffectStackModel::updateMasterZones, pCore.get(), &Core::updateMasterZones);
5382     }
5383     return m_masterStack;
5384 }
5385 
importMasterEffects(std::weak_ptr<Mlt::Service> service)5386 void TimelineModel::importMasterEffects(std::weak_ptr<Mlt::Service> service)
5387 {
5388     READ_LOCK();
5389     if (m_masterStack == nullptr) {
5390         getMasterEffectStackModel();
5391     }
5392     m_masterStack->importEffects(std::move(service), PlaylistState::Disabled);
5393 }
5394 
5395 
extractCompositionLumas() const5396 QStringList TimelineModel::extractCompositionLumas() const
5397 {
5398     QStringList urls;
5399     for (const auto &compo : m_allCompositions) {
5400         QString luma = compo.second->getProperty(QStringLiteral("resource"));
5401         if(luma.isEmpty()) {
5402             luma = compo.second->getProperty(QStringLiteral("luma"));
5403         }
5404         if (!luma.isEmpty()) {
5405             urls << QUrl::fromLocalFile(luma).toLocalFile();
5406         }
5407     }
5408     urls.removeDuplicates();
5409     return urls;
5410 }
5411 
extractExternalEffectFiles() const5412 QStringList TimelineModel::extractExternalEffectFiles() const
5413 {
5414     QStringList urls;
5415     for (const auto &clip : m_allClips) {
5416         urls << clip.second->externalFiles();
5417     }
5418     return urls;
5419 }
5420 
adjustAssetRange(int clipId,int in,int out)5421 void TimelineModel::adjustAssetRange(int clipId, int in, int out)
5422 {
5423     Q_UNUSED(clipId)
5424     Q_UNUSED(in)
5425     Q_UNUSED(out)
5426     // pCore->adjustAssetRange(clipId, in, out);
5427 }
5428 
requestClipReload(int clipId,int forceDuration)5429 void TimelineModel::requestClipReload(int clipId, int forceDuration)
5430 {
5431     std::function<bool(void)> local_undo = []() { return true; };
5432     std::function<bool(void)> local_redo = []() { return true; };
5433 
5434     // in order to make the producer change effective, we need to unplant / replant the clip in int track
5435     int old_trackId = getClipTrackId(clipId);
5436     int oldPos = getClipPosition(clipId);
5437     int oldOut = getClipIn(clipId) + getClipPlaytime(clipId);
5438     int currentSubplaylist = m_allClips[clipId]->getSubPlaylistIndex();
5439     int maxDuration = m_allClips[clipId]->getMaxDuration();
5440     bool hasPitch = false;
5441     double speed = m_allClips[clipId]->getSpeed();
5442     PlaylistState::ClipState state = m_allClips[clipId]->clipState();
5443     if (!qFuzzyCompare(speed, 1.)) {
5444         hasPitch = m_allClips[clipId]->getIntProperty(QStringLiteral("warp_pitch"));
5445     }
5446     int audioStream = m_allClips[clipId]->getIntProperty(QStringLiteral("audio_index"));
5447     bool timeremap = m_allClips[clipId]->isChain();
5448     // Check if clip out is longer than actual producer duration (if user forced duration)
5449     std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(clipId));
5450     bool refreshView = oldOut > int(binClip->frameDuration()) || forceDuration > -1;
5451     if (old_trackId != -1) {
5452         getTrackById(old_trackId)->requestClipDeletion(clipId, refreshView, true, local_undo, local_redo, false, false);
5453     }
5454     if (old_trackId != -1) {
5455         m_allClips[clipId]->refreshProducerFromBin(old_trackId, state, audioStream, 0, hasPitch, currentSubplaylist == 1, timeremap);
5456         if (forceDuration > -1) {
5457             m_allClips[clipId]->requestResize(forceDuration, true, local_undo, local_redo);
5458         }
5459         getTrackById(old_trackId)->requestClipInsertion(clipId, oldPos, refreshView, true, local_undo, local_redo);
5460         if (maxDuration != m_allClips[clipId]->getMaxDuration()) {
5461             QModelIndex ix = makeClipIndexFromID(clipId);
5462             emit dataChanged(ix, ix, {TimelineModel::MaxDurationRole});
5463         }
5464     }
5465 }
5466 
replugClip(int clipId)5467 void TimelineModel::replugClip(int clipId)
5468 {
5469     int old_trackId = getClipTrackId(clipId);
5470     if (old_trackId != -1) {
5471         getTrackById(old_trackId)->replugClip(clipId);
5472     }
5473 }
5474 
requestClipUpdate(int clipId,const QVector<int> & roles)5475 void TimelineModel::requestClipUpdate(int clipId, const QVector<int> &roles)
5476 {
5477     QModelIndex modelIndex = makeClipIndexFromID(clipId);
5478     if (roles.contains(TimelineModel::ReloadThumbRole)) {
5479         m_allClips[clipId]->forceThumbReload = !m_allClips[clipId]->forceThumbReload;
5480     }
5481     notifyChange(modelIndex, modelIndex, roles);
5482 }
5483 
requestClipTimeWarp(int clipId,double speed,bool pitchCompensate,bool changeDuration,Fun & undo,Fun & redo)5484 bool TimelineModel::requestClipTimeWarp(int clipId, double speed, bool pitchCompensate, bool changeDuration, Fun &undo, Fun &redo)
5485 {
5486     QWriteLocker locker(&m_lock);
5487     std::function<bool(void)> local_undo = []() { return true; };
5488     std::function<bool(void)> local_redo = []() { return true; };
5489     int oldPos = getClipPosition(clipId);
5490     // in order to make the producer change effective, we need to unplant / replant the clip in int track
5491     bool success = true;
5492     int trackId = getClipTrackId(clipId);
5493     if (trackId != -1) {
5494         success = success && getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo, false, false);
5495     }
5496     if (success) {
5497         success = m_allClips[clipId]->useTimewarpProducer(speed, pitchCompensate, changeDuration, local_undo, local_redo);
5498     }
5499     if (trackId != -1) {
5500         success = success && getTrackById(trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo);
5501     }
5502     if (!success) {
5503         local_undo();
5504         return false;
5505     }
5506     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
5507     return success;
5508 }
5509 
requestClipTimeRemap(int clipId,bool enable)5510 bool TimelineModel::requestClipTimeRemap(int clipId, bool enable)
5511 {
5512     if (!enable || !m_allClips[clipId]->isChain()) {
5513         Fun undo = []() { return true; };
5514         Fun redo = []() { return true; };
5515         int splitId = m_groups->getSplitPartner(clipId);
5516         bool result = true;
5517         if (splitId > -1) {
5518             result = requestClipTimeRemap(splitId, enable, undo, redo);
5519         }
5520         result = result && requestClipTimeRemap(clipId, enable, undo, redo);
5521         if (result) {
5522             PUSH_UNDO(undo, redo, i18n("Enable time remap"));
5523             return true;
5524         } else {
5525             return false;
5526         }
5527     } else return true;
5528 }
5529 
getClipProducer(int clipId)5530 std::shared_ptr<Mlt::Producer> TimelineModel::getClipProducer(int clipId)
5531 {
5532     Q_ASSERT(m_allClips.count(clipId) > 0);
5533     return m_allClips[clipId]->getProducer();
5534 }
5535 
requestClipTimeRemap(int clipId,bool enable,Fun & undo,Fun & redo)5536 bool TimelineModel::requestClipTimeRemap(int clipId, bool enable, Fun &undo, Fun &redo)
5537 {
5538     QWriteLocker locker(&m_lock);
5539     std::function<bool(void)> local_undo = []() { return true; };
5540     std::function<bool(void)> local_redo = []() { return true; };
5541     int oldPos = getClipPosition(clipId);
5542     // in order to make the producer change effective, we need to unplant / replant the clip in int track
5543     bool success = true;
5544     int trackId = getClipTrackId(clipId);
5545     int previousDuration = 0;
5546     qDebug()<<"=== REQUEST REMAP: "<<enable<<"\n\nWWWWWWWWWWWWWWWWWWWWWWWWWWWW";
5547     if (!enable && m_allClips[clipId]->isChain()) {
5548         previousDuration = m_allClips[clipId]->getRemapInputDuration();
5549         qDebug()<<"==== CALCULATED INPIUT DURATION: "<<previousDuration<<"\n\nHHHHHHHHHHHHHH";
5550     }
5551     if (trackId != -1) {
5552         success = success && getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo, false, false);
5553     }
5554     if (success) {
5555         success = m_allClips[clipId]->useTimeRemapProducer(enable, local_undo, local_redo);
5556     }
5557     if (trackId != -1) {
5558         success = success && getTrackById(trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo);
5559         if (success && !enable && previousDuration > 0) {
5560             // Restore input duration
5561             requestItemResize(clipId, previousDuration, true, true, local_undo, local_redo);
5562         }
5563     }
5564     if (!success) {
5565         local_undo();
5566         return false;
5567     }
5568     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
5569     return success;
5570 }
5571 
requestClipTimeWarp(int clipId,double speed,bool pitchCompensate,bool changeDuration)5572 bool TimelineModel::requestClipTimeWarp(int clipId, double speed, bool pitchCompensate, bool changeDuration)
5573 {
5574     QWriteLocker locker(&m_lock);
5575     if (qFuzzyCompare(speed, m_allClips[clipId]->getSpeed()) && pitchCompensate == m_allClips[clipId]->getIntProperty("warp_pitch")) {
5576         return true;
5577     }
5578     TRACE(clipId, speed);
5579     Fun undo = []() { return true; };
5580     Fun redo = []() { return true; };
5581     // Get main clip info
5582     int trackId = getClipTrackId(clipId);
5583     bool result = true;
5584     if (trackId != -1) {
5585         // Check if clip has a split partner
5586         int splitId = m_groups->getSplitPartner(clipId);
5587         if (splitId > -1) {
5588             result = requestClipTimeWarp(splitId, speed / 100.0, pitchCompensate, changeDuration, undo, redo);
5589         }
5590         if (result) {
5591             result = requestClipTimeWarp(clipId, speed / 100.0, pitchCompensate, changeDuration, undo, redo);
5592         }
5593         if (!result) {
5594             pCore->displayMessage(i18n("Change speed failed"), ErrorMessage);
5595             undo();
5596             TRACE_RES(false);
5597             return false;
5598         }
5599     } else {
5600         // If clip is not inserted on a track, we just change the producer
5601         result = m_allClips[clipId]->useTimewarpProducer(speed, pitchCompensate, changeDuration, undo, redo);
5602     }
5603     if (result) {
5604         PUSH_UNDO(undo, redo, i18n("Change clip speed"));
5605     }
5606     TRACE_RES(result);
5607     return result;
5608 }
5609 
getTrackTagById(int trackId) const5610 const QString TimelineModel::getTrackTagById(int trackId) const
5611 {
5612     READ_LOCK();
5613     Q_ASSERT(isTrack(trackId));
5614     bool isAudio = getTrackById_const(trackId)->isAudioTrack();
5615     int count = 1;
5616     int totalAudio = 2;
5617     auto it = m_allTracks.cbegin();
5618     bool found = false;
5619     while ((isAudio || !found) && it != m_allTracks.cend()) {
5620         if ((*it)->isAudioTrack()) {
5621             totalAudio++;
5622             if (isAudio && !found) {
5623                 count++;
5624             }
5625         } else if (!isAudio) {
5626             count++;
5627         }
5628         if ((*it)->getId() == trackId) {
5629             found = true;
5630         }
5631         it++;
5632     }
5633     return isAudio ? QStringLiteral("A%1").arg(totalAudio - count) : QStringLiteral("V%1").arg(count - 1);
5634 }
5635 
updateProfile(Mlt::Profile * profile)5636 void TimelineModel::updateProfile(Mlt::Profile *profile)
5637 {
5638     m_profile = profile;
5639     m_tractor->set_profile(*m_profile);
5640     for (int i = 0; i < m_tractor->count(); i++) {
5641         std::shared_ptr<Mlt::Producer> tk(m_tractor->track(i));
5642         tk->set_profile(*m_profile);
5643         if (tk->type() == mlt_service_tractor_type) {
5644             Mlt::Tractor sub(*tk.get());
5645             for (int j = 0; j < sub.count(); j++) {
5646                 std::shared_ptr<Mlt::Producer> subtk(sub.track(j));
5647                 subtk->set_profile(*m_profile);
5648             }
5649         }
5650     }
5651     m_blackClip->set_profile(*m_profile);
5652     // Rebuild compositions since profile has changed
5653     buildTrackCompositing(true);
5654 }
5655 
getBlankSizeNearClip(int clipId,bool after) const5656 int TimelineModel::getBlankSizeNearClip(int clipId, bool after) const
5657 {
5658     READ_LOCK();
5659     Q_ASSERT(m_allClips.count(clipId) > 0);
5660     int trackId = getClipTrackId(clipId);
5661     if (trackId != -1) {
5662         return getTrackById_const(trackId)->getBlankSizeNearClip(clipId, after);
5663     }
5664     return 0;
5665 }
5666 
getPreviousTrackId(int trackId)5667 int TimelineModel::getPreviousTrackId(int trackId)
5668 {
5669     READ_LOCK();
5670     Q_ASSERT(isTrack(trackId));
5671     auto it = m_iteratorTable.at(trackId);
5672     bool audioWanted = (*it)->isAudioTrack();
5673     while (it != m_allTracks.cbegin()) {
5674         --it;
5675         if ((*it)->isAudioTrack() == audioWanted) {
5676             return (*it)->getId();
5677         }
5678     }
5679     return trackId;
5680 }
5681 
getNextTrackId(int trackId)5682 int TimelineModel::getNextTrackId(int trackId)
5683 {
5684     READ_LOCK();
5685     Q_ASSERT(isTrack(trackId));
5686     auto it = m_iteratorTable.at(trackId);
5687     bool audioWanted = (*it)->isAudioTrack();
5688     while (it != m_allTracks.cend()) {
5689         ++it;
5690         if (it != m_allTracks.cend() && (*it)->isAudioTrack() == audioWanted) {
5691             break;
5692         }
5693     }
5694     return it == m_allTracks.cend() ? trackId : (*it)->getId();
5695 }
5696 
requestClearSelection(bool onDeletion)5697 bool TimelineModel::requestClearSelection(bool onDeletion)
5698 {
5699     QWriteLocker locker(&m_lock);
5700     TRACE();
5701     if (m_selectedMix > -1) {
5702         m_selectedMix = -1;
5703         emit selectedMixChanged(-1, nullptr);
5704     }
5705     if (m_currentSelection == -1) {
5706         TRACE_RES(true);
5707         return true;
5708     }
5709     if (isGroup(m_currentSelection)) {
5710         // Reset offset display on clips
5711         std::unordered_set<int> items = m_groups->getLeaves(m_currentSelection);
5712         for (auto &id : items) {
5713             if (isGroup(id)) {
5714                 std::unordered_set<int> children = m_groups->getLeaves(id);
5715                 items.insert(children.begin(), children.end());
5716             } else if (isClip(id)) {
5717                 m_allClips[id]->clearOffset();
5718                 m_allClips[id]->setGrab(false);
5719                 m_allClips[id]->setSelected(false);
5720             } else if (isComposition(id)) {
5721                 m_allCompositions[id]->setGrab(false);
5722                 m_allCompositions[id]->setSelected(false);
5723             } else if (isSubTitle(id)) {
5724                 m_subtitleModel->setSelected(id, false);
5725             }
5726             if (m_groups->getType(m_currentSelection) == GroupType::Selection) {
5727                 m_groups->destructGroupItem(m_currentSelection);
5728             }
5729         }
5730     } else {
5731         if (isClip(m_currentSelection)) {
5732             m_allClips[m_currentSelection]->setGrab(false);
5733             m_allClips[m_currentSelection]->setSelected(false);
5734         } else if (isComposition(m_currentSelection)) {
5735             m_allCompositions[m_currentSelection]->setGrab(false);
5736             m_allCompositions[m_currentSelection]->setSelected(false);
5737         } else if (isSubTitle(m_currentSelection)) {
5738             m_subtitleModel->setSelected(m_currentSelection, false);
5739         }
5740         Q_ASSERT(onDeletion || isClip(m_currentSelection) || isComposition(m_currentSelection) || isSubTitle(m_currentSelection));
5741     }
5742     m_currentSelection = -1;
5743     if (m_subtitleModel) {
5744         m_subtitleModel->clearGrab();
5745     }
5746     emit selectionChanged();
5747     TRACE_RES(true);
5748     return true;
5749 }
5750 
requestMixSelection(int cid)5751 void TimelineModel::requestMixSelection(int cid)
5752 {
5753     requestClearSelection();
5754     int tid = getItemTrackId(cid);
5755     if (tid > -1) {
5756         m_selectedMix = cid;
5757         emit selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid));
5758     }
5759 }
5760 
requestClearSelection(bool onDeletion,Fun & undo,Fun & redo)5761 void TimelineModel::requestClearSelection(bool onDeletion, Fun &undo, Fun &redo)
5762 {
5763     Fun operation = [this, onDeletion]() {
5764         requestClearSelection(onDeletion);
5765         return true;
5766     };
5767     Fun reverse = [this, clips = getCurrentSelection()]() { return requestSetSelection(clips); };
5768     if (operation()) {
5769         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
5770     }
5771 }
5772 
clearGroupSelectionOnDelete(std::vector<int> groups)5773 void TimelineModel::clearGroupSelectionOnDelete(std::vector<int>groups)
5774 {
5775     READ_LOCK();
5776     if (std::find(groups.begin(), groups.end(), m_currentSelection) != groups.end()) {
5777         requestClearSelection(true);
5778     }
5779 }
5780 
5781 
getCurrentSelection() const5782 std::unordered_set<int> TimelineModel::getCurrentSelection() const
5783 {
5784     READ_LOCK();
5785     if (m_currentSelection == -1) {
5786         return {};
5787     }
5788     if (isGroup(m_currentSelection)) {
5789         return m_groups->getLeaves(m_currentSelection);
5790     } else {
5791         Q_ASSERT(isClip(m_currentSelection) || isComposition(m_currentSelection) || isSubTitle(m_currentSelection));
5792         return {m_currentSelection};
5793     }
5794 }
5795 
requestAddToSelection(int itemId,bool clear)5796 void TimelineModel::requestAddToSelection(int itemId, bool clear)
5797 {
5798     QWriteLocker locker(&m_lock);
5799     TRACE(itemId, clear);
5800     if (clear) {
5801         requestClearSelection();
5802     }
5803     std::unordered_set<int> selection = getCurrentSelection();
5804     if (selection.count(itemId) == 0) {
5805         selection.insert(itemId);
5806         requestSetSelection(selection);
5807     }
5808 }
5809 
requestRemoveFromSelection(int itemId)5810 void TimelineModel::requestRemoveFromSelection(int itemId)
5811 {
5812     QWriteLocker locker(&m_lock);
5813     TRACE(itemId);
5814     std::unordered_set<int> all_items = {itemId};
5815     int parentGroup = m_groups->getDirectAncestor(itemId);
5816     if (parentGroup > -1 && m_groups->getType(parentGroup) != GroupType::Selection) {
5817         all_items = m_groups->getLeaves(parentGroup);
5818     }
5819     std::unordered_set<int> selection = getCurrentSelection();
5820     for (int current_itemId : all_items) {
5821         if (selection.count(current_itemId) > 0) {
5822             selection.erase(current_itemId);
5823         }
5824     }
5825     requestSetSelection(selection);
5826 }
5827 
requestSetSelection(const std::unordered_set<int> & ids)5828 bool TimelineModel::requestSetSelection(const std::unordered_set<int> &ids)
5829 {
5830     QWriteLocker locker(&m_lock);
5831     TRACE(ids);
5832     requestClearSelection();
5833     // if the items are in groups, we must retrieve their topmost containing groups
5834     std::unordered_set<int> roots;
5835     std::transform(ids.begin(), ids.end(), std::inserter(roots, roots.begin()), [&](int id) { return m_groups->getRootId(id); });
5836 
5837     bool result = true;
5838     if (roots.size() == 0) {
5839         m_currentSelection = -1;
5840     } else if (roots.size() == 1) {
5841         m_currentSelection = *(roots.begin());
5842         setSelected(m_currentSelection, true);
5843     } else {
5844         Fun undo = []() { return true; };
5845         Fun redo = []() { return true; };
5846         if (ids.size() == 2) {
5847             // Check if we selected 2 clips from the same master
5848             QList<int> pairIds;
5849             for (auto &id : roots) {
5850                 if (isClip(id)) {
5851                     pairIds << id;
5852                 }
5853             }
5854             if (pairIds.size() == 2 && getClipBinId(pairIds.at(0)) == getClipBinId(pairIds.at(1))) {
5855                 // Check if they have same bin id
5856                 ClipType::ProducerType type = m_allClips[pairIds.at(0)]->clipType();
5857                 if (type == ClipType::AV || type == ClipType::Audio || type == ClipType::Video) {
5858                     // Both clips have same bin ID, display offset
5859                     int pos1 = getClipPosition(pairIds.at(0));
5860                     int pos2 = getClipPosition(pairIds.at(1));
5861                     if (pos2 > pos1) {
5862                         int offset = pos2 - getClipIn(pairIds.at(1)) - (pos1 - getClipIn(pairIds.at(0)));
5863                         if (offset != 0) {
5864                             m_allClips[pairIds.at(1)]->setOffset(offset);
5865                             m_allClips[pairIds.at(0)]->setOffset(-offset);
5866                         }
5867                     } else {
5868                         int offset = pos1 - getClipIn(pairIds.at(0)) - (pos2 - getClipIn(pairIds.at(1)));
5869                         if (offset != 0) {
5870                             m_allClips[pairIds.at(0)]->setOffset(offset);
5871                             m_allClips[pairIds.at(1)]->setOffset(-offset);
5872                         }
5873                     }
5874                 }
5875             }
5876         }
5877         result = (m_currentSelection = m_groups->groupItems(ids, undo, redo, GroupType::Selection)) >= 0;
5878         Q_ASSERT(m_currentSelection >= 0);
5879     }
5880     if (m_subtitleModel) {
5881         m_subtitleModel->clearGrab();
5882     }
5883     emit selectionChanged();
5884     return result;
5885 }
5886 
setSelected(int itemId,bool sel)5887 void TimelineModel::setSelected(int itemId, bool sel)
5888 {
5889     if (isClip(itemId)) {
5890         m_allClips[itemId]->setSelected(sel);
5891     } else if (isComposition(itemId)) {
5892         m_allCompositions[itemId]->setSelected(sel);
5893     } else if (isSubTitle(itemId)) {
5894         m_subtitleModel->setSelected(itemId, sel);
5895     } else if (isGroup(itemId)) {
5896         auto leaves = m_groups->getLeaves(itemId);
5897         for (auto &id : leaves) {
5898             setSelected(id, true);
5899         }
5900     }
5901 }
5902 
requestSetSelection(const std::unordered_set<int> & ids,Fun & undo,Fun & redo)5903 bool TimelineModel::requestSetSelection(const std::unordered_set<int> &ids, Fun &undo, Fun &redo)
5904 {
5905     QWriteLocker locker(&m_lock);
5906     Fun reverse = [this]() {
5907         requestClearSelection(false);
5908         return true;
5909     };
5910     Fun operation = [this, ids]() { return requestSetSelection(ids); };
5911     if (operation()) {
5912         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
5913         return true;
5914     }
5915     return false;
5916 }
5917 
setTrackLockedState(int trackId,bool lock)5918 void TimelineModel::setTrackLockedState(int trackId, bool lock)
5919 {
5920     QWriteLocker locker(&m_lock);
5921     TRACE(trackId, lock);
5922     Fun undo = []() { return true; };
5923     Fun redo = []() { return true; };
5924 
5925     Fun lock_lambda = [this, trackId]() {
5926         getTrackById(trackId)->lock();
5927         return true;
5928     };
5929     Fun unlock_lambda = [this, trackId]() {
5930         getTrackById(trackId)->unlock();
5931         return true;
5932     };
5933     if (lock) {
5934         if (lock_lambda()) {
5935             UPDATE_UNDO_REDO(lock_lambda, unlock_lambda, undo, redo);
5936             PUSH_UNDO(undo, redo, i18n("Lock track"));
5937         }
5938     } else {
5939         if (unlock_lambda()) {
5940             UPDATE_UNDO_REDO(unlock_lambda, lock_lambda, undo, redo);
5941             PUSH_UNDO(undo, redo, i18n("Unlock track"));
5942         }
5943     }
5944 }
5945 
getAllTracksIds() const5946 std::unordered_set<int> TimelineModel::getAllTracksIds() const
5947 {
5948     READ_LOCK();
5949     std::unordered_set<int> result;
5950     std::transform(m_iteratorTable.begin(), m_iteratorTable.end(), std::inserter(result, result.begin()), [&](const auto &track) { return track.first; });
5951     return result;
5952 }
5953 
switchComposition(int cid,const QString & compoId)5954 void TimelineModel::switchComposition(int cid, const QString &compoId)
5955 {
5956     Fun undo = []() {return true; };
5957     Fun redo = []() { return true; };
5958     if (isClip(cid)) {
5959         // We are working on a mix
5960         requestClearSelection(true);
5961         int tid = getClipTrackId(cid);
5962         MixInfo mixData = getTrackById_const(tid)->getMixInfo(cid).first;
5963         getTrackById(tid)->switchMix(cid, compoId, undo, redo);
5964         Fun local_update = [cid, mixData, this]() {
5965             requestMixSelection(cid);
5966             int in = mixData.secondClipInOut.first;
5967             int out = mixData.firstClipInOut.second;
5968             emit invalidateZone(in, out);
5969             checkRefresh(in, out);
5970             return true;
5971         };
5972         PUSH_LAMBDA(local_update, redo);
5973         PUSH_FRONT_LAMBDA(local_update, undo);
5974         if (redo()) {
5975             pCore->pushUndo(undo, redo, i18n("Change composition"));
5976         }
5977         return;
5978     }
5979     Q_ASSERT(isComposition(cid));
5980     std::shared_ptr<CompositionModel> compo = m_allCompositions.at(cid);
5981     int currentPos = compo->getPosition();
5982     int duration = compo->getPlaytime();
5983     int currentTrack = compo->getCurrentTrackId();
5984     int a_track = compo->getATrack();
5985     int forcedTrack = compo->getForcedTrack();
5986     // Clear selection
5987     requestClearSelection(true);
5988     if (m_groups->isInGroup(cid)) {
5989         pCore->displayMessage(i18n("Cannot operate on grouped composition, please ungroup"), ErrorMessage);
5990         return;
5991     }
5992 
5993     bool res = requestCompositionDeletion(cid, undo, redo);
5994     int newId = -1;
5995     res = res && requestCompositionInsertion(compoId, currentTrack, a_track, currentPos, duration, nullptr, newId, undo, redo);
5996     if (res) {
5997         if (forcedTrack > -1 && isComposition(newId)) {
5998             m_allCompositions[newId]->setForceTrack(true);
5999         }
6000         Fun local_redo = [newId, this]() {
6001             requestSetSelection({newId});
6002             return true;
6003         };
6004         Fun local_undo = [cid, this]() {
6005             requestSetSelection({cid});
6006             return true;
6007         };
6008         local_redo();
6009         PUSH_LAMBDA(local_redo, redo);
6010         PUSH_LAMBDA(local_undo, undo);
6011         PUSH_UNDO(undo, redo, i18n("Change composition"));
6012     } else {
6013         undo();
6014     }
6015 }
6016 
plantMix(int tid,Mlt::Transition * t)6017 void TimelineModel::plantMix(int tid, Mlt::Transition *t)
6018 {
6019     if (getTrackById_const(tid)->hasClipStart(t->get_in())) {
6020         getTrackById_const(tid)->getTrackService()->plant_transition(*t, 0, 1);
6021         getTrackById_const(tid)->loadMix(t);
6022     } else {
6023         qDebug()<<"=== INVALID MIX FOUND AT: "<<t->get_in()<<" - "<<t->get("mlt_service");
6024     }
6025 }
6026 
resizeStartMix(int cid,int duration,bool singleResize)6027 bool TimelineModel::resizeStartMix(int cid, int duration, bool singleResize)
6028 {
6029     Q_ASSERT(isClip(cid));
6030     int tid = m_allClips.at(cid)->getCurrentTrackId();
6031     if (tid > -1) {
6032         std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(cid);
6033         if (mixData.first.firstClipId > -1) {
6034             int clipToResize = mixData.first.firstClipId;
6035             Q_ASSERT(isClip(clipToResize));
6036             duration = qMin(duration, m_allClips.at(cid)->getPlaytime());
6037             int updatedDuration = m_allClips.at(cid)->getPosition() + duration - m_allClips[clipToResize]->getPosition();
6038             int result = requestItemResize(clipToResize, updatedDuration, true, true, 0, singleResize);
6039             return result > -1;
6040         }
6041     }
6042     return false;
6043 }
6044 
getMixDuration(int cid) const6045 int TimelineModel::getMixDuration(int cid) const
6046 {
6047     Q_ASSERT(isClip(cid));
6048     int tid = m_allClips.at(cid)->getCurrentTrackId();
6049     if (tid > -1) {
6050         if (getTrackById_const(tid)->hasStartMix(cid)) {
6051             return getTrackById_const(tid)->getMixDuration(cid);
6052         } else {
6053             // Mix is not yet inserted in timeline
6054             std::pair<int, int> mixInOut = getMixInOut(cid);
6055             return mixInOut.second - mixInOut.first;
6056         }
6057     }
6058     return 0;
6059 }
6060 
getMixInOut(int cid) const6061 std::pair<int, int> TimelineModel::getMixInOut(int cid) const
6062 {
6063     Q_ASSERT(isClip(cid));
6064     int tid = m_allClips.at(cid)->getCurrentTrackId();
6065     if (tid > -1) {
6066         MixInfo mixData = getTrackById_const(tid)->getMixInfo(cid).first;
6067         if (mixData.firstClipId > -1) {
6068             return {mixData.secondClipInOut.first, mixData.firstClipInOut.second};
6069         }
6070     }
6071     return {-1,-1};
6072 }
6073 
getMixCutPos(int cid) const6074 int TimelineModel::getMixCutPos(int cid) const
6075 {
6076     Q_ASSERT(isClip(cid));
6077     return m_allClips.at(cid)->getMixCutPosition();
6078 }
6079 
getMixAlign(int cid) const6080 MixAlignment TimelineModel::getMixAlign(int cid) const
6081 {
6082     Q_ASSERT(isClip(cid));
6083     int tid = m_allClips.at(cid)->getCurrentTrackId();
6084     if (tid > -1) {
6085         int mixDuration = m_allClips.at(cid)->getMixDuration();
6086         int mixCutPos = m_allClips.at(cid)->getMixCutPosition();
6087         if (mixCutPos == 0) {
6088             return  MixAlignment::AlignRight;
6089         } else if (mixCutPos == mixDuration) {
6090             return  MixAlignment::AlignLeft;
6091         } else if (mixCutPos == mixDuration - mixDuration / 2) {
6092             return  MixAlignment::AlignCenter;
6093         }
6094     }
6095     return MixAlignment::AlignNone;
6096 }
6097 
requestResizeMix(int cid,int duration,MixAlignment align,int rightFrames)6098 void TimelineModel::requestResizeMix(int cid, int duration, MixAlignment align, int rightFrames)
6099 {
6100     Q_ASSERT(isClip(cid));
6101     int tid = m_allClips.at(cid)->getCurrentTrackId();
6102     if (tid > -1) {
6103         MixInfo mixData = getTrackById_const(tid)->getMixInfo(cid).first;
6104         int clipToResize = mixData.firstClipId;
6105         if (clipToResize > -1) {
6106             Fun undo = []() { return true; };
6107             Fun redo = []() { return true; };
6108             // The mix cut position shoud never change through a resize operation
6109             int cutPos = m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime() - m_allClips.at(cid)->getMixCutPosition();
6110             int maxLengthLeft = m_allClips.at(clipToResize)->getMaxDuration();
6111             // Maximum space for expanding the right clip part
6112             int leftMax = maxLengthLeft > -1 ? (maxLengthLeft - 1 - m_allClips.at(clipToResize)->getOut()) : -1;
6113             // Maximum space available on the right clip
6114             int availableLeft = m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - (m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime());
6115             if (leftMax == -1) {
6116                 leftMax = availableLeft;
6117             } else {
6118                 leftMax = qMin(leftMax, availableLeft);
6119             }
6120 
6121             int maxLengthRight = m_allClips.at(cid)->getMaxDuration();
6122             // maximum space to resize clip on the left
6123             int availableRight = m_allClips.at(cid)->getPosition() - m_allClips.at(clipToResize)->getPosition();
6124             int rightMax = maxLengthRight > -1 ? (m_allClips.at(cid)->getIn()) : -1;
6125             if (rightMax == -1) {
6126                 rightMax = availableRight;
6127             } else {
6128                 rightMax = qMin(rightMax, availableRight);
6129             }
6130             Fun adjust_mix_undo = [this, tid, cid, prevCut = m_allClips.at(cid)->getMixCutPosition(), prevDuration = m_allClips.at(cid)->getMixDuration()]() {
6131                 getTrackById_const(tid)->setMixDuration(cid, prevDuration, prevCut);
6132                 QModelIndex ix = makeClipIndexFromID(cid);
6133                 emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
6134                 return true;
6135             };
6136             if (align == MixAlignment::AlignLeft) {
6137                 // Adjust left clip
6138                 int updatedDurationLeft = cutPos + duration - m_allClips.at(clipToResize)->getPosition();
6139                 if (leftMax > -1) {
6140                     updatedDurationLeft = qMin(updatedDurationLeft, m_allClips.at(clipToResize)->getPlaytime() + leftMax);
6141                 }
6142                 // Adjust right clip
6143                 int updatedDurationRight = m_allClips.at(cid)->getPlaytime();
6144                 if (cutPos != m_allClips.at(cid)->getPosition()) {
6145                     updatedDurationRight = m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - cutPos;
6146                     if (rightMax > -1) {
6147                         updatedDurationRight = qMin(updatedDurationRight, m_allClips.at(cid)->getPlaytime() + rightMax);
6148                     }
6149                 }
6150                 int updatedDuration = m_allClips.at(clipToResize)->getPosition() + updatedDurationLeft - (m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - updatedDurationRight);
6151                 if (updatedDuration < 1) {
6152                     //
6153                     pCore->displayMessage(i18n("Cannot resize mix to less than 1 frame"), ErrorMessage, 500);
6154                     // update mix widget
6155                     emit selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6156                     return;
6157                 }
6158                 requestItemResize(clipToResize, updatedDurationLeft, true, true, undo, redo);
6159                 if (m_allClips.at(cid)->getPlaytime() != updatedDurationRight) {
6160                     requestItemResize(cid, updatedDurationRight, false, true, undo, redo);
6161                 }
6162                 int updatedCutPosition = m_allClips.at(cid)->getPosition();
6163                 if (updatedCutPosition != cutPos) {
6164                     pCore->displayMessage(i18n("Cannot resize mix"), ErrorMessage, 500);
6165                     undo();
6166                     emit selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6167                     return;
6168                 }
6169                 Fun adjust_mix = [this, tid, cid, updatedDuration]() {
6170                     getTrackById_const(tid)->setMixDuration(cid, updatedDuration, updatedDuration);
6171                     QModelIndex ix = makeClipIndexFromID(cid);
6172                     emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
6173                     return true;
6174                 };
6175                 adjust_mix();
6176                 UPDATE_UNDO_REDO(adjust_mix, adjust_mix_undo, undo, redo);
6177             } else if (align == MixAlignment::AlignRight) {
6178                 int updatedDurationRight = m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - cutPos + duration;
6179                 if (rightMax > -1) {
6180                     updatedDurationRight = qMin(updatedDurationRight, m_allClips.at(cid)->getPlaytime() + rightMax);
6181                 }
6182                 int updatedDurationLeft = cutPos - m_allClips.at(clipToResize)->getPosition();
6183                 if (leftMax > -1) {
6184                     updatedDurationLeft = qMin(updatedDurationLeft, m_allClips.at(clipToResize)->getPlaytime() + leftMax);
6185                 }
6186                 int updatedDuration = m_allClips.at(clipToResize)->getPosition() + updatedDurationLeft - (m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - updatedDurationRight);
6187                 if (updatedDuration < 1) {
6188                     //
6189                     pCore->displayMessage(i18n("Cannot resize mix to less than 1 frame"), ErrorMessage, 500);
6190                     emit selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6191                     return;
6192                 }
6193                 requestItemResize(cid, updatedDurationRight, false, true, undo, redo);
6194                 requestItemResize(clipToResize, updatedDurationLeft, true, true, undo, redo);
6195                 int updatedCutPosition = m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime();
6196                 if (updatedCutPosition != cutPos) {
6197                     pCore->displayMessage(i18n("Cannot resize mix"), ErrorMessage, 500);
6198                     undo();
6199                     emit selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6200                     return;
6201                 }
6202                 Fun adjust_mix = [this, tid, cid, updatedDuration]() {
6203                     getTrackById_const(tid)->setMixDuration(cid, updatedDuration, 0);
6204                     QModelIndex ix = makeClipIndexFromID(cid);
6205                     emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
6206                     return true;
6207                 };
6208                 adjust_mix();
6209                 UPDATE_UNDO_REDO(adjust_mix, adjust_mix_undo, undo, redo);
6210             } else if (align == MixAlignment::AlignCenter) {
6211                 int updatedDurationRight = m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - cutPos + duration / 2;
6212                 if (rightMax > -1) {
6213                     updatedDurationRight = qMin(updatedDurationRight, m_allClips.at(cid)->getPlaytime() + rightMax);
6214                 }
6215                 int updatedDurationLeft = cutPos + (duration - duration / 2) - m_allClips.at(clipToResize)->getPosition();
6216                 if (leftMax > -1) {
6217                     updatedDurationLeft = qMin(updatedDurationLeft, m_allClips.at(clipToResize)->getPlaytime() + leftMax);
6218                 }
6219                 int updatedDuration = m_allClips.at(clipToResize)->getPosition() + updatedDurationLeft - (m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - updatedDurationRight);
6220                 if (updatedDuration < 1) {
6221                     pCore->displayMessage(i18n("Cannot resize mix to less than 1 frame"), ErrorMessage, 500);
6222                     emit selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6223                     return;
6224                 }
6225                 int deltaLeft = m_allClips.at(clipToResize)->getPosition() + updatedDurationLeft - cutPos;
6226                 int deltaRight = cutPos - (m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - updatedDurationRight);
6227                 if (deltaRight) {
6228                     requestItemResize(cid, updatedDurationRight, false, true, undo, redo);
6229                 }
6230                 if (deltaLeft > 0) {
6231                     requestItemResize(clipToResize, updatedDurationLeft, true, true, undo, redo);
6232                 }
6233                 int mixCutPos = m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime() - cutPos;
6234                 if (mixCutPos > updatedDuration) {
6235                     pCore->displayMessage(i18n("Cannot resize mix"), ErrorMessage, 500);
6236                     undo();
6237                     emit selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6238                     return;
6239                 }
6240                 if (qAbs(deltaLeft - deltaRight > 2)) {
6241                     // Mix not exactly centered
6242                     emit selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6243                     return;
6244                 }
6245                 Fun adjust_mix = [this, tid, cid, updatedDuration, mixCutPos]() {
6246                     getTrackById_const(tid)->setMixDuration(cid, updatedDuration, mixCutPos);
6247                     QModelIndex ix = makeClipIndexFromID(cid);
6248                     emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
6249                     return true;
6250                 };
6251                 adjust_mix();
6252                 UPDATE_UNDO_REDO(adjust_mix, adjust_mix_undo, undo, redo);
6253             } else {
6254                 // No alignment specified
6255                 int updatedDurationRight;
6256                 int updatedDurationLeft;
6257                 if (rightFrames > -1) {
6258                     // A right frame offset was specified
6259                     updatedDurationRight = qBound(0, rightFrames, duration);
6260                     updatedDurationLeft = duration - updatedDurationRight;
6261                 } else {
6262                     updatedDurationRight = m_allClips.at(cid)->getMixCutPosition();
6263                     updatedDurationLeft = m_allClips.at(cid)->getMixDuration() - updatedDurationRight;
6264                     int currentDuration = m_allClips.at(cid)->getMixDuration();
6265                     if (qAbs(duration - currentDuration) == 1) {
6266                         if (duration < currentDuration) {
6267                             // We are reducing the duration
6268                             if (currentDuration %2 == 0) {
6269                                 updatedDurationRight --;
6270                                 if (updatedDurationRight < 0) {
6271                                     updatedDurationRight = 0;
6272                                     updatedDurationLeft--;
6273                                 }
6274                             } else {
6275                                 updatedDurationLeft --;
6276                                 if (updatedDurationLeft < 0) {
6277                                     updatedDurationLeft = 0;
6278                                     updatedDurationRight--;
6279                                 }
6280                             }
6281                         } else {
6282                             // Increasing duration
6283                             if (currentDuration %2 == 0) {
6284                                 updatedDurationRight ++;
6285                             } else {
6286                                 updatedDurationLeft ++;
6287                             }
6288                         }
6289                     } else {
6290                         double ratio = double (duration) / currentDuration;
6291                         updatedDurationRight *= ratio;
6292                         updatedDurationLeft = duration - updatedDurationRight;
6293                     }
6294                 }
6295                 if (updatedDurationLeft + updatedDurationRight < 1) {
6296                     //
6297                     pCore->displayMessage(i18n("Cannot resize mix to less than 1 frame"), ErrorMessage, 500);
6298                     emit selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6299                     return;
6300                 }
6301                 updatedDurationLeft -= (m_allClips.at(cid)->getMixDuration() - m_allClips.at(cid)->getMixCutPosition());
6302                 updatedDurationRight -= m_allClips.at(cid)->getMixCutPosition();
6303                 if (leftMax > -1) {
6304                     updatedDurationLeft = qMin(updatedDurationLeft, m_allClips.at(clipToResize)->getPlaytime() + leftMax);
6305                 }
6306                 if (rightMax > -1) {
6307                     updatedDurationRight = qMin(updatedDurationRight, m_allClips.at(cid)->getPlaytime() + rightMax);
6308                 }
6309                 if (updatedDurationLeft != 0) {
6310                     int updatedDurL = m_allClips.at(cid)->getPlaytime() + updatedDurationLeft;
6311                     requestItemResize(cid, updatedDurL, false, true, undo, redo);
6312                 }
6313                 if (updatedDurationRight != 0) {
6314                     int updatedDurR = m_allClips.at(clipToResize)->getPlaytime() + updatedDurationRight;
6315                     requestItemResize(clipToResize, updatedDurR, true, true, undo, redo);
6316                 }
6317                 int mixCutPos = m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime() - cutPos;
6318                 int updatedDuration = m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime() - m_allClips.at(cid)->getPosition();
6319                 if (mixCutPos > updatedDuration) {
6320                     pCore->displayMessage(i18n("Cannot resize mix"), ErrorMessage, 500);
6321                     undo();
6322                     emit selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true);
6323                     return;
6324                 }
6325                 Fun adjust_mix = [this, tid, cid, updatedDuration, mixCutPos]() {
6326                     getTrackById_const(tid)->setMixDuration(cid, updatedDuration, mixCutPos);
6327                     QModelIndex ix = makeClipIndexFromID(cid);
6328                     emit dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
6329                     return true;
6330                 };
6331                 adjust_mix();
6332                 UPDATE_UNDO_REDO(adjust_mix, adjust_mix_undo, undo, redo);
6333             }
6334             pCore->pushUndo(undo, redo, i18n("Resize mix"));
6335         }
6336     }
6337 }
6338 
setSubModel(std::shared_ptr<SubtitleModel> model)6339 void TimelineModel::setSubModel(std::shared_ptr<SubtitleModel> model)
6340 {
6341     m_subtitleModel = std::move(model);
6342     m_subtitleModel->registerSnap(std::static_pointer_cast<SnapInterface>(m_snaps));
6343 }
6344 
getSubtitleIndex(int subId) const6345 int TimelineModel::getSubtitleIndex(int subId) const
6346 {
6347     if (m_allSubtitles.count(subId) == 0) {
6348         return -1;
6349     }
6350     auto it = m_allSubtitles.find(subId);
6351     return int(std::distance( m_allSubtitles.begin(), it));
6352 }
6353 
getSubtitleIdFromIndex(int index) const6354 std::pair<int, GenTime> TimelineModel::getSubtitleIdFromIndex(int index) const
6355 {
6356     if (index >= static_cast<int> (m_allSubtitles.size())) {
6357         return {-1, GenTime()};
6358     }
6359     auto it = m_allSubtitles.begin();
6360     std::advance(it, index);
6361     return {it->first, it->second};
6362 }
6363 
getMasterEffectZones() const6364 QVariantList TimelineModel::getMasterEffectZones() const
6365 {
6366     if (m_masterStack) {
6367         return m_masterStack->getEffectZones();
6368     }
6369     return {};
6370 }
6371 
getCompositionSizeOnTrack(const ObjectId & id)6372 const QSize TimelineModel::getCompositionSizeOnTrack(const ObjectId &id)
6373 {
6374     int pos = getCompositionPosition(id.second);
6375     int tid = getCompositionTrackId(id.second);
6376     int cid = getTrackById_const(tid)->getClipByPosition(pos);
6377     if (cid > -1) {
6378         return getClipFrameSize(cid);
6379     }
6380     return QSize();
6381 }
6382 
getProxiesAt(int position)6383 QStringList TimelineModel::getProxiesAt(int position)
6384 {
6385     QStringList done;
6386     QStringList proxied;
6387     auto it = m_allTracks.begin();
6388     while (it != m_allTracks.end()) {
6389         if ((*it)->isAudioTrack()) {
6390             ++it;
6391             continue;
6392         }
6393         int clip1 = (*it)->getClipByPosition(position, 0);
6394         int clip2 = (*it)->getClipByPosition(position, 1);
6395         if (clip1 > -1) {
6396             // Check if proxied
6397             const QString binId = m_allClips[clip1]->binId();
6398             if (!done.contains(binId)) {
6399                 done << binId;
6400                 std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(binId);
6401                 if (binClip->hasProxy()) {
6402                     proxied << binId;
6403                 }
6404             }
6405         }
6406         if (clip2 > -1) {
6407             // Check if proxied
6408             const QString binId = m_allClips[clip2]->binId();
6409             if (!done.contains(binId)) {
6410                 done << binId;
6411                 std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(binId);
6412                 if (binClip->hasProxy()) {
6413                     proxied << binId;
6414                 }
6415             }
6416         }
6417         ++it;
6418     }
6419     return proxied;
6420 }
6421