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