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