1 /*
2     SPDX-FileCopyrightText: 2017 Nicolas Carion
3     SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
4 */
5 
6 #include "trackmodel.hpp"
7 #include "clipmodel.hpp"
8 #include "core.h"
9 #include "compositionmodel.hpp"
10 #include "effects/effectstack/model/effectstackmodel.hpp"
11 #include "transitions/transitionsrepository.hpp"
12 #include "kdenlivesettings.h"
13 #ifdef CRASH_AUTO_TEST
14 #include "logger.hpp"
15 #else
16 #define TRACE_CONSTR(...)
17 #endif
18 #include "snapmodel.hpp"
19 #include "timelinemodel.hpp"
20 #include <QDebug>
21 #include <QModelIndex>
22 #include <memory>
23 #include <mlt++/MltTransition.h>
24 
TrackModel(const std::weak_ptr<TimelineModel> & parent,int id,const QString & trackName,bool audioTrack)25 TrackModel::TrackModel(const std::weak_ptr<TimelineModel> &parent, int id, const QString &trackName, bool audioTrack)
26     : m_parent(parent)
27     , m_id(id == -1 ? TimelineModel::getNextId() : id)
28     , m_lock(QReadWriteLock::Recursive)
29 {
30     if (auto ptr = parent.lock()) {
31         m_track = std::make_shared<Mlt::Tractor>(*ptr->getProfile());
32         m_playlists[0].set_profile(*ptr->getProfile());
33         m_playlists[1].set_profile(*ptr->getProfile());
34         m_track->insert_track(m_playlists[0], 0);
35         m_track->insert_track(m_playlists[1], 1);
36         m_mainPlaylist = std::make_shared<Mlt::Producer>(&m_playlists[0]);
37         if (!trackName.isEmpty()) {
38             m_track->set("kdenlive:track_name", trackName.toUtf8().constData());
39         }
40         if (audioTrack) {
41             m_track->set("kdenlive:audio_track", 1);
42             for (auto &m_playlist : m_playlists) {
43                 m_playlist.set("hide", 1);
44             }
45         }
46         // For now we never use the second playlist, only planned for same track transitions
47         m_track->set("kdenlive:trackheight", KdenliveSettings::trackheight());
48         m_track->set("kdenlive:timeline_active", 1);
49         m_effectStack = EffectStackModel::construct(m_track, {ObjectType::TimelineTrack, m_id}, ptr->m_undoStack);
50         // TODO
51         // When we use the second playlist, register it's stask as child of main playlist effectstack
52         // m_subPlaylist = std::make_shared<Mlt::Producer>(&m_playlists[1]);
53         // m_effectStack->addService(m_subPlaylist);
54         QObject::connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&](const QModelIndex &, const QModelIndex &, QVector<int> roles) {
55             if (auto ptr2 = m_parent.lock()) {
56                 QModelIndex ix = ptr2->makeTrackIndexFromID(m_id);
57                 qDebug()<<"==== TRACK ZONES CHANGED";
58                 emit ptr2->dataChanged(ix, ix, roles);
59             }
60         });
61     } else {
62         qDebug() << "Error : construction of track failed because parent timeline is not available anymore";
63         Q_ASSERT(false);
64     }
65 }
66 
TrackModel(const std::weak_ptr<TimelineModel> & parent,Mlt::Tractor mltTrack,int id)67 TrackModel::TrackModel(const std::weak_ptr<TimelineModel> &parent, Mlt::Tractor mltTrack, int id)
68     : m_parent(parent)
69     , m_id(id == -1 ? TimelineModel::getNextId() : id)
70 {
71     if (auto ptr = parent.lock()) {
72         m_track = std::make_shared<Mlt::Tractor>(mltTrack);
73         m_playlists[0] = *m_track->track(0);
74         m_playlists[1] = *m_track->track(1);
75         m_effectStack = EffectStackModel::construct(m_track, {ObjectType::TimelineTrack, m_id}, ptr->m_undoStack);
76     } else {
77         qDebug() << "Error : construction of track failed because parent timeline is not available anymore";
78         Q_ASSERT(false);
79     }
80 }
81 
~TrackModel()82 TrackModel::~TrackModel()
83 {
84     m_track->remove_track(1);
85     m_track->remove_track(0);
86 }
87 
construct(const std::weak_ptr<TimelineModel> & parent,int id,int pos,const QString & trackName,bool audioTrack)88 int TrackModel::construct(const std::weak_ptr<TimelineModel> &parent, int id, int pos, const QString &trackName, bool audioTrack)
89 {
90     std::shared_ptr<TrackModel> track(new TrackModel(parent, id, trackName, audioTrack));
91     TRACE_CONSTR(track.get(), parent, id, pos, trackName, audioTrack);
92     id = track->m_id;
93     if (auto ptr = parent.lock()) {
94         ptr->registerTrack(std::move(track), pos);
95     } else {
96         qDebug() << "Error : construction of track failed because parent timeline is not available anymore";
97         Q_ASSERT(false);
98     }
99     return id;
100 }
101 
getClipsCount()102 int TrackModel::getClipsCount()
103 {
104     READ_LOCK();
105 #ifdef QT_DEBUG
106     int count = 0;
107     for (auto &m_playlist : m_playlists) {
108         for (int i = 0; i < m_playlist.count(); i++) {
109             if (!m_playlist.is_blank(i)) {
110                 count++;
111             }
112         }
113     }
114     Q_ASSERT(count == static_cast<int>(m_allClips.size()));
115 #else
116     int count = int(m_allClips.size());
117 #endif
118     return count;
119 }
120 
switchPlaylist(int clipId,int position,int sourcePlaylist,int destPlaylist)121 bool TrackModel::switchPlaylist(int clipId, int position, int sourcePlaylist, int destPlaylist)
122 {
123     QWriteLocker locker(&m_lock);
124     if (sourcePlaylist == destPlaylist) {
125         return true;
126     }
127     Q_ASSERT(!m_playlists[sourcePlaylist].is_blank_at(position) && m_playlists[destPlaylist].is_blank_at(position));
128     int target_clip = m_playlists[sourcePlaylist].get_clip_index_at(position);
129     std::unique_ptr<Mlt::Producer> prod(m_playlists[sourcePlaylist].replace_with_blank(target_clip));
130     m_playlists[sourcePlaylist].consolidate_blanks();
131     if (auto ptr = m_parent.lock()) {
132         std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
133         clip->setSubPlaylistIndex(destPlaylist, m_id);
134         int index = m_playlists[destPlaylist].insert_at(position, *clip, 1);
135         m_playlists[destPlaylist].consolidate_blanks();
136         return index != -1;
137     }
138     return false;
139 }
140 
requestClipInsertion_lambda(int clipId,int position,bool updateView,bool finalMove,bool groupMove,QList<int> allowedClipMixes)141 Fun TrackModel::requestClipInsertion_lambda(int clipId, int position, bool updateView, bool finalMove, bool groupMove, QList<int> allowedClipMixes)
142 {
143     QWriteLocker locker(&m_lock);
144     // By default, insertion occurs in topmost track
145     int target_playlist = 0;
146     int length = -1;
147     if (auto ptr = m_parent.lock()) {
148         Q_ASSERT(ptr->getClipPtr(clipId)->getCurrentTrackId() == -1);
149         target_playlist = ptr->getClipPtr(clipId)->getSubPlaylistIndex();
150         length = ptr->getClipPtr(clipId)->getPlaytime() - 1;
151         /*if (target_playlist == 1 && ptr->getClipPtr(clipId)->getMixDuration() == 0) {
152             target_playlist = 0;
153         }*/
154         //qDebug()<<"==== GOT TRARGET PLAYLIST: "<<target_playlist;
155     } else {
156         qDebug() << "impossible to get parent timeline";
157         Q_ASSERT(false);
158     }
159     // Find out the clip id at position
160     int target_clip = m_playlists[target_playlist].get_clip_index_at(position);
161     int count = m_playlists[target_playlist].count();
162 
163     // we create the function that has to be executed after the melt order. This is essentially book-keeping
164     auto end_function = [clipId, this, position, updateView, finalMove](int subPlaylist) {
165         if (auto ptr = m_parent.lock()) {
166             std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
167             m_allClips[clip->getId()] = clip; // store clip
168             // update clip position and track
169             clip->setPosition(position);
170             if (finalMove) {
171                 clip->setSubPlaylistIndex(subPlaylist, m_id);
172             }
173             int new_in = clip->getPosition();
174             int new_out = new_in + clip->getPlaytime();
175             ptr->m_snaps->addPoint(new_in);
176             ptr->m_snaps->addPoint(new_out);
177             if (updateView) {
178                 int clip_index = getRowfromClip(clipId);
179                 ptr->_beginInsertRows(ptr->makeTrackIndexFromID(m_id), clip_index, clip_index);
180                 ptr->_endInsertRows();
181                 bool audioOnly = clip->isAudioOnly();
182                 if (!audioOnly && !isHidden() && !isAudioTrack()) {
183                     // only refresh monitor if not an audio track and not hidden
184                     ptr->checkRefresh(new_in, new_out);
185                 }
186                 if (!audioOnly && finalMove && !isAudioTrack()) {
187                     emit ptr->invalidateZone(new_in, new_out);
188                 }
189             }
190             return true;
191         }
192         qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
193         return false;
194     };
195     if (!finalMove && !hasMix(clipId)) {
196         if (allowedClipMixes.isEmpty()) {
197             if (!m_playlists[0].is_blank_at(position) || !m_playlists[1].is_blank_at(position)) {
198                 // Track is not empty
199                 qWarning() << "clip insert failed - non blank 1";
200                 return []() { return false; };
201             }
202         } else {
203             // This is a group move with a mix, some clips are allowed
204             if (!m_playlists[target_playlist].is_blank_at(position)) {
205                 // Track is not empty
206                 qWarning() << "clip insert failed - non blank 2";
207                 return []() { return false; };
208             }
209             // Check if there are clips on the other playlist, and if they are in the allowed list
210             std::unordered_set<int> collisions = getClipsInRange(position, position + length);
211             qDebug()<<"==== DETECTING COLLISIONS AT: "<<position<<" to "<<(position+length)<<" COUNT: "<<collisions.size();
212             for (int c : collisions) {
213                 if (!allowedClipMixes.contains(c)) {
214                     // Track is not empty
215                     qWarning() << "clip insert failed - non blank 3";
216                     return []() { return false; };
217                 }
218             }
219         }
220     }
221     if (target_clip >= count && m_playlists[target_playlist].is_blank_at(position)) {
222         // In that case, we append after, in the first playlist
223         return [this, position, clipId, end_function, finalMove, groupMove, target_playlist]() {
224             if (isLocked()) {
225                 qWarning() << "clip insert failed - locked track";
226                 return false;
227             }
228             if (auto ptr = m_parent.lock()) {
229                 // Lock MLT playlist so that we don't end up with an invalid frame being displayed
230                 m_playlists[target_playlist].lock();
231                 std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
232                 clip->setCurrentTrackId(m_id, finalMove);
233                 int index = m_playlists[target_playlist].insert_at(position, *clip, 1);
234                 m_playlists[target_playlist].consolidate_blanks();
235                 m_playlists[target_playlist].unlock();
236                 if (finalMove && !groupMove) {
237                     ptr->updateDuration();
238                 }
239                 return index != -1 && end_function(target_playlist);
240             }
241             qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
242             return false;
243         };
244     }
245     if (m_playlists[target_playlist].is_blank_at(position)) {
246         int blank_end = getBlankEnd(position, target_playlist);
247         if (blank_end >= position + length) {
248             return [this, position, clipId, end_function, target_playlist]() {
249                 if (isLocked()) return false;
250                 if (auto ptr = m_parent.lock()) {
251                     // Lock MLT playlist so that we don't end up with an invalid frame being displayed
252                     m_playlists[target_playlist].lock();
253                     std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
254                     clip->setCurrentTrackId(m_id);
255                     int index = m_playlists[target_playlist].insert_at(position, *clip, 1);
256                     m_playlists[target_playlist].consolidate_blanks();
257                     m_playlists[target_playlist].unlock();
258                     return index != -1 && end_function(target_playlist);
259                 }
260                 qDebug() << "Error : Clip Insertion failed because timeline is not available anymore";
261                 return false;
262             };
263         }
264     }
265     return []() { return false; };
266 }
267 
requestClipInsertion(int clipId,int position,bool updateView,bool finalMove,Fun & undo,Fun & redo,bool groupMove,QList<int> allowedClipMixes)268 bool TrackModel::requestClipInsertion(int clipId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove, QList<int> allowedClipMixes)
269 {
270     QWriteLocker locker(&m_lock);
271     if (isLocked()) {
272         qDebug()<<"==== ERROR INSERT OK LOCKED TK";
273         return false;
274     }
275     if (position < 0) {
276         qDebug()<<"==== ERROR INSERT ON NEGATIVE POS: "<<position;
277         return false;
278     }
279     if (auto ptr = m_parent.lock()) {
280         std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
281         if (isAudioTrack() && !clip->canBeAudio()) {
282             qDebug() << "// ATTEMPTING TO INSERT NON AUDIO CLIP ON AUDIO TRACK";
283             return false;
284         }
285         if (!isAudioTrack() && !clip->canBeVideo()) {
286             qDebug() << "// ATTEMPTING TO INSERT NON VIDEO CLIP ON VIDEO TRACK";
287             return false;
288         }
289         Fun local_undo = []() { return true; };
290         Fun local_redo = []() { return true; };
291         bool res = true;
292         if (clip->clipState() != PlaylistState::Disabled) {
293             res = clip->setClipState(isAudioTrack() ? PlaylistState::AudioOnly : PlaylistState::VideoOnly, local_undo, local_redo);
294         }
295         int duration = trackDuration();
296         auto operation = requestClipInsertion_lambda(clipId, position, updateView, finalMove, groupMove, allowedClipMixes);
297         res = res && operation();
298         if (res) {
299             if (finalMove && duration != trackDuration()) {
300                 // A clip move changed the track duration, update track effects
301                 m_effectStack->adjustStackLength(true, 0, duration, 0, trackDuration(), 0, undo, redo, true);
302             }
303             auto reverse = requestClipDeletion_lambda(clipId, updateView, finalMove, groupMove, finalMove);
304             UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo);
305             UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
306             return true;
307         }
308         bool undone = local_undo();
309         Q_ASSERT(undone);
310         return false;
311     }
312     return false;
313 }
314 
temporaryUnplugClip(int clipId)315 void TrackModel::temporaryUnplugClip(int clipId)
316 {
317     QWriteLocker locker(&m_lock);
318     int clip_position = m_allClips[clipId]->getPosition();
319     auto clip_loc = getClipIndexAt(clip_position);
320     int target_track = clip_loc.first;
321     int target_clip = clip_loc.second;
322     // lock MLT playlist so that we don't end up with invalid frames in monitor
323     m_playlists[target_track].lock();
324     Q_ASSERT(target_clip < m_playlists[target_track].count());
325     Q_ASSERT(!m_playlists[target_track].is_blank(target_clip));
326     std::unique_ptr<Mlt::Producer> prod(m_playlists[target_track].replace_with_blank(target_clip));
327     m_playlists[target_track].unlock();
328 }
329 
temporaryReplugClip(int cid)330 void TrackModel::temporaryReplugClip(int cid)
331 {
332     QWriteLocker locker(&m_lock);
333     int clip_position = m_allClips[cid]->getPosition();
334     int target_track = m_allClips[cid]->getSubPlaylistIndex();
335     m_playlists[target_track].lock();
336     if (auto ptr = m_parent.lock()) {
337         std::shared_ptr<ClipModel> clip = ptr->getClipPtr(cid);
338         m_playlists[target_track].insert_at(clip_position, *clip, 1);
339     }
340     m_playlists[target_track].unlock();
341 }
342 
343 
replugClip(int clipId)344 void TrackModel::replugClip(int clipId)
345 {
346     QWriteLocker locker(&m_lock);
347     int clip_position = m_allClips[clipId]->getPosition();
348     auto clip_loc = getClipIndexAt(clip_position, m_allClips[clipId]->getSubPlaylistIndex());
349     int target_track = clip_loc.first;
350     int target_clip = clip_loc.second;
351     // lock MLT playlist so that we don't end up with invalid frames in monitor
352     m_playlists[target_track].lock();
353     Q_ASSERT(target_clip < m_playlists[target_track].count());
354     Q_ASSERT(!m_playlists[target_track].is_blank(target_clip));
355     std::unique_ptr<Mlt::Producer> prod(m_playlists[target_track].replace_with_blank(target_clip));
356     if (auto ptr = m_parent.lock()) {
357         std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId);
358         m_playlists[target_track].insert_at(clip_position, *clip, 1);
359         if (!clip->isAudioOnly() && !isAudioTrack()) {
360             emit ptr->invalidateZone(clip->getIn(), clip->getOut());
361         }
362         if (!clip->isAudioOnly() && !isHidden() && !isAudioTrack()) {
363             // only refresh monitor if not an audio track and not hidden
364             ptr->checkRefresh(clip->getIn(), clip->getOut());
365         }
366     }
367     m_playlists[target_track].consolidate_blanks();
368     m_playlists[target_track].unlock();
369 }
370 
requestClipDeletion_lambda(int clipId,bool updateView,bool finalMove,bool groupMove,bool finalDeletion)371 Fun TrackModel::requestClipDeletion_lambda(int clipId, bool updateView, bool finalMove, bool groupMove, bool finalDeletion)
372 {
373     QWriteLocker locker(&m_lock);
374     // Find index of clip
375     int clip_position = m_allClips[clipId]->getPosition();
376     bool audioOnly = m_allClips[clipId]->isAudioOnly();
377     int old_in = clip_position;
378     int old_out = old_in + m_allClips[clipId]->getPlaytime();
379     return [clip_position, clipId, old_in, old_out, updateView, audioOnly, finalMove, groupMove, finalDeletion, this]() {
380         if (isLocked()) return false;
381         if (finalDeletion && m_allClips[clipId]->selected) {
382             m_allClips[clipId]->selected = false;
383             if (auto ptr = m_parent.lock()) {
384                 // item was selected, unselect
385                 ptr->requestClearSelection(true);
386             }
387         }
388         int target_track = m_allClips[clipId]->getSubPlaylistIndex();
389         auto clip_loc = getClipIndexAt(clip_position, target_track);
390         if (updateView) {
391             int old_clip_index = getRowfromClip(clipId);
392             auto ptr = m_parent.lock();
393             ptr->_beginRemoveRows(ptr->makeTrackIndexFromID(getId()), old_clip_index, old_clip_index);
394             ptr->_endRemoveRows();
395         }
396         int target_clip = clip_loc.second;
397         // lock MLT playlist so that we don't end up with invalid frames in monitor
398         m_playlists[target_track].lock();
399         Q_ASSERT(target_clip < m_playlists[target_track].count());
400         Q_ASSERT(!m_playlists[target_track].is_blank(target_clip));
401         auto prod = m_playlists[target_track].replace_with_blank(target_clip);
402         if (prod != nullptr) {
403             m_playlists[target_track].consolidate_blanks();
404             m_allClips[clipId]->setCurrentTrackId(-1);
405             //m_allClips[clipId]->setSubPlaylistIndex(-1);
406             m_allClips.erase(clipId);
407             delete prod;
408             m_playlists[target_track].unlock();
409             if (auto ptr = m_parent.lock()) {
410                 ptr->m_snaps->removePoint(old_in);
411                 ptr->m_snaps->removePoint(old_out);
412                 if (finalMove) {
413                     if (!audioOnly && !isAudioTrack()) {
414                         emit ptr->invalidateZone(old_in, old_out);
415                     }
416                     if (!groupMove && target_clip >= m_playlists[target_track].count()) {
417                         // deleted last clip in playlist
418                         ptr->updateDuration();
419                     }
420                 }
421                 if (!audioOnly && !isHidden() && !isAudioTrack()) {
422                     // only refresh monitor if not an audio track and not hidden
423                     ptr->checkRefresh(old_in, old_out);
424                 }
425             }
426             return true;
427         }
428         m_playlists[target_track].unlock();
429         return false;
430     };
431 }
432 
requestClipDeletion(int clipId,bool updateView,bool finalMove,Fun & undo,Fun & redo,bool groupMove,bool finalDeletion,QList<int> allowedClipMixes)433 bool TrackModel::requestClipDeletion(int clipId, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove, bool finalDeletion, QList<int> allowedClipMixes)
434 {
435     QWriteLocker locker(&m_lock);
436     Q_ASSERT(m_allClips.count(clipId) > 0);
437     if (isLocked()) {
438         return false;
439     }
440     auto old_clip = m_allClips[clipId];
441     int old_position = old_clip->getPosition();
442     // qDebug() << "/// REQUESTOING CLIP DELETION_: " << updateView;
443     int duration = trackDuration();
444     if (finalDeletion) {
445         pCore->taskManager.discardJobs({ObjectType::TimelineClip,clipId});
446     }
447     auto operation = requestClipDeletion_lambda(clipId, updateView, finalMove, groupMove, finalDeletion);
448     if (operation()) {
449         if (finalMove && duration != trackDuration()) {
450             // A clip move changed the track duration, update track effects
451             m_effectStack->adjustStackLength(true, 0, duration, 0, trackDuration(), 0, undo, redo, true);
452         }
453         auto reverse = requestClipInsertion_lambda(clipId, old_position, updateView, finalMove, groupMove, allowedClipMixes);
454         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
455         return true;
456     }
457     return false;
458 }
459 
getBlankSizeAtPos(int frame)460 int TrackModel::getBlankSizeAtPos(int frame)
461 {
462     READ_LOCK();
463     int min_length = 0;
464     int blank_length = 0;
465     for (auto &m_playlist : m_playlists) {
466         int playlistLength = m_playlist.get_length();
467         if (frame >= playlistLength) {
468             continue;
469         } else {
470             int ix = m_playlist.get_clip_index_at(frame);
471             if (m_playlist.is_blank(ix)) {
472                 blank_length = m_playlist.clip_length(ix);
473             } else {
474                 // There is a clip at that position, abort
475                 return 0;
476             }
477         }
478         if (min_length == 0 || blank_length < min_length) {
479             min_length = blank_length;
480         }
481     }
482     if (blank_length == 0) {
483         // playlists are shorter than frame
484         return -1;
485     }
486     return min_length;
487 }
488 
suggestCompositionLength(int position)489 int TrackModel::suggestCompositionLength(int position)
490 {
491     READ_LOCK();
492     if (m_playlists[0].is_blank_at(position) && m_playlists[1].is_blank_at(position)) {
493         return -1;
494     }
495     auto clip_loc = getClipIndexAt(position);
496     int track = clip_loc.first;
497     int index = clip_loc.second;
498     int other_index; // index in the other track
499     int other_track = 1 - track;
500     int end_pos = m_playlists[track].clip_start(index) + m_playlists[track].clip_length(index);
501     other_index = m_playlists[other_track].get_clip_index_at(end_pos);
502     if (other_index < m_playlists[other_track].count()) {
503         end_pos = std::min(end_pos, m_playlists[other_track].clip_start(other_index) + m_playlists[other_track].clip_length(other_index));
504     }
505     return end_pos - position;
506 }
507 
validateCompositionLength(int pos,int offset,int duration,int endPos)508 QPair <int, int> TrackModel::validateCompositionLength(int pos, int offset, int duration, int endPos)
509 {
510     int startPos = pos;
511     bool startingFromOffset = false;
512     if (duration < offset) {
513         startPos += offset;
514         startingFromOffset = true;
515         if (startPos + duration > endPos) {
516             startPos = endPos - duration;
517         }
518     }
519 
520     int compsitionEnd = startPos + duration;
521     std::unordered_set<int> existing;
522     if (startingFromOffset) {
523         existing = getCompositionsInRange(startPos, compsitionEnd);
524         for (int id : existing) {
525             if (m_allCompositions[id]->getPosition() < startPos) {
526                 int end = m_allCompositions[id]->getPosition() + m_allCompositions[id]->getPlaytime();
527                 startPos = qMax(startPos, end);
528             }
529         }
530     } else if (offset > 0) {
531         existing = getCompositionsInRange(startPos, startPos + offset);
532         for (int id : existing) {
533             int end = m_allCompositions[id]->getPosition() + m_allCompositions[id]->getPlaytime();
534             startPos = qMax(startPos, end);
535         }
536     }
537     existing = getCompositionsInRange(startPos, compsitionEnd);
538     for (int id : existing) {
539         int start = m_allCompositions[id]->getPosition();
540         compsitionEnd = qMin(compsitionEnd, start);
541     }
542     return {startPos, compsitionEnd - startPos};
543 }
544 
getBlankSizeNearClip(int clipId,bool after)545 int TrackModel::getBlankSizeNearClip(int clipId, bool after)
546 {
547     READ_LOCK();
548     Q_ASSERT(m_allClips.count(clipId) > 0);
549     int clip_position = m_allClips[clipId]->getPosition();
550     auto clip_loc = getClipIndexAt(clip_position);
551     int track = clip_loc.first;
552     int index = clip_loc.second;
553     int other_index; // index in the other track
554     int other_track = 1 - track;
555     if (after) {
556         int first_pos = m_playlists[track].clip_start(index) + m_playlists[track].clip_length(index);
557         other_index = m_playlists[other_track].get_clip_index_at(first_pos);
558         index++;
559     } else {
560         int last_pos = m_playlists[track].clip_start(index) - 1;
561         other_index = m_playlists[other_track].get_clip_index_at(last_pos);
562         index--;
563     }
564     if (index < 0) return 0;
565     int length = INT_MAX;
566     if (index < m_playlists[track].count()) {
567         if (!m_playlists[track].is_blank(index)) {
568             return 0;
569         }
570         length = std::min(length, m_playlists[track].clip_length(index));
571     } else if (!after) {
572         length = std::min(length, m_playlists[track].clip_start(clip_loc.second) - m_playlists[track].get_length());
573     }
574     if (other_index < m_playlists[other_track].count()) {
575         if (!m_playlists[other_track].is_blank(other_index)) {
576             return 0;
577         }
578         length = std::min(length, m_playlists[other_track].clip_length(other_index));
579     } else if (!after) {
580         length = std::min(length, m_playlists[track].clip_start(clip_loc.second) - m_playlists[other_track].get_length());
581     }
582     return length;
583 }
584 
getBlankSizeNearComposition(int compoId,bool after)585 int TrackModel::getBlankSizeNearComposition(int compoId, bool after)
586 {
587     READ_LOCK();
588     Q_ASSERT(m_allCompositions.count(compoId) > 0);
589     int clip_position = m_allCompositions[compoId]->getPosition();
590     Q_ASSERT(m_compoPos.count(clip_position) > 0);
591     Q_ASSERT(m_compoPos[clip_position] == compoId);
592     auto it = m_compoPos.find(clip_position);
593     int clip_length = m_allCompositions[compoId]->getPlaytime();
594     int length = INT_MAX;
595     if (after) {
596         ++it;
597         if (it != m_compoPos.end()) {
598             return it->first - clip_position - clip_length;
599         }
600     } else {
601         if (it != m_compoPos.begin()) {
602             --it;
603             return clip_position - it->first - m_allCompositions[it->second]->getPlaytime();
604         }
605         return clip_position;
606     }
607     return length;
608 }
609 
requestClipResize_lambda(int clipId,int in,int out,bool right,bool hasMix)610 Fun TrackModel::requestClipResize_lambda(int clipId, int in, int out, bool right, bool hasMix)
611 {
612     QWriteLocker locker(&m_lock);
613     int clip_position = m_allClips[clipId]->getPosition();
614     int old_in = clip_position;
615     int old_out = old_in + m_allClips[clipId]->getPlaytime();
616     auto clip_loc = getClipIndexAt(clip_position, m_allClips[clipId]->getSubPlaylistIndex());
617     int target_track = clip_loc.first;
618     int target_clip = clip_loc.second;
619     Q_ASSERT(target_clip < m_playlists[target_track].count());
620     int size = out - in + 1;
621     bool checkRefresh = false;
622     if (!isHidden() && !isAudioTrack()) {
623         checkRefresh = true;
624     }
625     auto update_snaps = [old_in, old_out, checkRefresh, right, this](int new_in, int new_out) {
626         if (auto ptr = m_parent.lock()) {
627             if (right) {
628                 ptr->m_snaps->removePoint(old_out);
629                 ptr->m_snaps->addPoint(new_out);
630             } else {
631                 ptr->m_snaps->removePoint(old_in);
632                 ptr->m_snaps->addPoint(new_in);
633             }
634             if (checkRefresh) {
635                 if (right) {
636                     if (old_out < new_out) {
637                         ptr->checkRefresh(old_out, new_out);
638                     } else {
639                         ptr->checkRefresh(new_out, old_out);
640                     }
641                 } else if (old_in < new_in) {
642                     ptr->checkRefresh(old_in, new_in);
643                 } else {
644                     ptr->checkRefresh(new_in, old_in);
645                 }
646             }
647         } else {
648             qDebug() << "Error : clip resize failed because parent timeline is not available anymore";
649             Q_ASSERT(false);
650         }
651     };
652 
653     int delta = m_allClips[clipId]->getPlaytime() - size;
654     if (delta == 0) {
655         return []() { return true; };
656     }
657     if (delta > 0) { // we shrink clip
658         return [right, target_clip, target_track, clip_position, delta, in, out, clipId, update_snaps, this]() {
659             if (isLocked()) return false;
660             int target_clip_mutable = target_clip;
661             int blank_index = right ? (target_clip_mutable + 1) : target_clip_mutable;
662             // insert blank to space that is going to be empty
663             m_playlists[target_track].lock();
664             // The second is parameter is delta - 1 because this function expects an out time, which is basically size - 1
665             m_playlists[target_track].insert_blank(blank_index, delta - 1);
666             if (!right) {
667                 m_allClips[clipId]->setPosition(clip_position + delta);
668                 // Because we inserted blank before, the index of our clip has increased
669                 target_clip_mutable++;
670             }
671             int err = m_playlists[target_track].resize_clip(target_clip_mutable, in, out);
672             // make sure to do this after, to avoid messing the indexes
673             m_playlists[target_track].consolidate_blanks();
674             m_playlists[target_track].unlock();
675             if (err == 0) {
676                 update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1);
677                 if (right && m_playlists[target_track].count() - 1 == target_clip_mutable) {
678                     // deleted last clip in playlist
679                     if (auto ptr = m_parent.lock()) {
680                         ptr->updateDuration();
681                     }
682                 }
683             }
684             return err == 0;
685         };
686     }
687     int blank = -1;
688     int startPos = clip_position;
689     if (hasMix) {
690         startPos += m_allClips[clipId]->getMixDuration();
691     }
692     int endPos = m_allClips[clipId]->getPosition() + (out - in);
693     int other_blank_end = getBlankEnd(startPos, 1 - target_track);
694     if (right) {
695         if (target_clip == m_playlists[target_track].count() - 1 && (hasMix || other_blank_end >= endPos)) {
696             // clip is last, it can always be extended
697             if (hasMix && other_blank_end < endPos && !hasEndMix(clipId)) {
698                 // If clip has a start mix only, limit to next clip on other track
699                 return []() { return false; };
700             }
701             return [this, target_clip, target_track, in, out, update_snaps, clipId]() {
702                 if (isLocked()) return false;
703                 // color, image and title clips can have unlimited resize
704                 QScopedPointer<Mlt::Producer> clip(m_playlists[target_track].get_clip(target_clip));
705                 if (out >= clip->get_length()) {
706                     clip->parent().set("length", out + 1);
707                     clip->parent().set("out", out);
708                     clip->set("length", out + 1);
709                 }
710                 int err = m_playlists[target_track].resize_clip(target_clip, in, out);
711                 if (err == 0) {
712                     update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1);
713                 }
714                 m_playlists[target_track].consolidate_blanks();
715                 if (m_playlists[target_track].count() - 1 == target_clip) {
716                     // Resized last clip in playlist
717                     if (auto ptr = m_parent.lock()) {
718                         ptr->updateDuration();
719                     }
720                 }
721                 return err == 0;
722             };
723         } else {
724         }
725         blank = target_clip + 1;
726     } else {
727         if (target_clip == 0) {
728             // clip is first, it can never be extended on the left
729             return []() { return false; };
730         }
731         blank = target_clip - 1;
732     }
733     if (m_playlists[target_track].is_blank(blank)) {
734         int blank_length = m_playlists[target_track].clip_length(blank);
735         if (blank_length + delta >= 0 && (hasMix || other_blank_end >= out - in)) {
736             return [blank_length, blank, right, clipId, delta, update_snaps, this, in, out, target_clip, target_track]() {
737                 if (isLocked()) return false;
738                 int target_clip_mutable = target_clip;
739                 int err = 0;
740                 m_playlists[target_track].lock();
741                 if (blank_length + delta == 0) {
742                     err = m_playlists[target_track].remove(blank);
743                     if (!right) {
744                         target_clip_mutable--;
745                     }
746                 } else {
747                     err = m_playlists[target_track].resize_clip(blank, 0, blank_length + delta - 1);
748                 }
749                 if (err == 0) {
750                     QScopedPointer<Mlt::Producer> clip(m_playlists[target_track].get_clip(target_clip_mutable));
751                     if (out >= clip->get_length()) {
752                         clip->parent().set("length", out + 1);
753                         clip->parent().set("out", out);
754                         clip->set("length", out + 1);
755                         clip->set("out", out);
756                     }
757                     err = m_playlists[target_track].resize_clip(target_clip_mutable, in, out);
758                 }
759                 if (!right && err == 0) {
760                     m_allClips[clipId]->setPosition(m_playlists[target_track].clip_start(target_clip_mutable));
761                 }
762                 if (err == 0) {
763                     update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1);
764                 }
765                 m_playlists[target_track].consolidate_blanks();
766                 m_playlists[target_track].unlock();
767                 return err == 0;
768             };
769         } else {
770         }
771     }
772     return []() { qDebug()<<"=====FULL FAILURE "; return false; };
773 }
774 
getId() const775 int TrackModel::getId() const
776 {
777     return m_id;
778 }
779 
getClipByStartPosition(int position) const780 int TrackModel::getClipByStartPosition(int position) const
781 {
782     READ_LOCK();
783     for (auto &clip : m_allClips) {
784         if (clip.second->getPosition() == position) {
785             return clip.second->getId();
786         }
787     }
788     return -1;
789 }
790 
getClipByPosition(int position,int playlist)791 int TrackModel::getClipByPosition(int position, int playlist)
792 {
793     READ_LOCK();
794     QSharedPointer<Mlt::Producer> prod(nullptr);
795     if ((playlist == 0 || playlist == -1) && m_playlists[0].count() > 0) {
796         prod = QSharedPointer<Mlt::Producer>(m_playlists[0].get_clip_at(position));
797     }
798     if (playlist != 0 && (!prod || prod->is_blank()) && m_playlists[1].count() > 0) {
799         prod = QSharedPointer<Mlt::Producer>(m_playlists[1].get_clip_at(position));
800     }
801     if (!prod || prod->is_blank()) {
802         return -1;
803     }
804     int cid = prod->get_int("_kdenlive_cid");
805     if (playlist == -1) {
806         if (hasStartMix(cid)) {
807             if (position < m_allClips[cid]->getPosition() + m_allClips[cid]->getMixCutPosition()) {
808                 return m_mixList.key(cid, -1);
809             }
810         }
811         if (m_mixList.contains(cid)) {
812             // Clip has end mix
813             int otherId = m_mixList.value(cid);
814             if (position >= m_allClips[cid]->getPosition() + m_allClips[cid]->getPlaytime() - m_allClips[otherId]->getMixCutPosition()) {
815                 return otherId;
816             }
817         }
818     }
819     return cid;
820 }
821 
getClipProducer(int clipId)822 QSharedPointer<Mlt::Producer> TrackModel::getClipProducer(int clipId)
823 {
824     READ_LOCK();
825     QSharedPointer<Mlt::Producer> prod(nullptr);
826     if (m_playlists[0].count() > 0) {
827         prod = QSharedPointer<Mlt::Producer>(m_playlists[0].get_clip(clipId));
828     }
829     if ((!prod || prod->is_blank()) && m_playlists[1].count() > 0) {
830         prod = QSharedPointer<Mlt::Producer>(m_playlists[1].get_clip(clipId));
831     }
832     return prod;
833 }
834 
getCompositionByPosition(int position)835 int TrackModel::getCompositionByPosition(int position)
836 {
837     READ_LOCK();
838     for (const auto &comp : m_compoPos) {
839         if (comp.first == position) {
840             return comp.second;
841         } else if (comp.first < position) {
842             if (comp.first + m_allCompositions[comp.second]->getPlaytime() >= position) {
843                 return comp.second;
844             }
845         }
846     }
847     return -1;
848 }
849 
getClipByRow(int row) const850 int TrackModel::getClipByRow(int row) const
851 {
852     READ_LOCK();
853     if (row >= static_cast<int>(m_allClips.size())) {
854         return -1;
855     }
856     auto it = m_allClips.cbegin();
857     std::advance(it, row);
858     return (*it).first;
859 }
860 
getClipsInRange(int position,int end)861 std::unordered_set<int> TrackModel::getClipsInRange(int position, int end)
862 {
863     READ_LOCK();
864     std::unordered_set<int> ids;
865     for (const auto &clp : m_allClips) {
866         int pos = clp.second->getPosition();
867         int length = clp.second->getPlaytime();
868         if (end > -1 && pos >= end) {
869             continue;
870         }
871         if (pos >= position || pos + length - 1 >= position) {
872             ids.insert(clp.first);
873         }
874     }
875     return ids;
876 }
877 
getRowfromClip(int clipId) const878 int TrackModel::getRowfromClip(int clipId) const
879 {
880     READ_LOCK();
881     Q_ASSERT(m_allClips.count(clipId) > 0);
882     return int(std::distance(m_allClips.begin(), m_allClips.find(clipId)));
883 }
884 
getCompositionsInRange(int position,int end)885 std::unordered_set<int> TrackModel::getCompositionsInRange(int position, int end)
886 {
887     READ_LOCK();
888     // TODO: this function doesn't take into accounts the fact that there are two tracks
889     std::unordered_set<int> ids;
890     for (const auto &compo : m_allCompositions) {
891         int pos = compo.second->getPosition();
892         int length = compo.second->getPlaytime();
893         if (end > -1 && pos >= end) {
894             continue;
895         }
896         if (pos >= position || pos + length - 1 >= position) {
897 
898             ids.insert(compo.first);
899         }
900     }
901     return ids;
902 }
903 
getRowfromComposition(int tid) const904 int TrackModel::getRowfromComposition(int tid) const
905 {
906     READ_LOCK();
907     Q_ASSERT(m_allCompositions.count(tid) > 0);
908     return int(m_allClips.size()) + int(std::distance(m_allCompositions.begin(), m_allCompositions.find(tid)));
909 }
910 
getProperty(const QString & name) const911 QVariant TrackModel::getProperty(const QString &name) const
912 {
913     READ_LOCK();
914     return QVariant(m_track->get(name.toUtf8().constData()));
915 }
916 
setProperty(const QString & name,const QString & value)917 void TrackModel::setProperty(const QString &name, const QString &value)
918 {
919     QWriteLocker locker(&m_lock);
920     m_track->set(name.toUtf8().constData(), value.toUtf8().constData());
921     // Hide property must be defined at playlist level or it won't be saved
922     if (name == QLatin1String("kdenlive:audio_track") || name == QLatin1String("hide")) {
923         for (auto &m_playlist : m_playlists) {
924             m_playlist.set(name.toUtf8().constData(), value.toInt());
925         }
926     }
927 }
928 
checkConsistency()929 bool TrackModel::checkConsistency()
930 {
931     auto ptr = m_parent.lock();
932     if (!ptr) {
933         return false;
934     }
935     auto check_blank_zone = [&](int playlist, int in, int out) {
936         if (in >= m_playlists[playlist].get_playtime()) {
937             return true;
938         }
939         int index = m_playlists[playlist].get_clip_index_at(in);
940         if (!m_playlists[playlist].is_blank(index)) {
941             return false;
942         }
943         int cin = m_playlists[playlist].clip_start(index);
944         if (cin > in) {
945             return false;
946         }
947         if (cin + m_playlists[playlist].clip_length(index) - 1 < out) {
948             return false;
949         }
950         return true;
951     };
952     std::vector<std::pair<int, int>> clips; // clips stored by (position, id)
953     for (const auto &c : m_allClips) {
954         Q_ASSERT(c.second);
955         Q_ASSERT(c.second.get() == ptr->getClipPtr(c.first).get());
956         clips.emplace_back(c.second->getPosition(), c.first);
957     }
958     std::sort(clips.begin(), clips.end());
959     int last_out = 0;
960     for (size_t i = 0; i < clips.size(); ++i) {
961         auto cur_clip = m_allClips[clips[i].second];
962         if (last_out < clips[i].first) {
963             // we have some blank space before this clip, check it
964             for (int pl = 0; pl <= 1; ++pl) {
965                 if (!check_blank_zone(pl, last_out, clips[i].first - 1)) {
966                     qDebug() << "ERROR: Some blank was required on playlist " << pl << " between " << last_out << " and " << clips[i].first - 1;
967                     return false;
968                 }
969             }
970         }
971         int cur_playlist = cur_clip->getSubPlaylistIndex();
972         int clip_index = m_playlists[cur_playlist].get_clip_index_at(clips[i].first);
973         if (m_playlists[cur_playlist].is_blank(clip_index)) {
974             qDebug() << "ERROR: Found blank when clip was required at position " << clips[i].first;
975             return false;
976         }
977         if (m_playlists[cur_playlist].clip_start(clip_index) != clips[i].first) {
978             qDebug() << "ERROR: Inconsistent start position for clip at position " << clips[i].first;
979             return false;
980         }
981         if (m_playlists[cur_playlist].clip_start(clip_index) != clips[i].first) {
982             qDebug() << "ERROR: Inconsistent start position for clip at position " << clips[i].first;
983             return false;
984         }
985         if (m_playlists[cur_playlist].clip_length(clip_index) != cur_clip->getPlaytime()) {
986             qDebug() << "ERROR: Inconsistent length for clip at position " << clips[i].first;
987             return false;
988         }
989         auto pr = m_playlists[cur_playlist].get_clip(clip_index);
990         Mlt::Producer prod(pr);
991         if (!prod.same_clip(*cur_clip)) {
992             qDebug() << "ERROR: Wrong clip at position " << clips[i].first;
993             delete pr;
994             return false;
995         }
996         delete pr;
997 
998         // the current playlist is valid, we check that the other is essentially blank
999         int other_playlist = (cur_playlist + 1) % 2;
1000         int in_blank = clips[i].first;
1001         int out_blank = clips[i].first + cur_clip->getPlaytime() - 1;
1002 
1003         // the previous clip on the same playlist must not intersect
1004         int prev_clip_id_same_playlist = -1;
1005         for (int j = int(i) - 1; j >= 0; --j) {
1006             if (cur_playlist == m_allClips[clips[size_t(j)].second]->getSubPlaylistIndex()) {
1007                 prev_clip_id_same_playlist = j;
1008                 break;
1009             }
1010         }
1011         if (prev_clip_id_same_playlist >= 0 &&
1012             clips[size_t(prev_clip_id_same_playlist)].first + m_allClips[clips[size_t(prev_clip_id_same_playlist)].second]->getPlaytime() > clips[i].first) {
1013             qDebug() << "ERROR: found overlapping clips at position " << clips[i].first;
1014             return false;
1015         }
1016 
1017         // the previous clip on the other playlist might restrict the blank in/out
1018         int prev_clip_id_other_playlist = -1;
1019         for (int j = int(i) - 1; j >= 0; --j) {
1020             if (other_playlist == m_allClips[clips[size_t(j)].second]->getSubPlaylistIndex()) {
1021                 prev_clip_id_other_playlist = j;
1022                 break;
1023             }
1024         }
1025         if (prev_clip_id_other_playlist >= 0) {
1026             in_blank = std::max(in_blank, clips[size_t(prev_clip_id_other_playlist)].first +
1027                                               m_allClips[clips[size_t(prev_clip_id_other_playlist)].second]->getPlaytime());
1028         }
1029 
1030         // the next clip on the other playlist might restrict the blank in/out
1031         int next_clip_id_other_playlist = -1;
1032         for (size_t j = i + 1; j < clips.size(); ++j) {
1033             if (other_playlist == m_allClips[clips[j].second]->getSubPlaylistIndex()) {
1034                 next_clip_id_other_playlist = int(j);
1035                 break;
1036             }
1037         }
1038         if (next_clip_id_other_playlist >= 0) {
1039             out_blank = std::min(out_blank, clips[size_t(next_clip_id_other_playlist)].first - 1);
1040         }
1041         if (in_blank <= out_blank && !check_blank_zone(other_playlist, in_blank, out_blank)) {
1042             qDebug() << "ERROR: we expected blank on playlist " << other_playlist << " between " << in_blank << " and " << out_blank;
1043             return false;
1044         }
1045 
1046         last_out = clips[i].first + cur_clip->getPlaytime();
1047     }
1048     int playtime = std::max(m_playlists[0].get_playtime(), m_playlists[1].get_playtime());
1049     if (!clips.empty() && playtime != clips.back().first + m_allClips[clips.back().second]->getPlaytime()) {
1050         qDebug() << "Error: playtime is " << playtime << " but was expected to be" << clips.back().first + m_allClips[clips.back().second]->getPlaytime();
1051         return false;
1052     }
1053 
1054     // We now check compositions positions
1055     if (m_allCompositions.size() != m_compoPos.size()) {
1056         qDebug() << "Error: the number of compositions position doesn't match number of compositions";
1057         return false;
1058     }
1059     for (const auto &compo : m_allCompositions) {
1060         int pos = compo.second->getPosition();
1061         if (m_compoPos.count(pos) == 0) {
1062             qDebug() << "Error: the position of composition " << compo.first << " is not properly stored";
1063             return false;
1064         }
1065         if (m_compoPos[pos] != compo.first) {
1066             qDebug() << "Error: found composition" << m_compoPos[pos] << "instead of " << compo.first << "at position" << pos;
1067             return false;
1068         }
1069     }
1070     for (auto it = m_compoPos.begin(); it != m_compoPos.end(); ++it) {
1071         int compoId = it->second;
1072         int cur_in = m_allCompositions[compoId]->getPosition();
1073         Q_ASSERT(cur_in == it->first);
1074         int cur_out = cur_in + m_allCompositions[compoId]->getPlaytime() - 1;
1075         ++it;
1076         if (it != m_compoPos.end()) {
1077             int next_compoId = it->second;
1078             int next_in = m_allCompositions[next_compoId]->getPosition();
1079             int next_out = next_in + m_allCompositions[next_compoId]->getPlaytime() - 1;
1080             if (next_in <= cur_out) {
1081                 qDebug() << "Error: found collision between composition " << compoId << "[ " << cur_in << ", " << cur_out << "] and " << next_compoId << "[ "
1082                          << next_in << ", " << next_out << "]";
1083                 return false;
1084             }
1085         }
1086         --it;
1087     }
1088     // Check Mixes
1089     QScopedPointer<Mlt::Service> service(m_track->field());
1090     int mixCount = 0;
1091     while (service != nullptr && service->is_valid()) {
1092         if (service->type() == mlt_service_transition_type) {
1093             Mlt::Transition t(mlt_transition(service->get_service()));
1094             service.reset(service->producer());
1095             // Check that the mix has correct in/out
1096             int mainId = -1;
1097             int mixIn = t.get_in();
1098             for (auto & m_sameComposition : m_sameCompositions) {
1099                 if (static_cast<Mlt::Transition *>(m_sameComposition.second->getAsset())->get_in() == mixIn) {
1100                     // Found mix in list
1101                     mainId = m_sameComposition.first;
1102                     break;
1103                 }
1104             }
1105             if (mainId == -1) {
1106                 qDebug()<<"=== Incoherent mix found at: "<<mixIn;
1107                 return false;
1108             }
1109             // Check in/out)
1110             if (mixIn != m_allClips[mainId]->getPosition()) {
1111                 qDebug()<<"=== Mix not aligned with its master clip: "<<mainId<<", at: "<<m_allClips[mainId]->getPosition()<<", MIX at: "<<mixIn;
1112                 return false;
1113             }
1114             int secondClipId = m_mixList.key(mainId);
1115             if (t.get_out() != m_allClips[secondClipId]->getPosition() + m_allClips[secondClipId]->getPlaytime()) {
1116                 qDebug()<<"=== Mix not aligned with its second clip: "<<secondClipId<<", end at: "<<m_allClips[secondClipId]->getPosition() + m_allClips[secondClipId]->getPlaytime()<<", MIX at: "<<t.get_out();
1117                 return false;
1118             }
1119             mixCount++;
1120         } else {
1121             service.reset(service->producer());
1122         }
1123     }
1124     if (mixCount != static_cast<int>(m_sameCompositions.size()) || static_cast<int>(m_sameCompositions.size()) != m_mixList.count()) {
1125         // incoherent mix count
1126         qDebug()<<"=== INCORRECT mix count. Existing: "<<mixCount<<"; REGISTERED: "<<m_mixList.count();
1127         return false;
1128     }
1129     return true;
1130 }
1131 
getClipIndexAt(int position,int playlist)1132 std::pair<int, int> TrackModel::getClipIndexAt(int position, int playlist)
1133 {
1134     READ_LOCK();
1135     if (playlist == -1) {
1136         for (int j = 0; j < 2; j++) {
1137             if (!m_playlists[j].is_blank_at(position)) {
1138                 return {j, m_playlists[j].get_clip_index_at(position)};
1139             }
1140         }
1141     }
1142     if (!m_playlists[playlist].is_blank_at(position)) {
1143         return {playlist, m_playlists[playlist].get_clip_index_at(position)};
1144     }
1145     qDebug()<<"=== CANNOT FIND CLIP ON PLAYLIST: "<<playlist<<" AT POSITION: "<<position<<", TID: "<<m_id;
1146     Q_ASSERT(false);
1147     return {-1, -1};
1148 }
1149 
isLastClip(int position)1150 bool TrackModel::isLastClip(int position)
1151 {
1152     READ_LOCK();
1153     for (auto & m_playlist : m_playlists) {
1154         if (!m_playlist.is_blank_at(position)) {
1155             return m_playlist.get_clip_index_at(position) == m_playlist.count() - 1;
1156         }
1157     }
1158     return false;
1159 }
1160 
isBlankAt(int position,int playlist)1161 bool TrackModel::isBlankAt(int position, int playlist)
1162 {
1163     READ_LOCK();
1164     if (playlist == -1) {
1165         return m_playlists[0].is_blank_at(position) && m_playlists[1].is_blank_at(position);
1166     }
1167     return m_playlists[playlist].is_blank_at(position);
1168 }
1169 
getBlankStart(int position)1170 int TrackModel::getBlankStart(int position)
1171 {
1172     READ_LOCK();
1173     int result = 0;
1174     for (auto &m_playlist : m_playlists) {
1175         if (m_playlist.count() == 0) {
1176             break;
1177         }
1178         if (!m_playlist.is_blank_at(position)) {
1179             result = position;
1180             break;
1181         }
1182         int clip_index = m_playlist.get_clip_index_at(position);
1183         int start = m_playlist.clip_start(clip_index);
1184         if (start > result) {
1185             result = start;
1186         }
1187     }
1188     return result;
1189 }
1190 
getClipStart(int position,int track)1191 int TrackModel::getClipStart(int position, int track)
1192 {
1193     if (track == -1) {
1194         return getBlankStart(position);
1195     }
1196     READ_LOCK();
1197     if (m_playlists[track].is_blank_at(position)) {
1198         return position;
1199     }
1200     int clip_index = m_playlists[track].get_clip_index_at(position);
1201     return m_playlists[track].clip_start(clip_index);
1202 }
1203 
getClipEnd(int position,int track)1204 int TrackModel::getClipEnd(int position, int track)
1205 {
1206     if (track == -1) {
1207         return getBlankStart(position);
1208     }
1209     READ_LOCK();
1210     if (m_playlists[track].is_blank_at(position)) {
1211         return position;
1212     }
1213     int clip_index = m_playlists[track].get_clip_index_at(position);
1214     clip_index++;
1215     return m_playlists[track].clip_start(clip_index);
1216 }
1217 
getBlankStart(int position,int track)1218 int TrackModel::getBlankStart(int position, int track)
1219 {
1220     if (track == -1) {
1221         return getBlankStart(position);
1222     }
1223     READ_LOCK();
1224     int result = 0;
1225     if (!m_playlists[track].is_blank_at(position)) {
1226         return position;
1227     }
1228     int clip_index = m_playlists[track].get_clip_index_at(position);
1229     int start = m_playlists[track].clip_start(clip_index);
1230     if (start > result) {
1231         result = start;
1232     }
1233     return result;
1234 }
1235 
getBlankEnd(int position,int track)1236 int TrackModel::getBlankEnd(int position, int track)
1237 {
1238     if (track == -1) {
1239         return getBlankEnd(position);
1240     }
1241     READ_LOCK();
1242     // Q_ASSERT(m_playlists[track].is_blank_at(position));
1243     if (!m_playlists[track].is_blank_at(position)) {
1244         return position;
1245     }
1246     int clip_index = m_playlists[track].get_clip_index_at(position);
1247     int count = m_playlists[track].count();
1248     if (clip_index < count) {
1249         int blank_start = m_playlists[track].clip_start(clip_index);
1250         int blank_length = m_playlists[track].clip_length(clip_index) - 1;
1251         return blank_start + blank_length;
1252     }
1253     return INT_MAX;
1254 }
1255 
getBlankEnd(int position)1256 int TrackModel::getBlankEnd(int position)
1257 {
1258     READ_LOCK();
1259     int end = INT_MAX;
1260     for (int j = 0; j < 2; j++) {
1261         end = std::min(getBlankEnd(position, j), end);
1262     }
1263     return end;
1264 }
1265 
requestCompositionResize_lambda(int compoId,int in,int out,bool logUndo)1266 Fun TrackModel::requestCompositionResize_lambda(int compoId, int in, int out, bool logUndo)
1267 {
1268     QWriteLocker locker(&m_lock);
1269     int compo_position = m_allCompositions[compoId]->getPosition();
1270     Q_ASSERT(m_compoPos.count(compo_position) > 0);
1271     Q_ASSERT(m_compoPos[compo_position] == compoId);
1272     int old_in = compo_position;
1273     int old_out = old_in + m_allCompositions[compoId]->getPlaytime() - 1;
1274     qDebug() << "compo resize " << compoId << in << "-" << out << " / " << old_in << "-" << old_out;
1275     if (out == -1) {
1276         out = in + old_out - old_in;
1277     }
1278 
1279     auto update_snaps = [old_in, old_out, logUndo, this](int new_in, int new_out) {
1280         if (auto ptr = m_parent.lock()) {
1281             ptr->m_snaps->removePoint(old_in);
1282             ptr->m_snaps->removePoint(old_out + 1);
1283             ptr->m_snaps->addPoint(new_in);
1284             ptr->m_snaps->addPoint(new_out);
1285             ptr->checkRefresh(old_in, old_out);
1286             ptr->checkRefresh(new_in, new_out);
1287             if (logUndo) {
1288                 emit ptr->invalidateZone(old_in, old_out);
1289                 emit ptr->invalidateZone(new_in, new_out);
1290             }
1291             // ptr->adjustAssetRange(compoId, new_in, new_out);
1292         } else {
1293             qDebug() << "Error : Composition resize failed because parent timeline is not available anymore";
1294             Q_ASSERT(false);
1295         }
1296     };
1297 
1298     if (in == compo_position && (out == -1 || out == old_out)) {
1299         return []() {
1300             qDebug() << "//// NO MOVE PERFORMED\n!!!!!!!!!!!!!!!!!!!!!!!!!!";
1301             return true;
1302         };
1303     }
1304 
1305     // temporary remove of current compo to check collisions
1306     qDebug() << "// CURRENT COMPOSITIONS ----\n" << m_compoPos << "\n--------------";
1307     m_compoPos.erase(compo_position);
1308     bool intersecting = hasIntersectingComposition(in, out);
1309     // put it back
1310     m_compoPos[compo_position] = compoId;
1311 
1312     if (intersecting) {
1313         return []() {
1314             qDebug() << "//// FALSE MOVE PERFORMED\n!!!!!!!!!!!!!!!!!!!!!!!!!!";
1315             return false;
1316         };
1317     }
1318 
1319     return [in, out, compoId, update_snaps, this]() {
1320         if (isLocked()) return false;
1321         m_compoPos.erase(m_allCompositions[compoId]->getPosition());
1322         m_allCompositions[compoId]->setInOut(in, out);
1323         update_snaps(in, out + 1);
1324         m_compoPos[m_allCompositions[compoId]->getPosition()] = compoId;
1325         return true;
1326     };
1327 }
1328 
requestCompositionInsertion(int compoId,int position,bool updateView,bool finalMove,Fun & undo,Fun & redo)1329 bool TrackModel::requestCompositionInsertion(int compoId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo)
1330 {
1331     QWriteLocker locker(&m_lock);
1332     if (isLocked()) {
1333         return false;
1334     }
1335     auto operation = requestCompositionInsertion_lambda(compoId, position, updateView, finalMove);
1336     if (operation()) {
1337         auto reverse = requestCompositionDeletion_lambda(compoId, updateView, finalMove);
1338         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
1339         return true;
1340     }
1341     return false;
1342 }
1343 
requestCompositionDeletion(int compoId,bool updateView,bool finalMove,Fun & undo,Fun & redo,bool finalDeletion)1344 bool TrackModel::requestCompositionDeletion(int compoId, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool finalDeletion)
1345 {
1346     QWriteLocker locker(&m_lock);
1347     if (isLocked()) {
1348         return false;
1349     }
1350     Q_ASSERT(m_allCompositions.count(compoId) > 0);
1351     auto old_composition = m_allCompositions[compoId];
1352     int old_position = old_composition->getPosition();
1353     Q_ASSERT(m_compoPos.count(old_position) > 0);
1354     Q_ASSERT(m_compoPos[old_position] == compoId);
1355     auto operation = requestCompositionDeletion_lambda(compoId, updateView, finalMove && finalDeletion);
1356     if (operation()) {
1357         auto reverse = requestCompositionInsertion_lambda(compoId, old_position, updateView, finalMove);
1358         UPDATE_UNDO_REDO(operation, reverse, undo, redo);
1359         return true;
1360     }
1361     return false;
1362 }
1363 
requestCompositionDeletion_lambda(int compoId,bool updateView,bool finalMove)1364 Fun TrackModel::requestCompositionDeletion_lambda(int compoId, bool updateView, bool finalMove)
1365 {
1366     QWriteLocker locker(&m_lock);
1367     // Find index of clip
1368     int clip_position = m_allCompositions[compoId]->getPosition();
1369     int old_in = clip_position;
1370     int old_out = old_in + m_allCompositions[compoId]->getPlaytime();
1371     return [compoId, old_in, old_out, updateView, finalMove, this]() {
1372         if (isLocked()) return false;
1373         int old_clip_index = getRowfromComposition(compoId);
1374         if (finalMove && m_allCompositions[compoId]->selected) {
1375             m_allCompositions[compoId]->selected = false;
1376             if (auto ptr = m_parent.lock()) {
1377                 // item was selected, unselect
1378                 ptr->requestClearSelection(true);
1379             }
1380         }
1381         auto ptr = m_parent.lock();
1382         if (updateView) {
1383             ptr->_beginRemoveRows(ptr->makeTrackIndexFromID(getId()), old_clip_index, old_clip_index);
1384             ptr->_endRemoveRows();
1385         }
1386         m_allCompositions[compoId]->setCurrentTrackId(-1);
1387         m_allCompositions.erase(compoId);
1388         m_compoPos.erase(old_in);
1389         ptr->m_snaps->removePoint(old_in);
1390         ptr->m_snaps->removePoint(old_out);
1391         if (finalMove) {
1392             emit ptr->invalidateZone(old_in, old_out);
1393         }
1394         return true;
1395     };
1396 }
1397 
getCompositionByRow(int row) const1398 int TrackModel::getCompositionByRow(int row) const
1399 {
1400     READ_LOCK();
1401     if (row < int(m_allClips.size())) {
1402         return -1;
1403     }
1404     Q_ASSERT(row <= int(m_allClips.size() + m_allCompositions.size()));
1405     auto it = m_allCompositions.cbegin();
1406     std::advance(it, row - int(m_allClips.size()));
1407     return (*it).first;
1408 }
1409 
getCompositionsCount() const1410 int TrackModel::getCompositionsCount() const
1411 {
1412     READ_LOCK();
1413     return int(m_allCompositions.size());
1414 }
1415 
requestCompositionInsertion_lambda(int compoId,int position,bool updateView,bool finalMove)1416 Fun TrackModel::requestCompositionInsertion_lambda(int compoId, int position, bool updateView, bool finalMove)
1417 {
1418     QWriteLocker locker(&m_lock);
1419     bool intersecting = true;
1420     if (auto ptr = m_parent.lock()) {
1421         intersecting = hasIntersectingComposition(position, position + ptr->getCompositionPlaytime(compoId) - 1);
1422     } else {
1423         qDebug() << "Error : Composition Insertion failed because timeline is not available anymore";
1424     }
1425     if (!intersecting) {
1426         return [compoId, this, position, updateView, finalMove]() {
1427             if (isLocked()) return false;
1428             if (auto ptr = m_parent.lock()) {
1429                 std::shared_ptr<CompositionModel> composition = ptr->getCompositionPtr(compoId);
1430                 m_allCompositions[composition->getId()] = composition; // store clip
1431                 // update clip position and track
1432                 composition->setCurrentTrackId(getId());
1433                 int new_in = position;
1434                 int new_out = new_in + composition->getPlaytime();
1435                 composition->setInOut(new_in, new_out - 1);
1436                 if (updateView) {
1437                     int composition_index = getRowfromComposition(composition->getId());
1438                     ptr->_beginInsertRows(ptr->makeTrackIndexFromID(composition->getCurrentTrackId()), composition_index, composition_index);
1439                     ptr->_endInsertRows();
1440                 }
1441                 ptr->m_snaps->addPoint(new_in);
1442                 ptr->m_snaps->addPoint(new_out);
1443                 m_compoPos[new_in] = composition->getId();
1444                 if (finalMove) {
1445                     emit ptr->invalidateZone(new_in, new_out);
1446                 }
1447                 return true;
1448             }
1449             qDebug() << "Error : Composition Insertion failed because timeline is not available anymore";
1450             return false;
1451         };
1452     }
1453     return []() { return false; };
1454 }
1455 
hasIntersectingComposition(int in,int out) const1456 bool TrackModel::hasIntersectingComposition(int in, int out) const
1457 {
1458     READ_LOCK();
1459     auto it = m_compoPos.lower_bound(in);
1460     if (m_compoPos.empty()) {
1461         return false;
1462     }
1463     if (it != m_compoPos.end() && it->first <= out) {
1464         // compo at it intersects
1465         return true;
1466     }
1467     if (it == m_compoPos.begin()) {
1468         return false;
1469     }
1470     --it;
1471     int end = it->first + m_allCompositions.at(it->second)->getPlaytime() - 1;
1472     return end >= in;
1473 
1474     return false;
1475 }
1476 
addEffect(const QString & effectId)1477 bool TrackModel::addEffect(const QString &effectId)
1478 {
1479     READ_LOCK();
1480     return m_effectStack->appendEffect(effectId);
1481 }
1482 
effectNames() const1483 const QString TrackModel::effectNames() const
1484 {
1485     READ_LOCK();
1486     return m_effectStack->effectNames();
1487 }
1488 
stackEnabled() const1489 bool TrackModel::stackEnabled() const
1490 {
1491     READ_LOCK();
1492     return m_effectStack->isStackEnabled();
1493 }
1494 
setEffectStackEnabled(bool enable)1495 void TrackModel::setEffectStackEnabled(bool enable)
1496 {
1497     m_effectStack->setEffectStackEnabled(enable);
1498 }
1499 
trackDuration() const1500 int TrackModel::trackDuration() const
1501 {
1502     return m_track->get_length();
1503 }
1504 
isLocked() const1505 bool TrackModel::isLocked() const
1506 {
1507     READ_LOCK();
1508     return m_track->get_int("kdenlive:locked_track");
1509 }
1510 
isTimelineActive() const1511 bool TrackModel::isTimelineActive() const
1512 {
1513     READ_LOCK();
1514     return m_track->get_int("kdenlive:timeline_active");
1515 }
1516 
shouldReceiveTimelineOp() const1517 bool TrackModel::shouldReceiveTimelineOp() const
1518 {
1519     READ_LOCK();
1520     return isTimelineActive() && !isLocked();
1521 }
1522 
isAudioTrack() const1523 bool TrackModel::isAudioTrack() const
1524 {
1525     return m_track->get_int("kdenlive:audio_track") == 1;
1526 }
1527 
getTrackService()1528 std::shared_ptr<Mlt::Tractor> TrackModel::getTrackService()
1529 {
1530     return m_track;
1531 }
1532 
trackType() const1533 PlaylistState::ClipState TrackModel::trackType() const
1534 {
1535     return (m_track->get_int("kdenlive:audio_track") == 1 ? PlaylistState::AudioOnly : PlaylistState::VideoOnly);
1536 }
1537 
isHidden() const1538 bool TrackModel::isHidden() const
1539 {
1540     return m_track->get_int("hide") & 1;
1541 }
1542 
isMute() const1543 bool TrackModel::isMute() const
1544 {
1545     return m_track->get_int("hide") & 2;
1546 }
1547 
importEffects(std::weak_ptr<Mlt::Service> service)1548 bool TrackModel::importEffects(std::weak_ptr<Mlt::Service> service)
1549 {
1550     QWriteLocker locker(&m_lock);
1551     m_effectStack->importEffects(std::move(service), trackType());
1552     return true;
1553 }
1554 
copyEffect(const std::shared_ptr<EffectStackModel> & stackModel,int rowId)1555 bool TrackModel::copyEffect(const std::shared_ptr<EffectStackModel> &stackModel, int rowId)
1556 {
1557     QWriteLocker locker(&m_lock);
1558     return m_effectStack->copyEffect(stackModel->getEffectStackRow(rowId), isAudioTrack() ? PlaylistState::AudioOnly : PlaylistState::VideoOnly);
1559 }
1560 
lock()1561 void TrackModel::lock()
1562 {
1563     setProperty(QStringLiteral("kdenlive:locked_track"), QStringLiteral("1"));
1564     if (auto ptr = m_parent.lock()) {
1565         QModelIndex ix = ptr->makeTrackIndexFromID(m_id);
1566         emit ptr->dataChanged(ix, ix, {TimelineModel::IsLockedRole});
1567     }
1568 }
unlock()1569 void TrackModel::unlock()
1570 {
1571     setProperty(QStringLiteral("kdenlive:locked_track"), nullptr);
1572     if (auto ptr = m_parent.lock()) {
1573         QModelIndex ix = ptr->makeTrackIndexFromID(m_id);
1574         emit ptr->dataChanged(ix, ix, {TimelineModel::IsLockedRole});
1575     }
1576 }
1577 
1578 
isAvailable(int position,int duration,int playlist)1579 bool TrackModel::isAvailable(int position, int duration, int playlist)
1580 {
1581     if (playlist == -1) {
1582         // Check on both playlists
1583         for (auto &playlist : m_playlists) {
1584             int start_clip = playlist.get_clip_index_at(position);
1585             int end_clip = playlist.get_clip_index_at(position + duration - 1);
1586             if (start_clip != end_clip || !playlist.is_blank(start_clip)) {
1587                 return false;
1588             }
1589         }
1590         return true;
1591     }
1592     int start_clip = m_playlists[playlist].get_clip_index_at(position);
1593     int end_clip = m_playlists[playlist].get_clip_index_at(position + duration - 1);
1594     if (start_clip != end_clip) {
1595         return false;
1596     }
1597     return m_playlists[playlist].is_blank(start_clip);
1598 }
1599 
isAvailableWithExceptions(int position,int duration,QVector<int> exceptions)1600 bool TrackModel::isAvailableWithExceptions(int position, int duration, QVector<int>exceptions)
1601 {
1602     // Check on both playlists
1603     QSharedPointer<Mlt::Producer> prod = nullptr;
1604     for (auto &playlist : m_playlists) {
1605         int start_clip = playlist.get_clip_index_at(position);
1606         int end_clip = playlist.get_clip_index_at(position + duration - 1);
1607         for (int ix = start_clip; ix <= end_clip; ix++) {
1608             if (playlist.is_blank(ix)) {
1609                 continue;
1610             }
1611             prod.reset(playlist.get_clip(ix));
1612             if (prod) {
1613                 if (!exceptions.contains(prod->get_int("_kdenlive_cid"))) {
1614                     return false;
1615                 }
1616             }
1617         }
1618     }
1619     return true;
1620 }
1621 
requestRemoveMix(std::pair<int,int> clipIds,Fun & undo,Fun & redo)1622 bool TrackModel::requestRemoveMix(std::pair<int, int> clipIds, Fun &undo, Fun &redo)
1623 {
1624     int mixDuration;
1625     int mixCutPos;
1626     int endPos;
1627     int firstInPos;
1628     int secondInPos;
1629     int mixPosition;
1630     int src_track = 0;
1631     int first_src_track = 0;
1632     bool secondClipHasEndMix = false;
1633     bool firstClipHasStartMix = false;
1634     QList <int> allowedMixes = {clipIds.first};
1635     if (auto ptr = m_parent.lock()) {
1636         // The clip that will be moved to playlist 1
1637         std::shared_ptr<ClipModel> firstClip(ptr->getClipPtr(clipIds.first));
1638         std::shared_ptr<ClipModel> secondClip(ptr->getClipPtr(clipIds.second));
1639         mixDuration = secondClip->getMixDuration();
1640         mixCutPos = secondClip->getMixCutPosition();
1641         mixPosition = secondClip->getPosition();
1642         firstInPos = firstClip->getPosition();
1643         secondInPos = mixPosition + mixDuration - mixCutPos;
1644         endPos = mixPosition + secondClip->getPlaytime();
1645         secondClipHasEndMix = hasEndMix(clipIds.second);
1646         firstClipHasStartMix = hasStartMix(clipIds.first);
1647         src_track = secondClip->getSubPlaylistIndex();
1648         first_src_track = firstClip->getSubPlaylistIndex();
1649     } else {
1650         return false;
1651     }
1652     bool result = false;
1653     bool closing = false;
1654     Fun local_undo = []() { return true; };
1655     Fun local_redo = []() { return true; };
1656     if (auto ptr = m_parent.lock()) {
1657         // Resize first part clip
1658         closing = ptr->m_closing;
1659         if (closing) {
1660             result = true;
1661         } else {
1662             result = ptr->getClipPtr(clipIds.first)->requestResize(secondInPos - firstInPos, true, local_undo, local_redo, true, true);
1663             // Resize main clip
1664             result = result && ptr->getClipPtr(clipIds.second)->requestResize(endPos - secondInPos, false, local_undo, local_redo, true, true);
1665         }
1666     }
1667     if (result) {
1668         QString assetId = m_sameCompositions[clipIds.second]->getAssetId();
1669         QVector<QPair<QString, QVariant>> params = m_sameCompositions[clipIds.second]->getAllParameters();
1670         bool switchSecondTrack = false;
1671         bool switchFirstTrack = false;
1672         if (src_track == 1 && !secondClipHasEndMix && !closing) {
1673             switchSecondTrack = true;
1674         }
1675         if (first_src_track == 1 && !firstClipHasStartMix && !closing) {
1676             switchFirstTrack = true;
1677         }
1678         Fun replay = [this, clipIds, firstInPos, secondInPos, switchFirstTrack, switchSecondTrack]() {
1679             if (switchFirstTrack) {
1680                 // Revert clip to playlist 0 since it has no mix
1681                 switchPlaylist(clipIds.first, firstInPos, 1, 0);
1682             }
1683             if (switchSecondTrack) {
1684                 // Revert clip to playlist 0 since it has no mix
1685                 switchPlaylist(clipIds.second, secondInPos, 1, 0);
1686             }
1687             // Delete transition
1688             Mlt::Transition &transition = *static_cast<Mlt::Transition*>(m_sameCompositions[clipIds.second]->getAsset());
1689             QScopedPointer<Mlt::Field> field(m_track->field());
1690             field->lock();
1691             field->disconnect_service(transition);
1692             field->unlock();
1693             m_sameCompositions.erase(clipIds.second);
1694             m_mixList.remove(clipIds.first);
1695             if (auto ptr = m_parent.lock()) {
1696                 std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipIds.second));
1697                 movedClip->setMixDuration(0);
1698                 /*QModelIndex ix = ptr->makeClipIndexFromID(clipIds.first);
1699                 emit ptr->dataChanged(ix, ix, {TimelineModel::DurationRole});*/
1700                 QModelIndex ix2 = ptr->makeClipIndexFromID(clipIds.second);
1701                 emit ptr->dataChanged(ix2, ix2, {TimelineModel::MixRole,TimelineModel::MixCutRole});
1702             }
1703             return true;
1704         };
1705         replay();
1706         Fun reverse = [this, clipIds, assetId, params, mixDuration, mixPosition, mixCutPos, firstInPos, secondInPos, switchFirstTrack, switchSecondTrack]() {
1707             // First restore correct playlist
1708             if (switchFirstTrack) {
1709                 // Revert clip to playlist 1
1710                 switchPlaylist(clipIds.first, firstInPos, 0, 1);
1711             }
1712             if (switchSecondTrack) {
1713                 // Revert clip to playlist 1
1714                 switchPlaylist(clipIds.second, secondInPos, 0, 1);
1715             }
1716             // Build mix
1717             if (auto ptr = m_parent.lock()) {
1718                 std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipIds.second));
1719                 movedClip->setMixDuration(mixDuration, mixCutPos);
1720                 // Insert mix transition
1721                 std::unique_ptr<Mlt::Transition> t = TransitionsRepository::get()->getTransition(assetId);
1722                 t->set_in_and_out(mixPosition, mixPosition + mixDuration);
1723                 t->set("kdenlive:mixcut", mixCutPos);
1724                 t->set("kdenlive_id", assetId.toUtf8().constData());
1725                 m_track->plant_transition(*t.get(), 0, 1);
1726                 QDomElement xml = TransitionsRepository::get()->getXml(assetId);
1727                 QDomNodeList xmlParams = xml.elementsByTagName(QStringLiteral("parameter"));
1728                 for (int i = 0; i < xmlParams.count(); ++i) {
1729                     QDomElement currentParameter = xmlParams.item(i).toElement();
1730                     QString paramName = currentParameter.attribute(QStringLiteral("name"));
1731                     for (const auto &p : qAsConst(params)) {
1732                         if (p.first == paramName) {
1733                             currentParameter.setAttribute(QStringLiteral("value"), p.second.toString());
1734                             break;
1735                         }
1736                     }
1737                 }
1738                 std::shared_ptr<AssetParameterModel> asset(new AssetParameterModel(std::move(t), xml, assetId, {ObjectType::TimelineMix, clipIds.second}, QString()));
1739                 m_sameCompositions[clipIds.second] = asset;
1740                 m_mixList.insert(clipIds.first, clipIds.second);
1741                 QModelIndex ix2 = ptr->makeClipIndexFromID(clipIds.second);
1742                 emit ptr->dataChanged(ix2, ix2, {TimelineModel::MixRole,TimelineModel::MixCutRole});
1743             }
1744             return true;
1745         };
1746         PUSH_LAMBDA(replay, local_redo);
1747         PUSH_FRONT_LAMBDA(reverse, local_undo);
1748         UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
1749         return true;
1750     }
1751     return false;
1752 }
1753 
requestClipMix(const QString & mixId,std::pair<int,int> clipIds,std::pair<int,int> mixDurations,bool updateView,bool finalMove,Fun & undo,Fun & redo,bool groupMove)1754 bool TrackModel::requestClipMix(const QString &mixId, std::pair<int, int> clipIds, std::pair<int, int> mixDurations, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove)
1755 {
1756     QWriteLocker locker(&m_lock);
1757     // By default, insertion occurs in topmost track
1758     // Find out the clip id at position
1759     int firstClipPos;
1760     int secondClipPos;
1761     int secondClipDuration;
1762     int firstClipDuration;
1763     int source_track;
1764     int mixPosition;
1765     int secondClipCut = 0;
1766     int dest_track = 1;
1767     bool remixPlaylists = false;
1768     bool clipHasEndMix = false;
1769     if (auto ptr = m_parent.lock()) {
1770         // The clip that will be moved to playlist 1
1771         std::shared_ptr<ClipModel> secondClip(ptr->getClipPtr(clipIds.second));
1772         secondClipDuration = secondClip->getPlaytime();
1773         secondClipPos = secondClip->getPosition();
1774         source_track = secondClip->getSubPlaylistIndex();
1775         std::shared_ptr<ClipModel> firstClip(ptr->getClipPtr(clipIds.first));
1776         firstClipDuration = firstClip->getPlaytime();
1777         // Ensure mix is not longer than clip and doesn't overlap other mixes
1778         firstClipPos = firstClip->getPosition();
1779         if (firstClip->getSubPlaylistIndex() == 1) {
1780             dest_track = 0;
1781         }
1782         mixPosition = qMax(firstClipPos, secondClipPos - mixDurations.second);
1783         int maxPos = qMin(secondClipPos + secondClipDuration, secondClipPos + mixDurations.first);
1784         if (hasStartMix(clipIds.first)) {
1785             std::pair<MixInfo, MixInfo> mixData = getMixInfo(clipIds.first);
1786             mixPosition = qMax(mixData.first.firstClipInOut.second, mixPosition);
1787         }
1788         if (hasEndMix(clipIds.second)) {
1789             std::pair<MixInfo, MixInfo> mixData = getMixInfo(clipIds.second);
1790             clipHasEndMix = true;
1791             maxPos = qMin(mixData.second.secondClipInOut.first, maxPos);
1792             if (ptr->m_allClips[mixData.second.secondClipId]->getSubPlaylistIndex() == dest_track) {
1793                 remixPlaylists = true;
1794             }
1795         }
1796         mixDurations.first = qMin(mixDurations.first, maxPos - mixPosition);
1797         secondClipCut = maxPos - secondClipPos;
1798     } else {
1799         // Error, timeline unavailable
1800         qDebug()<<"=== ERROR NO TIMELINE!!!!";
1801         return false;
1802     }
1803 
1804     // Rearrange subsequent mixes
1805     Fun rearrange_playlists = []() { return true; };
1806     Fun rearrange_playlists_undo = []() { return true; };
1807     if (remixPlaylists && source_track != dest_track) {
1808         // A list of clip ids x playlists
1809         QMap<int, int> rearrangedPlaylists;
1810         int ix = 0;
1811         int moveId = m_mixList.value(clipIds.second, -1);
1812         while (moveId > -1) {
1813             int current = m_allClips[moveId]->getSubPlaylistIndex();
1814             rearrangedPlaylists.insert(moveId, current);
1815             if (hasEndMix(moveId)) {
1816                 moveId = m_mixList.value(moveId, -1);
1817             } else {
1818                 break;
1819             }
1820             ix++;
1821         }
1822 
1823         rearrange_playlists = [this, rearrangedPlaylists]() {
1824             // First, remove all clips on playlist 0
1825             QMapIterator<int, int> i(rearrangedPlaylists);
1826             while (i.hasNext()) {
1827                 i.next();
1828                 if (i.value() == 0) {
1829                     int target_clip = m_playlists[0].get_clip_index_at(m_allClips[i.key()]->getPosition());
1830                     std::unique_ptr<Mlt::Producer> prod(m_playlists[0].replace_with_blank(target_clip));
1831                 }
1832                 m_playlists[0].consolidate_blanks();
1833             }
1834             // Then move all clips from playlist 1 to playlist 0
1835             i.toFront();
1836             if (auto ptr = m_parent.lock()) {
1837                 while (i.hasNext()) {
1838                     i.next();
1839                     if (i.value() == 1) {
1840                         // Remove
1841                         int pos = m_allClips[i.key()]->getPosition();
1842                         int target_clip = m_playlists[1].get_clip_index_at(pos);
1843                         std::unique_ptr<Mlt::Producer> prod(m_playlists[1].replace_with_blank(target_clip));
1844                         // Replug
1845                         std::shared_ptr<ClipModel> clip = ptr->getClipPtr(i.key());
1846                         clip->setSubPlaylistIndex(0, m_id);
1847                         int index = m_playlists[0].insert_at(pos, *clip, 1);
1848                         m_playlists[0].consolidate_blanks();
1849                         if (index == -1) {
1850                             // Something went wrong, abort
1851                             m_playlists[1].insert_at(pos, *clip, 1);
1852                             m_playlists[1].consolidate_blanks();
1853                             return false;
1854                         }
1855                     }
1856                     m_playlists[1].consolidate_blanks();
1857                 }
1858                 // Finally replug playlist 0 clips in playlist 1 and fix transition direction
1859                 i.toFront();
1860                 while (i.hasNext()) {
1861                     i.next();
1862                     if (i.value() == 0) {
1863                         int pos = m_allClips[i.key()]->getPosition();
1864                         std::shared_ptr<ClipModel> clip = ptr->getClipPtr(i.key());
1865                         clip->setSubPlaylistIndex(1, m_id);
1866                         int index = m_playlists[1].insert_at(pos, *clip, 1);
1867                         m_playlists[1].consolidate_blanks();
1868                         if (index == -1) {
1869                             // Something went wrong, abort
1870                             m_playlists[0].insert_at(pos, *clip, 1);
1871                             m_playlists[0].consolidate_blanks();
1872                             return false;
1873                         }
1874                     }
1875                     if (m_sameCompositions.count(i.key()) > 0) {
1876                         // There is a mix at clip start, adjust direction
1877                         Mlt::Transition &transition = *static_cast<Mlt::Transition*>(m_sameCompositions[i.key()]->getAsset());
1878                         transition.set("reverse", i.value());
1879                     }
1880                 }
1881                 return true;
1882             } else {
1883                 return false;
1884             }
1885         };
1886         rearrange_playlists_undo = [this, rearrangedPlaylists]() {
1887             // First, remove all clips on playlist 1
1888             QMapIterator<int, int> i(rearrangedPlaylists);
1889             while (i.hasNext()) {
1890                 i.next();
1891                 if (i.value() == 0) {
1892                     int target_clip = m_playlists[1].get_clip_index_at(m_allClips[i.key()]->getPosition());
1893                     std::unique_ptr<Mlt::Producer> prod(m_playlists[1].replace_with_blank(target_clip));
1894                 }
1895                 m_playlists[1].consolidate_blanks();
1896             }
1897             // Then move all clips from playlist 0 to playlist 1
1898             i.toFront();
1899             if (auto ptr = m_parent.lock()) {
1900                 while (i.hasNext()) {
1901                     i.next();
1902                     if (i.value() == 1) {
1903                         // Remove
1904                         int pos = m_allClips[i.key()]->getPosition();
1905                         int target_clip = m_playlists[0].get_clip_index_at(pos);
1906                         std::unique_ptr<Mlt::Producer> prod(m_playlists[0].replace_with_blank(target_clip));
1907                         // Replug
1908                         std::shared_ptr<ClipModel> clip = ptr->getClipPtr(i.key());
1909                         clip->setSubPlaylistIndex(1, m_id);
1910                         int index = m_playlists[1].insert_at(pos, *clip, 1);
1911                         m_playlists[1].consolidate_blanks();
1912                         if (index == -1) {
1913                             // Something went wrong, abort
1914                             m_playlists[0].insert_at(pos, *clip, 1);
1915                             m_playlists[0].consolidate_blanks();
1916                             return false;
1917                         }
1918                     }
1919                     m_playlists[0].consolidate_blanks();
1920                 }
1921                 // Finally replug playlist 1 clips in playlist 0 and fix transition direction
1922                 i.toFront();
1923                 while (i.hasNext()) {
1924                     i.next();
1925                     if (i.value() == 0) {
1926                         int pos = m_allClips[i.key()]->getPosition();
1927                         std::shared_ptr<ClipModel> clip = ptr->getClipPtr(i.key());
1928                         clip->setSubPlaylistIndex(0, m_id);
1929                         int index = m_playlists[0].insert_at(pos, *clip, 1);
1930                         m_playlists[0].consolidate_blanks();
1931                         if (index == -1) {
1932                             // Something went wrong, abort
1933                             m_playlists[1].insert_at(pos, *clip, 1);
1934                             m_playlists[1].consolidate_blanks();
1935                             return false;
1936                         }
1937                     }
1938                     if (m_sameCompositions.count(i.key()) > 0) {
1939                         // There is a mix at clip start, adjust direction
1940                         Mlt::Transition &transition = *static_cast<Mlt::Transition*>(m_sameCompositions[i.key()]->getAsset());
1941                         transition.set("reverse", 1 - i.value());
1942                     }
1943                 }
1944                 return true;
1945             } else return false;
1946         };
1947     }
1948     // Create mix compositing
1949     Fun build_mix = [clipIds, mixPosition, mixDurations, dest_track, secondClipCut, mixId, this]() {
1950         if (auto ptr = m_parent.lock()) {
1951             std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipIds.second));
1952             movedClip->setMixDuration(mixDurations.first + mixDurations.second, secondClipCut);
1953             // Insert mix transition
1954             QString assetName;
1955             std::unique_ptr<Mlt::Transition>t;
1956             QDomElement xml;
1957             if (isAudioTrack()) {
1958                 // Mix is the only audio transition
1959                 t = TransitionsRepository::get()->getTransition(QStringLiteral("mix"));
1960                 t->set_in_and_out(mixPosition, mixPosition + mixDurations.first + mixDurations.second);
1961                 t->set("kdenlive:mixcut", secondClipCut);
1962                 t->set("start", -1);
1963                 t->set("accepts_blanks", 1);
1964                 m_track->plant_transition(*t.get(), 0, 1);
1965                 assetName = QStringLiteral("mix");
1966                 xml = TransitionsRepository::get()->getXml(assetName);
1967             } else {
1968                 assetName = mixId.isEmpty() || mixId == QLatin1String("mix") ? QStringLiteral("luma") : mixId;
1969                 t = TransitionsRepository::get()->getTransition(assetName);
1970                 t->set_in_and_out(mixPosition, mixPosition + mixDurations.first + mixDurations.second);
1971                 xml = TransitionsRepository::get()->getXml(assetName);
1972                 t->set("kdenlive:mixcut", secondClipCut);
1973                 t->set("kdenlive_id", "luma");
1974                 m_track->plant_transition(*t.get(), 0, 1);
1975                 if (dest_track == 0) {
1976                     t->set("reverse", 1);
1977                 }
1978             }
1979             if (dest_track == 0 && Xml::hasXmlParameter(xml, QStringLiteral("reverse"))) {
1980                 Xml::setXmlParameter(xml, QStringLiteral("reverse"), QStringLiteral("1"));
1981             }
1982             std::shared_ptr<AssetParameterModel> asset(new AssetParameterModel(std::move(t), xml, assetName, {ObjectType::TimelineMix, clipIds.second}, QString()));
1983             m_sameCompositions[clipIds.second] = asset;
1984             m_mixList.insert(clipIds.first, clipIds.second);
1985         }
1986         return true;
1987     };
1988 
1989     Fun destroy_mix = [clipIds, this]() {
1990         if (auto ptr = m_parent.lock()) {
1991             if (m_sameCompositions.count(clipIds.second) == 0) {
1992                 // Mix was already deleted
1993                 if (m_mixList.contains(clipIds.first)) {
1994                     m_mixList.remove(clipIds.first);
1995                 }
1996                 return true;
1997             }
1998             Mlt::Transition &transition = *static_cast<Mlt::Transition*>(m_sameCompositions[clipIds.second]->getAsset());
1999             std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipIds.second));
2000             movedClip->setMixDuration(0);
2001             QModelIndex ix = ptr->makeClipIndexFromID(clipIds.second);
2002             emit ptr->dataChanged(ix, ix, {TimelineModel::StartRole,TimelineModel::MixRole,TimelineModel::MixCutRole});
2003             QScopedPointer<Mlt::Field> field(m_track->field());
2004             field->lock();
2005             field->disconnect_service(transition);
2006             field->unlock();
2007             m_sameCompositions.erase(clipIds.second);
2008             m_mixList.remove(clipIds.first);
2009         }
2010         return true;
2011     };
2012     // lock MLT playlist so that we don't end up with invalid frames in monitor
2013     auto operation = requestClipDeletion_lambda(clipIds.second, updateView, finalMove, groupMove, false);
2014     bool res = operation();
2015     if (res) {
2016         Fun replay = [this, clipIds, dest_track, firstClipPos, secondClipDuration, mixPosition, mixDurations, build_mix, secondClipPos, clipHasEndMix, updateView, finalMove, groupMove, rearrange_playlists]() {
2017             if (auto ptr = m_parent.lock()) {
2018                 ptr->getClipPtr(clipIds.second)->setSubPlaylistIndex(dest_track, m_id);
2019             }
2020             bool result = rearrange_playlists();
2021             auto op = requestClipInsertion_lambda(clipIds.second, secondClipPos, updateView, finalMove, groupMove);
2022             result = result && op();
2023             if (result) {
2024                 build_mix();
2025                 std::function<bool(void)> local_undo = []() { return true; };
2026                 std::function<bool(void)> local_redo = []() { return true; };
2027                 if (auto ptr = m_parent.lock()) {
2028                     result = ptr->getClipPtr(clipIds.second)->requestResize(secondClipPos + secondClipDuration - mixPosition, false, local_undo, local_redo, true, clipHasEndMix);
2029                     result = result && ptr->getClipPtr(clipIds.first)->requestResize(mixPosition + mixDurations.first + mixDurations.second - firstClipPos, true, local_undo, local_redo, true, true);
2030                     QModelIndex ix = ptr->makeClipIndexFromID(clipIds.second);
2031                     emit ptr->dataChanged(ix, ix, {TimelineModel::StartRole,TimelineModel::MixRole,TimelineModel::MixCutRole});
2032                 }
2033             }
2034             return result;
2035         };
2036 
2037         Fun reverse = [this, clipIds, source_track, secondClipDuration, firstClipDuration, destroy_mix, secondClipPos, updateView, finalMove, groupMove, operation, rearrange_playlists_undo]() {
2038             destroy_mix();
2039             std::function<bool(void)> local_undo = []() { return true; };
2040             std::function<bool(void)> local_redo = []() { return true; };
2041             if (auto ptr = m_parent.lock()) {
2042                 ptr->getClipPtr(clipIds.second)->requestResize(secondClipDuration, false, local_undo, local_redo, false);
2043                 ptr->getClipPtr(clipIds.first)->requestResize(firstClipDuration, true, local_undo, local_redo, false);
2044             }
2045             bool result = operation();
2046             if (auto ptr = m_parent.lock()) {
2047                 ptr->getClipPtr(clipIds.second)->setSubPlaylistIndex(source_track, m_id);
2048             }
2049             result = result && rearrange_playlists_undo();
2050             auto op = requestClipInsertion_lambda(clipIds.second, secondClipPos, updateView, finalMove, groupMove);
2051             result = result && op();
2052             return result;
2053         };
2054         res = replay();
2055         if (res) {
2056             PUSH_LAMBDA(replay, operation);
2057             UPDATE_UNDO_REDO(operation, reverse, undo, redo);
2058         } else {
2059             qDebug()<<"=============\nSECOND INSERT FAILED\n\n=================";
2060             reverse();
2061         }
2062     } else {
2063         qDebug()<<"=== CLIP DELETION FAILED";
2064     }
2065     return res;
2066 }
2067 
2068 
getMixInfo(int clipId) const2069 std::pair<MixInfo, MixInfo> TrackModel::getMixInfo(int clipId) const
2070 {
2071     std::pair<MixInfo, MixInfo> result;
2072     MixInfo startMix;
2073     MixInfo endMix;
2074     if (m_sameCompositions.count(clipId) > 0) {
2075         // There is a mix at clip start
2076         startMix.firstClipId = m_mixList.key(clipId, -1);
2077         startMix.secondClipId = clipId;
2078         if (auto ptr = m_parent.lock()) {
2079             if (ptr->isClip(startMix.firstClipId)) {
2080                 std::shared_ptr<ClipModel> clip1 = ptr->getClipPtr(startMix.firstClipId);
2081                 std::shared_ptr<ClipModel> clip2 = ptr->getClipPtr(startMix.secondClipId);
2082                 startMix.firstClipInOut.first = clip1->getPosition();
2083                 startMix.firstClipInOut.second = startMix.firstClipInOut.first + clip1->getPlaytime();
2084                 startMix.secondClipInOut.first = clip2->getPosition();
2085                 startMix.secondClipInOut.second = startMix.secondClipInOut.first + clip2->getPlaytime();
2086                 startMix.mixOffset = clip2->getMixCutPosition();
2087             } else {
2088                 // Clip was deleted
2089                 startMix.firstClipId = -1;
2090             }
2091         }
2092     } else {
2093         startMix.firstClipId = -1;
2094         startMix.secondClipId = -1;
2095     }
2096     int secondClip = m_mixList.value(clipId, -1);
2097     if (secondClip > -1) {
2098         // There is a mix at clip end
2099         endMix.firstClipId = clipId;
2100         endMix.secondClipId = secondClip;
2101         if (auto ptr = m_parent.lock()) {
2102             if (ptr->isClip(secondClip)) {
2103                 std::shared_ptr<ClipModel> clip1 = ptr->getClipPtr(endMix.firstClipId);
2104                 std::shared_ptr<ClipModel> clip2 = ptr->getClipPtr(endMix.secondClipId);
2105                 endMix.firstClipInOut.first = clip1->getPosition();
2106                 endMix.firstClipInOut.second = endMix.firstClipInOut.first + clip1->getPlaytime();
2107                 endMix.secondClipInOut.first = clip2->getPosition();
2108                 endMix.secondClipInOut.second = endMix.secondClipInOut.first + clip2->getPlaytime();
2109             } else {
2110                 // Clip was deleted
2111                 endMix.firstClipId = -1;
2112             }
2113         }
2114     } else {
2115         endMix.firstClipId = -1;
2116         endMix.secondClipId = -1;
2117     }
2118     result = {startMix, endMix};
2119     return result;
2120 }
2121 
deleteMix(int clipId,bool final,bool notify)2122 bool TrackModel::deleteMix(int clipId, bool final, bool notify)
2123 {
2124     Q_ASSERT(m_sameCompositions.count(clipId) > 0);
2125     if (auto ptr = m_parent.lock()) {
2126         if (notify) {
2127             std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipId));
2128             movedClip->setMixDuration(final ? 0 : 1);
2129             QModelIndex ix = ptr->makeClipIndexFromID(clipId);
2130             emit ptr->dataChanged(ix, ix, {TimelineModel::StartRole,TimelineModel::MixRole,TimelineModel::MixCutRole});
2131         }
2132         if (final) {
2133             Mlt::Transition &transition = *static_cast<Mlt::Transition*>(m_sameCompositions[clipId]->getAsset());
2134             QScopedPointer<Mlt::Field> field(m_track->field());
2135             field->lock();
2136             field->disconnect_service(transition);
2137             field->unlock();
2138             m_sameCompositions.erase(clipId);
2139             int firstClip = m_mixList.key(clipId, -1);
2140             if (firstClip > -1) {
2141                 m_mixList.remove(firstClip);
2142             }
2143         }
2144         return true;
2145     }
2146     return false;
2147 }
2148 
2149 
createMix(MixInfo info,std::pair<QString,QVector<QPair<QString,QVariant>>> params,bool finalMove)2150 bool TrackModel::createMix(MixInfo info, std::pair<QString,QVector<QPair<QString, QVariant>>> params, bool finalMove)
2151 {
2152     if (m_sameCompositions.count(info.secondClipId) > 0) {
2153         // Clip already has a mix
2154         Q_ASSERT(false);
2155         return false;
2156     }
2157     if (auto ptr = m_parent.lock()) {
2158         // Insert mix transition
2159         std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(info.secondClipId));
2160         int in = movedClip->getPosition();
2161         //int out = in + info.firstClipInOut.second - info.secondClipInOut.first;
2162         int duration = info.firstClipInOut.second - info.secondClipInOut.first;
2163         int out = in + duration;
2164         movedClip->setMixDuration(duration, info.mixOffset);
2165         std::unique_ptr<Mlt::Transition> t;
2166         const QString assetId = params.first;
2167         t = std::make_unique<Mlt::Transition>(*ptr->getProfile(), assetId.toUtf8().constData());
2168         t->set_in_and_out(in, out);
2169         t->set("kdenlive:mixcut", info.mixOffset);
2170         t->set("kdenlive_id", assetId.toUtf8().constData());
2171         m_track->plant_transition(*t.get(), 0, 1);
2172         QDomElement xml = TransitionsRepository::get()->getXml(assetId);
2173         QDomNodeList xmlParams = xml.elementsByTagName(QStringLiteral("parameter"));
2174         for (int i = 0; i < xmlParams.count(); ++i) {
2175             QDomElement currentParameter = xmlParams.item(i).toElement();
2176             QString paramName = currentParameter.attribute(QStringLiteral("name"));
2177             for (const auto &p : qAsConst(params.second)) {
2178                 if (p.first == paramName) {
2179                     currentParameter.setAttribute(QStringLiteral("value"), p.second.toString());
2180                     break;
2181                 }
2182             }
2183         }
2184         std::shared_ptr<AssetParameterModel> asset(new AssetParameterModel(std::move(t), xml, assetId, {ObjectType::TimelineMix, info.secondClipId}, QString()));
2185         m_sameCompositions[info.secondClipId] = asset;
2186         m_mixList.insert(info.firstClipId, info.secondClipId);
2187         if (finalMove) {
2188             QModelIndex ix2 = ptr->makeClipIndexFromID(info.secondClipId);
2189             emit ptr->dataChanged(ix2, ix2, {TimelineModel::MixRole,TimelineModel::MixCutRole});
2190         }
2191         return true;
2192     }
2193     qDebug()<<"== COULD NOT PLANT MIX; TRACK UNAVAILABLE";
2194     return false;
2195 }
2196 
createMix(MixInfo info,bool isAudio)2197 bool TrackModel::createMix(MixInfo info, bool isAudio)
2198 {
2199     if (m_sameCompositions.count(info.secondClipId) > 0) {
2200         return false;
2201     }
2202     if (auto ptr = m_parent.lock()) {
2203         // Insert mix transition
2204         std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(info.secondClipId));
2205         int in = movedClip->getPosition();
2206         //int out = in + info.firstClipInOut.second - info.secondClipInOut.first;
2207         int out = in + movedClip->getMixDuration();
2208         movedClip->setMixDuration(out - in);
2209         bool reverse = movedClip->getSubPlaylistIndex() == 0;
2210         QString assetName;
2211         std::unique_ptr<Mlt::Transition> t;
2212         if (isAudio) {
2213             t = std::make_unique<Mlt::Transition>(*ptr->getProfile(), "mix");
2214             t->set_in_and_out(in, out);
2215             t->set("start", -1);
2216             t->set("accepts_blanks", 1);
2217             if (reverse) {
2218                 t->set("reverse", 1);
2219             }
2220             m_track->plant_transition(*t.get(), 0, 1);
2221             assetName = QStringLiteral("mix");
2222         } else {
2223             t = std::make_unique<Mlt::Transition>(*ptr->getProfile(), "luma");
2224             t->set_in_and_out(in, out);
2225             t->set("kdenlive_id", "luma");
2226             if (reverse) {
2227                 t->set("reverse", 1);
2228             }
2229             m_track->plant_transition(*t.get(), 0, 1);
2230             assetName = QStringLiteral("luma");
2231         }
2232         QDomElement xml = TransitionsRepository::get()->getXml(assetName);
2233         std::shared_ptr<AssetParameterModel> asset(new AssetParameterModel(std::move(t), xml, assetName, {ObjectType::TimelineMix, info.secondClipId}, QString()));
2234         m_sameCompositions[info.secondClipId] = asset;
2235         m_mixList.insert(info.firstClipId, info.secondClipId);
2236         return true;
2237     }
2238     return false;
2239 }
2240 
createMix(std::pair<int,int> clipIds,std::pair<int,int> mixData)2241 bool TrackModel::createMix(std::pair<int, int> clipIds, std::pair<int, int> mixData)
2242 {
2243     if (m_sameCompositions.count(clipIds.second) > 0) {
2244         return false;
2245     }
2246     if (auto ptr = m_parent.lock()) {
2247         std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipIds.second));
2248         movedClip->setMixDuration(mixData.second);
2249         QModelIndex ix = ptr->makeClipIndexFromID(clipIds.second);
2250         emit ptr->dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
2251         bool reverse = movedClip->getSubPlaylistIndex() == 0;
2252         // Insert mix transition
2253         QString assetName;
2254         std::unique_ptr<Mlt::Transition> t;
2255         if (isAudioTrack()) {
2256             t = std::make_unique<Mlt::Transition>(*ptr->getProfile(), "mix");
2257             t->set_in_and_out(mixData.first, mixData.first + mixData.second);
2258             t->set("start", -1);
2259             t->set("accepts_blanks", 1);
2260             if (reverse) {
2261                 t->set("reverse", 1);
2262             }
2263             m_track->plant_transition(*t.get(), 0, 1);
2264             assetName = QStringLiteral("mix");
2265         } else {
2266             t = std::make_unique<Mlt::Transition>(*ptr->getProfile(), "luma");
2267             t->set("kdenlive_id", "luma");
2268             t->set_in_and_out(mixData.first, mixData.first + mixData.second);
2269             if (reverse) {
2270                 t->set("reverse", 1);
2271             }
2272             m_track->plant_transition(*t.get(), 0, 1);
2273             assetName = QStringLiteral("luma");
2274         }
2275         QDomElement xml = TransitionsRepository::get()->getXml(assetName);
2276         std::shared_ptr<AssetParameterModel> asset(new AssetParameterModel(std::move(t), xml, assetName, {ObjectType::TimelineMix, clipIds.second}, QString()));
2277         m_sameCompositions[clipIds.second] = asset;
2278         m_mixList.insert(clipIds.first, clipIds.second);
2279         return true;
2280     }
2281     return false;
2282 }
2283 
setMixDuration(int cid,int mixDuration,int mixCut)2284 void TrackModel::setMixDuration(int cid, int mixDuration, int mixCut)
2285 {
2286     m_allClips[cid]->setMixDuration(mixDuration, mixCut);
2287     m_sameCompositions[cid]->getAsset()->set("kdenlive:mixcut", mixCut);
2288     int in = m_allClips[cid]->getPosition();
2289     int out = in + mixDuration;
2290     Mlt::Transition &transition = *static_cast<Mlt::Transition*>(m_sameCompositions[cid]->getAsset());
2291     transition.set_in_and_out(in, out);
2292     emit m_sameCompositions[cid]->dataChanged(QModelIndex(), QModelIndex(), {AssetParameterModel::ParentDurationRole});
2293 }
2294 
getMixDuration(int cid) const2295 int TrackModel::getMixDuration(int cid) const
2296 {
2297     Q_ASSERT(m_sameCompositions.count(cid) > 0);
2298     Mlt::Transition &transition = *static_cast<Mlt::Transition*>(m_sameCompositions.at(cid)->getAsset());
2299     return transition.get_length() - 1;
2300 }
2301 
removeMix(MixInfo info)2302 void TrackModel::removeMix(MixInfo info)
2303 {
2304     Q_ASSERT(m_sameCompositions.count(info.secondClipId) > 0);
2305     Mlt::Transition &transition = *static_cast<Mlt::Transition*>(m_sameCompositions[info.secondClipId]->getAsset());
2306     QScopedPointer<Mlt::Field> field(m_track->field());
2307     field->lock();
2308     field->disconnect_service(transition);
2309     field->unlock();
2310     m_sameCompositions.erase(info.secondClipId);
2311     m_mixList.remove(info.firstClipId);
2312 }
2313 
syncronizeMixes(bool finalMove)2314 void TrackModel::syncronizeMixes(bool finalMove)
2315 {
2316     QList<int>toDelete;
2317     for( const auto& n : m_sameCompositions ) {
2318         //qDebug() << "Key:[" << n.first << "] Value:[" << n.second << "]\n";
2319         int secondClipId = n.first;
2320         int firstClip = m_mixList.key(secondClipId, -1);
2321         Q_ASSERT(firstClip > -1);
2322         if (m_allClips.find(firstClip) == m_allClips.end() || m_allClips.find(secondClipId) == m_allClips.end()) {
2323             // One of the clip was removed, delete the mix
2324             qDebug()<<"=== CLIPS: "<<firstClip<<" / "<<secondClipId<<" ARE MISSING!!!!";
2325             Mlt::Transition &transition = *static_cast<Mlt::Transition*>(m_sameCompositions[secondClipId]->getAsset());
2326             QScopedPointer<Mlt::Field> field(m_track->field());
2327             field->lock();
2328             field->disconnect_service(transition);
2329             field->unlock();
2330             toDelete << secondClipId;
2331             m_mixList.remove(firstClip);
2332             continue;
2333         }
2334         // Asjust mix in/out
2335         int mixIn = m_allClips[secondClipId]->getPosition();
2336         int mixOut = m_allClips[firstClip]->getPosition() + m_allClips[firstClip]->getPlaytime();
2337         if (mixOut <= mixIn) {
2338             if (finalMove) {
2339                 // Delete mix
2340                 mixOut = mixIn;
2341             } else {
2342                 mixOut = mixIn + 1;
2343             }
2344         }
2345         Mlt::Transition &transition = *static_cast<Mlt::Transition*>(m_sameCompositions[secondClipId]->getAsset());
2346         if (mixIn == mixOut) {
2347             QScopedPointer<Mlt::Field> field(m_track->field());
2348             field->lock();
2349             field->disconnect_service(transition);
2350             field->unlock();
2351             toDelete << secondClipId;
2352             m_mixList.remove(firstClip);
2353         } else {
2354             transition.set_in_and_out(mixIn, mixOut);
2355         }
2356         if (auto ptr = m_parent.lock()) {
2357             ptr->getClipPtr(secondClipId)->setMixDuration(mixOut - mixIn);
2358             QModelIndex ix = ptr->makeClipIndexFromID(secondClipId);
2359             emit ptr->dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole});
2360         }
2361     }
2362     for (int i : qAsConst(toDelete)) {
2363         m_sameCompositions.erase(i);
2364     }
2365 }
2366 
mixCount() const2367 int TrackModel::mixCount() const
2368 {
2369     Q_ASSERT(m_mixList.size() == int(m_sameCompositions.size()));
2370     return m_mixList.size();
2371 }
2372 
hasMix(int cid) const2373 bool TrackModel::hasMix(int cid) const
2374 {
2375     if (m_mixList.contains(cid)) {
2376         return true;
2377     }
2378     if (m_sameCompositions.count(cid) > 0) {
2379         return true;
2380     }
2381     return false;
2382 }
2383 
hasStartMix(int cid) const2384 bool TrackModel::hasStartMix(int cid) const
2385 {
2386     return m_sameCompositions.count(cid) > 0;
2387 }
2388 
isOnCut(int cid)2389 int TrackModel::isOnCut(int cid)
2390 {
2391     if (auto ptr = m_parent.lock()) {
2392         std::shared_ptr<CompositionModel> composition = ptr->getCompositionPtr(cid);
2393         int startPos = composition->getPosition();
2394         int endPos = startPos + composition->getPlaytime() - 1;
2395         int cid1 = getClipByPosition(startPos);
2396         int cid2 = getClipByPosition(endPos);
2397         if (cid1 == -1 || cid2 == -1 || cid1 == cid2) {
2398             return -1;
2399         }
2400         if (m_mixList.contains(cid1) || m_sameCompositions.count(cid2) > 0) {
2401             // Clips are already mixed, abort
2402             return -1;
2403         }
2404         std::shared_ptr<ClipModel> clip = ptr->getClipPtr(cid2);
2405         int mixRight = clip->getPosition();
2406         std::shared_ptr<ClipModel> clip2 = ptr->getClipPtr(cid1);
2407         int mixLeft = clip2->getPosition() + clip2->getPlaytime();
2408         if (mixLeft == mixRight) {
2409             return mixLeft;
2410         }
2411     }
2412     return -1;
2413 }
2414 
hasEndMix(int cid) const2415 bool TrackModel::hasEndMix(int cid) const
2416 {
2417     return m_mixList.contains(cid);
2418 }
2419 
getSecondMixPartner(int cid) const2420 int TrackModel::getSecondMixPartner(int cid) const
2421 {
2422     return hasEndMix(cid) ? m_mixList.find(cid).value() : -1;
2423 }
2424 
mixXml(QDomDocument & document,int cid) const2425 QDomElement TrackModel::mixXml(QDomDocument &document, int cid) const
2426 {
2427     QDomElement container = document.createElement(QStringLiteral("mix"));
2428     int firstClipId = m_mixList.key(cid);
2429     container.setAttribute(QStringLiteral("firstClip"), firstClipId);
2430     container.setAttribute(QStringLiteral("secondClip"), cid);
2431     if (auto ptr = m_parent.lock()) {
2432         std::shared_ptr<ClipModel> clip = ptr->getClipPtr(firstClipId);
2433         container.setAttribute(QStringLiteral("firstClipPosition"), clip->getPosition());
2434     }
2435     std::shared_ptr<AssetParameterModel> asset = m_sameCompositions.at(cid);
2436     const QString assetId = m_sameCompositions.at(cid)->getAssetId();
2437     QVector<QPair<QString, QVariant>> params = m_sameCompositions.at(cid)->getAllParameters();
2438     container.setAttribute(QStringLiteral("asset"), assetId);
2439     for (const auto &p : qAsConst(params)) {
2440         QDomElement para = document.createElement(QStringLiteral("param"));
2441         para.setAttribute(QStringLiteral("name"), p.first);
2442         QDomText val = document.createTextNode(p.second.toString());
2443         para.appendChild(val);
2444         container.appendChild(para);
2445     }
2446     std::pair<MixInfo, MixInfo> mixData = getMixInfo(cid);
2447     container.setAttribute(QStringLiteral("mixStart"), mixData.first.secondClipInOut.first);
2448     container.setAttribute(QStringLiteral("mixEnd"), mixData.first.firstClipInOut.second);
2449     container.setAttribute(QStringLiteral("mixOffset"), mixData.first.mixOffset);
2450     return container;
2451 }
2452 
loadMix(Mlt::Transition * t)2453 bool TrackModel::loadMix(Mlt::Transition *t)
2454 {
2455     int in = t->get_in();
2456     int out = t->get_out() - 1;
2457     bool reverse = t->get_int("reverse") == 1;
2458     int cid1 = getClipByPosition(in, reverse ? 1 : 0);
2459     int cid2 = getClipByPosition(out, reverse ? 0 : 1);
2460     if (cid1 < 0 || cid2 < 0) {
2461         qDebug()<<"INVALID CLIP MIX: "<<cid1<<" - "<<cid2;
2462         // Check if reverse setting was not correctly set
2463         cid1 = getClipByPosition(in, reverse ? 0 : 1);
2464         cid2 = getClipByPosition(out, reverse ? 1 : 0);
2465         if (cid1 < 0 || cid2 < 0) {
2466             QScopedPointer<Mlt::Field> field(m_track->field());
2467             field->lock();
2468             field->disconnect_service(*t);
2469             field->unlock();
2470             return false;
2471         }
2472     }
2473     // Ensure in/out points are in sync with the clips
2474     int clipIn = m_allClips[cid2]->getPosition();
2475     int clipOut = m_allClips[cid1]->getPosition() + m_allClips[cid1]->getPlaytime();
2476     if (in != clipIn || out != clipOut) {
2477         t->set_in_and_out(clipIn, clipOut);
2478         in = clipIn;
2479         out = clipOut;
2480     }
2481     QString assetId(t->get("kdenlive_id"));
2482     if (assetId.isEmpty()) {
2483         assetId = QString(t->get("mlt_service"));
2484     }
2485     std::unique_ptr<Mlt::Transition>tr(t);
2486     QDomElement xml = TransitionsRepository::get()->getXml(assetId);
2487     // Paste parameters from existing mix
2488     //std::unique_ptr<Mlt::Properties> sourceProperties(t);
2489     QStringList sourceProps;
2490     for (int i = 0; i < tr->count(); i++) {
2491         sourceProps << tr->get_name(i);
2492     }
2493     QDomNodeList params = xml.elementsByTagName(QStringLiteral("parameter"));
2494     for (int i = 0; i < params.count(); ++i) {
2495         QDomElement currentParameter = params.item(i).toElement();
2496         QString paramName = currentParameter.attribute(QStringLiteral("name"));
2497         if (!sourceProps.contains(paramName)) {
2498             continue;
2499         }
2500         QString paramValue = tr->get(paramName.toUtf8().constData());
2501         currentParameter.setAttribute(QStringLiteral("value"), paramValue);
2502     }
2503     std::shared_ptr<AssetParameterModel> asset(new AssetParameterModel(std::move(tr), xml, assetId, {ObjectType::TimelineMix, cid2}, QString()));
2504     m_sameCompositions[cid2] = asset;
2505     m_mixList.insert(cid1, cid2);
2506     int mixDuration = t->get_length() - 1;
2507     int mixCutPos = qMin(t->get_int("kdenlive:mixcut"), mixDuration);
2508     setMixDuration(cid2, mixDuration, mixCutPos);
2509     return true;
2510 }
2511 
mixModel(int cid)2512 const std::shared_ptr<AssetParameterModel> TrackModel::mixModel(int cid)
2513 {
2514     if (m_sameCompositions.count(cid) > 0) {
2515         return m_sameCompositions[cid];
2516     }
2517     return nullptr;
2518 }
2519 
reAssignEndMix(int currentId,int newId)2520 bool TrackModel::reAssignEndMix(int currentId, int newId)
2521 {
2522     Q_ASSERT(m_mixList.contains(currentId));
2523     int mixedClip = m_mixList.value(currentId);
2524     m_mixList.remove(currentId);
2525     m_mixList.insert(newId, mixedClip);
2526     return true;
2527 }
2528 
getMixParams(int cid)2529 std::pair<QString,QVector<QPair<QString, QVariant>>> TrackModel::getMixParams(int cid)
2530 {
2531     Q_ASSERT(m_sameCompositions.count(cid) > 0);
2532     const QString assetId = m_sameCompositions[cid]->getAssetId();
2533     QVector<QPair<QString, QVariant>> params = m_sameCompositions[cid]->getAllParameters();
2534     return {assetId,params};
2535 }
2536 
switchMix(int cid,const QString composition,Fun & undo,Fun & redo)2537 void TrackModel::switchMix(int cid, const QString composition, Fun &undo, Fun &redo)
2538 {
2539     // First remove existing mix
2540     // lock MLT playlist so that we don't end up with invalid frames in monitor
2541     const QString currentAsset = m_sameCompositions[cid]->getAssetId();
2542     Fun local_redo = [this, cid, composition]() {
2543         m_playlists[0].lock();
2544         m_playlists[1].lock();
2545         Mlt::Transition &transition = *static_cast<Mlt::Transition*>(m_sameCompositions[cid]->getAsset());
2546         int in = transition.get_in();
2547         int out = transition.get_out();
2548         int mixCutPos = transition.get_int("kdenlive:mixcut");
2549         QScopedPointer<Mlt::Field> field(m_track->field());
2550         field->lock();
2551         field->disconnect_service(transition);
2552         field->unlock();
2553         m_sameCompositions.erase(cid);
2554         if (auto ptr = m_parent.lock()) {
2555             std::unique_ptr<Mlt::Transition> t = TransitionsRepository::get()->getTransition(composition);
2556             t->set_in_and_out(in, out);
2557             m_track->plant_transition(*t.get(), 0, 1);
2558             t->set("kdenlive:mixcut", mixCutPos);
2559             QDomElement xml = TransitionsRepository::get()->getXml(composition);
2560             std::shared_ptr<AssetParameterModel> asset(new AssetParameterModel(std::move(t), xml, composition, {ObjectType::TimelineMix, cid}, QString()));
2561             m_sameCompositions[cid] = asset;
2562         }
2563         m_playlists[0].unlock();
2564         m_playlists[1].unlock();
2565         return true;
2566     };
2567     Fun local_undo = [this, cid, currentAsset]() {
2568         m_playlists[0].lock();
2569         m_playlists[1].lock();
2570         Mlt::Transition &transition = *static_cast<Mlt::Transition*>(m_sameCompositions[cid]->getAsset());
2571         int in = transition.get_in();
2572         int out = transition.get_out();
2573         QScopedPointer<Mlt::Field> field(m_track->field());
2574         field->lock();
2575         field->disconnect_service(transition);
2576         field->unlock();
2577         m_sameCompositions.erase(cid);
2578         if (auto ptr = m_parent.lock()) {
2579             std::unique_ptr<Mlt::Transition> t = TransitionsRepository::get()->getTransition(currentAsset);
2580             t->set_in_and_out(in, out);
2581             m_track->plant_transition(*t.get(), 0, 1);
2582             QDomElement xml = TransitionsRepository::get()->getXml(currentAsset);
2583             std::shared_ptr<AssetParameterModel> asset(new AssetParameterModel(std::move(t), xml, currentAsset, {ObjectType::TimelineMix, cid}, QString()));
2584             m_sameCompositions[cid] = asset;
2585         }
2586         m_playlists[0].unlock();
2587         m_playlists[1].unlock();
2588         return true;
2589     };
2590     UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo);
2591 }
2592 
stackZones() const2593 QVariantList TrackModel::stackZones() const
2594 {
2595     return m_effectStack->getEffectZones();
2596 }
2597 
hasClipStart(int pos)2598 bool TrackModel::hasClipStart(int pos)
2599 {
2600     for (auto &m_playlist : m_playlists) {
2601         if (m_playlist.is_blank_at(pos)) {
2602             continue;
2603         }
2604         if (m_playlist.get_clip_index_at(pos) != m_playlist.get_clip_index_at(pos - 1)) {
2605             return true;
2606         }
2607     }
2608     return false;
2609 }
2610