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