1 /*
2 SPDX-FileCopyrightText: 2017 Jean-Baptiste Mardelle <jb@kdenlive.org>
3 This file is part of Kdenlive. See www.kdenlive.org.
4 
5 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
6 */
7 
8 #include "timelinefunctions.hpp"
9 #include "bin/bin.h"
10 #include "bin/projectclip.h"
11 #include "bin/projectfolder.h"
12 #include "bin/projectitemmodel.h"
13 #include "bin/model/subtitlemodel.hpp"
14 #include "bin/model/markerlistmodel.hpp"
15 #include "clipmodel.hpp"
16 #include "compositionmodel.hpp"
17 #include "core.h"
18 #include "doc/kdenlivedoc.h"
19 #include "effects/effectstack/model/effectstackmodel.hpp"
20 #include "groupsmodel.hpp"
21 #include "timelineitemmodel.hpp"
22 #include "trackmodel.hpp"
23 #include "transitions/transitionsrepository.hpp"
24 #include "mainwindow.h"
25 #include "project/projectmanager.h"
26 
27 #include <QApplication>
28 #include <QDebug>
29 #include <QInputDialog>
30 #include <QSemaphore>
31 #include <klocalizedstring.h>
32 #include <unordered_map>
33 
34 #ifdef CRASH_AUTO_TEST
35 #include "logger.hpp"
36 #pragma GCC diagnostic push
37 #pragma GCC diagnostic ignored "-Wunused-parameter"
38 #pragma GCC diagnostic ignored "-Wsign-conversion"
39 #pragma GCC diagnostic ignored "-Wfloat-equal"
40 #pragma GCC diagnostic ignored "-Wshadow"
41 #pragma GCC diagnostic ignored "-Wpedantic"
42 #include <rttr/registration>
43 #pragma GCC diagnostic pop
44 
45 RTTR_REGISTRATION
46 {
47     using namespace rttr;
48     registration::class_<TimelineFunctions>("TimelineFunctions")
49         .method("requestClipCut", select_overload<bool(std::shared_ptr<TimelineItemModel>, int, int)>(&TimelineFunctions::requestClipCut))(
50             parameter_names("timeline", "clipId", "position"))
51         .method("requestDeleteBlankAt", select_overload<bool(const std::shared_ptr<TimelineItemModel>&, int, int, bool)>(&TimelineFunctions::requestDeleteBlankAt))(
52             parameter_names("timeline", "trackId", "position", "affectAllTracks"));
53 }
54 #else
55 #define TRACE_STATIC(...)
56 #define TRACE_RES(...)
57 #endif
58 
59 QStringList waitingBinIds;
60 QMap<QString, QString> mappedIds;
61 QMap<int, int> tracksMap;
62 QMap<int, int> spacerUngroupedItems;
63 int spacerMinPosition;
64 QSemaphore semaphore(1);
65 
cloneClip(const std::shared_ptr<TimelineItemModel> & timeline,int clipId,int & newId,PlaylistState::ClipState state,Fun & undo,Fun & redo)66 bool TimelineFunctions::cloneClip(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo,
67                                   Fun &redo)
68 {
69     // Special case: slowmotion clips
70     double clipSpeed = timeline->m_allClips[clipId]->getSpeed();
71     bool warp_pitch = timeline->m_allClips[clipId]->getIntProperty(QStringLiteral("warp_pitch"));
72     int audioStream = timeline->m_allClips[clipId]->getIntProperty(QStringLiteral("audio_index"));
73     bool res = timeline->requestClipCreation(timeline->getClipBinId(clipId), newId, state, audioStream, clipSpeed, warp_pitch, undo, redo);
74     timeline->m_allClips[newId]->m_endlessResize = timeline->m_allClips[clipId]->m_endlessResize;
75 
76     // copy useful timeline properties
77     timeline->m_allClips[clipId]->passTimelineProperties(timeline->m_allClips[newId]);
78 
79     int duration = timeline->getClipPlaytime(clipId);
80     int init_duration = timeline->getClipPlaytime(newId);
81     if (duration != init_duration) {
82         init_duration -= timeline->m_allClips[clipId]->getIn();
83         res = res && timeline->requestItemResize(newId, init_duration, false, true, undo, redo);
84         res = res && timeline->requestItemResize(newId, duration, true, true, undo, redo);
85     }
86     if (!res) {
87         return false;
88     }
89     std::shared_ptr<EffectStackModel> sourceStack = timeline->getClipEffectStackModel(clipId);
90     std::shared_ptr<EffectStackModel> destStack = timeline->getClipEffectStackModel(newId);
91     destStack->importEffects(sourceStack, state);
92     return res;
93 }
94 
requestMultipleClipsInsertion(const std::shared_ptr<TimelineItemModel> & timeline,const QStringList & binIds,int trackId,int position,QList<int> & clipIds,bool logUndo,bool refreshView)95 bool TimelineFunctions::requestMultipleClipsInsertion(const std::shared_ptr<TimelineItemModel> &timeline, const QStringList &binIds, int trackId, int position,
96                                                       QList<int> &clipIds, bool logUndo, bool refreshView)
97 {
98     std::function<bool(void)> undo = []() { return true; };
99     std::function<bool(void)> redo = []() { return true; };
100     for (const QString &binId : binIds) {
101         int clipId;
102         if (timeline->requestClipInsertion(binId, trackId, position, clipId, logUndo, refreshView, false, undo, redo)) {
103             clipIds.append(clipId);
104             position += timeline->getItemPlaytime(clipId);
105         } else {
106             undo();
107             clipIds.clear();
108             return false;
109         }
110     }
111 
112     if (logUndo) {
113         pCore->pushUndo(undo, redo, i18n("Insert Clips"));
114     }
115 
116     return true;
117 }
118 
processClipCut(const std::shared_ptr<TimelineItemModel> & timeline,int clipId,int position,int & newId,Fun & undo,Fun & redo)119 bool TimelineFunctions::processClipCut(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo)
120 {
121     bool isSubtitle = timeline->isSubTitle(clipId);
122     int trackId = isSubtitle ? -1 : timeline->getClipTrackId(clipId);
123     int trackDuration = isSubtitle ? -1 : timeline->getTrackById_const(trackId)->trackDuration();
124     int start = timeline->getItemPosition(clipId);
125     int duration = timeline->getItemPlaytime(clipId);
126     if (start > position || (start + duration) < position) {
127         return false;
128     }
129     if (isSubtitle) {
130         newId = timeline->cutSubtitle(position, undo, redo);
131         return newId > -1;
132     }
133     PlaylistState::ClipState state = timeline->m_allClips[clipId]->clipState();
134     // Check if clip has an end Mix
135     bool res = cloneClip(timeline, clipId, newId, state, undo, redo);
136     timeline->m_blockRefresh = true;
137     int updatedDuration = position - start;
138     res = res && timeline->requestItemResize(clipId, updatedDuration, true, true, undo, redo);
139     int newDuration = timeline->getClipPlaytime(clipId);
140     // parse effects
141     std::shared_ptr<EffectStackModel> sourceStack = timeline->getClipEffectStackModel(clipId);
142     sourceStack->cleanFadeEffects(true, undo, redo);
143     std::shared_ptr<EffectStackModel> destStack = timeline->getClipEffectStackModel(newId);
144     destStack->cleanFadeEffects(false, undo, redo);
145     updatedDuration = duration - newDuration;
146     res = res && timeline->requestItemResize(newId, updatedDuration, false, true, undo, redo);
147     // The next requestclipmove does not check for duration change since we don't invalidate timeline, so check duration change now
148     bool durationChanged = trackDuration != timeline->getTrackById_const(trackId)->trackDuration();
149     timeline->m_allClips[newId]->setSubPlaylistIndex(timeline->m_allClips[clipId]->getSubPlaylistIndex(), trackId);
150     res = res && timeline->requestClipMove(newId, trackId, position, true, true, false, true, undo, redo);
151 
152     if (timeline->getTrackById_const(trackId)->hasEndMix(clipId)) {
153         Fun local_undo = [timeline, trackId, clipId, newId]() {
154             timeline->getTrackById_const(trackId)->reAssignEndMix(newId, clipId);
155             return true; };
156         Fun local_redo = [timeline, trackId, clipId, newId]() {
157             timeline->getTrackById_const(trackId)->reAssignEndMix(clipId, newId);
158             return true; };
159         local_redo();
160         UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
161     }
162 
163     if (durationChanged) {
164         // Track length changed, check project duration
165         Fun updateDuration = [timeline]() {
166             timeline->updateDuration();
167             return true;
168         };
169         updateDuration();
170         PUSH_LAMBDA(updateDuration, redo);
171     }
172     timeline->m_blockRefresh = false;
173     return res;
174 }
175 
requestClipCut(std::shared_ptr<TimelineItemModel> timeline,int clipId,int position)176 bool TimelineFunctions::requestClipCut(std::shared_ptr<TimelineItemModel> timeline, int clipId, int position)
177 {
178     std::function<bool(void)> undo = []() { return true; };
179     std::function<bool(void)> redo = []() { return true; };
180     TRACE_STATIC(timeline, clipId, position);
181     bool result = TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo);
182     if (result) {
183         pCore->pushUndo(undo, redo, i18n("Cut clip"));
184     }
185     TRACE_RES(result);
186     return result;
187 }
188 
requestClipCut(const std::shared_ptr<TimelineItemModel> & timeline,int clipId,int position,Fun & undo,Fun & redo)189 bool TimelineFunctions::requestClipCut(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int position, Fun &undo, Fun &redo)
190 {
191     const std::unordered_set<int> clipselect = timeline->getGroupElements(clipId);
192     // Remove locked items
193     std::unordered_set<int> clips;
194     for (int cid : clipselect) {
195         if (timeline->isSubTitle(cid)) {
196             clips.insert(cid);
197             continue;
198         }
199         if (!timeline->isClip(cid)) {
200             continue;
201         }
202         int tk = timeline->getClipTrackId(cid);
203         if (tk != -1 && !timeline->getTrackById_const(tk)->isLocked()) {
204             clips.insert(cid);
205         }
206     }
207     // Shall we reselect after the split
208     int trackToSelect = -1;
209     if (timeline->isClip(clipId) && timeline->m_allClips[clipId]->selected) {
210         int mainIn = timeline->getItemPosition(clipId);
211         int mainOut = mainIn + timeline->getItemPlaytime(clipId);
212         if (position > mainIn && position < mainOut) {
213             trackToSelect = timeline->getItemTrackId(clipId);
214         }
215     }
216 
217     // We need to call clearSelection before attempting the split or the group split will be corrupted by the selection group (no undo support)
218     timeline->requestClearSelection();
219 
220     std::unordered_set<int> topElements;
221     std::transform(clips.begin(), clips.end(), std::inserter(topElements, topElements.begin()), [&](int id) { return timeline->m_groups->getRootId(id); });
222 
223     int count = 0;
224     QList<int> newIds;
225     QList<int> clipsToCut;
226     for (int cid : clips) {
227         if (!timeline->isClip(cid) && !timeline->isSubTitle(cid)) {
228             continue;
229         }
230         int start = timeline->getItemPosition(cid);
231         int duration = timeline->getItemPlaytime(cid);
232         if (start < position && (start + duration) > position) {
233             clipsToCut << cid;
234         }
235     }
236     if (clipsToCut.isEmpty()) {
237         return true;
238     }
239     for (int cid : qAsConst(clipsToCut)) {
240         count++;
241         int newId;
242         bool res = processClipCut(timeline, cid, position, newId, undo, redo);
243         if (!res) {
244             bool undone = undo();
245             Q_ASSERT(undone);
246             return false;
247         }
248         // splitted elements go temporarily in the same group as original ones.
249         timeline->m_groups->setInGroupOf(newId, cid, undo, redo);
250         newIds << newId;
251     }
252     if (count > 0 && timeline->m_groups->isInGroup(clipId)) {
253         // we now split the group hierarchy.
254         // As a splitting criterion, we compare start point with split position
255         auto criterion = [timeline, position](int cid) { return timeline->getItemPosition(cid) < position; };
256         bool res = true;
257         for (const int topId : topElements) {
258             qDebug()<<"// CHECKING REGROUP ELEMENT: "<<topId<<", ISCLIP: "<<timeline->isClip(topId)<<timeline->isGroup(topId);
259             res = res && timeline->m_groups->split(topId, criterion, undo, redo);
260         }
261         if (!res) {
262             bool undone = undo();
263             Q_ASSERT(undone);
264             return false;
265         }
266     }
267     if (count > 0 && trackToSelect > -1) {
268         int newClip = timeline->getClipByPosition(trackToSelect, position);
269         if (newClip > -1) {
270             timeline->requestSetSelection({newClip});
271         }
272     }
273     return count > 0;
274 }
275 
requestClipCutAll(std::shared_ptr<TimelineItemModel> timeline,int position)276 bool TimelineFunctions::requestClipCutAll(std::shared_ptr<TimelineItemModel> timeline, int position)
277 {
278     QVector<std::shared_ptr<TrackModel>> affectedTracks;
279     std::function<bool(void)> undo = []() { return true; };
280     std::function<bool(void)> redo = []() { return true; };
281 
282     for (const auto &track: timeline->m_allTracks) {
283         if (!track->isLocked()) {
284             affectedTracks << track;
285         }
286     }
287 
288     if (affectedTracks.isEmpty()) {
289         pCore->displayMessage(i18n("All tracks are locked"), ErrorMessage, 500);
290         return false;
291     }
292 
293     unsigned count = 0;
294     for (auto track: qAsConst(affectedTracks)) {
295         int clipId = track->getClipByPosition(position);
296         if (clipId > -1) {
297             // Found clip at position in track, cut it. Update undo/redo as we go.
298             if (!TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo)) {
299                 qWarning() << "Failed to cut clip " << clipId << " at " << position;
300                 pCore->displayMessage(i18n("Failed to cut clip"), ErrorMessage, 500);
301                 // Undo all cuts made, assert successful undo.
302                 bool undone = undo();
303                 Q_ASSERT(undone);
304                 return false;
305             }
306             count++;
307         }
308     }
309 
310     if (!count) {
311         pCore->displayMessage(i18n("No clips to cut"), ErrorMessage);
312     } else {
313         pCore->pushUndo(undo, redo, i18n("Cut all clips"));
314     }
315 
316     return count > 0;
317 }
318 
requestSpacerStartOperation(const std::shared_ptr<TimelineItemModel> & timeline,int trackId,int position,bool ignoreMultiTrackGroups,bool allowGroupBreaking)319 int TimelineFunctions::requestSpacerStartOperation(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position, bool ignoreMultiTrackGroups, bool allowGroupBreaking)
320 {
321     std::unordered_set<int> clips = timeline->getItemsInRange(trackId, position, -1);
322     timeline->requestClearSelection();
323     spacerMinPosition = -1;
324     if (!clips.empty()) {
325         // Remove grouped items that are before the click position
326         // First get top groups ids
327         std::unordered_set<int> roots;
328         spacerUngroupedItems.clear();
329         std::transform(clips.begin(), clips.end(), std::inserter(roots, roots.begin()), [&](int id) { return timeline->m_groups->getRootId(id); });
330         std::unordered_set<int> groupsToRemove;
331         int firstCid = -1;
332         int firstPosition = -1;
333         QMap<int,int>firstPositions;
334         std::unordered_set<int> toSelect;
335         for (int r : roots) {
336             if (timeline->isGroup(r)) {
337                 std::unordered_set<int> leaves = timeline->m_groups->getLeaves(r);
338                 std::unordered_set<int> leavesToRemove;
339                 std::unordered_set<int> leavesToKeep;
340                 for (int l : leaves) {
341                     int pos = timeline->getItemPosition(l);
342                     bool outOfRange = pos + timeline->getItemPlaytime(l) < position;
343                     int tid = timeline->getItemTrackId(l);
344                     bool unaffectedTrack = ignoreMultiTrackGroups && trackId > -1 && tid != trackId;
345                     if (allowGroupBreaking) {
346                         if (outOfRange || unaffectedTrack) {
347                             leavesToRemove.insert(l);
348                         } else {
349                             leavesToKeep.insert(l);
350                         }
351                     }
352                     if (!outOfRange && !unaffectedTrack) {
353                         // Check space in all tracks
354                         if (!firstPositions.contains(tid)) {
355                             firstPositions.insert(tid, pos);
356                         } else {
357                             if (pos < firstPositions.value(tid)) {
358                                 firstPositions.insert(tid, pos);
359                             }
360                         }
361                         // Find first item
362                         if (firstPosition == -1 || pos < firstPosition) {
363                             firstCid = l;
364                             firstPosition = pos;
365                         }
366                     }
367                 }
368                 for (int l : leavesToRemove) {
369                     int checkedParent = timeline->m_groups->getDirectAncestor(l);
370                     if (checkedParent < 0) {
371                         checkedParent = l;
372                     }
373                     spacerUngroupedItems.insert(l, checkedParent);
374                 }
375                 if (leavesToKeep.size() == 1) {
376                     toSelect.insert(*leavesToKeep.begin());
377                     groupsToRemove.insert(r);
378                 }
379             } else {
380                 int pos = timeline->getItemPosition(r);
381                 int tid = timeline->getItemTrackId(r);
382                 // Check space in all tracks
383                 if (!firstPositions.contains(tid)) {
384                     firstPositions.insert(tid, pos);
385                 } else {
386                     if (pos < firstPositions.value(tid)) {
387                         firstPositions.insert(tid, pos);
388                     }
389                 }
390                 if (firstPosition == -1 || pos < firstPosition) {
391                     firstCid = r;
392                     firstPosition = pos;
393                 }
394             }
395         }
396         toSelect.insert(roots.begin(), roots.end());
397         for (int r : groupsToRemove) {
398             toSelect.erase(r);
399         }
400 
401         Fun undo = []() { return true; };
402         Fun redo = []() { return true; };
403         QMapIterator<int, int> i(spacerUngroupedItems);
404         while (i.hasNext()) {
405             i.next();
406             timeline->m_groups->removeFromGroup(i.key());
407         }
408 
409         timeline->requestSetSelection(toSelect);
410         if (!firstPositions.isEmpty()) {
411             // Find minimum position, parse all tracks
412             if (trackId > -1) {
413                 // Easy, check blank size
414                 int spaceDuration = timeline->getTrackById_const(trackId)->getBlankSizeAtPos(firstPosition - 1);
415                 if (spaceDuration > 0 ) {
416                     spacerMinPosition = firstPosition - spaceDuration;
417                 }
418             } else {
419                 // Check space in all tracks
420                 auto it = timeline->m_allTracks.cbegin();
421                 int space = -1;
422                 while (it != timeline->m_allTracks.cend()) {
423                     int tid = (*it)->getId();
424                     if (!firstPositions.contains(tid)) {
425                         ++it;
426                         continue;
427                     }
428                     int spaceDuration = (*it)->getBlankSizeAtPos(firstPositions.value(tid) - 1);
429                     if (space == -1 || spaceDuration < space) {
430                         space = spaceDuration;
431                     }
432                     ++it;
433                 }
434                 if (space > -1) {
435                     spacerMinPosition = firstPosition - space;
436                 }
437             }
438         }
439         return (firstCid);
440     }
441     return -1;
442 }
443 
requestSpacerEndOperation(const std::shared_ptr<TimelineItemModel> & timeline,int itemId,int startPosition,int endPosition,int affectedTrack,bool moveGuides,Fun & undo,Fun & redo,bool pushUndo)444 bool TimelineFunctions::requestSpacerEndOperation(const std::shared_ptr<TimelineItemModel> &timeline, int itemId, int startPosition, int endPosition, int affectedTrack, bool moveGuides, Fun &undo, Fun &redo, bool pushUndo)
445 {
446     // Move group back to original position
447     spacerMinPosition = -1;
448     int track = timeline->getItemTrackId(itemId);
449     bool isClip = timeline->isClip(itemId);
450     if (isClip) {
451         timeline->requestClipMove(itemId, track, startPosition, true, false, false, false, true);
452     } else if (timeline->isComposition(itemId)) {
453         timeline->requestCompositionMove(itemId, track, startPosition, false, false);
454     } else {
455         timeline->requestSubtitleMove(itemId, startPosition, false, false);
456     }
457     // Move guides
458     if (moveGuides) {
459         GenTime fromPos(startPosition, pCore->getCurrentFps());
460         GenTime toPos(endPosition, pCore->getCurrentFps());
461         QList<CommentedTime> guides = pCore->projectManager()->getGuideModel()->getMarkersInRange(startPosition, -1);
462         pCore->projectManager()->getGuideModel()->moveMarkers(guides, fromPos, toPos, undo, redo);
463     }
464 
465     std::unordered_set<int> clips = timeline->getGroupElements(itemId);
466     int mainGroup = timeline->m_groups->getRootId(itemId);
467     bool final = false;
468     bool liftOk = true;
469     if (timeline->m_editMode == TimelineMode::OverwriteEdit && endPosition < startPosition) {
470         // Remove zone between end and start pos
471         if (affectedTrack == -1) {
472             // touch all tracks
473             auto it = timeline->m_allTracks.cbegin();
474             while (it != timeline->m_allTracks.cend()) {
475                 int target_track = (*it)->getId();
476                 if (!timeline->getTrackById_const(target_track)->isLocked()) {
477                     liftOk = liftOk && TimelineFunctions::liftZone(timeline, target_track, QPoint(endPosition, startPosition), undo, redo);
478                 }
479                 ++it;
480             }
481         } else if (timeline->isTrack(affectedTrack)) {
482             liftOk = TimelineFunctions::liftZone(timeline, affectedTrack, QPoint(endPosition, startPosition), undo, redo);
483         }
484         // The lift operation destroys selection group, so regroup now
485         if (clips.size() > 1) {
486             timeline->requestSetSelection(clips);
487             mainGroup = timeline->m_groups->getRootId(itemId);
488         }
489     }
490     if (liftOk && (mainGroup > -1 || clips.size() == 1)) {
491         if (clips.size() > 1) {
492             final = timeline->requestGroupMove(itemId, mainGroup, 0, endPosition - startPosition, true, true, undo, redo);
493         } else {
494             // only 1 clip to be moved
495             if (isClip) {
496                 final = timeline->requestClipMove(itemId, track, endPosition, true, true, true, true, undo, redo);
497             } else if (timeline->isComposition(itemId)) {
498                 final = timeline->requestCompositionMove(itemId, track, -1, endPosition, true, true, undo, redo);
499             } else {
500                 final = timeline->requestSubtitleMove(itemId, endPosition, true, true, true, true, undo, redo);
501             }
502         }
503     }
504     timeline->requestClearSelection();
505     if (final) {
506         if (pushUndo) {
507             if (startPosition < endPosition) {
508                 pCore->pushUndo(undo, redo, i18n("Insert space"));
509             } else {
510                 pCore->pushUndo(undo, redo, i18n("Remove space"));
511             }
512         }
513         // Regroup temporarily ungrouped items
514         QMapIterator<int, int> i(spacerUngroupedItems);
515         Fun local_undo = []() { return true; };
516         Fun local_redo = []() { return true; };
517         std::unordered_set<int> newlyGrouped;
518         while (i.hasNext()) {
519             i.next();
520             if (timeline->isItem(i.value())) {
521                 if (newlyGrouped.count(i.value()) > 0) {
522                     Q_ASSERT(timeline->m_groups->isInGroup(i.value()));
523                     timeline->m_groups->setInGroupOf(i.key(), i.value(), local_undo, local_redo);
524                 } else {
525                     std::unordered_set<int> items = {i.key(), i.value()};
526                     timeline->m_groups->groupItems(items, local_undo, local_redo);
527                     newlyGrouped.insert(i.value());
528                 }
529             } else {
530                 // i.value() is either a group (detectable via timeline->isGroup) or an empty group
531                 if (timeline->isGroup(i.key())) {
532                     std::unordered_set<int> items = {i.key(), i.value()};
533                     timeline->m_groups->groupItems(items, local_undo, local_redo);
534                 } else {
535                     timeline->m_groups->setGroup(i.key(), i.value());
536                 }
537             }
538         }
539         spacerUngroupedItems.clear();
540         return true;
541     } else {
542         undo();
543     }
544     return false;
545 }
546 
547 
breakAffectedGroups(const std::shared_ptr<TimelineItemModel> & timeline,QVector<int> tracks,QPoint zone,Fun & undo,Fun & redo)548 bool TimelineFunctions::breakAffectedGroups(const std::shared_ptr<TimelineItemModel> &timeline, QVector<int> tracks, QPoint zone, Fun &undo, Fun &redo)
549 {
550     // Check if we have grouped clips that are on unaffected tracks, and ungroup them
551     bool result = true;
552     std::unordered_set<int> affectedItems;
553     // First find all affected items
554     for (int &trackId : tracks) {
555         std::unordered_set<int> items = timeline->getItemsInRange(trackId, zone.x(), zone.y());
556         affectedItems.insert(items.begin(), items.end());
557     }
558     for (int item : affectedItems) {
559         if (timeline->m_groups->isInGroup(item)) {
560             int groupId = timeline->m_groups->getRootId(item);
561             std::unordered_set<int> all_children = timeline->m_groups->getLeaves(groupId);
562             for (int child: all_children) {
563                 int childTrackId = timeline->getItemTrackId(child);
564                 if (!tracks.contains(childTrackId) && timeline->m_groups->isInGroup(child)) {
565                     // This item should not be affected by the operation, ungroup it
566                     result = result && timeline->requestClipUngroup(child, undo, redo);
567                 }
568             }
569         }
570     }
571     return result;
572 }
573 
extractZone(const std::shared_ptr<TimelineItemModel> & timeline,QVector<int> tracks,QPoint zone,bool liftOnly)574 bool TimelineFunctions::extractZone(const std::shared_ptr<TimelineItemModel> &timeline, QVector<int> tracks, QPoint zone, bool liftOnly)
575 {
576     // Start undoable command
577     std::function<bool(void)> undo = []() { return true; };
578     std::function<bool(void)> redo = []() { return true; };
579     bool result = true;
580     result = breakAffectedGroups(timeline, tracks, zone, undo, redo);
581 
582     for (int &trackId : tracks) {
583         if (timeline->getTrackById_const(trackId)->isLocked()) {
584             continue;
585         }
586         result = result && TimelineFunctions::liftZone(timeline, trackId, zone, undo, redo);
587     }
588     if (result && !liftOnly) {
589         result = TimelineFunctions::removeSpace(timeline, zone, undo, redo, tracks);
590     }
591     pCore->pushUndo(undo, redo, liftOnly ? i18n("Lift zone") : i18n("Extract zone"));
592     return result;
593 }
594 
insertZone(const std::shared_ptr<TimelineItemModel> & timeline,QList<int> trackIds,const QString & binId,int insertFrame,QPoint zone,bool overwrite,bool useTargets)595 bool TimelineFunctions::insertZone(const std::shared_ptr<TimelineItemModel> &timeline, QList<int> trackIds, const QString &binId, int insertFrame, QPoint zone,
596                                    bool overwrite, bool useTargets)
597 {
598     std::function<bool(void)> undo = []() { return true; };
599     std::function<bool(void)> redo = []() { return true; };
600     bool res = TimelineFunctions::insertZone(timeline, trackIds, binId, insertFrame, zone, overwrite, useTargets, undo, redo);
601     if (res) {
602         pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone"));
603     } else {
604         pCore->displayMessage(i18n("Could not insert zone"), ErrorMessage);
605         undo();
606     }
607     return res;
608 }
609 
insertZone(const std::shared_ptr<TimelineItemModel> & timeline,QList<int> trackIds,const QString & binId,int insertFrame,QPoint zone,bool overwrite,bool useTargets,Fun & undo,Fun & redo)610 bool TimelineFunctions::insertZone(const std::shared_ptr<TimelineItemModel> &timeline, QList<int> trackIds, const QString &binId, int insertFrame, QPoint zone,
611                                    bool overwrite, bool useTargets, Fun &undo, Fun &redo)
612 {
613     // Start undoable command
614     bool result = true;
615     QVector<int> affectedTracks;
616     auto it = timeline->m_allTracks.cbegin();
617     if (!useTargets) {
618         // Timeline drop in overwrite mode
619         for (int target_track : trackIds) {
620             if (!timeline->getTrackById_const(target_track)->isLocked()) {
621                 affectedTracks << target_track;
622             }
623         }
624     } else {
625         while (it != timeline->m_allTracks.cend()) {
626             int target_track = (*it)->getId();
627             if (timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
628                 affectedTracks << target_track;
629             } else if (trackIds.contains(target_track)) {
630                 // Track is marked as target but not active, remove it
631                 trackIds.removeAll(target_track);
632             }
633             ++it;
634         }
635     }
636     if (affectedTracks.isEmpty()) {
637         pCore->displayMessage(i18n("Please activate a track by clicking on a track's label"), ErrorMessage);
638         return false;
639     }
640     result = breakAffectedGroups(timeline, affectedTracks, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo);
641     if (overwrite) {
642         // Cut all tracks
643         for (int target_track : qAsConst(affectedTracks)) {
644             result = result && TimelineFunctions::liftZone(timeline, target_track, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo);
645             if (!result) {
646                 qDebug() << "// LIFTING ZONE FAILED\n";
647                 break;
648             }
649         }
650     } else {
651         // Cut all tracks
652         for (int target_track : qAsConst(affectedTracks)) {
653             int startClipId = timeline->getClipByPosition(target_track, insertFrame);
654             if (startClipId > -1) {
655                 // There is a clip, cut it
656                 result = result && TimelineFunctions::requestClipCut(timeline, startClipId, insertFrame, undo, redo);
657             }
658         }
659         result = result && TimelineFunctions::requestInsertSpace(timeline, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo, affectedTracks);
660     }
661     if (result) {
662         if (!trackIds.isEmpty()) {
663             int newId = -1;
664             QString binClipId;
665             if (binId.contains(QLatin1Char('/'))) {
666                 binClipId = QString("%1/%2/%3").arg(binId.section(QLatin1Char('/'), 0, 0)).arg(zone.x()).arg(zone.y() - 1);
667             } else {
668                 binClipId = QString("%1/%2/%3").arg(binId).arg(zone.x()).arg(zone.y() - 1);
669             }
670             result = timeline->requestClipInsertion(binClipId, trackIds.first(), insertFrame, newId, true, true, useTargets, undo, redo, affectedTracks);
671         }
672     }
673     return result;
674 }
675 
liftZone(const std::shared_ptr<TimelineItemModel> & timeline,int trackId,QPoint zone,Fun & undo,Fun & redo)676 bool TimelineFunctions::liftZone(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo)
677 {
678     // Check if there is a clip at start point
679     int startClipId = timeline->getClipByPosition(trackId, zone.x());
680     if (startClipId > -1) {
681         // There is a clip, cut it
682         if (timeline->getClipPosition(startClipId) < zone.x()) {
683             // Check if we have a mix
684             std::pair<MixInfo,MixInfo> mixData = timeline->getTrackById_const(trackId)->getMixInfo(startClipId);
685             bool abortCut = false;
686             if (mixData.first.firstClipId > -1) {
687                 // Clip has a start mix
688                 if (mixData.first.secondClipInOut.first + (mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first) - mixData.first.mixOffset >= zone.x()) {
689                     // Cut pos is in the mix zone before clip cut, completely remove clip
690                     abortCut = true;
691                 }
692             }
693             if (!abortCut) {
694                 TimelineFunctions::requestClipCut(timeline, startClipId, zone.x(), undo, redo);
695             } else {
696                 // Remove the clip now, so that the mix is deleted before checking items in range
697                 timeline->requestClipUngroup(startClipId, undo, redo);
698                 timeline->requestItemDeletion(startClipId, undo, redo);
699             }
700         }
701     }
702     int endClipId = timeline->getClipByPosition(trackId, zone.y());
703     if (endClipId > -1) {
704         // There is a clip, cut it
705         if (timeline->getClipPosition(endClipId) + timeline->getClipPlaytime(endClipId) > zone.y()) {
706             // Check if we have a mix
707             std::pair<MixInfo,MixInfo> mixData = timeline->getTrackById_const(trackId)->getMixInfo(endClipId);
708             bool abortCut = false;
709             if (mixData.second.firstClipId > -1) {
710                 // Clip has an end mix
711                 if (mixData.second.firstClipInOut.second - (mixData.second.firstClipInOut.second - mixData.second.secondClipInOut.first) - mixData.first.mixOffset <= zone.y()) {
712                     // Cut pos is in the mix zone after clip cut, completely remove clip
713                     abortCut = true;
714                 }
715             }
716             if (!abortCut) {
717                 TimelineFunctions::requestClipCut(timeline, endClipId, zone.y(), undo, redo);
718             } else {
719                 // Remove the clip now, so that the mix is deleted before checking items in range
720                 timeline->requestClipUngroup(endClipId, undo, redo);
721                 timeline->requestItemDeletion(endClipId, undo, redo);
722             }
723         }
724     }
725     std::unordered_set<int> clips = timeline->getItemsInRange(trackId, zone.x(), zone.y());
726     for (const auto &clipId : clips) {
727         timeline->requestClipUngroup(clipId, undo, redo);
728         timeline->requestItemDeletion(clipId, undo, redo);
729     }
730     return true;
731 }
732 
removeSpace(const std::shared_ptr<TimelineItemModel> & timeline,QPoint zone,Fun & undo,Fun & redo,QVector<int> allowedTracks,bool useTargets)733 bool TimelineFunctions::removeSpace(const std::shared_ptr<TimelineItemModel> &timeline, QPoint zone, Fun &undo, Fun &redo, QVector<int> allowedTracks, bool useTargets)
734 {
735     std::unordered_set<int> clips;
736     if (useTargets) {
737         auto it = timeline->m_allTracks.cbegin();
738         while (it != timeline->m_allTracks.cend()) {
739             int target_track = (*it)->getId();
740             if (timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) {
741                 std::unordered_set<int> subs = timeline->getItemsInRange(target_track, zone.y() - 1, -1, true);
742                 clips.insert(subs.begin(), subs.end());
743             }
744             ++it;
745         }
746     } else {
747         for (int &tid : allowedTracks) {
748             std::unordered_set<int> subs = timeline->getItemsInRange(tid, zone.y() - 1, -1, true);
749             clips.insert(subs.begin(), subs.end());
750         }
751     }
752     if (clips.size() == 0) {
753         // TODO: inform user no change will be performed
754         return true;
755     }
756     bool result = false;
757     timeline->requestSetSelection(clips);
758     int itemId = *clips.begin();
759     int targetTrackId = timeline->getItemTrackId(itemId);
760     int targetPos = timeline->getItemPosition(itemId) + zone.x() - zone.y();
761 
762     if (timeline->m_groups->isInGroup(itemId)) {
763         result = timeline->requestGroupMove(itemId, timeline->m_groups->getRootId(itemId), 0, zone.x() - zone.y(), true, true, undo, redo, true, true, true, allowedTracks);
764     } else if (timeline->isClip(itemId)) {
765         result = timeline->requestClipMove(itemId, targetTrackId, targetPos, true, true, true, true, undo, redo);
766     } else {
767         result = timeline->requestCompositionMove(itemId, targetTrackId, timeline->m_allCompositions[itemId]->getForcedTrack(), targetPos, true, true, undo, redo);
768     }
769     timeline->requestClearSelection();
770     if (!result) {
771         undo();
772     }
773     return result;
774 }
775 
requestInsertSpace(const std::shared_ptr<TimelineItemModel> & timeline,QPoint zone,Fun & undo,Fun & redo,QVector<int> allowedTracks)776 bool TimelineFunctions::requestInsertSpace(const std::shared_ptr<TimelineItemModel> &timeline, QPoint zone, Fun &undo, Fun &redo, QVector<int> allowedTracks)
777 {
778     timeline->requestClearSelection();
779     Fun local_undo = []() { return true; };
780     Fun local_redo = []() { return true; };
781     std::unordered_set<int> items;
782     if (allowedTracks.isEmpty()) {
783         // Select clips in all tracks
784         items = timeline->getItemsInRange(-1, zone.x(), -1, true);
785     } else {
786         // Select clips in target and active tracks only
787         for (int target_track : allowedTracks) {
788             std::unordered_set<int> subs = timeline->getItemsInRange(target_track, zone.x(), -1, true);
789             items.insert(subs.begin(), subs.end());
790         }
791     }
792     if (items.empty()) {
793         return true;
794     }
795     timeline->requestSetSelection(items);
796     bool result = true;
797     int itemId = *(items.begin());
798     int targetTrackId = timeline->getItemTrackId(itemId);
799     int targetPos = timeline->getItemPosition(itemId) + zone.y() - zone.x();
800 
801     // TODO the three move functions should be unified in a "requestItemMove" function
802     if (timeline->m_groups->isInGroup(itemId)) {
803         result =
804             result && timeline->requestGroupMove(itemId, timeline->m_groups->getRootId(itemId), 0, zone.y() - zone.x(), true, true, local_undo, local_redo, true, true, true, allowedTracks);
805     } else if (timeline->isClip(itemId)) {
806         result = result && timeline->requestClipMove(itemId, targetTrackId, targetPos, true, true, true, true, local_undo, local_redo);
807     } else {
808         result = result && timeline->requestCompositionMove(itemId, targetTrackId, timeline->m_allCompositions[itemId]->getForcedTrack(), targetPos, true, true,
809                                                             local_undo, local_redo);
810     }
811     timeline->requestClearSelection();
812     if (!result) {
813         bool undone = local_undo();
814         Q_ASSERT(undone);
815         pCore->displayMessage(i18n("Cannot move selected group"), ErrorMessage);
816     }
817     UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
818     return result;
819 }
820 
requestItemCopy(const std::shared_ptr<TimelineItemModel> & timeline,int clipId,int trackId,int position)821 bool TimelineFunctions::requestItemCopy(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int trackId, int position)
822 {
823     Q_ASSERT(timeline->isClip(clipId) || timeline->isComposition(clipId));
824     Fun undo = []() { return true; };
825     Fun redo = []() { return true; };
826     int deltaTrack = timeline->getTrackPosition(trackId) - timeline->getTrackPosition(timeline->getItemTrackId(clipId));
827     int deltaPos = position - timeline->getItemPosition(clipId);
828     std::unordered_set<int> allIds = timeline->getGroupElements(clipId);
829     std::unordered_map<int, int> mapping; // keys are ids of the source clips, values are ids of the copied clips
830     bool res = true;
831     for (int id : allIds) {
832         int newId = -1;
833         if (timeline->isClip(id)) {
834             PlaylistState::ClipState state = timeline->m_allClips[id]->clipState();
835             res = cloneClip(timeline, id, newId, state, undo, redo);
836             res = res && (newId != -1);
837         }
838         int target_position = timeline->getItemPosition(id) + deltaPos;
839         int target_track_position = timeline->getTrackPosition(timeline->getItemTrackId(id)) + deltaTrack;
840         if (target_track_position >= 0 && target_track_position < timeline->getTracksCount()) {
841             auto it = timeline->m_allTracks.cbegin();
842             std::advance(it, target_track_position);
843             int target_track = (*it)->getId();
844             if (timeline->isClip(id)) {
845                 res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, true, true, undo, redo);
846             } else {
847                 const QString &transitionId = timeline->m_allCompositions[id]->getAssetId();
848                 std::unique_ptr<Mlt::Properties> transProps(timeline->m_allCompositions[id]->properties());
849                 res = res && timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position,
850                                                                    timeline->m_allCompositions[id]->getPlaytime(), std::move(transProps), newId, undo, redo);
851             }
852         } else {
853             res = false;
854         }
855         if (!res) {
856             bool undone = undo();
857             Q_ASSERT(undone);
858             return false;
859         }
860         mapping[id] = newId;
861     }
862     qDebug() << "Successful copy, copying groups...";
863     res = timeline->m_groups->copyGroups(mapping, undo, redo);
864     if (!res) {
865         bool undone = undo();
866         Q_ASSERT(undone);
867         return false;
868     }
869     return true;
870 }
871 
showClipKeyframes(const std::shared_ptr<TimelineItemModel> & timeline,int clipId,bool value)872 void TimelineFunctions::showClipKeyframes(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, bool value)
873 {
874     timeline->m_allClips[clipId]->setShowKeyframes(value);
875     QModelIndex modelIndex = timeline->makeClipIndexFromID(clipId);
876     emit timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
877 }
878 
showCompositionKeyframes(const std::shared_ptr<TimelineItemModel> & timeline,int compoId,bool value)879 void TimelineFunctions::showCompositionKeyframes(const std::shared_ptr<TimelineItemModel> &timeline, int compoId, bool value)
880 {
881     timeline->m_allCompositions[compoId]->setShowKeyframes(value);
882     QModelIndex modelIndex = timeline->makeCompositionIndexFromID(compoId);
883     emit timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole});
884 }
885 
switchEnableState(const std::shared_ptr<TimelineItemModel> & timeline,std::unordered_set<int> selection)886 bool TimelineFunctions::switchEnableState(const std::shared_ptr<TimelineItemModel> &timeline, std::unordered_set<int> selection)
887 {
888     Fun undo = []() { return true; };
889     Fun redo = []() { return true; };
890     bool result = false;
891     bool disable = true;
892     for (int clipId : selection) {
893         if (!timeline->isClip(clipId)) {
894             continue;
895         }
896         PlaylistState::ClipState oldState = timeline->getClipPtr(clipId)->clipState();
897         PlaylistState::ClipState state = PlaylistState::Disabled;
898         disable = true;
899         if (oldState == PlaylistState::Disabled) {
900             state = timeline->getTrackById_const(timeline->getClipTrackId(clipId))->trackType();
901             disable = false;
902         }
903         result = changeClipState(timeline, clipId, state, undo, redo);
904         if (!result) {
905             break;
906         }
907     }
908     // Update action name since clip will be switched
909     int id = *selection.begin();
910     Fun local_redo = []() { return true; };
911     Fun local_undo = []() { return true; };
912     if (timeline->isClip(id)) {
913         bool disabled = timeline->m_allClips[id]->clipState() == PlaylistState::Disabled;
914         QAction *action = pCore->window()->actionCollection()->action(QStringLiteral("clip_switch"));
915         local_redo = [disabled, action]() {
916             action->setText(disabled ? i18n("Enable clip") : i18n("Disable clip"));
917             return true;
918         };
919         local_undo = [disabled, action]() {
920             action->setText(disabled ? i18n("Disable clip") : i18n("Enable clip"));
921             return true;
922         };
923     }
924     if (result) {
925         local_redo();
926         UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
927         pCore->pushUndo(undo, redo, disable ? i18n("Disable clip") : i18n("Enable clip"));
928     }
929     return result;
930 }
931 
changeClipState(const std::shared_ptr<TimelineItemModel> & timeline,int clipId,PlaylistState::ClipState status,Fun & undo,Fun & redo)932 bool TimelineFunctions::changeClipState(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo)
933 {
934     int track = timeline->getClipTrackId(clipId);
935     int start = -1;
936     bool invalidate = false;
937     if (track > -1) {
938         if (!timeline->getTrackById_const(track)->isAudioTrack()) {
939             invalidate = true;
940         }
941         start = timeline->getItemPosition(clipId);
942     }
943     Fun local_undo = []() { return true; };
944     Fun local_redo = []() { return true; };
945     // For the state change to work, we need to unplant/replant the clip
946     bool result = true;
947     if (track > -1) {
948         result = timeline->getTrackById(track)->requestClipDeletion(clipId, true, invalidate, local_undo, local_redo, false, false);
949     }
950     result = timeline->m_allClips[clipId]->setClipState(status, local_undo, local_redo);
951     if (result && track > -1) {
952         result = timeline->getTrackById(track)->requestClipInsertion(clipId, start, true, true, local_undo, local_redo);
953     }
954     UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo);
955     return result;
956 }
957 
requestSplitAudio(const std::shared_ptr<TimelineItemModel> & timeline,int clipId,int audioTarget)958 bool TimelineFunctions::requestSplitAudio(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int audioTarget)
959 {
960     std::function<bool(void)> undo = []() { return true; };
961     std::function<bool(void)> redo = []() { return true; };
962     const std::unordered_set<int> clips = timeline->getGroupElements(clipId);
963     bool done = false;
964     // Now clear selection so we don't mess with groups
965     timeline->requestClearSelection(false, undo, redo);
966     for (int cid : clips) {
967         if (!timeline->getClipPtr(cid)->canBeAudio() || timeline->getClipPtr(cid)->clipState() == PlaylistState::AudioOnly) {
968             // clip without audio or audio only, skip
969             pCore->displayMessage(i18n("One or more clips do not have audio, or are already audio"), ErrorMessage);
970             return false;
971         }
972         int position = timeline->getClipPosition(cid);
973         int track = timeline->getClipTrackId(cid);
974         QList<int> possibleTracks;
975         if (audioTarget >= 0) {
976             possibleTracks = {audioTarget};
977         } else {
978             int mirror = timeline->getMirrorAudioTrackId(track);
979             if (mirror > -1) {
980                 possibleTracks = {mirror};
981             }
982         }
983         if (possibleTracks.isEmpty()) {
984             // No available audio track for splitting, abort
985             undo();
986             pCore->displayMessage(i18n("No available audio track for restore operation"), ErrorMessage);
987             return false;
988         }
989         int newId;
990         bool res = cloneClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo);
991         if (!res) {
992             bool undone = undo();
993             Q_ASSERT(undone);
994             pCore->displayMessage(i18n("Audio restore failed"), ErrorMessage);
995             return false;
996         }
997         bool success = false;
998         while (!success && !possibleTracks.isEmpty()) {
999             int newTrack = possibleTracks.takeFirst();
1000             success = timeline->requestClipMove(newId, newTrack, position, true, true, false, true, undo, redo);
1001         }
1002         TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo);
1003         success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set<int>{newId}, GroupType::AVSplit, undo, redo);
1004         if (!success) {
1005             bool undone = undo();
1006             Q_ASSERT(undone);
1007             pCore->displayMessage(i18n("Audio restore failed"), ErrorMessage);
1008             return false;
1009         }
1010         done = true;
1011     }
1012     if (done) {
1013         timeline->requestSetSelection(clips, undo, redo);
1014         pCore->pushUndo(undo, redo, i18n("Restore Audio"));
1015     }
1016     return done;
1017 }
1018 
requestSplitVideo(const std::shared_ptr<TimelineItemModel> & timeline,int clipId,int videoTarget)1019 bool TimelineFunctions::requestSplitVideo(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int videoTarget)
1020 {
1021     std::function<bool(void)> undo = []() { return true; };
1022     std::function<bool(void)> redo = []() { return true; };
1023     const std::unordered_set<int> clips = timeline->getGroupElements(clipId);
1024     bool done = false;
1025     // Now clear selection so we don't mess with groups
1026     timeline->requestClearSelection();
1027     for (int cid : clips) {
1028         if (!timeline->getClipPtr(cid)->canBeVideo() || timeline->getClipPtr(cid)->clipState() == PlaylistState::VideoOnly) {
1029             // clip without audio or audio only, skip
1030             continue;
1031         }
1032         int position = timeline->getClipPosition(cid);
1033         int track = timeline->getClipTrackId(cid);
1034         QList<int> possibleTracks;
1035         if (videoTarget >= 0) {
1036             possibleTracks = {videoTarget};
1037         } else {
1038             int mirror = timeline->getMirrorVideoTrackId(track);
1039             if (mirror > -1) {
1040                 possibleTracks = {mirror};
1041             }
1042         }
1043         if (possibleTracks.isEmpty()) {
1044             // No available audio track for splitting, abort
1045             undo();
1046             pCore->displayMessage(i18n("No available video track for restore operation"), ErrorMessage);
1047             return false;
1048         }
1049         int newId;
1050         bool res = cloneClip(timeline, cid, newId, PlaylistState::VideoOnly, undo, redo);
1051         if (!res) {
1052             bool undone = undo();
1053             Q_ASSERT(undone);
1054             pCore->displayMessage(i18n("Video restore failed"), ErrorMessage);
1055             return false;
1056         }
1057         bool success = false;
1058         while (!success && !possibleTracks.isEmpty()) {
1059             int newTrack = possibleTracks.takeFirst();
1060             success = timeline->requestClipMove(newId, newTrack, position, true, true, true, true, undo, redo);
1061         }
1062         TimelineFunctions::changeClipState(timeline, cid, PlaylistState::AudioOnly, undo, redo);
1063         success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set<int>{newId}, GroupType::AVSplit, undo, redo);
1064         if (!success) {
1065             bool undone = undo();
1066             Q_ASSERT(undone);
1067             pCore->displayMessage(i18n("Video restore failed"), ErrorMessage);
1068             return false;
1069         }
1070         done = true;
1071     }
1072     if (done) {
1073         pCore->pushUndo(undo, redo, i18n("Restore Video"));
1074     }
1075     return done;
1076 }
1077 
setCompositionATrack(const std::shared_ptr<TimelineItemModel> & timeline,int cid,int aTrack)1078 void TimelineFunctions::setCompositionATrack(const std::shared_ptr<TimelineItemModel> &timeline, int cid, int aTrack)
1079 {
1080     std::function<bool(void)> undo = []() { return true; };
1081     std::function<bool(void)> redo = []() { return true; };
1082     std::shared_ptr<CompositionModel> compo = timeline->getCompositionPtr(cid);
1083     int previousATrack = compo->getATrack();
1084     int previousAutoTrack = static_cast<int>(compo->getForcedTrack() == -1);
1085     bool autoTrack = aTrack < 0;
1086     if (autoTrack) {
1087         // Automatic track compositing, find lower video track
1088         aTrack = timeline->getPreviousVideoTrackPos(compo->getCurrentTrackId());
1089     }
1090     int start = timeline->getItemPosition(cid);
1091     int end = start + timeline->getItemPlaytime(cid);
1092     Fun local_redo = [timeline, cid, aTrack, autoTrack, start, end]() {
1093         timeline->unplantComposition(cid);
1094         QScopedPointer<Mlt::Field> field(timeline->m_tractor->field());
1095         field->lock();
1096         timeline->getCompositionPtr(cid)->setForceTrack(!autoTrack);
1097         timeline->getCompositionPtr(cid)->setATrack(aTrack, aTrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(aTrack - 1));
1098         field->unlock();
1099         timeline->replantCompositions(cid, true);
1100         emit timeline->invalidateZone(start, end);
1101         timeline->checkRefresh(start, end);
1102         return true;
1103     };
1104     Fun local_undo = [timeline, cid, previousATrack, previousAutoTrack, start, end]() {
1105         timeline->unplantComposition(cid);
1106         QScopedPointer<Mlt::Field> field(timeline->m_tractor->field());
1107         field->lock();
1108         timeline->getCompositionPtr(cid)->setForceTrack(previousAutoTrack == 0);
1109         timeline->getCompositionPtr(cid)->setATrack(previousATrack, previousATrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(previousATrack - 1));
1110         field->unlock();
1111         timeline->replantCompositions(cid, true);
1112         emit timeline->invalidateZone(start, end);
1113         timeline->checkRefresh(start, end);
1114         return true;
1115     };
1116     if (local_redo()) {
1117         PUSH_LAMBDA(local_undo, undo);
1118         PUSH_LAMBDA(local_redo, redo);
1119     }
1120     pCore->pushUndo(undo, redo, i18n("Change Composition Track"));
1121 }
1122 
enableMultitrackView(const std::shared_ptr<TimelineItemModel> & timeline,bool enable,bool refresh)1123 QStringList TimelineFunctions::enableMultitrackView(const std::shared_ptr<TimelineItemModel> &timeline, bool enable, bool refresh)
1124 {
1125     QStringList trackNames;
1126     std::vector<int> videoTracks;
1127     for (int i = 0; i < int(timeline->m_allTracks.size()); i++) {
1128         int tid = timeline->getTrackIndexFromPosition(i);
1129         if (timeline->getTrackById_const(tid)->isAudioTrack() || timeline->getTrackById_const(tid)->isHidden()) {
1130             continue;
1131         }
1132         videoTracks.push_back(tid);
1133     }
1134     if (videoTracks.size() < 2) {
1135         pCore->displayMessage(i18n("Cannot enable multitrack view on a single track"), ErrorMessage);
1136     }
1137     // First, dis/enable track compositing
1138     QScopedPointer<Mlt::Service> service(timeline->m_tractor->field());
1139     Mlt::Field *field = timeline->m_tractor->field();
1140     field->lock();
1141     while ((service != nullptr) && service->is_valid()) {
1142         if (service->type() == mlt_service_transition_type) {
1143             Mlt::Transition t(mlt_transition(service->get_service()));
1144             service.reset(service->producer());
1145             QString serviceName = t.get("mlt_service");
1146             int added = t.get_int("internal_added");
1147             if (added == 237 && serviceName != QLatin1String("mix")) {
1148                 // Disable all compositing transitions
1149                 t.set("disable", enable ? "1" : nullptr);
1150             } else if (added == 200) {
1151                 field->disconnect_service(t);
1152                 t.disconnect_all_producers();
1153             }
1154         } else {
1155             service.reset(service->producer());
1156         }
1157     }
1158     if (enable) {
1159         int count = 0;
1160 
1161         for (int tid : videoTracks) {
1162             int b_track = timeline->getTrackMltIndex(tid);
1163             Mlt::Transition transition(*timeline->m_tractor->profile(), "qtblend");
1164             //transition.set("mlt_service", "composite");
1165             transition.set("a_track", 0);
1166             transition.set("b_track", b_track);
1167             // 200 is an arbitrary number so we can easily remove these transition later
1168             transition.set("internal_added", 200);
1169             QString geometry;
1170             trackNames << timeline->getTrackFullName(tid);
1171             switch (count) {
1172             case 0:
1173                 switch (videoTracks.size()) {
1174                 case 1:
1175                     geometry = QStringLiteral("0 0 100% 100% 100%");
1176                     break;
1177                 case 2:
1178                     geometry = QStringLiteral("0 0 50% 100% 100%");
1179                     break;
1180                 case 3:
1181                 case 4:
1182                     geometry = QStringLiteral("0 0 50% 50% 100%");
1183                     break;
1184                 case 5:
1185                 case 6:
1186                     geometry = QStringLiteral("0 0 33% 50% 100%");
1187                     break;
1188                 default:
1189                     geometry = QStringLiteral("0 0 33% 33% 100%");
1190                     break;
1191                 }
1192                 break;
1193             case 1:
1194                 switch (videoTracks.size()) {
1195                 case 2:
1196                     geometry = QStringLiteral("50% 0 50% 100% 100%");
1197                     break;
1198                 case 3:
1199                 case 4:
1200                     geometry = QStringLiteral("50% 0 50% 50% 100%");
1201                     break;
1202                 case 5:
1203                 case 6:
1204                     geometry = QStringLiteral("33% 0 33% 50% 100%");
1205                     break;
1206                 default:
1207                     geometry = QStringLiteral("33% 0 33% 33% 100%");
1208                     break;
1209                 }
1210                 break;
1211             case 2:
1212                 switch (videoTracks.size()) {
1213                 case 3:
1214                 case 4:
1215                     geometry = QStringLiteral("0 50% 50% 50% 100%");
1216                     break;
1217                 case 5:
1218                 case 6:
1219                     geometry = QStringLiteral("66% 0 33% 50% 100%");
1220                     break;
1221                 default:
1222                     geometry = QStringLiteral("66% 0 33% 33% 100%");
1223                     break;
1224                 }
1225                 break;
1226             case 3:
1227                 switch (videoTracks.size()) {
1228                 case 4:
1229                     geometry = QStringLiteral("50% 50% 50% 50% 100%");
1230                     break;
1231                 case 5:
1232                 case 6:
1233                     geometry = QStringLiteral("0 50% 33% 50% 100%");
1234                     break;
1235                 default:
1236                     geometry = QStringLiteral("0 33% 33% 33% 100%");
1237                     break;
1238                 }
1239                 break;
1240             case 4:
1241                 switch (videoTracks.size()) {
1242                 case 5:
1243                 case 6:
1244                     geometry = QStringLiteral("33% 50% 33% 50% 100%");
1245                     break;
1246                 default:
1247                     geometry = QStringLiteral("33% 33% 33% 33% 100%");
1248                     break;
1249                 }
1250                 break;
1251             case 5:
1252                 switch (videoTracks.size()) {
1253                 case 6:
1254                     geometry = QStringLiteral("66% 50% 33% 50% 100%");
1255                     break;
1256                 default:
1257                     geometry = QStringLiteral("66% 33% 33% 33% 100%");
1258                     break;
1259                 }
1260                 break;
1261             case 6:
1262                 geometry = QStringLiteral("0 66% 33% 33% 100%");
1263                 break;
1264             case 7:
1265                 geometry = QStringLiteral("33% 66% 33% 33% 100%");
1266                 break;
1267             default:
1268                 geometry = QStringLiteral("66% 66% 33% 33% 100%");
1269                 break;
1270             }
1271             count++;
1272             // Add transition to track:
1273             transition.set("rect", geometry.toUtf8().constData());
1274             transition.set("always_active", 1);
1275             field->plant_transition(transition, 0, b_track);
1276         }
1277     }
1278     field->unlock();
1279     if (refresh) {
1280         emit timeline->requestMonitorRefresh();
1281     }
1282     return trackNames;
1283 }
1284 
saveTimelineSelection(const std::shared_ptr<TimelineItemModel> & timeline,const std::unordered_set<int> & selection,const QDir & targetDir)1285 void TimelineFunctions::saveTimelineSelection(const std::shared_ptr<TimelineItemModel> &timeline, const std::unordered_set<int> &selection,
1286                                               const QDir &targetDir)
1287 {
1288     bool ok;
1289     QString name = QInputDialog::getText(qApp->activeWindow(), i18n("Add Clip to Library"), i18n("Enter a name for the clip in Library"), QLineEdit::Normal,
1290                                          QString(), &ok);
1291     if (name.isEmpty() || !ok) {
1292         return;
1293     }
1294     if (targetDir.exists(name + QStringLiteral(".mlt"))) {
1295         // TODO: warn and ask for overwrite / rename
1296     }
1297     int offset = -1;
1298     int lowerAudioTrack = -1;
1299     int lowerVideoTrack = -1;
1300     QString fullPath = targetDir.absoluteFilePath(name + QStringLiteral(".mlt"));
1301     // Build a copy of selected tracks.
1302     QMap<int, int> sourceTracks;
1303     for (int i : selection) {
1304         int sourceTrack = timeline->getItemTrackId(i);
1305         int clipPos = timeline->getItemPosition(i);
1306         if (offset < 0 || clipPos < offset) {
1307             offset = clipPos;
1308         }
1309         int trackPos = timeline->getTrackMltIndex(sourceTrack);
1310         if (!sourceTracks.contains(trackPos)) {
1311             sourceTracks.insert(trackPos, sourceTrack);
1312         }
1313     }
1314     // Build target timeline
1315     Mlt::Tractor newTractor(*timeline->m_tractor->profile());
1316     QScopedPointer<Mlt::Field> field(newTractor.field());
1317     int ix = 0;
1318     QString composite = TransitionsRepository::get()->getCompositingTransition();
1319     QMapIterator<int, int> i(sourceTracks);
1320     QList<Mlt::Transition *> compositions;
1321     while (i.hasNext()) {
1322         i.next();
1323         QScopedPointer<Mlt::Playlist> newTrackPlaylist(new Mlt::Playlist(*newTractor.profile()));
1324         newTractor.set_track(*newTrackPlaylist, ix);
1325         // QScopedPointer<Mlt::Producer> trackProducer(newTractor.track(ix));
1326         int trackId = i.value();
1327         sourceTracks.insert(timeline->getTrackMltIndex(trackId), ix);
1328         std::shared_ptr<TrackModel> track = timeline->getTrackById_const(trackId);
1329         bool isAudio = track->isAudioTrack();
1330         if (isAudio) {
1331             newTrackPlaylist->set("hide", 1);
1332             if (lowerAudioTrack < 0) {
1333                 lowerAudioTrack = ix;
1334             }
1335         } else {
1336             newTrackPlaylist->set("hide", 2);
1337             if (lowerVideoTrack < 0) {
1338                 lowerVideoTrack = ix;
1339             }
1340         }
1341         for (int itemId : selection) {
1342             if (timeline->getItemTrackId(itemId) == trackId) {
1343                 // Copy clip on the destination track
1344                 if (timeline->isClip(itemId)) {
1345                     int clip_position = timeline->m_allClips[itemId]->getPosition();
1346                     auto clip_loc = track->getClipIndexAt(clip_position);
1347                     int target_clip = clip_loc.second;
1348                     QSharedPointer<Mlt::Producer> clip = track->getClipProducer(target_clip);
1349                     newTrackPlaylist->insert_at(clip_position - offset, clip.data(), 1);
1350                 } else if (timeline->isComposition(itemId)) {
1351                     // Composition
1352                     auto *t = new Mlt::Transition(*timeline->m_allCompositions[itemId].get());
1353                     QString id(t->get("kdenlive_id"));
1354                     QString internal(t->get("internal_added"));
1355                     if (internal.isEmpty()) {
1356                         compositions << t;
1357                         if (id.isEmpty()) {
1358                             qDebug() << "// Warning, this should not happen, transition without id: " << t->get("id") << " = " << t->get("mlt_service");
1359                             t->set("kdenlive_id", t->get("mlt_service"));
1360                         }
1361                     }
1362                 }
1363             }
1364         }
1365         ix++;
1366     }
1367     // Sort compositions and insert
1368     if (!compositions.isEmpty()) {
1369         std::sort(compositions.begin(), compositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); });
1370         while (!compositions.isEmpty()) {
1371             QScopedPointer<Mlt::Transition> t(compositions.takeFirst());
1372             int a_track = t->get_a_track();
1373             if ((sourceTracks.contains(a_track) || a_track == 0) && sourceTracks.contains(t->get_b_track())) {
1374                 Mlt::Transition newComposition(*newTractor.profile(), t->get("mlt_service"));
1375                 Mlt::Properties sourceProps(t->get_properties());
1376                 newComposition.inherit(sourceProps);
1377                 int in = qMax(0, t->get_in() - offset);
1378                 int out = t->get_out() - offset;
1379                 newComposition.set_in_and_out(in, out);
1380                 if (sourceTracks.contains(a_track)) {
1381                     a_track = sourceTracks.value(a_track);
1382                 }
1383                 int b_track = sourceTracks.value(t->get_b_track());
1384                 field->plant_transition(newComposition, a_track, b_track);
1385             }
1386         }
1387     }
1388     // Track compositing
1389     i.toFront();
1390     ix = 0;
1391     while (i.hasNext()) {
1392         i.next();
1393         int trackId = i.value();
1394         std::shared_ptr<TrackModel> track = timeline->getTrackById_const(trackId);
1395         bool isAudio = track->isAudioTrack();
1396         if ((isAudio && ix > lowerAudioTrack) || (!isAudio && ix > lowerVideoTrack)) {
1397             // add track compositing / mix
1398             Mlt::Transition t(*newTractor.profile(), isAudio ? "mix" : composite.toUtf8().constData());
1399             if (isAudio) {
1400                 t.set("sum", 1);
1401                 t.set("accepts_blanks", 1);
1402             }
1403             t.set("always_active", 1);
1404             t.set("internal_added", 237);
1405             t.set_tracks(isAudio ? lowerAudioTrack : lowerVideoTrack, ix);
1406             field->plant_transition(t, isAudio ? lowerAudioTrack : lowerVideoTrack, ix);
1407         }
1408         ix++;
1409     }
1410     Mlt::Consumer xmlConsumer(*newTractor.profile(), ("xml:" + fullPath).toUtf8().constData());
1411     xmlConsumer.set("terminate_on_pause", 1);
1412     xmlConsumer.connect(newTractor);
1413     xmlConsumer.run();
1414 }
1415 
getTrackOffset(const std::shared_ptr<TimelineItemModel> & timeline,int startTrack,int destTrack)1416 int TimelineFunctions::getTrackOffset(const std::shared_ptr<TimelineItemModel> &timeline, int startTrack, int destTrack)
1417 {
1418     qDebug() << "+++++++\nGET TRACK OFFSET: " << startTrack << " - " << destTrack;
1419     int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack);
1420     int destTrackMltIndex = timeline->getTrackMltIndex(destTrack);
1421     int offset = 0;
1422     qDebug() << "+++++++\nGET TRACK MLT: " << masterTrackMltIndex << " - " << destTrackMltIndex;
1423     if (masterTrackMltIndex == destTrackMltIndex) {
1424         return offset;
1425     }
1426     int step = masterTrackMltIndex > destTrackMltIndex ? -1 : 1;
1427     bool isAudio = timeline->isAudioTrack(startTrack);
1428     int track = masterTrackMltIndex;
1429     while (track != destTrackMltIndex) {
1430         track += step;
1431         qDebug() << "+ + +TESTING TRACK: " << track;
1432         int trackId = timeline->getTrackIndexFromPosition(track - 1);
1433         if (isAudio == timeline->isAudioTrack(trackId)) {
1434             offset += step;
1435         }
1436     }
1437     return offset;
1438 }
1439 
getOffsetTrackId(const std::shared_ptr<TimelineItemModel> & timeline,int startTrack,int offset,bool audioOffset)1440 int TimelineFunctions::getOffsetTrackId(const std::shared_ptr<TimelineItemModel> &timeline, int startTrack, int offset, bool audioOffset)
1441 {
1442     int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack);
1443     bool isAudio = timeline->isAudioTrack(startTrack);
1444     if (isAudio != audioOffset) {
1445         offset = -offset;
1446     }
1447     qDebug() << "* ** * MASTER INDEX: " << masterTrackMltIndex << ", OFFSET: " << offset;
1448     while (offset != 0) {
1449         masterTrackMltIndex += offset > 0 ? 1 : -1;
1450         qDebug() << "#### TESTING TRACK: " << masterTrackMltIndex;
1451         if (masterTrackMltIndex < 0) {
1452             masterTrackMltIndex = 0;
1453             break;
1454         }
1455         if (masterTrackMltIndex > int(timeline->m_allTracks.size())) {
1456             masterTrackMltIndex = int(timeline->m_allTracks.size());
1457             break;
1458         }
1459         int trackId = timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1);
1460         if (timeline->isAudioTrack(trackId) == isAudio) {
1461             offset += offset > 0 ? -1 : 1;
1462         }
1463     }
1464     return timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1);
1465 }
1466 
getAVTracksIds(const std::shared_ptr<TimelineItemModel> & timeline)1467 QPair<QList<int>, QList<int>> TimelineFunctions::getAVTracksIds(const std::shared_ptr<TimelineItemModel> &timeline)
1468 {
1469     QList<int> audioTracks;
1470     QList<int> videoTracks;
1471     for (const auto &track : timeline->m_allTracks) {
1472         if (track->isAudioTrack()) {
1473             audioTracks << track->getId();
1474         } else {
1475             videoTracks << track->getId();
1476         }
1477     }
1478     return {audioTracks, videoTracks};
1479 }
1480 
copyClips(const std::shared_ptr<TimelineItemModel> & timeline,const std::unordered_set<int> & itemIds)1481 QString TimelineFunctions::copyClips(const std::shared_ptr<TimelineItemModel> &timeline, const std::unordered_set<int> &itemIds)
1482 {
1483     int mainId = *(itemIds.begin());
1484     // We need to retrieve ALL the involved clips, ie those who are also grouped with the given clips
1485     std::unordered_set<int> allIds;
1486     for (const auto &itemId : itemIds) {
1487         std::unordered_set<int> siblings = timeline->getGroupElements(itemId);
1488         allIds.insert(siblings.begin(), siblings.end());
1489     }
1490     // Avoid using a subtitle item as reference since it doesn't work with track offset
1491     if (timeline->isSubTitle(mainId)) {
1492         for (const auto &id : allIds) {
1493             if (!timeline->isSubTitle(id)) {
1494                 mainId = id;
1495                 break;
1496             }
1497         }
1498     }
1499     bool subtitleOnlyCopy = false;
1500     if (timeline->isSubTitle(mainId)) {
1501         subtitleOnlyCopy = true;
1502     }
1503 
1504     timeline->requestClearSelection();
1505     // TODO better guess for master track
1506     int masterTid = timeline->getItemTrackId(mainId);
1507     bool audioCopy = subtitleOnlyCopy ? false : timeline->isAudioTrack(masterTid);
1508     int masterTrack = subtitleOnlyCopy ? -1 : timeline->getTrackPosition(masterTid);
1509     QDomDocument copiedItems;
1510     int offset = -1;
1511     QDomElement container = copiedItems.createElement(QStringLiteral("kdenlive-scene"));
1512     copiedItems.appendChild(container);
1513     QStringList binIds;
1514     for (int id : allIds) {
1515         if (offset == -1 || timeline->getItemPosition(id) < offset) {
1516             offset = timeline->getItemPosition(id);
1517         }
1518         if (timeline->isClip(id)) {
1519             QDomElement clipXml = timeline->m_allClips[id]->toXml(copiedItems);
1520             container.appendChild(clipXml);
1521             const QString bid = timeline->m_allClips[id]->binId();
1522             if (!binIds.contains(bid)) {
1523                 binIds << bid;
1524             }
1525             int tid = timeline->getItemTrackId(id);
1526             if (timeline->getTrackById_const(tid)->hasStartMix(id)) {
1527                 QDomElement mix = timeline->getTrackById_const(tid)->mixXml(copiedItems, id);
1528                 clipXml.appendChild(mix);
1529             }
1530         } else if (timeline->isComposition(id)) {
1531             container.appendChild(timeline->m_allCompositions[id]->toXml(copiedItems));
1532         } else if (timeline->isSubTitle(id)) {
1533             container.appendChild(timeline->getSubtitleModel()->toXml(id, copiedItems));
1534         } else {
1535             Q_ASSERT(false);
1536         }
1537     }
1538     QDomElement container2 = copiedItems.createElement(QStringLiteral("bin"));
1539     container.appendChild(container2);
1540     for (const QString &id : qAsConst(binIds)) {
1541         std::shared_ptr<ProjectClip> clip = pCore->projectItemModel()->getClipByBinID(id);
1542         QDomDocument tmp;
1543         container2.appendChild(clip->toXml(tmp));
1544     }
1545     container.setAttribute(QStringLiteral("offset"), offset);
1546     if (audioCopy) {
1547         container.setAttribute(QStringLiteral("masterAudioTrack"), masterTrack);
1548         int masterMirror = timeline->getMirrorVideoTrackId(masterTid);
1549         if (masterMirror == -1) {
1550             QPair<QList<int>, QList<int>> projectTracks = TimelineFunctions::getAVTracksIds(timeline);
1551             if (!projectTracks.second.isEmpty()) {
1552                 masterTrack = timeline->getTrackPosition(projectTracks.second.first());
1553             }
1554         } else {
1555             masterTrack = timeline->getTrackPosition(masterMirror);
1556         }
1557     }
1558     /* masterTrack contains the reference track over which we want to paste.
1559        this is a video track, unless audioCopy is defined */
1560     container.setAttribute(QStringLiteral("masterTrack"), masterTrack);
1561     container.setAttribute(QStringLiteral("documentid"), pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid")));
1562     QDomElement grp = copiedItems.createElement(QStringLiteral("groups"));
1563     container.appendChild(grp);
1564 
1565     std::unordered_set<int> groupRoots;
1566     std::transform(allIds.begin(), allIds.end(), std::inserter(groupRoots, groupRoots.begin()), [&](int id) { return timeline->m_groups->getRootId(id); });
1567 
1568     qDebug() << "==============\n GROUP ROOTS: ";
1569     for (int gp : groupRoots) {
1570         qDebug() << "GROUP: " << gp;
1571     }
1572     qDebug() << "\n=======";
1573     grp.appendChild(copiedItems.createTextNode(timeline->m_groups->toJson(groupRoots)));
1574 
1575     qDebug() << " / // / PASTED DOC: \n\n" << copiedItems.toString() << "\n\n------------";
1576     return copiedItems.toString();
1577 }
1578 
pasteClips(const std::shared_ptr<TimelineItemModel> & timeline,const QString & pasteString,int trackId,int position)1579 bool TimelineFunctions::pasteClips(const std::shared_ptr<TimelineItemModel> &timeline, const QString &pasteString, int trackId, int position)
1580 {
1581     std::function<bool(void)> undo = []() { return true; };
1582     std::function<bool(void)> redo = []() { return true; };
1583     if (TimelineFunctions::pasteClips(timeline, pasteString, trackId, position, undo, redo)) {
1584         pCore->pushUndo(undo, redo, i18n("Paste clips"));
1585         return true;
1586     }
1587     return false;
1588 }
1589 
pasteClips(const std::shared_ptr<TimelineItemModel> & timeline,const QString & pasteString,int trackId,int position,Fun & undo,Fun & redo)1590 bool TimelineFunctions::pasteClips(const std::shared_ptr<TimelineItemModel> &timeline, const QString &pasteString, int trackId, int position, Fun &undo, Fun &redo)
1591 {
1592     timeline->requestClearSelection();
1593     while(!semaphore.tryAcquire(1)) {
1594         qApp->processEvents();
1595     }
1596     waitingBinIds.clear();
1597     QDomDocument copiedItems;
1598     copiedItems.setContent(pasteString);
1599     if (copiedItems.documentElement().tagName() == QLatin1String("kdenlive-scene")) {
1600         qDebug() << " / / READING CLIPS FROM CLIPBOARD";
1601     } else {
1602         semaphore.release(1);
1603         pCore->displayMessage(i18n("No valid data in clipboard"), ErrorMessage, 500);
1604         return false;
1605     }
1606     const QString docId = copiedItems.documentElement().attribute(QStringLiteral("documentid"));
1607     mappedIds.clear();
1608     // Check available tracks
1609     QPair<QList<int>, QList<int>> projectTracks = TimelineFunctions::getAVTracksIds(timeline);
1610     int masterSourceTrack = copiedItems.documentElement().attribute(QStringLiteral("masterTrack"), QStringLiteral("-1")).toInt();
1611     QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip"));
1612     QDomNodeList compositions = copiedItems.documentElement().elementsByTagName(QStringLiteral("composition"));
1613     QDomNodeList subtitles = copiedItems.documentElement().elementsByTagName(QStringLiteral("subtitle"));
1614     // find paste tracks
1615     // List of all source audio tracks
1616     QList<int> audioTracks;
1617     // List of all source video tracks
1618     QList<int> videoTracks;
1619     // List of all audio tracks with their corresponding video mirror
1620     std::unordered_map<int, int> audioMirrors;
1621     // List of all source audio tracks that don't have video mirror
1622     QList<int> singleAudioTracks;
1623     // Number of required video tracks with mirror
1624     int topAudioMirror = 0;
1625     for (int i = 0; i < clips.count(); i++) {
1626         QDomElement prod = clips.at(i).toElement();
1627         int trackPos = prod.attribute(QStringLiteral("track")).toInt();
1628         if (trackPos < 0) {
1629             pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500);
1630             semaphore.release(1);
1631             return false;
1632         }
1633         bool audioTrack = prod.hasAttribute(QStringLiteral("audioTrack"));
1634         if (audioTrack) {
1635             if (!audioTracks.contains(trackPos)) {
1636                 audioTracks << trackPos;
1637             }
1638             int videoMirror = prod.attribute(QStringLiteral("mirrorTrack")).toInt();
1639             if (videoMirror == -1 || masterSourceTrack == -1) {
1640                 if (singleAudioTracks.contains(trackPos)) {
1641                     continue;
1642                 }
1643                 singleAudioTracks << trackPos;
1644                 continue;
1645             }
1646             audioMirrors[trackPos] = videoMirror;
1647             if (videoMirror > topAudioMirror) {
1648                 // We have to check how many video tracks with mirror are needed
1649                 topAudioMirror = videoMirror;
1650             }
1651             if (videoTracks.contains(videoMirror)) {
1652                 continue;
1653             }
1654             videoTracks << videoMirror;
1655         } else {
1656             if (videoTracks.contains(trackPos)) {
1657                 continue;
1658             }
1659             videoTracks << trackPos;
1660         }
1661     }
1662     for (int i = 0; i < compositions.count(); i++) {
1663         QDomElement prod = compositions.at(i).toElement();
1664         int trackPos = prod.attribute(QStringLiteral("track")).toInt();
1665         if (!videoTracks.contains(trackPos)) {
1666             videoTracks << trackPos;
1667         }
1668         int atrackPos = prod.attribute(QStringLiteral("a_track")).toInt();
1669         if (atrackPos == 0 || videoTracks.contains(atrackPos)) {
1670             continue;
1671         }
1672         videoTracks << atrackPos;
1673     }
1674     if (audioTracks.isEmpty() && videoTracks.isEmpty() && subtitles.isEmpty()) {
1675         // playlist does not have any tracks, exit
1676         semaphore.release(1);
1677         return true;
1678     }
1679     // Now we have a list of all source tracks, check that we have enough target tracks
1680     std::sort(videoTracks.begin(), videoTracks.end());
1681     std::sort(audioTracks.begin(), audioTracks.end());
1682     std::sort(singleAudioTracks.begin(), singleAudioTracks.end());
1683     //qDebug()<<"== GOT WANTED TKS\n VIDEO: "<<videoTracks<<"\n AUDIO TKS: "<<audioTracks<<"\n SINGLE AUDIO: "<<singleAudioTracks;
1684     int requestedVideoTracks = videoTracks.isEmpty() ? 0 : videoTracks.last() - videoTracks.first() + 1;
1685     int requestedAudioTracks = audioTracks.isEmpty() ? 0 : audioTracks.last() - audioTracks.first() + 1;
1686     if (requestedVideoTracks > projectTracks.second.size() || requestedAudioTracks > projectTracks.first.size()) {
1687         pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500);
1688         semaphore.release(1);
1689         return false;
1690     }
1691 
1692     // Find destination master track
1693     // Check we have enough tracks above/below
1694     if (requestedVideoTracks > 0) {
1695         qDebug() << "MASTERSTK: " << masterSourceTrack << ", VTKS: " << videoTracks;
1696         int tracksBelow = masterSourceTrack - videoTracks.first();
1697         int tracksAbove = videoTracks.last() - masterSourceTrack;
1698         qDebug() << "// RQST TKS BELOW: " << tracksBelow << " / ABOVE: " << tracksAbove;
1699         qDebug() << "// EXISTING TKS BELOW: " << projectTracks.second.indexOf(trackId) << ", IX: " << trackId;
1700         qDebug() << "// EXISTING TKS ABOVE: " << projectTracks.second.size() << " - " << projectTracks.second.indexOf(trackId);
1701         if (projectTracks.second.indexOf(trackId) < tracksBelow) {
1702             qDebug() << "// UPDATING BELOW TID IX TO: " << tracksBelow;
1703             // not enough tracks below, try to paste on upper track
1704             trackId = projectTracks.second.at(tracksBelow);
1705         } else if ((projectTracks.second.size() - (projectTracks.second.indexOf(trackId) + 1)) < tracksAbove) {
1706             // not enough tracks above, try to paste on lower track
1707             qDebug() << "// UPDATING ABOVE TID IX TO: " << (projectTracks.second.size() - tracksAbove);
1708             trackId = projectTracks.second.at(projectTracks.second.size() - tracksAbove - 1);
1709         }
1710         // Find top-most video track that requires an audio mirror
1711         int topAudioOffset = videoTracks.indexOf(topAudioMirror) - videoTracks.indexOf(masterSourceTrack);
1712         // Check if we have enough video tracks with mirror at paste track position
1713         if (requestedAudioTracks > 0 && projectTracks.first.size() <= (projectTracks.second.indexOf(trackId) + topAudioOffset)) {
1714             int updatedPos = projectTracks.first.size() - topAudioOffset - 1;
1715             if (updatedPos < 0 || updatedPos >= projectTracks.second.size()) {
1716                 pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500);
1717                 semaphore.release(1);
1718                 return false;
1719             }
1720             trackId = projectTracks.second.at(updatedPos);
1721         }
1722     } else if (requestedAudioTracks > 0) {
1723         // Audio only
1724         masterSourceTrack = copiedItems.documentElement().attribute(QStringLiteral("masterAudioTrack")).toInt();
1725         int tracksBelow = masterSourceTrack - audioTracks.first();
1726         int tracksAbove = audioTracks.last() - masterSourceTrack;
1727         if (projectTracks.first.indexOf(trackId) < tracksBelow) {
1728             qDebug() << "// UPDATING BELOW TID IX TO: " << tracksBelow;
1729             // not enough tracks below, try to paste on upper track
1730             trackId = projectTracks.first.at(tracksBelow);
1731         } else if ((projectTracks.first.size() - (projectTracks.first.indexOf(trackId) + 1)) < tracksAbove) {
1732             // not enough tracks above, try to paste on lower track
1733             qDebug() << "// UPDATING ABOVE TID IX TO: " << (projectTracks.first.size() - tracksAbove);
1734             trackId = projectTracks.first.at(projectTracks.first.size() - tracksAbove - 1);
1735         }
1736     }
1737     tracksMap.clear();
1738     bool audioMaster = false;
1739     int masterIx = projectTracks.second.indexOf(trackId);
1740     if (masterIx == -1) {
1741         masterIx = projectTracks.first.indexOf(trackId);
1742         audioMaster = true;
1743     }
1744     for (int tk : qAsConst(videoTracks)) {
1745         int newPos = masterIx + tk - masterSourceTrack;
1746         if (newPos < 0 || newPos >= projectTracks.second.size()) {
1747             pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500);
1748             semaphore.release(1);
1749             return false;
1750         }
1751         tracksMap.insert(tk, projectTracks.second.at(newPos));
1752         //qDebug() << "/// MAPPING SOURCE TRACK: "<<tk<<" TO PROJECT TK: "<<projectTracks.second.at(newPos)<<" = "<<timeline->getTrackMltIndex(projectTracks.second.at(newPos));
1753     }
1754     bool audioOffsetCalculated = false;
1755     int audioOffset = 0;
1756     for (const auto &mirror : audioMirrors) {
1757         int videoIx = tracksMap.value(mirror.second);
1758         int mirrorIx = timeline->getMirrorAudioTrackId(videoIx);
1759         if (mirrorIx > 0) {
1760             tracksMap.insert(mirror.first, mirrorIx);
1761             if (!audioOffsetCalculated) {
1762                 int oldPosition = mirror.first;
1763                 int currentPosition = timeline->getTrackPosition(tracksMap.value(oldPosition));
1764                 audioOffset = currentPosition - oldPosition;
1765                 audioOffsetCalculated = true;
1766             }
1767         }
1768     }
1769     if (!audioOffsetCalculated && audioMaster) {
1770         audioOffset = masterIx - masterSourceTrack;
1771         audioOffsetCalculated = true;
1772     }
1773 
1774     for (int oldPos : qAsConst(singleAudioTracks)) {
1775         if (tracksMap.contains(oldPos)) {
1776             continue;
1777         }
1778         int offsetId = oldPos + audioOffset;
1779         if (offsetId < 0 || offsetId >= projectTracks.first.size()) {
1780             pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500);
1781             semaphore.release(1);
1782             return false;
1783         }
1784         tracksMap.insert(oldPos, projectTracks.first.at(offsetId));
1785     }
1786     std::function<void(const QString &)> callBack = [timeline, copiedItems, position](const QString &binId) {
1787         waitingBinIds.removeAll(binId);
1788         if (waitingBinIds.isEmpty()) {
1789             TimelineFunctions::pasteTimelineClips(timeline, copiedItems, position);
1790         }
1791     };
1792     bool clipsImported = false;
1793 
1794     if (docId == pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))) {
1795         // Check that the bin clips exists in case we try to paste in a copy of original project
1796         QDomNodeList binClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("producer"));
1797         QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Pasted clips"));
1798         for (int i = 0; i < binClips.count(); ++i) {
1799             QDomElement currentProd = binClips.item(i).toElement();
1800             QString clipId = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:id"));
1801             QString clipHash = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:file_hash"));
1802             if (!pCore->projectItemModel()->validateClip(clipId, clipHash)) {
1803                 // This clip is different in project and in paste data, create a copy
1804                 QString updatedId = QString::number(pCore->projectItemModel()->getFreeClipId());
1805                 Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), updatedId);
1806                 mappedIds.insert(clipId, updatedId);
1807                 if (folderId.isEmpty()) {
1808                     // Folder does not exist
1809                     const QString rootId = pCore->projectItemModel()->getRootFolder()->clipId();
1810                     folderId = QString::number(pCore->projectItemModel()->getFreeFolderId());
1811                     pCore->projectItemModel()->requestAddFolder(folderId, i18n("Pasted clips"), rootId, undo, redo);
1812                 }
1813                 waitingBinIds << updatedId;
1814                 clipsImported = true;
1815                 pCore->projectItemModel()->requestAddBinClip(updatedId, currentProd, folderId, undo, redo, callBack);
1816             }
1817         }
1818     }
1819 
1820     if (!docId.isEmpty() && docId != pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))) {
1821         // paste from another document, import bin clips
1822         QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Pasted clips"));
1823         if (folderId.isEmpty()) {
1824             // Folder does not exist
1825             const QString rootId = pCore->projectItemModel()->getRootFolder()->clipId();
1826             folderId = QString::number(pCore->projectItemModel()->getFreeFolderId());
1827             pCore->projectItemModel()->requestAddFolder(folderId, i18n("Pasted clips"), rootId, undo, redo);
1828         }
1829         QDomNodeList binClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("producer"));
1830         for (int i = 0; i < binClips.count(); ++i) {
1831             QDomElement currentProd = binClips.item(i).toElement();
1832             QString clipId = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:id"));
1833             QString clipHash = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:file_hash"));
1834             // Check if we already have a clip with same hash in pasted clips folder
1835             QString existingId = pCore->projectItemModel()->validateClipInFolder(folderId, clipHash);
1836             if (!existingId.isEmpty()) {
1837                 mappedIds.insert(clipId, existingId);
1838                 continue;
1839             }
1840             if (!pCore->projectItemModel()->isIdFree(clipId)) {
1841                 QString updatedId = QString::number(pCore->projectItemModel()->getFreeClipId());
1842                 Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), updatedId);
1843                 mappedIds.insert(clipId, updatedId);
1844                 clipId = updatedId;
1845             }
1846             waitingBinIds << clipId;
1847             clipsImported = true;
1848             bool insert = pCore->projectItemModel()->requestAddBinClip(clipId, currentProd, folderId, undo, redo, callBack);
1849             if (!insert) {
1850                 pCore->displayMessage(i18n("Could not add bin clip"), ErrorMessage, 500);
1851                 undo();
1852                 semaphore.release(1);
1853                 return false;
1854             }
1855         }
1856     }
1857 
1858     if (!clipsImported) {
1859         // Clips from same document, directly proceed to pasting
1860         return TimelineFunctions::pasteTimelineClips(timeline, copiedItems, position, undo, redo, false);
1861     }
1862     qDebug()<<"++++++++++++\nWAITIND FOR BIN INSERTION: "<<waitingBinIds<<"\n\n+++++++++++++";
1863     return true;
1864 }
1865 
pasteTimelineClips(const std::shared_ptr<TimelineItemModel> & timeline,QDomDocument copiedItems,int position)1866 bool TimelineFunctions::pasteTimelineClips(const std::shared_ptr<TimelineItemModel> &timeline, QDomDocument copiedItems, int position)
1867 {
1868     std::function<bool(void)> timeline_undo = []() { return true; };
1869     std::function<bool(void)> timeline_redo = []() { return true; };
1870     return TimelineFunctions::pasteTimelineClips(timeline, copiedItems, position, timeline_undo, timeline_redo, true);
1871 }
1872 
pasteTimelineClips(const std::shared_ptr<TimelineItemModel> & timeline,QDomDocument copiedItems,int position,Fun & timeline_undo,Fun & timeline_redo,bool pushToStack)1873 bool TimelineFunctions::pasteTimelineClips(const std::shared_ptr<TimelineItemModel> &timeline, QDomDocument copiedItems, int position, Fun &timeline_undo, Fun & timeline_redo, bool pushToStack)
1874 {
1875     // Wait until all bin clips are inserted
1876     QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip"));
1877     QDomNodeList compositions = copiedItems.documentElement().elementsByTagName(QStringLiteral("composition"));
1878     QDomNodeList subtitles = copiedItems.documentElement().elementsByTagName(QStringLiteral("subtitle"));
1879     int offset = copiedItems.documentElement().attribute(QStringLiteral("offset")).toInt();
1880     bool res = true;
1881     std::unordered_map<int, int> correspondingIds;
1882     QDomElement documentMixes = copiedItems.createElement(QStringLiteral("mixes"));
1883     for (int i = 0; i < clips.count(); i++) {
1884         QDomElement prod = clips.at(i).toElement();
1885         QString originalId = prod.attribute(QStringLiteral("binid"));
1886         if (mappedIds.contains(originalId)) {
1887             // Map id
1888             originalId = mappedIds.value(originalId);
1889         }
1890         int in = prod.attribute(QStringLiteral("in")).toInt();
1891         int out = prod.attribute(QStringLiteral("out")).toInt();
1892         int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt());
1893         if (!timeline->isTrack(curTrackId)) {
1894             // Something is broken
1895             pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500);
1896             timeline_undo();
1897             semaphore.release(1);
1898             return false;
1899         }
1900         int pos = prod.attribute(QStringLiteral("position")).toInt() - offset;
1901         double speed = prod.attribute(QStringLiteral("speed")).toDouble();
1902         bool warp_pitch = false;
1903         if (!qFuzzyCompare(speed, 1.)) {
1904             warp_pitch = prod.attribute(QStringLiteral("warp_pitch")).toInt();
1905         }
1906         int audioStream = prod.attribute(QStringLiteral("audioStream")).toInt();
1907         int newId;
1908         bool created = timeline->requestClipCreation(originalId, newId, timeline->getTrackById_const(curTrackId)->trackType(), audioStream, speed, warp_pitch, timeline_undo, timeline_redo);
1909         if (!created) {
1910             // Something is broken
1911             pCore->displayMessage(i18n("Could not paste items in timeline"), ErrorMessage, 500);
1912             timeline_undo();
1913             semaphore.release(1);
1914             return false;
1915         }
1916         if (prod.hasAttribute(QStringLiteral("timemap"))) {
1917             // This is a timeremap
1918             timeline->m_allClips[newId]->useTimeRemapProducer(true, timeline_undo, timeline_redo);
1919             if (timeline->m_allClips[newId]->m_producer->parent().type() == mlt_service_chain_type) {
1920                 Mlt::Chain fromChain(timeline->m_allClips[newId]->m_producer->parent());
1921                 int count = fromChain.link_count();
1922                 for (int i = 0; i < count; i++) {
1923                     QScopedPointer<Mlt::Link> fromLink(fromChain.link(i));
1924                     if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) {
1925                         if (fromLink->get("mlt_service") == QLatin1String("timeremap")) {
1926                             // Found a timeremap effect, read params
1927                             fromLink->set("map", prod.attribute(QStringLiteral("timemap")).toUtf8().constData());
1928                             fromLink->set("pitch", prod.attribute(QStringLiteral("timepitch")).toInt());
1929                             fromLink->set("image_mode", prod.attribute(QStringLiteral("timeblend")).toUtf8().constData());
1930                             break;
1931                         }
1932                     }
1933                 }
1934             }
1935 
1936         }
1937         if (timeline->m_allClips[newId]->m_endlessResize) {
1938             out = out - in;
1939             in = 0;
1940             timeline->m_allClips[newId]->m_producer->set("length", out + 1);
1941             timeline->m_allClips[newId]->m_producer->set("out", out);
1942         }
1943         timeline->m_allClips[newId]->setInOut(in, out);
1944         int targetId = prod.attribute(QStringLiteral("id")).toInt();
1945         int targetPlaylist = prod.attribute(QStringLiteral("playlist")).toInt();
1946         if (targetPlaylist > 0) {
1947             timeline->m_allClips[newId]->setSubPlaylistIndex(targetPlaylist, curTrackId);
1948         }
1949         correspondingIds[targetId] = newId;
1950         res = res && timeline->getTrackById(curTrackId)->requestClipInsertion(newId, position + pos, true, true, timeline_undo, timeline_redo);
1951         // paste effects
1952         if (res) {
1953             std::shared_ptr<EffectStackModel> destStack = timeline->getClipEffectStackModel(newId);
1954             destStack->fromXml(prod.firstChildElement(QStringLiteral("effects")), timeline_undo, timeline_redo);
1955         } else {
1956             qDebug()<<"=== COULD NOT PASTE CLIP: "<<newId<<" ON TRACK: "<<curTrackId<<" AT: "<<position;
1957             break;
1958         }
1959         // Mixes (same track transitions)
1960         if (prod.hasChildNodes()) {
1961             QDomNodeList mixes = prod.elementsByTagName(QLatin1String("mix"));
1962             if (!mixes.isEmpty()) {
1963                 QDomElement mix = mixes.at(0).toElement();
1964                 if (mix.tagName() == QLatin1String("mix")) {
1965                     mix.setAttribute(QStringLiteral("tid"), curTrackId);
1966                     documentMixes.appendChild(mix);
1967                 }
1968             }
1969         }
1970     }
1971     // Process mix insertion
1972     QDomNodeList mixes = documentMixes.childNodes();
1973     for (int k = 0; k < mixes.count(); k++) {
1974         QDomElement mix = mixes.at(k).toElement();
1975         int originalFirstClipId = mix.attribute(QLatin1String("firstClip")).toInt();
1976         int originalSecondClipId = mix.attribute(QLatin1String("secondClip")).toInt();
1977         if (correspondingIds.count(originalFirstClipId) > 0 && correspondingIds.count(originalSecondClipId) > 0) {
1978             QVector<QPair<QString, QVariant>> params;
1979             QDomNodeList paramsXml = mix.elementsByTagName(QLatin1String("param"));
1980             for (int j = 0; j < paramsXml.count(); j++) {
1981                 QDomElement e = paramsXml.at(j).toElement();
1982                 params.append({e.attribute(QLatin1String("name")), e.text()});
1983             }
1984             std::pair<QString,QVector<QPair<QString, QVariant>>> mixParams = {mix.attribute(QLatin1String("asset")),params};
1985             MixInfo mixData;
1986             mixData.firstClipId = correspondingIds[originalFirstClipId];
1987             mixData.secondClipId = correspondingIds[originalSecondClipId];
1988             mixData.firstClipInOut.second = mix.attribute(QLatin1String("mixEnd")).toInt();
1989             mixData.secondClipInOut.first = mix.attribute(QLatin1String("mixStart")).toInt();
1990             mixData.mixOffset =  mix.attribute(QLatin1String("mixOffset")).toInt();
1991             timeline->getTrackById_const(mix.attribute(QLatin1String("tid")).toInt())->createMix(mixData, mixParams, true);
1992         }
1993     }
1994     // Compositions
1995     if (res) {
1996         for (int i = 0; res && i < compositions.count(); i++) {
1997             QDomElement prod = compositions.at(i).toElement();
1998             QString originalId = prod.attribute(QStringLiteral("composition"));
1999             int in = prod.attribute(QStringLiteral("in")).toInt();
2000             int out = prod.attribute(QStringLiteral("out")).toInt();
2001             int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt());
2002             int aTrackId = prod.attribute(QStringLiteral("a_track")).toInt();
2003             if (tracksMap.contains(aTrackId)) {
2004                 aTrackId = timeline->getTrackPosition(tracksMap.value(aTrackId));
2005             } else {
2006                 aTrackId = 0;
2007             }
2008             int pos = prod.attribute(QStringLiteral("position")).toInt() - offset;
2009             int newId;
2010             auto transProps = std::make_unique<Mlt::Properties>();
2011             QDomNodeList props = prod.elementsByTagName(QStringLiteral("property"));
2012             for (int j = 0; j < props.count(); j++) {
2013                 transProps->set(props.at(j).toElement().attribute(QStringLiteral("name")).toUtf8().constData(),
2014                             props.at(j).toElement().text().toUtf8().constData());
2015             }
2016             res = res && timeline->requestCompositionInsertion(originalId, curTrackId, aTrackId, position + pos, out - in + 1, std::move(transProps), newId, timeline_undo, timeline_redo);
2017         }
2018     }
2019     if (res && !subtitles.isEmpty()) {
2020         auto subModel = pCore->getSubtitleModel(true);
2021         for (int i = 0; res && i < subtitles.count(); i++) {
2022             QDomElement prod = subtitles.at(i).toElement();
2023             int in = prod.attribute(QStringLiteral("in")).toInt() - offset;
2024             int out = prod.attribute(QStringLiteral("out")).toInt() - offset;
2025             QString text = prod.attribute(QStringLiteral("text"));
2026             res = res && subModel->addSubtitle(GenTime(position + in, pCore->getCurrentFps()), GenTime(position + out, pCore->getCurrentFps()), text, timeline_undo, timeline_redo);
2027         }
2028     }
2029     if (!res) {
2030         timeline_undo();
2031         pCore->displayMessage(i18n("Could not paste items in timeline"), ErrorMessage, 500);
2032         semaphore.release(1);
2033         return false;
2034     }
2035     // Rebuild groups
2036     const QString groupsData = copiedItems.documentElement().firstChildElement(QStringLiteral("groups")).text();
2037     if (!groupsData.isEmpty()) {
2038         timeline->m_groups->fromJsonWithOffset(groupsData, tracksMap, position - offset, timeline_undo, timeline_redo);
2039     }
2040     // Ensure to clear selection in undo/redo too.
2041     Fun unselect = [timeline]() {
2042         qDebug() << "starting undo or redo. Selection " << timeline->m_currentSelection;
2043         timeline->requestClearSelection();
2044         qDebug() << "after Selection " << timeline->m_currentSelection;
2045         return true;
2046     };
2047     PUSH_FRONT_LAMBDA(unselect, timeline_undo);
2048     PUSH_FRONT_LAMBDA(unselect, timeline_redo);
2049     //UPDATE_UNDO_REDO_NOLOCK(timeline_redo, timeline_undo, undo, redo);
2050     if (pushToStack) {
2051         pCore->pushUndo(timeline_undo, timeline_redo, i18n("Paste timeline clips"));
2052     }
2053     semaphore.release(1);
2054     return true;
2055 }
2056 
requestDeleteBlankAt(const std::shared_ptr<TimelineItemModel> & timeline,int trackId,int position,bool affectAllTracks)2057 bool TimelineFunctions::requestDeleteBlankAt(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position, bool affectAllTracks)
2058 {
2059     // find blank duration
2060     int spaceStart = 0;
2061     if (affectAllTracks) {
2062         int lastFrame = 0;
2063         for (const auto &track: timeline->m_allTracks) {
2064             if (!track->isLocked()) {
2065                 if (!track->isBlankAt(position)) {
2066                     return false;
2067                 }
2068                 lastFrame = track->getBlankStart(position);
2069                 if (lastFrame > spaceStart) {
2070                     spaceStart = lastFrame;
2071                 }
2072             }
2073         }
2074         // check subtitle track
2075         if (timeline->getSubtitleModel() && !timeline->getSubtitleModel()->isLocked()) {
2076             lastFrame = timeline->getSubtitleModel()->getBlankStart(position);
2077             if (lastFrame > spaceStart) {
2078                 spaceStart = lastFrame;
2079             }
2080         }
2081     } else {
2082         if (trackId == -2) {
2083             // Subtitle track
2084             if (!timeline->getSubtitleModel()->isBlankAt(position)) {
2085                 return false;
2086             }
2087             spaceStart = timeline->getSubtitleModel()->getBlankStart(position);
2088         } else {
2089             if (!timeline->getTrackById_const(trackId)->isBlankAt(position)) {
2090                 return false;
2091             }
2092             spaceStart = timeline->getTrackById_const(trackId)->getBlankStart(position);
2093         }
2094     }
2095     if (spaceStart > position) {
2096         return false;
2097     }
2098     int cid = requestSpacerStartOperation(timeline, affectAllTracks ? -1 : trackId, position);
2099     if (cid == -1) {
2100         return false;
2101     }
2102     int start = timeline->getItemPosition(cid);
2103     // Start undoable command
2104     std::function<bool(void)> undo = []() { return true; };
2105     std::function<bool(void)> redo = []() { return true; };
2106     requestSpacerEndOperation(timeline, cid, start, spaceStart, affectAllTracks ? -1 : trackId, !KdenliveSettings::lockedGuides(), undo, redo);
2107     return true;
2108 }
2109 
extractClip(const std::shared_ptr<TimelineItemModel> & timeline,int cid,const QString & binId)2110 QDomDocument TimelineFunctions::extractClip(const std::shared_ptr<TimelineItemModel> &timeline, int cid, const QString &binId)
2111 {
2112     int tid = timeline->getClipTrackId(cid);
2113     int pos = timeline->getClipPosition(cid);
2114     std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(binId);
2115     const QString url = clip->clipUrl();
2116     QFile f(url);
2117     QDomDocument sourceDoc;
2118     sourceDoc.setContent(&f, false);
2119     f.close();
2120     QDomDocument destDoc;
2121     QDomElement container = destDoc.createElement(QStringLiteral("kdenlive-scene"));
2122     destDoc.appendChild(container);
2123     QDomElement bin = destDoc.createElement(QStringLiteral("bin"));
2124     container.appendChild(bin);
2125     bool isAudio = timeline->isAudioTrack(tid);
2126     container.setAttribute(QStringLiteral("offset"), pos);
2127     container.setAttribute(QStringLiteral("documentid"), QStringLiteral("000000"));
2128     // Process producers
2129     QList <int> processedProducers;
2130     QMap <QString, int> producerMap;
2131     QMap <QString, double> producerSpeed;
2132     QMap <QString, int> producerSpeedResource;
2133     QDomNodeList producers = sourceDoc.elementsByTagName(QLatin1String("producer"));
2134     for (int i = 0; i < producers.count(); ++i) {
2135         QDomElement currentProd = producers.item(i).toElement();
2136         bool ok;
2137         int clipId = Xml::getXmlProperty(currentProd, QLatin1String("kdenlive:id")).toInt(&ok);
2138         if (!ok) {
2139             const QString resource = Xml::getXmlProperty(currentProd, QLatin1String("resource"));
2140             qDebug()<<"===== CLIP NOT FOUND: "<<resource;
2141             if (producerSpeedResource.contains(resource)) {
2142                 clipId = producerSpeedResource.value(resource);
2143                 qDebug()<<"===== GOT PREVIOUS ID: "<<clipId;
2144                 QString baseProducerId;
2145                 int baseProducerClipId = 0;
2146                 QMapIterator<QString, int>m(producerMap);
2147                 while (m.hasNext()) {
2148                     m.next();
2149                     if (m.value() == clipId) {
2150                         baseProducerId = m.key();
2151                         baseProducerClipId = m.value();
2152                         qDebug()<<"=== FOUND PRODUCER FOR ID: "<<m.key();
2153                         break;
2154                     }
2155                 }
2156                 if (!baseProducerId.isEmpty()) {
2157                     producerSpeed.insert(currentProd.attribute(QLatin1String("id")), producerSpeed.value(baseProducerId));
2158                     producerMap.insert(currentProd.attribute(QLatin1String("id")), baseProducerClipId);
2159                     qDebug()<<"/// INSERTING PRODUCERMAP: "<<currentProd.attribute(QLatin1String("id"))<<"="<<baseProducerClipId;
2160                 }
2161                 // Producer already processed;
2162                 continue;
2163             } else {
2164                 clipId = pCore->projectItemModel()->getFreeClipId();
2165             }
2166             Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), QString::number(clipId));
2167             qDebug()<<"=== UNKNOWN CLIP FOUND: "<<Xml::getXmlProperty(currentProd, QLatin1String("resource"));
2168         }
2169         producerMap.insert(currentProd.attribute(QLatin1String("id")), clipId);
2170         qDebug()<<"/// INSERTING SOURCE PRODUCERMAP: "<<currentProd.attribute(QLatin1String("id"))<<"="<<clipId;
2171         QString mltService = Xml::getXmlProperty(currentProd, QStringLiteral("mlt_service"));
2172         if (mltService == QLatin1String("timewarp")) {
2173             // Speed producer
2174             double speed = Xml::getXmlProperty(currentProd, QStringLiteral("warp_speed")).toDouble();
2175             Xml::setXmlProperty(currentProd, QStringLiteral("mlt_service"), QStringLiteral("avformat"));
2176             producerSpeedResource.insert(Xml::getXmlProperty(currentProd, QLatin1String("resource")), clipId);
2177             qDebug()<<"===== CLIP SPEED RESOURCE: "<<Xml::getXmlProperty(currentProd, QLatin1String("resource"))<<" = "<<clipId;
2178             QString resource = Xml::getXmlProperty(currentProd, QStringLiteral("warp_resource"));
2179             Xml::setXmlProperty(currentProd, QStringLiteral("resource"), resource);
2180             producerSpeed.insert(currentProd.attribute(QLatin1String("id")), speed);
2181         }
2182         if (processedProducers.contains(clipId)) {
2183             // Producer already processed
2184             continue;
2185         }
2186         processedProducers << clipId;
2187         // This could be a timeline track producer, reset custom audio/video setting
2188         Xml::removeXmlProperty(currentProd, QLatin1String("set.test_audio"));
2189         Xml::removeXmlProperty(currentProd, QLatin1String("set.test_image"));
2190         bin.appendChild(destDoc.importNode(currentProd, true));
2191     }
2192     // Check for audio tracks
2193     QMap <QString, bool> tracksType;
2194     int audioTracks = 0;
2195     int videoTracks = 0;
2196     QDomNodeList tracks = sourceDoc.elementsByTagName(QLatin1String("track"));
2197     for (int i = 0; i < tracks.count(); ++i) {
2198         QDomElement currentTrack = tracks.item(i).toElement();
2199         if (currentTrack.attribute(QLatin1String("hide")) == QLatin1String("video")) {
2200             // Audio track
2201             tracksType.insert(currentTrack.attribute(QLatin1String("producer")), true);
2202             audioTracks++;
2203         } else {
2204             // Video track
2205             tracksType.insert(currentTrack.attribute(QLatin1String("producer")), false);
2206             videoTracks++;
2207         }
2208     }
2209     int track = 1;
2210     if (isAudio) {
2211         container.setAttribute(QStringLiteral("masterAudioTrack"), 0);
2212     } else {
2213         track = audioTracks;
2214         container.setAttribute(QStringLiteral("masterTrack"), track);
2215     }
2216     // Process playlists
2217     QDomNodeList playlists = sourceDoc.elementsByTagName(QLatin1String("playlist"));
2218     for (int i = 0; i < playlists.count(); ++i) {
2219         QDomElement currentPlay = playlists.item(i).toElement();
2220         int position = 0;
2221         bool audioTrack = tracksType.value(currentPlay.attribute("id"));
2222         if (audioTrack != isAudio) {
2223             continue;
2224         }
2225         QDomNodeList elements = currentPlay.childNodes();
2226         for (int j = 0; j < elements.count(); ++j) {
2227             QDomElement currentElement = elements.item(j).toElement();
2228             if (currentElement.tagName() == QLatin1String("blank")) {
2229                 position += currentElement.attribute(QLatin1String("length")).toInt();
2230                 continue;
2231             }
2232             if (currentElement.tagName() == QLatin1String("entry")) {
2233                 QDomElement clipElement = destDoc.createElement(QStringLiteral("clip"));
2234                 container.appendChild(clipElement);
2235                 int in = currentElement.attribute(QLatin1String("in")).toInt();
2236                 int out = currentElement.attribute(QLatin1String("out")).toInt();
2237                 const QString originalProducer = currentElement.attribute(QLatin1String("producer"));
2238                 clipElement.setAttribute(QLatin1String("binid"), producerMap.value(originalProducer));
2239                 clipElement.setAttribute(QLatin1String("in"), in);
2240                 clipElement.setAttribute(QLatin1String("out"), out);
2241                 clipElement.setAttribute(QLatin1String("position"), position + pos);
2242                 clipElement.setAttribute(QLatin1String("track"), track);
2243                 //clipElement.setAttribute(QStringLiteral("state"), (int)m_currentState);
2244                 clipElement.setAttribute(QStringLiteral("state"), audioTrack ? 2 : 1);
2245                 if (audioTrack) {
2246                     clipElement.setAttribute(QLatin1String("audioTrack"), 1);
2247                     int mirror = audioTrack + videoTracks - track - 1;
2248                     if (track <= videoTracks) {
2249                         clipElement.setAttribute(QLatin1String("mirrorTrack"), mirror);
2250                     } else {
2251                         clipElement.setAttribute(QLatin1String("mirrorTrack"), -1);
2252                     }
2253                 }
2254                 if (producerSpeed.contains(originalProducer)) {
2255                     clipElement.setAttribute(QStringLiteral("speed"), producerSpeed.value(originalProducer));
2256                 } else {
2257                     clipElement.setAttribute(QStringLiteral("speed"), 1);
2258                 }
2259                 position += (out - in + 1);
2260                 QDomNodeList effects = currentElement.elementsByTagName(QLatin1String("filter"));
2261                 if (effects.count() == 0) {
2262                     continue;
2263                 }
2264                 QDomElement effectsList = destDoc.createElement(QStringLiteral("effects"));
2265                 clipElement.appendChild(effectsList);
2266                 effectsList.setAttribute(QStringLiteral("parentIn"), in);
2267                 for (int k = 0; k < effects.count(); ++k) {
2268                     QDomElement effect = effects.item(k).toElement();
2269                     QString filterId = Xml::getXmlProperty(effect, QLatin1String("kdenlive_id"));
2270                     QDomElement clipEffect = destDoc.createElement(QStringLiteral("effect"));
2271                     effectsList.appendChild(clipEffect);
2272                     clipEffect.setAttribute(QStringLiteral("id"), filterId);
2273                     QDomNodeList properties = effect.childNodes();
2274                     if (effect.hasAttribute(QStringLiteral("in"))) {
2275                         clipEffect.setAttribute(QStringLiteral("in"), effect.attribute(QStringLiteral("in")));
2276                     }
2277                     if (effect.hasAttribute(QStringLiteral("out"))) {
2278                         clipEffect.setAttribute(QStringLiteral("out"), effect.attribute(QStringLiteral("out")));
2279                     }
2280                     for (int l = 0; l < properties.count(); ++l) {
2281                         QDomElement prop = properties.item(l).toElement();
2282                         const QString propName = prop.attribute(QLatin1String("name"));
2283                         if (propName == QLatin1String("mlt_service") || propName == QLatin1String("kdenlive_id")) {
2284                             continue;
2285                         }
2286                         Xml::setXmlProperty(clipEffect, propName, prop.text());
2287                     }
2288                 }
2289             }
2290         }
2291         track++;
2292     }
2293     track = audioTracks;
2294     if (!isAudio) {
2295         // Compositions
2296         QDomNodeList compositions = sourceDoc.elementsByTagName(QLatin1String("transition"));
2297         for (int i = 0; i < compositions.count(); ++i) {
2298             QDomElement currentCompo = compositions.item(i).toElement();
2299             if (Xml::getXmlProperty(currentCompo, QLatin1String("internal_added")).toInt() > 0) {
2300                 // Track compositing, discard
2301                 continue;
2302             }
2303             QDomElement composition = destDoc.createElement(QStringLiteral("composition"));
2304             container.appendChild(composition);
2305             int in = currentCompo.attribute(QLatin1String("in")).toInt();
2306             int out = currentCompo.attribute(QLatin1String("out")).toInt();
2307             const QString compoId = Xml::getXmlProperty(currentCompo, QLatin1String("kdenlive_id"));
2308             composition.setAttribute(QLatin1String("position"), in + pos);
2309             composition.setAttribute(QLatin1String("in"), in);
2310             composition.setAttribute(QLatin1String("out"), out);
2311             composition.setAttribute(QLatin1String("composition"), compoId);
2312             composition.setAttribute(QLatin1String("a_track"), Xml::getXmlProperty(currentCompo, QLatin1String("a_track")).toInt());
2313             composition.setAttribute(QLatin1String("track"), Xml::getXmlProperty(currentCompo, QLatin1String("b_track")).toInt());
2314             QDomNodeList properties = currentCompo.childNodes();
2315             for (int l = 0; l < properties.count(); ++l) {
2316                 QDomElement prop = properties.item(l).toElement();
2317                 const QString &propName = prop.attribute(QLatin1String("name"));
2318                 Xml::setXmlProperty(composition, propName, prop.text());
2319             }
2320         }
2321     }
2322     qDebug()<<"=== GOT CONVERTED DOCUMENT\n\n"<<destDoc.toString();
2323     return destDoc;
2324 }
2325 
spacerMinPos()2326 int TimelineFunctions::spacerMinPos()
2327 {
2328     return spacerMinPosition;
2329 }
2330