File indexing completed on 2024-05-12 08:54:37
0001 /* 0002 SPDX-FileCopyrightText: 2017 Jean-Baptiste Mardelle <jb@kdenlive.org> 0003 This file is part of Kdenlive. See www.kdenlive.org. 0004 0005 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0006 */ 0007 0008 #include "timelinefunctions.hpp" 0009 #include "bin/bin.h" 0010 #include "bin/model/markerlistmodel.hpp" 0011 #include "bin/model/subtitlemodel.hpp" 0012 #include "bin/projectclip.h" 0013 #include "bin/projectfolder.h" 0014 #include "bin/projectitemmodel.h" 0015 #include "clipmodel.hpp" 0016 #include "compositionmodel.hpp" 0017 #include "core.h" 0018 #include "doc/kdenlivedoc.h" 0019 #include "effects/effectstack/model/effectstackmodel.hpp" 0020 #include "groupsmodel.hpp" 0021 #include "mainwindow.h" 0022 #include "monitor/monitor.h" 0023 #include "project/projectmanager.h" 0024 #include "timelineitemmodel.hpp" 0025 #include "trackmodel.hpp" 0026 #include "transitions/transitionsrepository.hpp" 0027 0028 #include "utils/KMessageBox_KdenliveCompat.h" 0029 #include <KIO/RenameDialog> 0030 #include <KLocalizedString> 0031 #include <KMessageBox> 0032 #include <QApplication> 0033 #include <QDebug> 0034 #include <QInputDialog> 0035 #include <QSemaphore> 0036 #include <unordered_map> 0037 0038 #ifdef CRASH_AUTO_TEST 0039 #include "logger.hpp" 0040 #pragma GCC diagnostic push 0041 #pragma GCC diagnostic ignored "-Wunused-parameter" 0042 #pragma GCC diagnostic ignored "-Wsign-conversion" 0043 #pragma GCC diagnostic ignored "-Wfloat-equal" 0044 #pragma GCC diagnostic ignored "-Wshadow" 0045 #pragma GCC diagnostic ignored "-Wpedantic" 0046 #include <rttr/registration> 0047 #pragma GCC diagnostic pop 0048 0049 RTTR_REGISTRATION 0050 { 0051 using namespace rttr; 0052 registration::class_<TimelineFunctions>("TimelineFunctions") 0053 .method("requestClipCut", select_overload<bool(std::shared_ptr<TimelineItemModel>, int, int)>(&TimelineFunctions::requestClipCut))( 0054 parameter_names("timeline", "clipId", "position")) 0055 .method("requestDeleteBlankAt", select_overload<bool(const std::shared_ptr<TimelineItemModel> &, int, int, bool)>( 0056 &TimelineFunctions::requestDeleteBlankAt))(parameter_names("timeline", "trackId", "position", "affectAllTracks")); 0057 } 0058 #else 0059 #define TRACE_STATIC(...) 0060 #define TRACE_RES(...) 0061 #endif 0062 0063 QStringList waitingBinIds; 0064 QMap<QString, QString> mappedIds; 0065 QMap<int, int> tracksMap; 0066 QMap<int, int> spacerUngroupedItems; 0067 int spacerMinPosition; 0068 QSemaphore semaphore(1); 0069 0070 bool TimelineFunctions::cloneClip(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, 0071 Fun &redo) 0072 { 0073 // Special case: slowmotion clips 0074 double clipSpeed = timeline->m_allClips[clipId]->getSpeed(); 0075 bool warp_pitch = timeline->m_allClips[clipId]->getIntProperty(QStringLiteral("warp_pitch")); 0076 int audioStream = timeline->m_allClips[clipId]->getIntProperty(QStringLiteral("audio_index")); 0077 bool res = timeline->requestClipCreation(timeline->getClipBinId(clipId), newId, state, audioStream, clipSpeed, warp_pitch, undo, redo); 0078 timeline->m_allClips[newId]->m_endlessResize = timeline->m_allClips[clipId]->m_endlessResize; 0079 0080 // copy useful timeline properties 0081 timeline->m_allClips[clipId]->passTimelineProperties(timeline->m_allClips[newId]); 0082 0083 int duration = timeline->getClipPlaytime(clipId); 0084 int init_duration = timeline->getClipPlaytime(newId); 0085 if (duration != init_duration) { 0086 init_duration -= timeline->m_allClips[clipId]->getIn(); 0087 res = res && timeline->requestItemResize(newId, init_duration, false, true, undo, redo); 0088 res = res && timeline->requestItemResize(newId, duration, true, true, undo, redo); 0089 } 0090 if (!res) { 0091 return false; 0092 } 0093 std::shared_ptr<EffectStackModel> sourceStack = timeline->getClipEffectStackModel(clipId); 0094 std::shared_ptr<EffectStackModel> destStack = timeline->getClipEffectStackModel(newId); 0095 destStack->importEffects(sourceStack, state); 0096 return res; 0097 } 0098 0099 bool TimelineFunctions::requestMultipleClipsInsertion(const std::shared_ptr<TimelineItemModel> &timeline, const QStringList &binIds, int trackId, int position, 0100 QList<int> &clipIds, bool logUndo, bool refreshView) 0101 { 0102 std::function<bool(void)> undo = []() { return true; }; 0103 std::function<bool(void)> redo = []() { return true; }; 0104 for (const QString &binId : binIds) { 0105 int clipId; 0106 if (timeline->requestClipInsertion(binId, trackId, position, clipId, logUndo, refreshView, false, undo, redo)) { 0107 clipIds.append(clipId); 0108 position += timeline->getItemPlaytime(clipId); 0109 } else { 0110 undo(); 0111 clipIds.clear(); 0112 return false; 0113 } 0114 } 0115 0116 if (logUndo) { 0117 pCore->pushUndo(undo, redo, i18n("Insert Clips")); 0118 } 0119 0120 return true; 0121 } 0122 0123 bool TimelineFunctions::processClipCut(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo) 0124 { 0125 bool isSubtitle = timeline->isSubTitle(clipId); 0126 int trackId = isSubtitle ? -1 : timeline->getClipTrackId(clipId); 0127 int trackDuration = isSubtitle ? -1 : timeline->getTrackById_const(trackId)->trackDuration(); 0128 int start = timeline->getItemPosition(clipId); 0129 int duration = timeline->getItemPlaytime(clipId); 0130 if (start > position || (start + duration) < position) { 0131 return false; 0132 } 0133 if (isSubtitle) { 0134 newId = timeline->cutSubtitle(position, undo, redo); 0135 return newId > -1; 0136 } 0137 bool hasEndMix = timeline->getTrackById_const(trackId)->hasEndMix(clipId); 0138 bool hasStartMix = timeline->getTrackById_const(trackId)->hasStartMix(clipId); 0139 int subplaylist = timeline->m_allClips[clipId]->getSubPlaylistIndex(); 0140 PlaylistState::ClipState state = timeline->m_allClips[clipId]->clipState(); 0141 // Check if clip has an end Mix 0142 bool res = cloneClip(timeline, clipId, newId, state, undo, redo); 0143 timeline->m_blockRefresh = true; 0144 0145 int updatedDuration = position - start; 0146 // Resize original clip 0147 res = timeline->m_allClips[clipId]->requestResize(updatedDuration, true, undo, redo, true, hasEndMix || hasStartMix); 0148 0149 if (hasEndMix) { 0150 // Assing end mix to new clone clip 0151 Fun local_redo = [timeline, trackId, clipId, newId]() { return timeline->getTrackById_const(trackId)->reAssignEndMix(clipId, newId); }; 0152 local_redo(); 0153 PUSH_LAMBDA(local_redo, redo); 0154 // Reassing end mix to original clip on undo 0155 Fun local_undo = [timeline, trackId, clipId, newId]() { 0156 timeline->getTrackById_const(trackId)->reAssignEndMix(newId, clipId); 0157 return true; 0158 }; 0159 PUSH_LAMBDA(local_undo, undo); 0160 // Assing end mix to new clone clip 0161 if (!hasStartMix && subplaylist != 1) { 0162 Fun local_redo2 = [timeline, trackId, clipId, start]() { 0163 // If the clip has no start mix, move to playlist 1 0164 return timeline->getTrackById_const(trackId)->switchPlaylist(clipId, start, 0, 1); 0165 }; 0166 // Restore initial subplaylist on undo 0167 Fun local_undo2 = [timeline, trackId, clipId, start]() { 0168 // If the clip has no start mix, move back to playlist 0 0169 return timeline->getTrackById_const(trackId)->switchPlaylist(clipId, start, 1, 0); 0170 }; 0171 res = res && local_redo2(); 0172 if (res) { 0173 UPDATE_UNDO_REDO_NOLOCK(local_redo2, local_undo2, undo, redo); 0174 } 0175 } 0176 } 0177 int newDuration = timeline->getClipPlaytime(clipId); 0178 // parse effects 0179 if (res) { 0180 std::shared_ptr<EffectStackModel> sourceStack = timeline->getClipEffectStackModel(clipId); 0181 sourceStack->cleanFadeEffects(true, undo, redo); 0182 std::shared_ptr<EffectStackModel> destStack = timeline->getClipEffectStackModel(newId); 0183 destStack->cleanFadeEffects(false, undo, redo); 0184 } 0185 updatedDuration = duration - newDuration; 0186 res = res && timeline->requestItemResize(newId, updatedDuration, false, true, undo, redo); 0187 // The next requestclipmove does not check for duration change since we don't invalidate timeline, so check duration change now 0188 bool durationChanged = trackDuration != timeline->getTrackById_const(trackId)->trackDuration(); 0189 if (hasEndMix) { 0190 timeline->m_allClips[newId]->setSubPlaylistIndex(subplaylist, trackId); 0191 } 0192 res = res && timeline->requestClipMove(newId, trackId, position, true, true, false, true, undo, redo); 0193 0194 if (durationChanged) { 0195 // Track length changed, check project duration 0196 Fun updateDuration = [timeline]() { 0197 timeline->updateDuration(); 0198 return true; 0199 }; 0200 updateDuration(); 0201 PUSH_LAMBDA(updateDuration, redo); 0202 } 0203 timeline->m_blockRefresh = false; 0204 return res; 0205 } 0206 0207 bool TimelineFunctions::requestClipCut(std::shared_ptr<TimelineItemModel> timeline, int clipId, int position) 0208 { 0209 std::function<bool(void)> undo = []() { return true; }; 0210 std::function<bool(void)> redo = []() { return true; }; 0211 TRACE_STATIC(timeline, clipId, position); 0212 bool result = TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo); 0213 if (result) { 0214 pCore->pushUndo(undo, redo, i18n("Cut clip")); 0215 } 0216 TRACE_RES(result); 0217 return result; 0218 } 0219 0220 bool TimelineFunctions::requestClipCut(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int position, Fun &undo, Fun &redo) 0221 { 0222 const std::unordered_set<int> clipselect = timeline->getGroupElements(clipId); 0223 // Remove locked items 0224 std::unordered_set<int> clips; 0225 for (int cid : clipselect) { 0226 if (timeline->isSubTitle(cid)) { 0227 clips.insert(cid); 0228 continue; 0229 } 0230 if (!timeline->isClip(cid)) { 0231 continue; 0232 } 0233 int tk = timeline->getClipTrackId(cid); 0234 if (tk != -1 && !timeline->getTrackById_const(tk)->isLocked()) { 0235 clips.insert(cid); 0236 } 0237 } 0238 // Shall we reselect after the split 0239 int trackToSelect = -1; 0240 if (timeline->isClip(clipId) && timeline->m_allClips[clipId]->selected) { 0241 int mainIn = timeline->getItemPosition(clipId); 0242 int mainOut = mainIn + timeline->getItemPlaytime(clipId); 0243 if (position > mainIn && position < mainOut) { 0244 trackToSelect = timeline->getItemTrackId(clipId); 0245 } 0246 } 0247 0248 std::unordered_set<int> topElements; 0249 std::transform(clips.begin(), clips.end(), std::inserter(topElements, topElements.begin()), [&](int id) { return timeline->m_groups->getRootId(id); }); 0250 0251 int count = 0; 0252 QList<int> newIds; 0253 QList<int> clipsToCut; 0254 bool subtitleItemSelected = false; 0255 for (int cid : clips) { 0256 if (!timeline->isClip(cid) && !timeline->isSubTitle(cid)) { 0257 continue; 0258 } 0259 int start = timeline->getItemPosition(cid); 0260 int duration = timeline->getItemPlaytime(cid); 0261 if (start < position && (start + duration) > position) { 0262 clipsToCut << cid; 0263 if (timeline->isSubTitle(cid)) { 0264 if (subtitleItemSelected) { 0265 // We cannot cut 2 overlapping subtitles at the same position 0266 pCore->displayMessage(i18n("Failed to cut clip"), ErrorMessage, 500); 0267 bool undone = undo(); 0268 Q_ASSERT(undone); 0269 return false; 0270 } 0271 subtitleItemSelected = true; 0272 } 0273 } 0274 } 0275 if (clipsToCut.isEmpty()) { 0276 return true; 0277 } 0278 0279 // We need to call clearSelection before attempting the split or the group split will be corrupted by the selection group (no undo support) 0280 timeline->requestClearSelection(); 0281 0282 for (int cid : qAsConst(clipsToCut)) { 0283 count++; 0284 int newId = -1; 0285 bool res = processClipCut(timeline, cid, position, newId, undo, redo); 0286 if (!res) { 0287 bool undone = undo(); 0288 Q_ASSERT(undone); 0289 return false; 0290 } 0291 // splitted elements go temporarily in the same group as original ones. 0292 timeline->m_groups->setInGroupOf(newId, cid, undo, redo); 0293 newIds << newId; 0294 } 0295 if (count > 0 && timeline->m_groups->isInGroup(clipId)) { 0296 // we now split the group hierarchy. 0297 // As a splitting criterion, we compare start point with split position 0298 auto criterion = [timeline, position](int cid) { return timeline->getItemPosition(cid) < position; }; 0299 bool res = true; 0300 for (const int topId : topElements) { 0301 qDebug() << "// CHECKING REGROUP ELEMENT: " << topId << ", ISCLIP: " << timeline->isClip(topId) << timeline->isGroup(topId); 0302 res = res && timeline->m_groups->split(topId, criterion, undo, redo); 0303 } 0304 if (!res) { 0305 bool undone = undo(); 0306 Q_ASSERT(undone); 0307 return false; 0308 } 0309 } 0310 if (count > 0 && trackToSelect > -1) { 0311 int newClip = timeline->getClipByPosition(trackToSelect, position); 0312 if (newClip > -1) { 0313 timeline->requestSetSelection({newClip}); 0314 } 0315 } 0316 return count > 0; 0317 } 0318 0319 bool TimelineFunctions::requestClipCutAll(std::shared_ptr<TimelineItemModel> timeline, int position) 0320 { 0321 QVector<std::shared_ptr<TrackModel>> affectedTracks; 0322 std::function<bool(void)> undo = []() { return true; }; 0323 std::function<bool(void)> redo = []() { return true; }; 0324 0325 for (const auto &track : timeline->m_allTracks) { 0326 if (track->shouldReceiveTimelineOp()) { 0327 affectedTracks << track; 0328 } 0329 } 0330 0331 unsigned count = 0; 0332 auto subModel = timeline->getSubtitleModel(); 0333 if (subModel && !subModel->isLocked()) { 0334 int clipId = timeline->getClipByPosition(-2, position); 0335 if (clipId > -1) { 0336 // Found subtitle clip at position in track, cut it. Update undo/redo as we go. 0337 if (!TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo)) { 0338 qWarning() << "Failed to cut clip " << clipId << " at " << position; 0339 pCore->displayMessage(i18n("Failed to cut clip"), ErrorMessage, 500); 0340 // Undo all cuts made, assert successful undo. 0341 bool undone = undo(); 0342 Q_ASSERT(undone); 0343 return false; 0344 } 0345 count++; 0346 } 0347 } 0348 if (affectedTracks.isEmpty() && count == 0) { 0349 pCore->displayMessage(i18n("All tracks are locked"), ErrorMessage, 500); 0350 return false; 0351 } 0352 for (auto track : qAsConst(affectedTracks)) { 0353 int clipId = track->getClipByPosition(position); 0354 if (clipId > -1) { 0355 // Found clip at position in track, cut it. Update undo/redo as we go. 0356 if (!TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo)) { 0357 qWarning() << "Failed to cut clip " << clipId << " at " << position; 0358 pCore->displayMessage(i18n("Failed to cut clip"), ErrorMessage, 500); 0359 // Undo all cuts made, assert successful undo. 0360 bool undone = undo(); 0361 Q_ASSERT(undone); 0362 return false; 0363 } 0364 count++; 0365 } 0366 } 0367 0368 if (!count) { 0369 pCore->displayMessage(i18n("No clips to cut"), ErrorMessage); 0370 } else { 0371 pCore->pushUndo(undo, redo, i18n("Cut all clips")); 0372 } 0373 0374 return count > 0; 0375 } 0376 0377 std::pair<int, int> TimelineFunctions::requestSpacerStartOperation(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position, 0378 bool ignoreMultiTrackGroups, bool allowGroupBreaking) 0379 { 0380 if (trackId != -1 && timeline->trackIsLocked(trackId)) { 0381 timeline->flashLock(trackId); 0382 return {-1, -1}; 0383 } 0384 std::unordered_set<int> clips = timeline->getItemsInRange(trackId, position, -1); 0385 timeline->requestClearSelection(); 0386 // Find the first clip on each track to calculate the minimum space operation 0387 QMap<int, int> firstClipOnTrack; 0388 // Find the maximum space allowed by grouped clips placed before the operation start {trackid,blank_duration} 0389 QMap<int, int> relatedMaxSpace; 0390 spacerMinPosition = -1; 0391 if (!clips.empty()) { 0392 // Remove grouped items that are before the click position 0393 // First get top groups ids 0394 std::unordered_set<int> roots; 0395 spacerUngroupedItems.clear(); 0396 std::transform(clips.begin(), clips.end(), std::inserter(roots, roots.begin()), [&](int id) { return timeline->m_groups->getRootId(id); }); 0397 std::unordered_set<int> groupsToRemove; 0398 int firstCid = -1; 0399 int spaceDuration = -1; 0400 std::unordered_set<int> toSelect; 0401 // List all clips involved in the spacer operation 0402 std::unordered_set<int> allClips; 0403 for (int r : roots) { 0404 std::unordered_set<int> children = timeline->m_groups->getLeaves(r); 0405 allClips.insert(children.begin(), children.end()); 0406 } 0407 for (int r : roots) { 0408 if (timeline->isGroup(r)) { 0409 std::unordered_set<int> leaves = timeline->m_groups->getLeaves(r); 0410 std::unordered_set<int> leavesToRemove; 0411 std::unordered_set<int> leavesToKeep; 0412 for (int l : leaves) { 0413 int pos = timeline->getItemPosition(l); 0414 bool outOfRange = timeline->getItemEnd(l) < position; 0415 int tid = timeline->getItemTrackId(l); 0416 bool unaffectedTrack = ignoreMultiTrackGroups && trackId > -1 && tid != trackId; 0417 if (allowGroupBreaking) { 0418 if (outOfRange || unaffectedTrack) { 0419 leavesToRemove.insert(l); 0420 } else { 0421 leavesToKeep.insert(l); 0422 } 0423 } else if (outOfRange) { 0424 // This is a grouped clip positionned before the spacer operation position, check maximum space before 0425 std::unordered_set<int> beforeOnTrack = timeline->getItemsInRange(tid, 0, pos - 1); 0426 for (auto &c : allClips) { 0427 beforeOnTrack.erase(c); 0428 } 0429 int lastPos = 0; 0430 for (int c : beforeOnTrack) { 0431 int p = timeline->getClipEnd(c); 0432 if (p >= pos - 1) { 0433 lastPos = pos; 0434 break; 0435 } 0436 if (p > lastPos) { 0437 lastPos = p; 0438 } 0439 } 0440 if (relatedMaxSpace.contains(trackId)) { 0441 if (relatedMaxSpace.value(trackId) > (pos - lastPos)) { 0442 relatedMaxSpace.insert(trackId, pos - lastPos); 0443 } 0444 } else { 0445 relatedMaxSpace.insert(trackId, pos - lastPos); 0446 } 0447 } 0448 if (!outOfRange && !unaffectedTrack) { 0449 // Find first item 0450 if (!firstClipOnTrack.contains(tid)) { 0451 firstClipOnTrack.insert(tid, l); 0452 } else if (timeline->getItemPosition(firstClipOnTrack.value(tid)) > pos) { 0453 firstClipOnTrack.insert(tid, l); 0454 } 0455 } 0456 } 0457 for (int l : leavesToRemove) { 0458 int checkedParent = timeline->m_groups->getDirectAncestor(l); 0459 if (checkedParent < 0) { 0460 checkedParent = l; 0461 } 0462 spacerUngroupedItems.insert(l, checkedParent); 0463 } 0464 if (leavesToKeep.size() == 1) { 0465 toSelect.insert(*leavesToKeep.begin()); 0466 groupsToRemove.insert(r); 0467 } 0468 } else { 0469 // Find first clip on track 0470 int pos = timeline->getItemPosition(r); 0471 int tid = timeline->getItemTrackId(r); 0472 if (!firstClipOnTrack.contains(tid)) { 0473 firstClipOnTrack.insert(tid, r); 0474 } else if (timeline->getItemPosition(firstClipOnTrack.value(tid)) > pos) { 0475 firstClipOnTrack.insert(tid, r); 0476 } 0477 } 0478 } 0479 toSelect.insert(roots.begin(), roots.end()); 0480 for (int r : groupsToRemove) { 0481 toSelect.erase(r); 0482 } 0483 0484 Fun undo = []() { return true; }; 0485 Fun redo = []() { return true; }; 0486 QMapIterator<int, int> i(spacerUngroupedItems); 0487 while (i.hasNext()) { 0488 i.next(); 0489 timeline->m_groups->removeFromGroup(i.key()); 0490 } 0491 0492 timeline->requestSetSelection(toSelect); 0493 0494 QMapIterator<int, int> it(firstClipOnTrack); 0495 int firstPos = -1; 0496 if (firstClipOnTrack.isEmpty() && firstCid > -1) { 0497 int clipPos = timeline->getItemPosition(firstCid); 0498 spaceDuration = timeline->getTrackById_const(timeline->getItemTrackId(firstCid))->getBlankSizeAtPos(clipPos - 1); 0499 } 0500 while (it.hasNext()) { 0501 it.next(); 0502 int clipPos = timeline->getItemPosition(it.value()); 0503 if (trackId > -1) { 0504 if (it.key() == trackId) { 0505 firstCid = it.value(); 0506 } 0507 } else { 0508 if (firstPos == -1) { 0509 firstCid = it.value(); 0510 firstPos = clipPos; 0511 } else if (firstPos < clipPos) { 0512 firstCid = it.value(); 0513 } 0514 } 0515 if (timeline->isSubtitleTrack(it.key())) { 0516 if (timeline->getSubtitleModel()->isBlankAt(clipPos - 1)) { 0517 if (spaceDuration == -1) { 0518 spaceDuration = timeline->getSubtitleModel()->getBlankSizeAtPos(clipPos - 1); 0519 } else { 0520 int blank = timeline->getSubtitleModel()->getBlankSizeAtPos(clipPos - 1); 0521 spaceDuration = qMin(blank, spaceDuration); 0522 } 0523 } 0524 } else { 0525 if (timeline->getTrackById_const(it.key())->isBlankAt(clipPos - 1)) { 0526 if (spaceDuration == -1) { 0527 spaceDuration = timeline->getTrackById_const(it.key())->getBlankSizeAtPos(clipPos - 1); 0528 } else { 0529 int blank = timeline->getTrackById_const(it.key())->getBlankSizeAtPos(clipPos - 1); 0530 spaceDuration = qMin(blank, spaceDuration); 0531 } 0532 } 0533 } 0534 if (relatedMaxSpace.contains(it.key())) { 0535 spaceDuration = qMin(spaceDuration, relatedMaxSpace.value(it.key())); 0536 } 0537 } 0538 spacerMinPosition = timeline->getItemPosition(firstCid) - spaceDuration; 0539 return {firstCid, spaceDuration}; 0540 } 0541 return {-1, -1}; 0542 } 0543 0544 bool TimelineFunctions::requestSpacerEndOperation(const std::shared_ptr<TimelineItemModel> &timeline, int itemId, int startPosition, int endPosition, 0545 int affectedTrack, int moveGuidesPosition, Fun &undo, Fun &redo, bool pushUndo) 0546 { 0547 // Move guides if needed 0548 if (moveGuidesPosition > -1) { 0549 moveGuidesPosition = qMin(moveGuidesPosition, startPosition); 0550 GenTime fromPos(moveGuidesPosition, pCore->getCurrentFps()); 0551 GenTime toPos(endPosition - startPosition, pCore->getCurrentFps()); 0552 QList<CommentedTime> guides = timeline->getGuideModel()->getMarkersInRange(moveGuidesPosition, -1); 0553 if (!guides.isEmpty()) { 0554 timeline->getGuideModel()->moveMarkers(guides, fromPos, fromPos + toPos, undo, redo); 0555 } 0556 } 0557 0558 // Move group back to original position 0559 spacerMinPosition = -1; 0560 int track = timeline->getItemTrackId(itemId); 0561 bool isClip = timeline->isClip(itemId); 0562 if (isClip) { 0563 timeline->requestClipMove(itemId, track, startPosition, true, false, false, false, true); 0564 } else if (timeline->isComposition(itemId)) { 0565 timeline->requestCompositionMove(itemId, track, startPosition, false, false); 0566 } else { 0567 timeline->requestSubtitleMove(itemId, startPosition, false, false); 0568 } 0569 0570 std::unordered_set<int> clips = timeline->getGroupElements(itemId); 0571 int mainGroup = timeline->m_groups->getRootId(itemId); 0572 bool final = false; 0573 bool liftOk = true; 0574 if (timeline->m_editMode == TimelineMode::OverwriteEdit && endPosition < startPosition) { 0575 // Remove zone between end and start pos 0576 if (affectedTrack == -1) { 0577 // touch all tracks 0578 auto it = timeline->m_allTracks.cbegin(); 0579 while (it != timeline->m_allTracks.cend()) { 0580 int target_track = (*it)->getId(); 0581 if (!timeline->getTrackById_const(target_track)->isLocked()) { 0582 liftOk = liftOk && TimelineFunctions::liftZone(timeline, target_track, QPoint(endPosition, startPosition), undo, redo); 0583 } 0584 ++it; 0585 } 0586 } else if (timeline->isTrack(affectedTrack)) { 0587 liftOk = TimelineFunctions::liftZone(timeline, affectedTrack, QPoint(endPosition, startPosition), undo, redo); 0588 } 0589 // The lift operation destroys selection group, so regroup now 0590 if (clips.size() > 1) { 0591 timeline->requestSetSelection(clips); 0592 mainGroup = timeline->m_groups->getRootId(itemId); 0593 } 0594 } 0595 if (liftOk && (mainGroup > -1 || clips.size() == 1)) { 0596 if (clips.size() > 1) { 0597 final = timeline->requestGroupMove(itemId, mainGroup, 0, endPosition - startPosition, true, true, undo, redo); 0598 } else { 0599 // only 1 clip to be moved 0600 if (isClip) { 0601 final = timeline->requestClipMove(itemId, track, endPosition, true, true, true, true, undo, redo); 0602 } else if (timeline->isComposition(itemId)) { 0603 final = timeline->requestCompositionMove(itemId, track, -1, endPosition, true, true, undo, redo); 0604 } else { 0605 final = timeline->requestSubtitleMove(itemId, endPosition, true, true, true, true, undo, redo); 0606 } 0607 } 0608 } 0609 timeline->requestClearSelection(); 0610 if (final) { 0611 if (pushUndo) { 0612 if (startPosition < endPosition) { 0613 pCore->pushUndo(undo, redo, i18n("Insert space")); 0614 } else { 0615 pCore->pushUndo(undo, redo, i18n("Remove space")); 0616 } 0617 } 0618 // Regroup temporarily ungrouped items 0619 QMapIterator<int, int> i(spacerUngroupedItems); 0620 Fun local_undo = []() { return true; }; 0621 Fun local_redo = []() { return true; }; 0622 std::unordered_set<int> newlyGrouped; 0623 while (i.hasNext()) { 0624 i.next(); 0625 if (timeline->isItem(i.value())) { 0626 if (newlyGrouped.count(i.value()) > 0) { 0627 Q_ASSERT(timeline->m_groups->isInGroup(i.value())); 0628 timeline->m_groups->setInGroupOf(i.key(), i.value(), local_undo, local_redo); 0629 } else { 0630 std::unordered_set<int> items = {i.key(), i.value()}; 0631 timeline->m_groups->groupItems(items, local_undo, local_redo); 0632 newlyGrouped.insert(i.value()); 0633 } 0634 } else { 0635 // i.value() is either a group (detectable via timeline->isGroup) or an empty group 0636 if (timeline->isGroup(i.key())) { 0637 std::unordered_set<int> items = {i.key(), i.value()}; 0638 timeline->m_groups->groupItems(items, local_undo, local_redo); 0639 } else { 0640 timeline->m_groups->setGroup(i.key(), i.value()); 0641 } 0642 } 0643 } 0644 spacerUngroupedItems.clear(); 0645 return true; 0646 } else { 0647 undo(); 0648 } 0649 return false; 0650 } 0651 0652 bool TimelineFunctions::breakAffectedGroups(const std::shared_ptr<TimelineItemModel> &timeline, const QVector<int> &tracks, QPoint zone, Fun &undo, Fun &redo) 0653 { 0654 // Check if we have grouped clips that are on unaffected tracks, and ungroup them 0655 bool result = true; 0656 std::unordered_set<int> affectedItems; 0657 // First find all affected items 0658 for (auto trackId : tracks) { 0659 std::unordered_set<int> items = timeline->getItemsInRange(trackId, zone.x(), zone.y()); 0660 affectedItems.insert(items.begin(), items.end()); 0661 } 0662 for (int item : affectedItems) { 0663 if (timeline->m_groups->isInGroup(item)) { 0664 int groupId = timeline->m_groups->getRootId(item); 0665 std::unordered_set<int> all_children = timeline->m_groups->getLeaves(groupId); 0666 for (int child : all_children) { 0667 int childTrackId = timeline->getItemTrackId(child); 0668 if (!tracks.contains(childTrackId) && timeline->m_groups->isInGroup(child)) { 0669 // This item should not be affected by the operation, ungroup it 0670 result = result && timeline->requestClipUngroup(child, undo, redo); 0671 } 0672 } 0673 } 0674 } 0675 return result; 0676 } 0677 0678 bool TimelineFunctions::extractZone(const std::shared_ptr<TimelineItemModel> &timeline, const QVector<int> &tracks, QPoint zone, bool liftOnly, 0679 int clipToUnGroup, std::unordered_set<int> clipsToRegroup) 0680 { 0681 std::function<bool(void)> undo = []() { return true; }; 0682 std::function<bool(void)> redo = []() { return true; }; 0683 bool res = extractZoneWithUndo(timeline, tracks, zone, liftOnly, clipToUnGroup, clipsToRegroup, undo, redo); 0684 pCore->pushUndo(undo, redo, liftOnly ? i18n("Lift zone") : i18n("Extract zone")); 0685 return res; 0686 } 0687 0688 bool TimelineFunctions::extractZoneWithUndo(const std::shared_ptr<TimelineItemModel> &timeline, const QVector<int> &tracks, QPoint zone, bool liftOnly, 0689 int clipToUnGroup, std::unordered_set<int> clipsToRegroup, Fun &undo, Fun &redo) 0690 { 0691 // Start undoable command 0692 bool result = true; 0693 if (clipToUnGroup > -1) { 0694 result = timeline->requestClipUngroup(clipToUnGroup, undo, redo); 0695 } 0696 result = breakAffectedGroups(timeline, tracks, zone, undo, redo); 0697 for (auto trackId : tracks) { 0698 if (timeline->getTrackById_const(trackId)->isLocked()) { 0699 continue; 0700 } 0701 result = result && TimelineFunctions::liftZone(timeline, trackId, zone, undo, redo); 0702 } 0703 if (result && !liftOnly) { 0704 result = TimelineFunctions::removeSpace(timeline, zone, undo, redo, tracks); 0705 } 0706 if (clipsToRegroup.size() > 1) { 0707 result = timeline->requestClipsGroup(clipsToRegroup, undo, redo); 0708 } 0709 return result; 0710 } 0711 0712 bool TimelineFunctions::insertZone(const std::shared_ptr<TimelineItemModel> &timeline, const QList<int> &trackIds, const QString &binId, int insertFrame, 0713 QPoint zone, bool overwrite, bool useTargets) 0714 { 0715 std::function<bool(void)> undo = []() { return true; }; 0716 std::function<bool(void)> redo = []() { return true; }; 0717 bool res = TimelineFunctions::insertZone(timeline, trackIds, binId, insertFrame, zone, overwrite, useTargets, undo, redo); 0718 if (res) { 0719 pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone")); 0720 } else { 0721 pCore->displayMessage(i18n("Could not insert zone"), ErrorMessage); 0722 undo(); 0723 } 0724 return res; 0725 } 0726 0727 bool TimelineFunctions::insertZone(const std::shared_ptr<TimelineItemModel> &timeline, QList<int> trackIds, const QString &binId, int insertFrame, QPoint zone, 0728 bool overwrite, bool useTargets, Fun &undo, Fun &redo) 0729 { 0730 // Start undoable command 0731 bool result = true; 0732 QVector<int> affectedTracks; 0733 auto it = timeline->m_allTracks.cbegin(); 0734 if (!useTargets) { 0735 // Timeline drop in overwrite mode 0736 for (int target_track : trackIds) { 0737 if (!timeline->getTrackById_const(target_track)->isLocked()) { 0738 affectedTracks << target_track; 0739 } 0740 } 0741 } else { 0742 while (it != timeline->m_allTracks.cend()) { 0743 int target_track = (*it)->getId(); 0744 if (timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { 0745 affectedTracks << target_track; 0746 } else if (trackIds.contains(target_track)) { 0747 // Track is marked as target but not active, remove it 0748 trackIds.removeAll(target_track); 0749 } 0750 ++it; 0751 } 0752 } 0753 if (affectedTracks.isEmpty()) { 0754 pCore->displayMessage(i18n("Please activate a track by clicking on a track's label"), ErrorMessage); 0755 return false; 0756 } 0757 result = breakAffectedGroups(timeline, affectedTracks, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); 0758 if (overwrite) { 0759 // Cut all tracks 0760 for (int target_track : qAsConst(affectedTracks)) { 0761 result = result && TimelineFunctions::liftZone(timeline, target_track, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); 0762 if (!result) { 0763 qDebug() << "// LIFTING ZONE FAILED\n"; 0764 break; 0765 } 0766 } 0767 } else { 0768 // Cut all tracks 0769 for (int target_track : qAsConst(affectedTracks)) { 0770 int startClipId = timeline->getClipByPosition(target_track, insertFrame); 0771 if (startClipId > -1) { 0772 // There is a clip, cut it 0773 result = result && TimelineFunctions::requestClipCut(timeline, startClipId, insertFrame, undo, redo); 0774 } 0775 } 0776 result = 0777 result && TimelineFunctions::requestInsertSpace(timeline, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo, affectedTracks); 0778 } 0779 if (result) { 0780 if (!trackIds.isEmpty()) { 0781 int newId = -1; 0782 QString binClipId; 0783 if (binId.contains(QLatin1Char('/'))) { 0784 binClipId = QString("%1/%2/%3").arg(binId.section(QLatin1Char('/'), 0, 0)).arg(zone.x()).arg(zone.y() - 1); 0785 } else { 0786 binClipId = QString("%1/%2/%3").arg(binId).arg(zone.x()).arg(zone.y() - 1); 0787 } 0788 result = timeline->requestClipInsertion(binClipId, trackIds.first(), insertFrame, newId, true, true, useTargets, undo, redo, affectedTracks); 0789 } 0790 } 0791 return result; 0792 } 0793 0794 bool TimelineFunctions::liftZone(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) 0795 { 0796 // Check if there is a clip at start point 0797 int startClipId = timeline->getClipByPosition(trackId, zone.x()); 0798 if (startClipId > -1) { 0799 // There is a clip, cut it 0800 if (timeline->getClipPosition(startClipId) < zone.x()) { 0801 // Check if we have a mix 0802 std::pair<MixInfo, MixInfo> mixData = timeline->getTrackById_const(trackId)->getMixInfo(startClipId); 0803 bool abortCut = false; 0804 if (mixData.first.firstClipId > -1) { 0805 // Clip has a start mix 0806 if (mixData.first.secondClipInOut.first + (mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first) - 0807 mixData.first.mixOffset >= 0808 zone.x()) { 0809 // Cut pos is in the mix zone before clip cut, completely remove clip 0810 abortCut = true; 0811 } 0812 } 0813 if (!abortCut) { 0814 TimelineFunctions::requestClipCut(timeline, startClipId, zone.x(), undo, redo); 0815 } else { 0816 // Remove the clip now, so that the mix is deleted before checking items in range 0817 timeline->requestClipUngroup(startClipId, undo, redo); 0818 timeline->requestItemDeletion(startClipId, undo, redo); 0819 } 0820 } 0821 } 0822 int endClipId = timeline->getClipByPosition(trackId, zone.y()); 0823 if (endClipId > -1) { 0824 // There is a clip, cut it 0825 if (timeline->getClipPosition(endClipId) + timeline->getClipPlaytime(endClipId) > zone.y()) { 0826 // Check if we have a mix 0827 std::pair<MixInfo, MixInfo> mixData = timeline->getTrackById_const(trackId)->getMixInfo(endClipId); 0828 bool abortCut = false; 0829 if (mixData.second.firstClipId > -1) { 0830 // Clip has an end mix 0831 if (mixData.second.firstClipInOut.second - (mixData.second.firstClipInOut.second - mixData.second.secondClipInOut.first) - 0832 mixData.first.mixOffset <= 0833 zone.y()) { 0834 // Cut pos is in the mix zone after clip cut, completely remove clip 0835 abortCut = true; 0836 } 0837 } 0838 if (!abortCut) { 0839 TimelineFunctions::requestClipCut(timeline, endClipId, zone.y(), undo, redo); 0840 } else { 0841 // Remove the clip now, so that the mix is deleted before checking items in range 0842 timeline->requestClipUngroup(endClipId, undo, redo); 0843 timeline->requestItemDeletion(endClipId, undo, redo); 0844 } 0845 } 0846 } 0847 std::unordered_set<int> clips = timeline->getItemsInRange(trackId, zone.x(), zone.y()); 0848 for (const auto &clipId : clips) { 0849 timeline->requestClipUngroup(clipId, undo, redo); 0850 timeline->requestItemDeletion(clipId, undo, redo); 0851 } 0852 return true; 0853 } 0854 0855 bool TimelineFunctions::removeSpace(const std::shared_ptr<TimelineItemModel> &timeline, QPoint zone, Fun &undo, Fun &redo, const QVector<int> &allowedTracks, 0856 bool useTargets) 0857 { 0858 std::unordered_set<int> clips; 0859 if (useTargets) { 0860 auto it = timeline->m_allTracks.cbegin(); 0861 while (it != timeline->m_allTracks.cend()) { 0862 int target_track = (*it)->getId(); 0863 if (timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { 0864 std::unordered_set<int> subs = timeline->getItemsInRange(target_track, zone.y() - 1, -1, true); 0865 clips.insert(subs.begin(), subs.end()); 0866 } 0867 ++it; 0868 } 0869 } else { 0870 for (auto tid : allowedTracks) { 0871 std::unordered_set<int> subs = timeline->getItemsInRange(tid, zone.y() - 1, -1, true); 0872 clips.insert(subs.begin(), subs.end()); 0873 } 0874 } 0875 if (clips.size() == 0) { 0876 // TODO: inform user no change will be performed 0877 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 0878 return true; 0879 } 0880 bool result = false; 0881 timeline->requestSetSelection(clips); 0882 int itemId = *clips.begin(); 0883 int targetTrackId = timeline->getItemTrackId(itemId); 0884 int targetPos = timeline->getItemPosition(itemId) + zone.x() - zone.y(); 0885 0886 if (timeline->m_groups->isInGroup(itemId)) { 0887 result = timeline->requestGroupMove(itemId, timeline->m_groups->getRootId(itemId), 0, zone.x() - zone.y(), true, true, undo, redo, true, true, true, 0888 allowedTracks); 0889 } else if (timeline->isClip(itemId)) { 0890 result = timeline->requestClipMove(itemId, targetTrackId, targetPos, true, true, true, true, undo, redo); 0891 } else { 0892 result = 0893 timeline->requestCompositionMove(itemId, targetTrackId, timeline->m_allCompositions[itemId]->getForcedTrack(), targetPos, true, true, undo, redo); 0894 } 0895 timeline->requestClearSelection(); 0896 if (!result) { 0897 undo(); 0898 } 0899 return result; 0900 } 0901 0902 bool TimelineFunctions::requestInsertSpace(const std::shared_ptr<TimelineItemModel> &timeline, QPoint zone, Fun &undo, Fun &redo, 0903 const QVector<int> &allowedTracks) 0904 { 0905 timeline->requestClearSelection(); 0906 Fun local_undo = []() { return true; }; 0907 Fun local_redo = []() { return true; }; 0908 std::unordered_set<int> items; 0909 if (allowedTracks.isEmpty()) { 0910 // Select clips in all tracks 0911 items = timeline->getItemsInRange(-1, zone.x(), -1, true); 0912 } else { 0913 // Select clips in target and active tracks only 0914 for (int target_track : allowedTracks) { 0915 std::unordered_set<int> subs = timeline->getItemsInRange(target_track, zone.x(), -1, true); 0916 items.insert(subs.begin(), subs.end()); 0917 } 0918 } 0919 if (items.empty()) { 0920 return true; 0921 } 0922 timeline->requestSetSelection(items); 0923 bool result = true; 0924 int itemId = *(items.begin()); 0925 int targetTrackId = timeline->getItemTrackId(itemId); 0926 int targetPos = timeline->getItemPosition(itemId) + zone.y() - zone.x(); 0927 0928 // TODO the three move functions should be unified in a "requestItemMove" function 0929 if (timeline->m_groups->isInGroup(itemId)) { 0930 result = result && timeline->requestGroupMove(itemId, timeline->m_groups->getRootId(itemId), 0, zone.y() - zone.x(), true, true, local_undo, local_redo, 0931 true, true, true, allowedTracks); 0932 } else if (timeline->isClip(itemId)) { 0933 result = result && timeline->requestClipMove(itemId, targetTrackId, targetPos, true, true, true, true, local_undo, local_redo); 0934 } else { 0935 result = result && timeline->requestCompositionMove(itemId, targetTrackId, timeline->m_allCompositions[itemId]->getForcedTrack(), targetPos, true, true, 0936 local_undo, local_redo); 0937 } 0938 timeline->requestClearSelection(); 0939 if (!result) { 0940 bool undone = local_undo(); 0941 Q_ASSERT(undone); 0942 pCore->displayMessage(i18n("Cannot move selected group"), ErrorMessage); 0943 } 0944 UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); 0945 return result; 0946 } 0947 0948 bool TimelineFunctions::requestItemCopy(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int trackId, int position) 0949 { 0950 Q_ASSERT(timeline->isClip(clipId) || timeline->isComposition(clipId)); 0951 Fun undo = []() { return true; }; 0952 Fun redo = []() { return true; }; 0953 int deltaTrack = timeline->getTrackPosition(trackId) - timeline->getTrackPosition(timeline->getItemTrackId(clipId)); 0954 int deltaPos = position - timeline->getItemPosition(clipId); 0955 std::unordered_set<int> allIds = timeline->getGroupElements(clipId); 0956 std::unordered_map<int, int> mapping; // keys are ids of the source clips, values are ids of the copied clips 0957 bool res = true; 0958 for (int id : allIds) { 0959 int newId = -1; 0960 if (timeline->isClip(id)) { 0961 PlaylistState::ClipState state = timeline->m_allClips[id]->clipState(); 0962 res = cloneClip(timeline, id, newId, state, undo, redo); 0963 res = res && (newId != -1); 0964 } 0965 int target_position = timeline->getItemPosition(id) + deltaPos; 0966 int target_track_position = timeline->getTrackPosition(timeline->getItemTrackId(id)) + deltaTrack; 0967 if (target_track_position >= 0 && target_track_position < timeline->getTracksCount()) { 0968 auto it = timeline->m_allTracks.cbegin(); 0969 std::advance(it, target_track_position); 0970 int target_track = (*it)->getId(); 0971 if (timeline->isClip(id)) { 0972 res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, true, true, undo, redo); 0973 } else { 0974 const QString &transitionId = timeline->m_allCompositions[id]->getAssetId(); 0975 std::unique_ptr<Mlt::Properties> transProps(timeline->m_allCompositions[id]->properties()); 0976 res = res && timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position, 0977 timeline->m_allCompositions[id]->getPlaytime(), std::move(transProps), newId, undo, redo); 0978 } 0979 } else { 0980 res = false; 0981 } 0982 if (!res) { 0983 bool undone = undo(); 0984 Q_ASSERT(undone); 0985 return false; 0986 } 0987 mapping[id] = newId; 0988 } 0989 qDebug() << "Successful copy, copying groups..."; 0990 res = timeline->m_groups->copyGroups(mapping, undo, redo); 0991 if (!res) { 0992 bool undone = undo(); 0993 Q_ASSERT(undone); 0994 return false; 0995 } 0996 return true; 0997 } 0998 0999 void TimelineFunctions::showClipKeyframes(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, bool value) 1000 { 1001 timeline->m_allClips[clipId]->setShowKeyframes(value); 1002 QModelIndex modelIndex = timeline->makeClipIndexFromID(clipId); 1003 Q_EMIT timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole}); 1004 } 1005 1006 void TimelineFunctions::showCompositionKeyframes(const std::shared_ptr<TimelineItemModel> &timeline, int compoId, bool value) 1007 { 1008 timeline->m_allCompositions[compoId]->setShowKeyframes(value); 1009 QModelIndex modelIndex = timeline->makeCompositionIndexFromID(compoId); 1010 Q_EMIT timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole}); 1011 } 1012 1013 bool TimelineFunctions::switchEnableState(const std::shared_ptr<TimelineItemModel> &timeline, std::unordered_set<int> selection) 1014 { 1015 Fun undo = []() { return true; }; 1016 Fun redo = []() { return true; }; 1017 bool result = false; 1018 bool disable = true; 1019 for (int clipId : selection) { 1020 if (!timeline->isClip(clipId)) { 1021 continue; 1022 } 1023 PlaylistState::ClipState oldState = timeline->getClipPtr(clipId)->clipState(); 1024 PlaylistState::ClipState state = PlaylistState::Disabled; 1025 disable = true; 1026 if (oldState == PlaylistState::Disabled) { 1027 state = timeline->getTrackById_const(timeline->getClipTrackId(clipId))->trackType(); 1028 disable = false; 1029 } 1030 result = changeClipState(timeline, clipId, state, undo, redo); 1031 if (!result) { 1032 break; 1033 } 1034 } 1035 // Update action name since clip will be switched 1036 int id = *selection.begin(); 1037 Fun local_redo = []() { return true; }; 1038 Fun local_undo = []() { return true; }; 1039 if (timeline->isClip(id)) { 1040 bool disabled = timeline->m_allClips[id]->clipState() == PlaylistState::Disabled; 1041 QAction *action = pCore->window()->actionCollection()->action(QStringLiteral("clip_switch")); 1042 local_redo = [disabled, action]() { 1043 action->setText(disabled ? i18n("Enable clip") : i18n("Disable clip")); 1044 return true; 1045 }; 1046 local_undo = [disabled, action]() { 1047 action->setText(disabled ? i18n("Disable clip") : i18n("Enable clip")); 1048 return true; 1049 }; 1050 } 1051 if (result) { 1052 local_redo(); 1053 UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); 1054 pCore->pushUndo(undo, redo, disable ? i18n("Disable clip") : i18n("Enable clip")); 1055 } 1056 return result; 1057 } 1058 1059 bool TimelineFunctions::changeClipState(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo) 1060 { 1061 int track = timeline->getClipTrackId(clipId); 1062 int start = -1; 1063 bool invalidate = false; 1064 if (track > -1) { 1065 if (!timeline->getTrackById_const(track)->isAudioTrack()) { 1066 invalidate = true; 1067 } 1068 start = timeline->getItemPosition(clipId); 1069 } 1070 Fun local_undo = []() { return true; }; 1071 Fun local_redo = []() { return true; }; 1072 // For the state change to work, we need to unplant/replant the clip 1073 bool result = true; 1074 if (track > -1) { 1075 result = timeline->getTrackById(track)->requestClipDeletion(clipId, true, invalidate, local_undo, local_redo, false, false); 1076 } 1077 result = timeline->m_allClips[clipId]->setClipState(status, local_undo, local_redo); 1078 if (result && track > -1) { 1079 result = timeline->getTrackById(track)->requestClipInsertion(clipId, start, true, true, local_undo, local_redo, false, false); 1080 } 1081 UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); 1082 return result; 1083 } 1084 1085 bool TimelineFunctions::requestSplitAudio(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int audioTarget) 1086 { 1087 std::function<bool(void)> undo = []() { return true; }; 1088 std::function<bool(void)> redo = []() { return true; }; 1089 const std::unordered_set<int> clips = timeline->getGroupElements(clipId); 1090 bool done = false; 1091 // Now clear selection so we don't mess with groups 1092 timeline->requestClearSelection(false, undo, redo); 1093 for (int cid : clips) { 1094 if (!timeline->getClipPtr(cid)->canBeAudio() || timeline->getClipPtr(cid)->clipState() == PlaylistState::AudioOnly) { 1095 // clip without audio or audio only, skip 1096 pCore->displayMessage(i18n("One or more clips do not have audio, or are already audio"), ErrorMessage); 1097 return false; 1098 } 1099 int position = timeline->getClipPosition(cid); 1100 int track = timeline->getClipTrackId(cid); 1101 QList<int> possibleTracks; 1102 // Try inserting in target track first, then mirror track 1103 if (audioTarget >= 0) { 1104 possibleTracks = {audioTarget}; 1105 } 1106 int mirror = timeline->getMirrorAudioTrackId(track); 1107 if (mirror > -1) { 1108 possibleTracks << mirror; 1109 } 1110 if (possibleTracks.isEmpty()) { 1111 // No available audio track for splitting, abort 1112 undo(); 1113 pCore->displayMessage(i18n("No available audio track for restore operation"), ErrorMessage); 1114 return false; 1115 } 1116 int newId; 1117 bool res = cloneClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo); 1118 if (!res) { 1119 bool undone = undo(); 1120 Q_ASSERT(undone); 1121 pCore->displayMessage(i18n("Audio restore failed"), ErrorMessage); 1122 return false; 1123 } 1124 bool success = false; 1125 while (!success && !possibleTracks.isEmpty()) { 1126 int newTrack = possibleTracks.takeFirst(); 1127 success = timeline->requestClipMove(newId, newTrack, position, true, true, false, true, undo, redo); 1128 } 1129 TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo); 1130 success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set<int>{newId}, GroupType::AVSplit, undo, redo); 1131 if (!success) { 1132 bool undone = undo(); 1133 Q_ASSERT(undone); 1134 pCore->displayMessage(i18n("Audio restore failed"), ErrorMessage); 1135 return false; 1136 } 1137 done = true; 1138 } 1139 if (done) { 1140 timeline->requestSetSelection(clips, undo, redo); 1141 pCore->pushUndo(undo, redo, i18n("Restore Audio")); 1142 } 1143 return done; 1144 } 1145 1146 bool TimelineFunctions::requestSplitVideo(const std::shared_ptr<TimelineItemModel> &timeline, int clipId, int videoTarget) 1147 { 1148 std::function<bool(void)> undo = []() { return true; }; 1149 std::function<bool(void)> redo = []() { return true; }; 1150 const std::unordered_set<int> clips = timeline->getGroupElements(clipId); 1151 bool done = false; 1152 // Now clear selection so we don't mess with groups 1153 timeline->requestClearSelection(); 1154 for (int cid : clips) { 1155 if (!timeline->getClipPtr(cid)->canBeVideo() || timeline->getClipPtr(cid)->clipState() == PlaylistState::VideoOnly) { 1156 // clip without audio or audio only, skip 1157 continue; 1158 } 1159 int position = timeline->getClipPosition(cid); 1160 int track = timeline->getClipTrackId(cid); 1161 QList<int> possibleTracks; 1162 // Try inserting in target track first, then mirror track 1163 if (videoTarget >= 0) { 1164 possibleTracks = {videoTarget}; 1165 } 1166 int mirror = timeline->getMirrorVideoTrackId(track); 1167 if (mirror > -1) { 1168 possibleTracks << mirror; 1169 } 1170 if (possibleTracks.isEmpty()) { 1171 // No available audio track for splitting, abort 1172 undo(); 1173 pCore->displayMessage(i18n("No available video track for restore operation"), ErrorMessage); 1174 return false; 1175 } 1176 int newId; 1177 bool res = cloneClip(timeline, cid, newId, PlaylistState::VideoOnly, undo, redo); 1178 if (!res) { 1179 bool undone = undo(); 1180 Q_ASSERT(undone); 1181 pCore->displayMessage(i18n("Video restore failed"), ErrorMessage); 1182 return false; 1183 } 1184 bool success = false; 1185 while (!success && !possibleTracks.isEmpty()) { 1186 int newTrack = possibleTracks.takeFirst(); 1187 success = timeline->requestClipMove(newId, newTrack, position, true, true, true, true, undo, redo); 1188 } 1189 TimelineFunctions::changeClipState(timeline, cid, PlaylistState::AudioOnly, undo, redo); 1190 success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set<int>{newId}, GroupType::AVSplit, undo, redo); 1191 if (!success) { 1192 bool undone = undo(); 1193 Q_ASSERT(undone); 1194 pCore->displayMessage(i18n("Video restore failed"), ErrorMessage); 1195 return false; 1196 } 1197 done = true; 1198 } 1199 if (done) { 1200 pCore->pushUndo(undo, redo, i18n("Restore Video")); 1201 } 1202 return done; 1203 } 1204 1205 void TimelineFunctions::setCompositionATrack(const std::shared_ptr<TimelineItemModel> &timeline, int cid, int aTrack) 1206 { 1207 std::function<bool(void)> undo = []() { return true; }; 1208 std::function<bool(void)> redo = []() { return true; }; 1209 std::shared_ptr<CompositionModel> compo = timeline->getCompositionPtr(cid); 1210 int previousATrack = compo->getATrack(); 1211 int previousAutoTrack = static_cast<int>(compo->getForcedTrack() == -1); 1212 bool autoTrack = aTrack < 0; 1213 if (autoTrack) { 1214 // Automatic track compositing, find lower video track 1215 aTrack = timeline->getPreviousVideoTrackPos(compo->getCurrentTrackId()); 1216 } 1217 int start = timeline->getItemPosition(cid); 1218 int end = start + timeline->getItemPlaytime(cid); 1219 Fun local_redo = [timeline, cid, aTrack, autoTrack, start, end]() { 1220 timeline->unplantComposition(cid); 1221 QScopedPointer<Mlt::Field> field(timeline->m_tractor->field()); 1222 field->lock(); 1223 timeline->getCompositionPtr(cid)->setForceTrack(!autoTrack); 1224 timeline->getCompositionPtr(cid)->setATrack(aTrack, aTrack < 1 ? -1 : timeline->getTrackIndexFromPosition(aTrack - 1)); 1225 field->unlock(); 1226 timeline->replantCompositions(cid, true); 1227 Q_EMIT timeline->invalidateZone(start, end); 1228 timeline->checkRefresh(start, end); 1229 return true; 1230 }; 1231 Fun local_undo = [timeline, cid, previousATrack, previousAutoTrack, start, end]() { 1232 timeline->unplantComposition(cid); 1233 QScopedPointer<Mlt::Field> field(timeline->m_tractor->field()); 1234 field->lock(); 1235 timeline->getCompositionPtr(cid)->setForceTrack(previousAutoTrack == 0); 1236 timeline->getCompositionPtr(cid)->setATrack(previousATrack, previousATrack < 1 ? -1 : timeline->getTrackIndexFromPosition(previousATrack - 1)); 1237 field->unlock(); 1238 timeline->replantCompositions(cid, true); 1239 Q_EMIT timeline->invalidateZone(start, end); 1240 timeline->checkRefresh(start, end); 1241 return true; 1242 }; 1243 if (local_redo()) { 1244 PUSH_LAMBDA(local_undo, undo); 1245 PUSH_LAMBDA(local_redo, redo); 1246 } 1247 pCore->pushUndo(undo, redo, i18n("Change Composition Track")); 1248 } 1249 1250 QStringList TimelineFunctions::enableMultitrackView(const std::shared_ptr<TimelineItemModel> &timeline, bool enable, bool refresh) 1251 { 1252 QStringList trackNames; 1253 std::vector<int> videoTracks; 1254 for (int i = 0; i < int(timeline->m_allTracks.size()); i++) { 1255 int tid = timeline->getTrackIndexFromPosition(i); 1256 if (timeline->getTrackById_const(tid)->isAudioTrack() || timeline->getTrackById_const(tid)->isHidden()) { 1257 continue; 1258 } 1259 videoTracks.push_back(tid); 1260 } 1261 if (videoTracks.size() < 2) { 1262 pCore->displayMessage(i18n("Cannot enable multitrack view on a single track"), ErrorMessage); 1263 } 1264 // First, dis/enable track compositing 1265 QScopedPointer<Mlt::Service> service(timeline->m_tractor->field()); 1266 Mlt::Field *field = timeline->m_tractor->field(); 1267 field->lock(); 1268 while ((service != nullptr) && service->is_valid()) { 1269 if (service->type() == mlt_service_transition_type) { 1270 Mlt::Transition t(mlt_transition(service->get_service())); 1271 service.reset(service->producer()); 1272 QString serviceName = t.get("mlt_service"); 1273 int added = t.get_int("internal_added"); 1274 if (added == 237 && serviceName != QLatin1String("mix")) { 1275 // Disable all compositing transitions 1276 t.set("disable", enable ? "1" : nullptr); 1277 } else if (added == 200) { 1278 field->disconnect_service(t); 1279 t.disconnect_all_producers(); 1280 } 1281 } else { 1282 service.reset(service->producer()); 1283 } 1284 } 1285 if (enable) { 1286 int count = 0; 1287 1288 for (int tid : videoTracks) { 1289 int b_track = timeline->getTrackMltIndex(tid); 1290 Mlt::Transition transition(timeline->m_tractor->get_profile(), "qtblend"); 1291 // transition.set("mlt_service", "composite"); 1292 transition.set("a_track", 0); 1293 transition.set("b_track", b_track); 1294 // 200 is an arbitrary number so we can easily remove these transition later 1295 transition.set("internal_added", 200); 1296 QString geometry; 1297 trackNames << timeline->getTrackFullName(tid); 1298 switch (count) { 1299 case 0: 1300 switch (videoTracks.size()) { 1301 case 1: 1302 geometry = QStringLiteral("0 0 100% 100% 100%"); 1303 break; 1304 case 2: 1305 geometry = QStringLiteral("0 0 50% 100% 100%"); 1306 break; 1307 case 3: 1308 case 4: 1309 geometry = QStringLiteral("0 0 50% 50% 100%"); 1310 break; 1311 case 5: 1312 case 6: 1313 geometry = QStringLiteral("0 0 33% 50% 100%"); 1314 break; 1315 default: 1316 geometry = QStringLiteral("0 0 33% 33% 100%"); 1317 break; 1318 } 1319 break; 1320 case 1: 1321 switch (videoTracks.size()) { 1322 case 2: 1323 geometry = QStringLiteral("50% 0 50% 100% 100%"); 1324 break; 1325 case 3: 1326 case 4: 1327 geometry = QStringLiteral("50% 0 50% 50% 100%"); 1328 break; 1329 case 5: 1330 case 6: 1331 geometry = QStringLiteral("33% 0 33% 50% 100%"); 1332 break; 1333 default: 1334 geometry = QStringLiteral("33% 0 33% 33% 100%"); 1335 break; 1336 } 1337 break; 1338 case 2: 1339 switch (videoTracks.size()) { 1340 case 3: 1341 case 4: 1342 geometry = QStringLiteral("0 50% 50% 50% 100%"); 1343 break; 1344 case 5: 1345 case 6: 1346 geometry = QStringLiteral("66% 0 33% 50% 100%"); 1347 break; 1348 default: 1349 geometry = QStringLiteral("66% 0 33% 33% 100%"); 1350 break; 1351 } 1352 break; 1353 case 3: 1354 switch (videoTracks.size()) { 1355 case 4: 1356 geometry = QStringLiteral("50% 50% 50% 50% 100%"); 1357 break; 1358 case 5: 1359 case 6: 1360 geometry = QStringLiteral("0 50% 33% 50% 100%"); 1361 break; 1362 default: 1363 geometry = QStringLiteral("0 33% 33% 33% 100%"); 1364 break; 1365 } 1366 break; 1367 case 4: 1368 switch (videoTracks.size()) { 1369 case 5: 1370 case 6: 1371 geometry = QStringLiteral("33% 50% 33% 50% 100%"); 1372 break; 1373 default: 1374 geometry = QStringLiteral("33% 33% 33% 33% 100%"); 1375 break; 1376 } 1377 break; 1378 case 5: 1379 switch (videoTracks.size()) { 1380 case 6: 1381 geometry = QStringLiteral("66% 50% 33% 50% 100%"); 1382 break; 1383 default: 1384 geometry = QStringLiteral("66% 33% 33% 33% 100%"); 1385 break; 1386 } 1387 break; 1388 case 6: 1389 geometry = QStringLiteral("0 66% 33% 33% 100%"); 1390 break; 1391 case 7: 1392 geometry = QStringLiteral("33% 66% 33% 33% 100%"); 1393 break; 1394 default: 1395 geometry = QStringLiteral("66% 66% 33% 33% 100%"); 1396 break; 1397 } 1398 count++; 1399 // Add transition to track: 1400 transition.set("rect", geometry.toUtf8().constData()); 1401 transition.set("always_active", 1); 1402 field->plant_transition(transition, 0, b_track); 1403 } 1404 } 1405 field->unlock(); 1406 if (refresh) { 1407 Q_EMIT timeline->requestMonitorRefresh(); 1408 } 1409 return trackNames; 1410 } 1411 1412 void TimelineFunctions::saveTimelineSelection(const std::shared_ptr<TimelineItemModel> &timeline, const std::unordered_set<int> &selection, 1413 const QDir &targetDir, int duration) 1414 { 1415 bool ok; 1416 QString name = QInputDialog::getText(qApp->activeWindow(), i18n("Add Clip to Library"), i18n("Enter a name for the clip in Library"), QLineEdit::Normal, 1417 QString(), &ok); 1418 if (name.isEmpty() || !ok) { 1419 return; 1420 } 1421 QString fullPath = targetDir.absoluteFilePath(name + QStringLiteral(".mlt")); 1422 if (QFile::exists(fullPath)) { 1423 QUrl url = QUrl::fromLocalFile(targetDir.absoluteFilePath(name + QStringLiteral(".mlt"))); 1424 KIO::RenameDialog renameDialog(QApplication::activeWindow(), i18n("File already exists"), url, url, KIO::RenameDialog_Option::RenameDialog_Overwrite); 1425 if (renameDialog.exec() != QDialog::Rejected) { 1426 QUrl final = renameDialog.newDestUrl(); 1427 if (final.isValid()) { 1428 fullPath = final.toLocalFile(); 1429 } 1430 } else { 1431 return; 1432 } 1433 } 1434 int offset = -1; 1435 int lowerAudioTrack = -1; 1436 int lowerVideoTrack = -1; 1437 // Build a copy of selected tracks. 1438 QMap<int, int> sourceTracks; 1439 for (int i : selection) { 1440 int sourceTrack = timeline->getItemTrackId(i); 1441 int clipPos = timeline->getItemPosition(i); 1442 if (offset < 0 || clipPos < offset) { 1443 offset = clipPos; 1444 } 1445 int trackPos = timeline->getTrackMltIndex(sourceTrack); 1446 if (!sourceTracks.contains(trackPos)) { 1447 sourceTracks.insert(trackPos, sourceTrack); 1448 } 1449 // Check if we have a composition with a track not yet listed 1450 if (timeline->isComposition(i)) { 1451 std::shared_ptr<CompositionModel> compo = timeline->getCompositionPtr(i); 1452 int aTrack = compo->getATrack(); 1453 if (!sourceTracks.contains(aTrack)) { 1454 if (aTrack == 0) { 1455 sourceTracks.insert(0, -1); 1456 } else { 1457 sourceTracks.insert(aTrack, timeline->getTrackIndexFromPosition(aTrack - 1)); 1458 } 1459 } 1460 } 1461 } 1462 qDebug() << "==========\nGOT SOUREC TRACKS: " << sourceTracks << "\n\nGGGGGGGGGGGGGGGGGGGGGGG"; 1463 // Build target timeline 1464 Mlt::Tractor newTractor(pCore->getProjectProfile()); 1465 QScopedPointer<Mlt::Field> field(newTractor.field()); 1466 int ix = 0; 1467 QString composite = TransitionsRepository::get()->getCompositingTransition(); 1468 QMapIterator<int, int> i(sourceTracks); 1469 QList<Mlt::Transition *> compositions; 1470 while (i.hasNext()) { 1471 i.next(); 1472 if (i.value() == -1) { 1473 // Insert a black background track 1474 QScopedPointer<Mlt::Producer> newTrackPlaylist(new Mlt::Producer(*newTractor.profile(), "color:black")); 1475 newTrackPlaylist->set("kdenlive:playlistid", "black_track"); 1476 newTrackPlaylist->set("mlt_type", "producer"); 1477 newTrackPlaylist->set("aspect_ratio", 1); 1478 newTrackPlaylist->set("length", INT_MAX); 1479 newTrackPlaylist->set("mlt_image_format", "rgba"); 1480 newTrackPlaylist->set("set.test_audio", 0); 1481 newTrackPlaylist->set_in_and_out(0, duration); 1482 newTractor.set_track(*newTrackPlaylist, ix); 1483 sourceTracks.insert(0, ix); 1484 ix++; 1485 continue; 1486 } 1487 QScopedPointer<Mlt::Playlist> newTrackPlaylist(new Mlt::Playlist(*newTractor.profile())); 1488 newTractor.set_track(*newTrackPlaylist, ix); 1489 // QScopedPointer<Mlt::Producer> trackProducer(newTractor.track(ix)); 1490 int trackId = i.value(); 1491 sourceTracks.insert(timeline->getTrackMltIndex(trackId), ix); 1492 std::shared_ptr<TrackModel> track = timeline->getTrackById_const(trackId); 1493 bool isAudio = track->isAudioTrack(); 1494 if (isAudio) { 1495 newTrackPlaylist->set("hide", 1); 1496 if (lowerAudioTrack < 0) { 1497 lowerAudioTrack = ix; 1498 } 1499 } else { 1500 newTrackPlaylist->set("hide", 2); 1501 if (lowerVideoTrack < 0) { 1502 lowerVideoTrack = ix; 1503 } 1504 } 1505 for (int itemId : selection) { 1506 if (timeline->getItemTrackId(itemId) == trackId) { 1507 // Copy clip on the destination track 1508 if (timeline->isClip(itemId)) { 1509 int clip_position = timeline->m_allClips[itemId]->getPosition(); 1510 auto clip_loc = track->getClipIndexAt(clip_position); 1511 int target_clip = clip_loc.second; 1512 QSharedPointer<Mlt::Producer> clip = track->getClipProducer(target_clip); 1513 newTrackPlaylist->insert_at(clip_position - offset, clip.data(), 1); 1514 } else if (timeline->isComposition(itemId)) { 1515 // Composition 1516 auto *t = new Mlt::Transition(*timeline->m_allCompositions[itemId].get()); 1517 QString id(t->get("kdenlive_id")); 1518 QString internal(t->get("internal_added")); 1519 if (internal.isEmpty()) { 1520 compositions << t; 1521 if (id.isEmpty()) { 1522 qDebug() << "// Warning, this should not happen, transition without id: " << t->get("id") << " = " << t->get("mlt_service"); 1523 t->set("kdenlive_id", t->get("mlt_service")); 1524 } 1525 } 1526 } 1527 } 1528 } 1529 ix++; 1530 } 1531 // Sort compositions and insert 1532 if (!compositions.isEmpty()) { 1533 std::sort(compositions.begin(), compositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() > b->get_b_track(); }); 1534 while (!compositions.isEmpty()) { 1535 QScopedPointer<Mlt::Transition> t(compositions.takeFirst()); 1536 int a_track = t->get_a_track(); 1537 if ((sourceTracks.contains(a_track) || a_track == 0) && sourceTracks.contains(t->get_b_track())) { 1538 Mlt::Transition newComposition(*newTractor.profile(), t->get("mlt_service")); 1539 Mlt::Properties sourceProps(t->get_properties()); 1540 newComposition.inherit(sourceProps); 1541 int in = qMax(0, t->get_in() - offset); 1542 int out = t->get_out() - offset; 1543 newComposition.set_in_and_out(in, out); 1544 if (sourceTracks.contains(a_track)) { 1545 a_track = sourceTracks.value(a_track); 1546 } 1547 int b_track = sourceTracks.value(t->get_b_track()); 1548 field->plant_transition(newComposition, a_track, b_track); 1549 } 1550 } 1551 } 1552 // Track compositing 1553 i.toFront(); 1554 ix = 0; 1555 while (i.hasNext()) { 1556 i.next(); 1557 int trackId = i.value(); 1558 if (trackId < 0) { 1559 // Black background 1560 continue; 1561 } 1562 std::shared_ptr<TrackModel> track = timeline->getTrackById_const(trackId); 1563 bool isAudio = track->isAudioTrack(); 1564 if ((isAudio && ix > lowerAudioTrack) || (!isAudio && ix > lowerVideoTrack)) { 1565 // add track compositing / mix 1566 Mlt::Transition t(*newTractor.profile(), isAudio ? "mix" : composite.toUtf8().constData()); 1567 if (isAudio) { 1568 t.set("sum", 1); 1569 t.set("accepts_blanks", 1); 1570 } 1571 t.set("always_active", 1); 1572 t.set("internal_added", 237); 1573 t.set_tracks(isAudio ? lowerAudioTrack : lowerVideoTrack, ix); 1574 field->plant_transition(t, isAudio ? lowerAudioTrack : lowerVideoTrack, ix); 1575 } 1576 ix++; 1577 } 1578 Mlt::Consumer xmlConsumer(*newTractor.profile(), ("xml:" + fullPath).toUtf8().constData()); 1579 xmlConsumer.set("terminate_on_pause", 1); 1580 xmlConsumer.connect(newTractor); 1581 xmlConsumer.run(); 1582 } 1583 1584 int TimelineFunctions::getTrackOffset(const std::shared_ptr<TimelineItemModel> &timeline, int startTrack, int destTrack) 1585 { 1586 qDebug() << "+++++++\nGET TRACK OFFSET: " << startTrack << " - " << destTrack; 1587 int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack); 1588 int destTrackMltIndex = timeline->getTrackMltIndex(destTrack); 1589 int offset = 0; 1590 qDebug() << "+++++++\nGET TRACK MLT: " << masterTrackMltIndex << " - " << destTrackMltIndex; 1591 if (masterTrackMltIndex == destTrackMltIndex) { 1592 return offset; 1593 } 1594 int step = masterTrackMltIndex > destTrackMltIndex ? -1 : 1; 1595 bool isAudio = timeline->isAudioTrack(startTrack); 1596 int track = masterTrackMltIndex; 1597 while (track != destTrackMltIndex) { 1598 track += step; 1599 qDebug() << "+ + +TESTING TRACK: " << track; 1600 if (track < 1) { 1601 continue; 1602 } 1603 int trackId = timeline->getTrackIndexFromPosition(track - 1); 1604 if (isAudio == timeline->isAudioTrack(trackId)) { 1605 offset += step; 1606 } 1607 } 1608 return offset; 1609 } 1610 1611 int TimelineFunctions::getOffsetTrackId(const std::shared_ptr<TimelineItemModel> &timeline, int startTrack, int offset, bool audioOffset) 1612 { 1613 int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack); 1614 bool isAudio = timeline->isAudioTrack(startTrack); 1615 if (isAudio != audioOffset) { 1616 offset = -offset; 1617 } 1618 qDebug() << "* ** * MASTER INDEX: " << masterTrackMltIndex << ", OFFSET: " << offset; 1619 while (offset != 0) { 1620 masterTrackMltIndex += offset > 0 ? 1 : -1; 1621 qDebug() << "#### TESTING TRACK: " << masterTrackMltIndex; 1622 if (masterTrackMltIndex < 1) { 1623 masterTrackMltIndex = 1; 1624 break; 1625 } 1626 if (masterTrackMltIndex > int(timeline->m_allTracks.size())) { 1627 masterTrackMltIndex = int(timeline->m_allTracks.size()); 1628 break; 1629 } 1630 int trackId = timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1); 1631 if (timeline->isAudioTrack(trackId) == isAudio) { 1632 offset += offset > 0 ? -1 : 1; 1633 } 1634 } 1635 return timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1); 1636 } 1637 1638 TimelineFunctions::TimelineTracksInfo TimelineFunctions::getAVTracksIds(const std::shared_ptr<TimelineItemModel> &timeline) 1639 { 1640 TimelineTracksInfo tracks; 1641 for (const auto &track : timeline->m_allTracks) { 1642 if (track->isAudioTrack()) { 1643 tracks.audioIds << track->getId(); 1644 } else { 1645 tracks.videoIds << track->getId(); 1646 } 1647 } 1648 return tracks; 1649 } 1650 1651 QString TimelineFunctions::copyClips(const std::shared_ptr<TimelineItemModel> &timeline, const std::unordered_set<int> &itemIds) 1652 { 1653 int mainId = *(itemIds.begin()); 1654 // We need to retrieve ALL the involved clips, ie those who are also grouped with the given clips 1655 std::unordered_set<int> allIds; 1656 if (timeline->singleSelectionMode()) { 1657 allIds = itemIds; 1658 } else { 1659 for (const auto &itemId : itemIds) { 1660 std::unordered_set<int> siblings = timeline->getGroupElements(itemId); 1661 allIds.insert(siblings.begin(), siblings.end()); 1662 } 1663 } 1664 // Avoid using a subtitle item as reference since it doesn't work with track offset 1665 if (timeline->isSubTitle(mainId)) { 1666 for (const auto &id : allIds) { 1667 if (!timeline->isSubTitle(id)) { 1668 mainId = id; 1669 break; 1670 } 1671 } 1672 } 1673 bool subtitleOnlyCopy = false; 1674 if (timeline->isSubTitle(mainId)) { 1675 subtitleOnlyCopy = true; 1676 } 1677 1678 // TODO better guess for master track 1679 int masterTid = timeline->getItemTrackId(mainId); 1680 bool audioCopy = subtitleOnlyCopy ? false : timeline->isAudioTrack(masterTid); 1681 int masterTrack = subtitleOnlyCopy ? -1 : timeline->getTrackPosition(masterTid); 1682 QDomDocument copiedItems; 1683 int offset = -1; 1684 int lastFrame = -1; 1685 QDomElement container = copiedItems.createElement(QStringLiteral("kdenlive-scene")); 1686 container.setAttribute(QStringLiteral("fps"), QString::number(pCore->getCurrentFps())); 1687 copiedItems.appendChild(container); 1688 QStringList binIds; 1689 for (int id : allIds) { 1690 int startPos = timeline->getItemPosition(id); 1691 if (offset == -1 || startPos < offset) { 1692 offset = timeline->getItemPosition(id); 1693 } 1694 if (startPos + timeline->getItemPlaytime(id) > lastFrame) { 1695 lastFrame = startPos + timeline->getItemPlaytime(id); 1696 } 1697 if (timeline->isClip(id)) { 1698 QDomElement clipXml = timeline->m_allClips[id]->toXml(copiedItems); 1699 container.appendChild(clipXml); 1700 const QString bid = timeline->m_allClips[id]->binId(); 1701 if (!binIds.contains(bid)) { 1702 binIds << bid; 1703 } 1704 int tid = timeline->getItemTrackId(id); 1705 if (timeline->getTrackById_const(tid)->hasStartMix(id)) { 1706 QDomElement mix = timeline->getTrackById_const(tid)->mixXml(copiedItems, id); 1707 clipXml.appendChild(mix); 1708 } 1709 } else if (timeline->isComposition(id)) { 1710 container.appendChild(timeline->m_allCompositions[id]->toXml(copiedItems)); 1711 } else if (timeline->isSubTitle(id)) { 1712 container.appendChild(timeline->getSubtitleModel()->toXml(id, copiedItems)); 1713 } else { 1714 Q_ASSERT(false); 1715 } 1716 } 1717 QDomElement container2 = copiedItems.createElement(QStringLiteral("bin")); 1718 container.appendChild(container2); 1719 for (const QString &id : qAsConst(binIds)) { 1720 std::shared_ptr<ProjectClip> clip = pCore->projectItemModel()->getClipByBinID(id); 1721 QDomDocument tmp; 1722 container2.appendChild(clip->toXml(tmp)); 1723 } 1724 container.setAttribute(QStringLiteral("offset"), offset); 1725 container.setAttribute(QStringLiteral("duration"), lastFrame - offset); 1726 if (audioCopy) { 1727 container.setAttribute(QStringLiteral("masterAudioTrack"), masterTrack); 1728 int masterMirror = timeline->getMirrorVideoTrackId(masterTid); 1729 if (masterMirror == -1) { 1730 TimelineTracksInfo timelineTracks = TimelineFunctions::getAVTracksIds(timeline); 1731 if (!timelineTracks.videoIds.isEmpty()) { 1732 masterTrack = timeline->getTrackPosition(timelineTracks.videoIds.first()); 1733 } 1734 } else { 1735 masterTrack = timeline->getTrackPosition(masterMirror); 1736 } 1737 } 1738 /* masterTrack contains the reference track over which we want to paste. 1739 this is a video track, unless audioCopy is defined */ 1740 container.setAttribute(QStringLiteral("masterTrack"), masterTrack); 1741 container.setAttribute(QStringLiteral("documentid"), pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))); 1742 QPair<int, int> avTracks = timeline->getAVtracksCount(); 1743 container.setAttribute(QStringLiteral("audioTracks"), avTracks.first); 1744 container.setAttribute(QStringLiteral("videoTracks"), avTracks.second); 1745 QDomElement grp = copiedItems.createElement(QStringLiteral("groups")); 1746 container.appendChild(grp); 1747 std::unordered_set<int> groupRoots; 1748 std::transform(allIds.begin(), allIds.end(), std::inserter(groupRoots, groupRoots.begin()), [&](int id) { 1749 int parent = timeline->m_groups->getRootId(id); 1750 if (timeline->m_groups->getType(parent) == GroupType::Selection) { 1751 std::unordered_set<int> children = timeline->m_groups->getDirectChildren(parent); 1752 for (const auto &gid : children) { 1753 std::unordered_set<int> leaves = timeline->m_groups->getLeaves(gid); 1754 if (leaves.count(id) == 1) { 1755 return gid; 1756 } 1757 } 1758 // This should not happen 1759 qDebug() << "INCORRECT GROUP ID FOUND"; 1760 return -1; 1761 } else { 1762 return parent; 1763 } 1764 }); 1765 1766 qDebug() << "==============\n GROUP ROOTS: "; 1767 for (int gp : groupRoots) { 1768 qDebug() << "GROUP: " << gp; 1769 } 1770 qDebug() << "\n======="; 1771 grp.appendChild(copiedItems.createTextNode(timeline->m_groups->toJson(groupRoots))); 1772 1773 qDebug() << " / // / PASTED DOC: \n\n" << copiedItems.toString() << "\n\n------------"; 1774 return copiedItems.toString(); 1775 } 1776 1777 bool TimelineFunctions::pasteClips(const std::shared_ptr<TimelineItemModel> &timeline, const QString &pasteString, int trackId, int position) 1778 { 1779 std::function<bool(void)> undo = []() { return true; }; 1780 std::function<bool(void)> redo = []() { return true; }; 1781 if (TimelineFunctions::pasteClips(timeline, pasteString, trackId, position, undo, redo)) { 1782 pCore->pushUndo(undo, redo, i18n("Paste clips")); 1783 return true; 1784 } 1785 return false; 1786 } 1787 1788 bool TimelineFunctions::getUsedTracks(const QDomNodeList &clips, const QDomNodeList &compositions, int sourceMasterTrack, int &topAudioMirror, TimelineTracksInfo &allTracks, QList<int> &singleAudioTracks, std::unordered_map<int, int> &audioMirrors) 1789 { 1790 // Tracks used by clips 1791 int max = clips.count(); 1792 for (int i = 0; i < max; i++) { 1793 QDomElement clipProducer = clips.at(i).toElement(); 1794 int trackPos = clipProducer.attribute(QStringLiteral("track")).toInt(); 1795 if (trackPos < 0) { 1796 pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500); 1797 semaphore.release(1); 1798 return false; 1799 } 1800 bool audioTrack = clipProducer.hasAttribute(QStringLiteral("audioTrack")); 1801 if (audioTrack) { 1802 if (!allTracks.audioIds.contains(trackPos)) { 1803 allTracks.audioIds << trackPos; 1804 } 1805 int videoMirror = clipProducer.attribute(QStringLiteral("mirrorTrack")).toInt(); 1806 if (videoMirror == -1 || sourceMasterTrack == -1) { 1807 // The clip has no mirror track 1808 if (!singleAudioTracks.contains(trackPos)) { 1809 singleAudioTracks << trackPos; 1810 } 1811 continue; 1812 } 1813 // The clip has mirror track 1814 audioMirrors[trackPos] = videoMirror; 1815 if (videoMirror > topAudioMirror) { 1816 // We have to check how many video tracks with mirror are needed 1817 topAudioMirror = videoMirror; 1818 } 1819 if (!allTracks.videoIds.contains(videoMirror)) { 1820 allTracks.videoIds << videoMirror; 1821 } 1822 } else { 1823 // Video clip 1824 if (!allTracks.videoIds.contains(trackPos)) { 1825 allTracks.videoIds << trackPos; 1826 } 1827 } 1828 } 1829 1830 // Tracks used by compositions 1831 max = compositions.count(); 1832 for (int i = 0; i < max; i++) { 1833 QDomElement composition = compositions.at(i).toElement(); 1834 int trackPos = composition.attribute(QStringLiteral("track")).toInt(); 1835 if (!allTracks.videoIds.contains(trackPos)) { 1836 allTracks.videoIds << trackPos; 1837 } 1838 int atrackPos = composition.attribute(QStringLiteral("a_track")).toInt(); 1839 if (atrackPos != 0 && !allTracks.videoIds.contains(atrackPos)) { 1840 allTracks.videoIds << atrackPos; 1841 } 1842 } 1843 1844 return true; 1845 } 1846 1847 bool TimelineFunctions::pasteClipsWithUndo(const std::shared_ptr<TimelineItemModel> &timeline, const QString &pasteString, int trackId, int position, Fun &undo, 1848 Fun &redo) 1849 { 1850 std::function<bool(void)> paste_undo = []() { return true; }; 1851 std::function<bool(void)> paste_redo = []() { return true; }; 1852 if (TimelineFunctions::pasteClips(timeline, pasteString, trackId, position, paste_undo, paste_redo)) { 1853 PUSH_FRONT_LAMBDA(paste_undo, undo); 1854 PUSH_FRONT_LAMBDA(paste_redo, redo); 1855 return true; 1856 } 1857 return false; 1858 } 1859 1860 bool TimelineFunctions::pasteClips(const std::shared_ptr<TimelineItemModel> &timeline, const QString &pasteString, int trackId, int position, Fun &undo, 1861 Fun &redo, int inPos, int duration) 1862 { 1863 timeline->requestClearSelection(); 1864 if (!semaphore.tryAcquire(1)) { 1865 pCore->displayMessage(i18n("Another paste operation is in progress"), ErrorMessage, 500); 1866 while (!semaphore.tryAcquire(1)) { 1867 qApp->processEvents(); 1868 } 1869 } 1870 waitingBinIds.clear(); 1871 QDomDocument copiedItems; 1872 copiedItems.setContent(pasteString); 1873 if (copiedItems.documentElement().tagName() == QLatin1String("kdenlive-scene")) { 1874 qDebug() << " / / READING CLIPS FROM CLIPBOARD"; 1875 } else { 1876 semaphore.release(1); 1877 pCore->displayMessage(i18n("No valid data in clipboard"), ErrorMessage, 500); 1878 return false; 1879 } 1880 const QString docId = copiedItems.documentElement().attribute(QStringLiteral("documentid")); 1881 mappedIds.clear(); 1882 // Check available tracks 1883 TimelineTracksInfo timelineTracks = TimelineFunctions::getAVTracksIds(timeline); 1884 int sourceMasterTrack = copiedItems.documentElement().attribute(QStringLiteral("masterTrack"), QStringLiteral("-1")).toInt(); 1885 QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip")); 1886 QDomNodeList compositions = copiedItems.documentElement().elementsByTagName(QStringLiteral("composition")); 1887 QDomNodeList subtitles = copiedItems.documentElement().elementsByTagName(QStringLiteral("subtitle")); 1888 // find paste tracks 1889 // Info about all source tracks 1890 TimelineTracksInfo sourceTracks; 1891 // List of all audio tracks with their corresponding video mirror 1892 std::unordered_map<int, int> audioMirrors; 1893 // List of all source audio tracks that don't have video mirror 1894 QList<int> singleAudioTracks; 1895 // Number of required video tracks with mirror 1896 int topAudioMirror = 0; 1897 1898 if(!getUsedTracks(clips, compositions, sourceMasterTrack, topAudioMirror, sourceTracks, singleAudioTracks, audioMirrors)) { 1899 return false; 1900 } 1901 1902 if (sourceTracks.audioIds.isEmpty() && sourceTracks.videoIds.isEmpty() && subtitles.isEmpty()) { 1903 // playlist does not have any tracks, exit 1904 semaphore.release(1); 1905 return true; 1906 } 1907 // Now we have a list of all source tracks, check that we have enough target tracks 1908 std::sort(sourceTracks.videoIds.begin(), sourceTracks.videoIds.end()); 1909 std::sort(sourceTracks.audioIds.begin(), sourceTracks.audioIds.end()); 1910 std::sort(singleAudioTracks.begin(), singleAudioTracks.end()); 1911 1912 // qDebug()<<"== GOT WANTED TKS\n VIDEO: "<<videoTracks<<"\n AUDIO TKS: "<<audioTracks<<"\n SINGLE AUDIO: "<<singleAudioTracks; 1913 int requestedVideoTracks = sourceTracks.videoIds.isEmpty() ? 0 : sourceTracks.videoIds.last() - sourceTracks.videoIds.first() + 1; 1914 int requestedAudioTracks = sourceTracks.audioIds.isEmpty() ? 0 : sourceTracks.audioIds.last() - sourceTracks.audioIds.first() + 1; 1915 if (requestedVideoTracks > timelineTracks.videoIds.size() || requestedAudioTracks > timelineTracks.audioIds.size()) { 1916 pCore->displayMessage(i18n("Not enough tracks to paste clipboard (requires %1 audio, %2 video tracks)", requestedAudioTracks, requestedVideoTracks), 1917 ErrorMessage, 500); 1918 semaphore.release(1); 1919 return false; 1920 } 1921 1922 auto findPerfectTargetTrack = [](int &sourceTrackId, const QList<int> &sourceTracks, int targetTrackId, const QList<int> &targetTracks) { 1923 const int neededTracksBelow = sourceTrackId - sourceTracks.first(); 1924 const int neededTracksAbove = sourceTracks.last() - sourceTrackId; 1925 1926 const int existingTracksBelow = targetTracks.indexOf(targetTrackId); 1927 const int existingTracksAbove = targetTracks.size() - (targetTracks.indexOf(targetTrackId) + 1); 1928 1929 if (sourceTracks.count() == 1 && targetTracks.count() == 1) { 1930 // we only have one source track and one target track 1931 // hence we have no choice and it is the one we want 1932 sourceTrackId = sourceTracks.first(); 1933 return targetTracks.first(); 1934 } 1935 1936 if (existingTracksBelow < neededTracksBelow) { 1937 qDebug() << "// UPDATING BELOW TID IX TO:" << neededTracksBelow; 1938 // not enough tracks below, try to paste on upper track 1939 return targetTracks.at(neededTracksBelow); 1940 } 1941 1942 if (existingTracksAbove < neededTracksAbove) { 1943 // not enough tracks above, try to paste on lower track 1944 qDebug() << "// UPDATING ABOVE TID IX TO:" << (targetTracks.size() - neededTracksAbove); 1945 return targetTracks.at(targetTracks.size() - neededTracksAbove - 1); 1946 } 1947 1948 // enough tracks above and below, keep the current 1949 return targetTrackId; 1950 }; 1951 1952 // Find destination master track 1953 // Check we have enough tracks above/below 1954 if (requestedVideoTracks > 0) { 1955 trackId = findPerfectTargetTrack(sourceMasterTrack, sourceTracks.videoIds, trackId, timelineTracks.videoIds); 1956 1957 // Find top-most video track that requires an audio mirror 1958 int topAudioOffset = sourceTracks.videoIds.indexOf(topAudioMirror) - sourceTracks.videoIds.indexOf(sourceMasterTrack); 1959 // Check if we have enough video tracks with mirror at paste track position 1960 if (requestedAudioTracks > 0 && timelineTracks.audioIds.size() <= (timelineTracks.videoIds.indexOf(trackId) + topAudioOffset)) { 1961 int updatedPos = sourceTracks.audioIds.size() - topAudioOffset - 1; 1962 if (updatedPos < 0 || updatedPos >= timelineTracks.videoIds.size()) { 1963 pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500); 1964 semaphore.release(1); 1965 return false; 1966 } 1967 trackId = timelineTracks.videoIds.at(updatedPos); 1968 } 1969 } else if (requestedAudioTracks > 0) { 1970 // Audio only 1971 sourceMasterTrack = copiedItems.documentElement().attribute(QStringLiteral("masterAudioTrack")).toInt(); 1972 trackId = findPerfectTargetTrack(sourceMasterTrack, sourceTracks.audioIds, trackId, timelineTracks.audioIds); 1973 } 1974 tracksMap.clear(); 1975 bool audioMaster = false; 1976 int targetMasterIx = timelineTracks.videoIds.indexOf(trackId); 1977 if (targetMasterIx == -1) { 1978 targetMasterIx = timelineTracks.audioIds.indexOf(trackId); 1979 audioMaster = true; 1980 } 1981 1982 int masterOffset = targetMasterIx - sourceMasterTrack; 1983 1984 for (int tk : qAsConst(sourceTracks.videoIds)) { 1985 int newPos = masterOffset + tk; 1986 if (newPos < 0 || newPos >= timelineTracks.videoIds.size()) { 1987 pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500); 1988 semaphore.release(1); 1989 return false; 1990 } 1991 tracksMap.insert(tk, timelineTracks.videoIds.at(newPos)); 1992 // qDebug() << "/// MAPPING SOURCE TRACK: "<<tk<<" TO PROJECT TK: "<<timelineTracks.videoIds.at(newPos)<<" = 1993 // "<<timeline->getTrackMltIndex(timelineTracks.videoIds.at(newPos)); 1994 } 1995 bool audioOffsetCalculated = false; 1996 int audioOffset = 0; 1997 for (const auto &mirror : audioMirrors) { 1998 int videoIx = tracksMap.value(mirror.second); 1999 int mirrorIx = timeline->getMirrorAudioTrackId(videoIx); 2000 if (mirrorIx > 0) { 2001 tracksMap.insert(mirror.first, mirrorIx); 2002 if (!audioOffsetCalculated) { 2003 int oldPosition = mirror.first; 2004 int currentPosition = timeline->getTrackPosition(tracksMap.value(oldPosition)); 2005 audioOffset = currentPosition - oldPosition; 2006 audioOffsetCalculated = true; 2007 } 2008 } 2009 } 2010 if (!audioOffsetCalculated && audioMaster) { 2011 audioOffset = masterOffset; 2012 audioOffsetCalculated = true; 2013 } else if (audioMirrors.size() == 0) { 2014 // We are passing ungrouped audio clips, calculate offset 2015 int sourceAudioTracks = copiedItems.documentElement().attribute(QStringLiteral("audioTracks")).toInt(); 2016 if (sourceAudioTracks > 0) { 2017 audioOffset = timelineTracks.audioIds.count() - sourceAudioTracks; 2018 } 2019 } 2020 for (int oldPos : qAsConst(singleAudioTracks)) { 2021 if (tracksMap.contains(oldPos)) { 2022 continue; 2023 } 2024 int offsetId = oldPos + audioOffset; 2025 if (offsetId < 0 || offsetId >= timelineTracks.audioIds.size()) { 2026 pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500); 2027 semaphore.release(1); 2028 return false; 2029 } 2030 tracksMap.insert(oldPos, timelineTracks.audioIds.at(offsetId)); 2031 } 2032 std::function<void(const QString &)> callBack = [timeline, copiedItems, position, inPos, duration](const QString &binId) { 2033 waitingBinIds.removeAll(binId); 2034 if (waitingBinIds.isEmpty()) { 2035 TimelineFunctions::pasteTimelineClips(timeline, copiedItems, position, inPos, duration); 2036 } 2037 }; 2038 bool clipsImported = false; 2039 int updatedPosition = 0; 2040 int pasteDuration = copiedItems.documentElement().attribute(QStringLiteral("duration")).toInt(); 2041 if (docId == pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))) { 2042 // Check that the bin clips exists in case we try to paste in a copy of original project 2043 QDomNodeList binClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("producer")); 2044 QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Pasted clips")); 2045 for (int i = 0; i < binClips.count(); ++i) { 2046 QDomElement currentProd = binClips.item(i).toElement(); 2047 QString clipId = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:id")); 2048 if (clipId.isEmpty()) { 2049 // Invalid clip, maybe black track from a sequence, ignore 2050 continue; 2051 } 2052 QString clipHash = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:file_hash")); 2053 if (!pCore->projectItemModel()->validateClip(clipId, clipHash)) { 2054 // This clip is different in project and in paste data, create a copy 2055 QString updatedId = QString::number(pCore->projectItemModel()->getFreeClipId()); 2056 Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), updatedId); 2057 mappedIds.insert(clipId, updatedId); 2058 if (folderId.isEmpty()) { 2059 // Folder does not exist 2060 const QString rootId = pCore->projectItemModel()->getRootFolder()->clipId(); 2061 folderId = QString::number(pCore->projectItemModel()->getFreeFolderId()); 2062 pCore->projectItemModel()->requestAddFolder(folderId, i18n("Pasted clips"), rootId, undo, redo); 2063 } 2064 waitingBinIds << updatedId; 2065 clipsImported = true; 2066 pCore->projectItemModel()->requestAddBinClip(updatedId, currentProd, folderId, undo, redo, callBack); 2067 } 2068 } 2069 updatedPosition = position + pasteDuration; 2070 } 2071 2072 if (!docId.isEmpty() && docId != pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))) { 2073 // paste from another document, import bin clips 2074 2075 // Check if the fps matches 2076 QString currentFps = QString::number(pCore->getCurrentFps()); 2077 QString sourceFps = copiedItems.documentElement().attribute(QStringLiteral("fps")); 2078 double ratio = 1.; 2079 if (currentFps != sourceFps && !sourceFps.isEmpty()) { 2080 if (KMessageBox::questionTwoActions( 2081 pCore->window(), 2082 i18n("The source project has a different framerate (%1fps) than your current project.<br/>Clips or keyframes might be messed up.", 2083 sourceFps), 2084 i18n("Pasting Warning"), KGuiItem(i18n("Paste")), KStandardGuiItem::cancel()) != KMessageBox::PrimaryAction) { 2085 semaphore.release(1); 2086 return false; 2087 } 2088 ratio = pCore->getCurrentFps() / sourceFps.toDouble(); 2089 copiedItems.documentElement().setAttribute(QStringLiteral("fps-ratio"), ratio); 2090 } 2091 2092 // Folder in the project for the pasted clips 2093 QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Pasted clips")); 2094 if (folderId.isEmpty()) { 2095 // Folder does not exist 2096 const QString rootId = pCore->projectItemModel()->getRootFolder()->clipId(); 2097 folderId = QString::number(pCore->projectItemModel()->getFreeFolderId()); 2098 pCore->projectItemModel()->requestAddFolder(folderId, i18n("Pasted clips"), rootId, undo, redo); 2099 } 2100 updatedPosition = position + (pasteDuration * ratio); 2101 2102 auto disableProxy = [](QDomElement &producer) { 2103 const QString proxy = Xml::getXmlProperty(producer, QStringLiteral("kdenlive:proxy")); 2104 if (proxy.length() < 4) { 2105 return; 2106 } 2107 const QString resource = Xml::getXmlProperty(producer, QStringLiteral("kdenlive:originalurl")); 2108 if (!resource.isEmpty()) { 2109 Xml::setXmlProperty(producer, QStringLiteral("resource"), resource); 2110 Xml::setXmlProperty(producer, QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); 2111 } 2112 }; 2113 2114 auto useFreeBinId = [](QDomElement &producer, const QString &clipId, QMap<QString, QString> &mappedIds) { 2115 if (!pCore->projectItemModel()->isIdFree(clipId)) { 2116 QString updatedId = QString::number(pCore->projectItemModel()->getFreeClipId()); 2117 Xml::setXmlProperty(producer, QStringLiteral("kdenlive:id"), updatedId); 2118 mappedIds.insert(clipId, updatedId); 2119 return updatedId; 2120 } 2121 return clipId; 2122 }; 2123 2124 auto pasteClip = [disableProxy, callBack, useFreeBinId](const QDomNodeList &clips, int ratio, const QString &folderId, bool &clipsImported, Fun &undo, 2125 Fun &redo){ 2126 for (int i = 0; i < clips.count(); ++i) { 2127 QDomElement currentProd = clips.item(i).toElement(); 2128 QString clipId = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:id")); 2129 if (clipId.isEmpty()) { 2130 // Not a bin clip 2131 continue; 2132 } 2133 2134 // Adjust duration in case of different fps on source and target 2135 if (ratio != 1.) { 2136 int out = currentProd.attribute(QStringLiteral("out")).toInt() * ratio; 2137 int length = Xml::getXmlProperty(currentProd, QStringLiteral("length")).toInt() * ratio; 2138 currentProd.setAttribute(QStringLiteral("out"), out); 2139 Xml::setXmlProperty(currentProd, QStringLiteral("length"), QString::number(length)); 2140 } 2141 2142 // Check if we already have a clip with same hash in pasted clips folder 2143 QString clipHash = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:file_hash")); 2144 QString existingId = pCore->projectItemModel()->validateClipInFolder(folderId, clipHash); 2145 if (!existingId.isEmpty()) { 2146 mappedIds.insert(clipId, existingId); 2147 continue; 2148 } 2149 clipId = useFreeBinId(currentProd, clipId, mappedIds); 2150 2151 // Disable proxy if any when pasting to another document 2152 disableProxy(currentProd); 2153 2154 waitingBinIds << clipId; 2155 clipsImported = true; 2156 bool insert = pCore->projectItemModel()->requestAddBinClip(clipId, currentProd, folderId, undo, redo, callBack); 2157 if (!insert) { 2158 return false; 2159 } 2160 } 2161 return true; 2162 }; 2163 2164 QDomNodeList binClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("producer")); 2165 if (!pasteClip(binClips, ratio, folderId, clipsImported, undo, redo)) { 2166 pCore->displayMessage(i18n("Could not add bin clip"), ErrorMessage, 500); 2167 undo(); 2168 semaphore.release(1); 2169 return false; 2170 } 2171 2172 QDomNodeList chainClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("chain")); 2173 if (!pasteClip(chainClips, ratio, folderId, clipsImported, undo, redo)) { 2174 pCore->displayMessage(i18n("Could not add bin clip"), ErrorMessage, 500); 2175 undo(); 2176 semaphore.release(1); 2177 return false; 2178 } 2179 2180 auto remapClipIds = [](QDomNodeList &elements, const QMap<QString, QString> &map) { 2181 int max = elements.count(); 2182 for (int i = 0; i < max; i++) { 2183 QDomElement e = elements.item(i).toElement(); 2184 const QString currentId = Xml::getXmlProperty(e, QStringLiteral("kdenlive:id")); 2185 if (map.contains(currentId)) { 2186 Xml::setXmlProperty(e, QStringLiteral("kdenlive:id"), map.value(currentId)); 2187 } 2188 } 2189 }; 2190 2191 QDomNodeList sequenceClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("mlt")); 2192 for (int i = 0; i < sequenceClips.count(); ++i) { 2193 QDomElement currentProd = sequenceClips.item(i).toElement(); 2194 QString clipId = currentProd.attribute(QStringLiteral("kdenlive:id")); 2195 const QString uuid = currentProd.attribute(QStringLiteral("kdenlive:uuid")); 2196 int duration = currentProd.attribute(QStringLiteral("kdenlive:duration")).toInt(); 2197 const QString clipname = currentProd.attribute(QStringLiteral("kdenlive:clipname")); 2198 if (clipId.isEmpty()) { 2199 // Not a bin clip 2200 continue; 2201 } 2202 2203 QDomDocument doc; 2204 doc.appendChild(doc.importNode(currentProd, true)); 2205 clipId = useFreeBinId(currentProd, clipId, mappedIds); 2206 2207 // update all bin ids 2208 QDomNodeList prods = doc.documentElement().elementsByTagName(QStringLiteral("producer")); 2209 remapClipIds(prods, mappedIds); 2210 QDomNodeList chains = doc.documentElement().elementsByTagName(QStringLiteral("chain")); 2211 remapClipIds(chains, mappedIds); 2212 QDomNodeList entries = doc.documentElement().elementsByTagName(QStringLiteral("entry")); 2213 remapClipIds(entries, mappedIds); 2214 2215 waitingBinIds << clipId; 2216 clipsImported = true; 2217 std::shared_ptr<Mlt::Producer> xmlProd(new Mlt::Producer(pCore->getProjectProfile(), "xml-string", doc.toString().toUtf8().constData())); 2218 if (!xmlProd->is_valid()) { 2219 qDebug() << ":::: CANNOT IMPORT SEQUENCE: " << clipId; 2220 continue; 2221 } 2222 xmlProd->set("kdenlive:id", clipId.toUtf8().constData()); 2223 xmlProd->set("kdenlive:producer_type", ClipType::Timeline); 2224 xmlProd->set("kdenlive:uuid", uuid.toUtf8().constData()); 2225 xmlProd->set("kdenlive:duration", xmlProd->frames_to_time(duration)); 2226 xmlProd->set("kdenlive:clipname", clipname.toUtf8().constData()); 2227 xmlProd->set("_kdenlive_processed", 1); 2228 Mlt::Service s(*xmlProd.get()); 2229 Mlt::Tractor tractor(s); 2230 std::shared_ptr<Mlt::Producer> prod(new Mlt::Producer(tractor.cut())); 2231 prod->set("id", uuid.toUtf8().constData()); 2232 prod->set("kdenlive:id", clipId.toUtf8().constData()); 2233 prod->set("kdenlive:producer_type", ClipType::Timeline); 2234 prod->set("kdenlive:uuid", uuid.toUtf8().constData()); 2235 prod->set("kdenlive:duration", xmlProd->frames_to_time(duration)); 2236 prod->set("kdenlive:clipname", clipname.toUtf8().constData()); 2237 prod->set("_kdenlive_processed", 1); 2238 bool insert = pCore->projectItemModel()->requestAddBinClip(clipId, prod, folderId, undo, redo, callBack); 2239 if (!insert) { 2240 pCore->displayMessage(i18n("Could not add bin clip"), ErrorMessage, 500); 2241 undo(); 2242 semaphore.release(1); 2243 return false; 2244 } 2245 } 2246 } 2247 2248 if (!clipsImported) { 2249 // Clips from same document, directly proceed to pasting 2250 bool result = TimelineFunctions::pasteTimelineClips(timeline, copiedItems, position, undo, redo, false, inPos, duration); 2251 if (result && updatedPosition > 0) { 2252 pCore->seekMonitor(Kdenlive::ProjectMonitor, updatedPosition); 2253 } 2254 return result; 2255 } 2256 qDebug() << "++++++++++++\nWAITIND FOR BIN INSERTION: " << waitingBinIds << "\n\n+++++++++++++"; 2257 return true; 2258 } 2259 2260 bool TimelineFunctions::pasteTimelineClips(const std::shared_ptr<TimelineItemModel> &timeline, const QDomDocument &copiedItems, int position, int inPos, 2261 int duration) 2262 { 2263 std::function<bool(void)> timeline_undo = []() { return true; }; 2264 std::function<bool(void)> timeline_redo = []() { return true; }; 2265 return TimelineFunctions::pasteTimelineClips(timeline, copiedItems, position, timeline_undo, timeline_redo, true, inPos, duration); 2266 } 2267 2268 bool TimelineFunctions::pasteTimelineClips(const std::shared_ptr<TimelineItemModel> &timeline, QDomDocument copiedItems, int position, Fun &timeline_undo, 2269 Fun &timeline_redo, bool pushToStack, int inPos, int duration) 2270 { 2271 // Wait until all bin clips are inserted 2272 QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip")); 2273 QDomNodeList compositions = copiedItems.documentElement().elementsByTagName(QStringLiteral("composition")); 2274 QDomNodeList subtitles = copiedItems.documentElement().elementsByTagName(QStringLiteral("subtitle")); 2275 int offset = copiedItems.documentElement().attribute(QStringLiteral("offset")).toInt(); 2276 bool res = true; 2277 std::unordered_map<int, int> correspondingIds; 2278 double ratio = 1.0; 2279 if (copiedItems.documentElement().hasAttribute(QStringLiteral("fps-ratio"))) { 2280 ratio = copiedItems.documentElement().attribute(QStringLiteral("fps-ratio")).toDouble(); 2281 offset *= ratio; 2282 } 2283 2284 QDomElement documentMixes = copiedItems.createElement(QStringLiteral("mixes")); 2285 for (int i = 0; i < clips.count(); i++) { 2286 QDomElement prod = clips.at(i).toElement(); 2287 QString originalId = prod.attribute(QStringLiteral("binid")); 2288 if (mappedIds.contains(originalId)) { 2289 // Map id 2290 originalId = mappedIds.value(originalId); 2291 } 2292 if (!pCore->projectItemModel()->hasClip(originalId)) { 2293 // Clip import was not successful, continue 2294 pCore->displayMessage(i18n("All clips were not successfully copied"), ErrorMessage, 500); 2295 continue; 2296 } 2297 int in = prod.attribute(QStringLiteral("in")).toInt(); 2298 int out = prod.attribute(QStringLiteral("out")).toInt(); 2299 int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt()); 2300 if (!timeline->isTrack(curTrackId)) { 2301 // Something is broken 2302 pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500); 2303 timeline_undo(); 2304 semaphore.release(1); 2305 return false; 2306 } 2307 int pos = prod.attribute(QStringLiteral("position")).toInt(); 2308 if (ratio != 1.0) { 2309 in = in * ratio; 2310 out = out * ratio; 2311 pos = pos * ratio; 2312 } 2313 int newIn = in; 2314 int newOut = out; 2315 if ((inPos > 0 && pos + (out - in) < inPos + offset) || (duration > -1 && (pos > inPos + duration + offset))) { 2316 // Clip outside paste range 2317 continue; 2318 } 2319 if (inPos > 0) { 2320 pos -= inPos; 2321 if (pos < offset) { 2322 newIn = in + (offset - pos); 2323 pos = offset; 2324 } 2325 } 2326 if (duration > -1) { 2327 if (pos + (out - in) > inPos + duration + offset) { 2328 newOut = out - (pos + (out - in) - (inPos + duration + offset)); 2329 } 2330 } 2331 2332 pos -= offset; 2333 double speed = prod.attribute(QStringLiteral("speed")).toDouble(); 2334 bool warp_pitch = false; 2335 if (!qFuzzyCompare(speed, 1.)) { 2336 warp_pitch = prod.attribute(QStringLiteral("warp_pitch")).toInt(); 2337 } 2338 int audioStream = prod.attribute(QStringLiteral("audioStream")).toInt(); 2339 int newId; 2340 bool created = timeline->requestClipCreation(originalId, newId, timeline->getTrackById_const(curTrackId)->trackType(), audioStream, speed, warp_pitch, 2341 timeline_undo, timeline_redo); 2342 if (!created) { 2343 // Something is broken 2344 pCore->displayMessage(i18n("Could not paste items in timeline"), ErrorMessage, 500); 2345 timeline_undo(); 2346 semaphore.release(1); 2347 return false; 2348 } 2349 if (prod.hasAttribute(QStringLiteral("timemap"))) { 2350 // This is a timeremap 2351 timeline->m_allClips[newId]->useTimeRemapProducer(true, timeline_undo, timeline_redo); 2352 if (timeline->m_allClips[newId]->m_producer->parent().type() == mlt_service_chain_type) { 2353 Mlt::Chain fromChain(timeline->m_allClips[newId]->m_producer->parent()); 2354 int count = fromChain.link_count(); 2355 for (int i = 0; i < count; i++) { 2356 QScopedPointer<Mlt::Link> fromLink(fromChain.link(i)); 2357 if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) { 2358 if (fromLink->get("mlt_service") == QLatin1String("timeremap")) { 2359 // Found a timeremap effect, read params 2360 fromLink->set("time_map", prod.attribute(QStringLiteral("timemap")).toUtf8().constData()); 2361 fromLink->set("pitch", prod.attribute(QStringLiteral("timepitch")).toInt()); 2362 fromLink->set("image_mode", prod.attribute(QStringLiteral("timeblend")).toUtf8().constData()); 2363 break; 2364 } 2365 } 2366 } 2367 } 2368 } 2369 if (timeline->m_allClips[newId]->m_endlessResize) { 2370 out = out - in; 2371 in = 0; 2372 timeline->m_allClips[newId]->m_producer->set("length", out + 1); 2373 timeline->m_allClips[newId]->m_producer->set("out", out); 2374 } 2375 timeline->m_allClips[newId]->setInOut(in, out); 2376 int targetId = prod.attribute(QStringLiteral("id")).toInt(); 2377 int targetPlaylist = prod.attribute(QStringLiteral("playlist")).toInt(); 2378 if (targetPlaylist > 0) { 2379 timeline->m_allClips[newId]->setSubPlaylistIndex(targetPlaylist, curTrackId); 2380 } 2381 correspondingIds[targetId] = newId; 2382 std::shared_ptr<EffectStackModel> destStack = timeline->getClipEffectStackModel(newId); 2383 destStack->fromXml(prod.firstChildElement(QStringLiteral("effects")), timeline_undo, timeline_redo); 2384 if (newIn != in) { 2385 int newSize = out - newIn + 1; 2386 res = res && timeline->requestItemResize(newId, newSize, false, true, timeline_undo, timeline_redo); 2387 if (res) { 2388 std::shared_ptr<EffectStackModel> sourceStack = timeline->getClipEffectStackModel(newId); 2389 sourceStack->cleanFadeEffects(true, timeline_undo, timeline_redo); 2390 } 2391 // TODO manage mixes 2392 } 2393 if (newOut != out) { 2394 int newSize = newOut - newIn; 2395 res = res && timeline->requestItemResize(newId, newSize, true, true, timeline_undo, timeline_redo); 2396 if (res) { 2397 std::shared_ptr<EffectStackModel> sourceStack = timeline->getClipEffectStackModel(newId); 2398 sourceStack->cleanFadeEffects(false, timeline_undo, timeline_redo); 2399 } 2400 // TODO manage mixes 2401 } 2402 res = res && timeline->getTrackById(curTrackId)->requestClipInsertion(newId, position + pos, true, true, timeline_undo, timeline_redo); 2403 // paste effects 2404 if (!res) { 2405 qDebug() << "=== COULD NOT PASTE CLIP: " << newId << " ON TRACK: " << curTrackId << " AT: " << position; 2406 break; 2407 } 2408 // Mixes (same track transitions) 2409 if (prod.hasChildNodes()) { 2410 // TODO: adjust position/duration with inPos / duration 2411 QDomNodeList mixes = prod.elementsByTagName(QLatin1String("mix")); 2412 if (!mixes.isEmpty()) { 2413 QDomElement mix = mixes.at(0).toElement(); 2414 if (mix.tagName() == QLatin1String("mix")) { 2415 mix.setAttribute(QStringLiteral("tid"), curTrackId); 2416 documentMixes.appendChild(mix); 2417 } 2418 } 2419 } 2420 } 2421 // Process mix insertion 2422 QDomNodeList mixes = documentMixes.childNodes(); 2423 for (int k = 0; k < mixes.count(); k++) { 2424 QDomElement mix = mixes.at(k).toElement(); 2425 int originalFirstClipId = mix.attribute(QLatin1String("firstClip")).toInt(); 2426 int originalSecondClipId = mix.attribute(QLatin1String("secondClip")).toInt(); 2427 if (correspondingIds.count(originalFirstClipId) > 0 && correspondingIds.count(originalSecondClipId) > 0) { 2428 QVector<QPair<QString, QVariant>> params; 2429 QDomNodeList paramsXml = mix.elementsByTagName(QLatin1String("param")); 2430 for (int j = 0; j < paramsXml.count(); j++) { 2431 QDomElement e = paramsXml.at(j).toElement(); 2432 params.append({e.attribute(QLatin1String("name")), e.text()}); 2433 } 2434 std::pair<QString, QVector<QPair<QString, QVariant>>> mixParams = {mix.attribute(QLatin1String("asset")), params}; 2435 MixInfo mixData; 2436 mixData.firstClipId = correspondingIds[originalFirstClipId]; 2437 mixData.secondClipId = correspondingIds[originalSecondClipId]; 2438 mixData.firstClipInOut.second = mix.attribute(QLatin1String("mixEnd")).toInt() * ratio; 2439 mixData.secondClipInOut.first = mix.attribute(QLatin1String("mixStart")).toInt() * ratio; 2440 mixData.mixOffset = mix.attribute(QLatin1String("mixOffset")).toInt() * ratio; 2441 std::pair<int, int> tracks = {mix.attribute(QLatin1String("a_track")).toInt(), mix.attribute(QLatin1String("b_track")).toInt()}; 2442 if (tracks.first == tracks.second) { 2443 tracks = {0, 1}; 2444 } 2445 timeline->getTrackById_const(mix.attribute(QLatin1String("tid")).toInt())->createMix(mixData, mixParams, tracks, true); 2446 } 2447 } 2448 // Compositions 2449 if (res) { 2450 for (int i = 0; res && i < compositions.count(); i++) { 2451 QDomElement prod = compositions.at(i).toElement(); 2452 QString originalId = prod.attribute(QStringLiteral("composition")); 2453 int in = prod.attribute(QStringLiteral("in")).toInt() * ratio; 2454 int out = prod.attribute(QStringLiteral("out")).toInt() * ratio; 2455 int pos = prod.attribute(QStringLiteral("position")).toInt() * ratio - offset; 2456 int newPos = pos; 2457 if (inPos > 0) { 2458 newPos -= inPos; 2459 } 2460 int compoDuration = out - in + 1; 2461 int compoDuration2 = out - in + 1; 2462 if (newPos < 0) { 2463 // resize composition 2464 compoDuration += newPos; 2465 newPos = 0; 2466 } 2467 if (duration > -1 && (newPos + compoDuration > inPos + duration)) { 2468 compoDuration2 = inPos + duration - newPos; 2469 } 2470 if (compoDuration2 <= 0) { 2471 continue; 2472 } 2473 int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt()); 2474 int trackOffset = Xml::getXmlProperty(prod, QStringLiteral("b_track")).toInt() - Xml::getXmlProperty(prod, QStringLiteral("a_track")).toInt(); 2475 // Add 1 to account for the black track 2476 int aTrackPos = timeline->getTrackPosition(curTrackId) - trackOffset + 1; 2477 int atrackId = -1; 2478 if (aTrackPos > 0 && aTrackPos < timeline->getTracksCount()) { 2479 atrackId = timeline->getTrackIndexFromPosition(aTrackPos - 1); 2480 } 2481 if (atrackId > -1 && !timeline->isAudioTrack(atrackId)) { 2482 // Ok, track found 2483 } else { 2484 aTrackPos = 0; 2485 } 2486 2487 int newId; 2488 auto transProps = std::make_unique<Mlt::Properties>(); 2489 QDomNodeList props = prod.elementsByTagName(QStringLiteral("property")); 2490 for (int j = 0; j < props.count(); j++) { 2491 transProps->set(props.at(j).toElement().attribute(QStringLiteral("name")).toUtf8().constData(), 2492 props.at(j).toElement().text().toUtf8().constData()); 2493 } 2494 res = res && timeline->requestCompositionCreation(originalId, out - in + 1, std::move(transProps), newId, timeline_undo, timeline_redo); 2495 if (newPos != pos) { 2496 // transition start resized 2497 timeline->requestItemResize(newId, compoDuration, false, true, timeline_undo, timeline_redo, false); 2498 } 2499 if (compoDuration != compoDuration2) { 2500 timeline->requestItemResize(newId, compoDuration2, true, true, timeline_undo, timeline_redo, false); 2501 } 2502 res = res && timeline->requestCompositionMove(newId, curTrackId, aTrackPos, position + newPos, true, true, timeline_undo, timeline_redo); 2503 } 2504 } 2505 if (res && !subtitles.isEmpty()) { 2506 auto subModel = timeline->getSubtitleModel(); 2507 if (!subModel) { 2508 // This timeline doesn't yet have subtitles, initiate 2509 pCore->window()->slotShowSubtitles(true); 2510 subModel = timeline->getSubtitleModel(); 2511 } 2512 for (int i = 0; res && i < subtitles.count(); i++) { 2513 QDomElement prod = subtitles.at(i).toElement(); 2514 int in = prod.attribute(QStringLiteral("in")).toInt() * ratio - offset; 2515 int out = prod.attribute(QStringLiteral("out")).toInt() * ratio - offset; 2516 QString text = prod.attribute(QStringLiteral("text")); 2517 res = res && subModel->addSubtitle(GenTime(position + in, pCore->getCurrentFps()), GenTime(position + out, pCore->getCurrentFps()), text, 2518 timeline_undo, timeline_redo); 2519 } 2520 } 2521 if (!res) { 2522 timeline_undo(); 2523 pCore->displayMessage(i18n("Could not paste items in timeline"), ErrorMessage, 500); 2524 semaphore.release(1); 2525 return false; 2526 } 2527 // Rebuild groups 2528 const QString groupsData = copiedItems.documentElement().firstChildElement(QStringLiteral("groups")).text(); 2529 if (!groupsData.isEmpty()) { 2530 timeline->m_groups->fromJsonWithOffset(groupsData, tracksMap, position - offset, ratio, timeline_undo, timeline_redo); 2531 } 2532 // Ensure to clear selection in undo/redo too. 2533 Fun unselect = [timeline]() { 2534 qDebug() << "starting undo or redo. Selection " << timeline->m_currentSelection.size(); 2535 timeline->requestClearSelection(); 2536 qDebug() << "after Selection " << timeline->m_currentSelection.size(); 2537 return true; 2538 }; 2539 PUSH_FRONT_LAMBDA(unselect, timeline_undo); 2540 PUSH_FRONT_LAMBDA(unselect, timeline_redo); 2541 // UPDATE_UNDO_REDO_NOLOCK(timeline_redo, timeline_undo, undo, redo); 2542 if (pushToStack) { 2543 pCore->pushUndo(timeline_undo, timeline_redo, i18n("Paste timeline clips")); 2544 } 2545 semaphore.release(1); 2546 return true; 2547 } 2548 2549 bool TimelineFunctions::requestDeleteBlankAt(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position, bool affectAllTracks) 2550 { 2551 // Check we have blank at position 2552 int startPos = -1; 2553 int endPos = -1; 2554 if (affectAllTracks) { 2555 for (const auto &track : timeline->m_allTracks) { 2556 if (!track->isLocked()) { 2557 if (!track->isBlankAt(position)) { 2558 return false; 2559 } 2560 startPos = track->getBlankStart(position) - 1; 2561 endPos = track->getBlankEnd(position) + 2; 2562 if (startPos > -1) { 2563 std::unordered_set<int> clips = timeline->getItemsInRange(trackId, startPos, endPos); 2564 if (clips.size() == 2) { 2565 auto it = clips.begin(); 2566 int firstCid = *it; 2567 ++it; 2568 int lastCid = *it; 2569 if (timeline->m_groups->isInGroup(firstCid)) { 2570 int groupId = timeline->m_groups->getRootId(firstCid); 2571 std::unordered_set<int> all_children = timeline->m_groups->getLeaves(groupId); 2572 if (all_children.find(lastCid) != all_children.end()) { 2573 return false; 2574 } 2575 } 2576 } 2577 } 2578 } 2579 } 2580 // check subtitle track 2581 if (timeline->getSubtitleModel() && !timeline->getSubtitleModel()->isLocked()) { 2582 if (!timeline->getSubtitleModel()->isBlankAt(position)) { 2583 return false; 2584 } 2585 startPos = timeline->getSubtitleModel()->getBlankStart(position) - 1; 2586 endPos = timeline->getSubtitleModel()->getBlankEnd(position) + 1; 2587 if (startPos > -1) { 2588 std::unordered_set<int> clips = timeline->getItemsInRange(trackId, startPos, endPos); 2589 if (clips.size() == 2) { 2590 auto it = clips.begin(); 2591 int firstCid = *it; 2592 ++it; 2593 int lastCid = *it; 2594 if (timeline->m_groups->isInGroup(firstCid)) { 2595 int groupId = timeline->m_groups->getRootId(firstCid); 2596 std::unordered_set<int> all_children = timeline->m_groups->getLeaves(groupId); 2597 if (all_children.find(lastCid) != all_children.end()) { 2598 return false; 2599 } 2600 } 2601 } 2602 } 2603 } 2604 } else { 2605 // Check we have a blank and that it is in not between 2 grouped clips 2606 if (timeline->trackIsLocked(trackId)) { 2607 timeline->flashLock(trackId); 2608 return false; 2609 } 2610 if (timeline->isSubtitleTrack(trackId)) { 2611 // Subtitle track 2612 if (!timeline->getSubtitleModel()->isBlankAt(position)) { 2613 return false; 2614 } 2615 startPos = timeline->getSubtitleModel()->getBlankStart(position) - 1; 2616 endPos = timeline->getSubtitleModel()->getBlankEnd(position) + 1; 2617 } else { 2618 if (!timeline->getTrackById_const(trackId)->isBlankAt(position)) { 2619 return false; 2620 } 2621 startPos = timeline->getTrackById_const(trackId)->getBlankStart(position) - 1; 2622 endPos = timeline->getTrackById_const(trackId)->getBlankEnd(position) + 2; 2623 } 2624 if (startPos > -1) { 2625 std::unordered_set<int> clips = timeline->getItemsInRange(trackId, startPos, endPos); 2626 if (clips.size() == 2) { 2627 auto it = clips.begin(); 2628 int firstCid = *it; 2629 ++it; 2630 int lastCid = *it; 2631 if (timeline->m_groups->isInGroup(firstCid)) { 2632 int groupId = timeline->m_groups->getRootId(firstCid); 2633 std::unordered_set<int> all_children = timeline->m_groups->getLeaves(groupId); 2634 if (all_children.find(lastCid) != all_children.end()) { 2635 return false; 2636 } 2637 } 2638 } 2639 } 2640 } 2641 std::pair<int, int> spacerOp = requestSpacerStartOperation(timeline, affectAllTracks ? -1 : trackId, position); 2642 int cid = spacerOp.first; 2643 if (cid == -1 || spacerOp.second == -1) { 2644 return false; 2645 } 2646 int start = timeline->getItemPosition(cid); 2647 int spaceStart = start - spacerOp.second; 2648 if (spaceStart >= start) { 2649 return false; 2650 } 2651 // Start undoable command 2652 std::function<bool(void)> undo = []() { return true; }; 2653 std::function<bool(void)> redo = []() { return true; }; 2654 requestSpacerEndOperation(timeline, cid, start, spaceStart, affectAllTracks ? -1 : trackId, KdenliveSettings::lockedGuides() ? -1 : position, undo, redo); 2655 return true; 2656 } 2657 2658 bool TimelineFunctions::requestDeleteAllBlanksFrom(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position) 2659 { 2660 // Abort if track is locked 2661 if (timeline->trackIsLocked(trackId)) { 2662 timeline->flashLock(trackId); 2663 return false; 2664 } 2665 // Start undoable command 2666 std::function<bool(void)> undo = []() { return true; }; 2667 std::function<bool(void)> redo = []() { return true; }; 2668 if (timeline->isSubtitleTrack(trackId)) { 2669 // Subtitle track 2670 int blankStart = timeline->getSubtitleModel()->getNextBlankStart(position); 2671 if (blankStart == -1) { 2672 return false; 2673 } 2674 while (blankStart != -1) { 2675 std::pair<int, int> spacerOp = requestSpacerStartOperation(timeline, trackId, blankStart, true); 2676 int cid = spacerOp.first; 2677 if (cid == -1) { 2678 break; 2679 } 2680 int start = timeline->getItemPosition(cid); 2681 // Start undoable command 2682 std::function<bool(void)> local_undo = []() { return true; }; 2683 std::function<bool(void)> local_redo = []() { return true; }; 2684 if (blankStart < start) { 2685 if (!requestSpacerEndOperation(timeline, cid, start, blankStart, trackId, KdenliveSettings::lockedGuides() ? -1 : position, local_undo, 2686 local_redo, false)) { 2687 // Failed to remove blank, maybe blocked because of a group. Pass to the next one 2688 blankStart = start; 2689 } else { 2690 UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); 2691 } 2692 } else { 2693 if (timeline->getSubtitleModel()->isBlankAt(blankStart)) { 2694 blankStart = timeline->getSubtitleModel()->getBlankEnd(blankStart) + 1; 2695 if (blankStart == 1) { 2696 break; 2697 } 2698 } else { 2699 blankStart = start + timeline->getItemPlaytime(cid) + 1; 2700 } 2701 } 2702 int nextBlank = timeline->getSubtitleModel()->getNextBlankStart(blankStart); 2703 if (nextBlank == blankStart) { 2704 blankStart = timeline->getSubtitleModel()->getBlankEnd(blankStart) + 1; 2705 nextBlank = timeline->getSubtitleModel()->getNextBlankStart(blankStart); 2706 if (nextBlank == blankStart) { 2707 break; 2708 } 2709 } 2710 if (nextBlank < blankStart) { 2711 // Done 2712 break; 2713 } 2714 blankStart = nextBlank; 2715 } 2716 } else { 2717 int blankStart = timeline->getTrackById_const(trackId)->getNextBlankStart(position); 2718 if (blankStart == -1) { 2719 return false; 2720 } 2721 while (blankStart != -1) { 2722 std::pair<int, int> spacerOp = requestSpacerStartOperation(timeline, trackId, blankStart, true); 2723 int cid = spacerOp.first; 2724 if (cid == -1) { 2725 break; 2726 } 2727 int start = timeline->getItemPosition(cid); 2728 // Start undoable command 2729 std::function<bool(void)> local_undo = []() { return true; }; 2730 std::function<bool(void)> local_redo = []() { return true; }; 2731 if (blankStart < start) { 2732 if (!requestSpacerEndOperation(timeline, cid, start, blankStart, trackId, KdenliveSettings::lockedGuides() ? -1 : position, local_undo, 2733 local_redo, false)) { 2734 // Failed to remove blank, maybe blocked because of a group. Pass to the next one 2735 blankStart = start; 2736 } else { 2737 UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); 2738 } 2739 } else { 2740 if (timeline->getTrackById_const(trackId)->isBlankAt(blankStart)) { 2741 blankStart = timeline->getTrackById_const(trackId)->getBlankEnd(blankStart) + 1; 2742 } else { 2743 blankStart = start + timeline->getItemPlaytime(cid); 2744 } 2745 } 2746 int nextBlank = timeline->getTrackById_const(trackId)->getNextBlankStart(blankStart); 2747 if (nextBlank == blankStart) { 2748 blankStart = timeline->getTrackById_const(trackId)->getBlankEnd(blankStart) + 1; 2749 nextBlank = timeline->getTrackById_const(trackId)->getNextBlankStart(blankStart); 2750 if (nextBlank == blankStart) { 2751 break; 2752 } 2753 } 2754 if (nextBlank < blankStart) { 2755 // Done 2756 break; 2757 } 2758 blankStart = nextBlank; 2759 } 2760 } 2761 pCore->pushUndo(undo, redo, i18n("Remove space on track")); 2762 return true; 2763 } 2764 2765 bool TimelineFunctions::requestDeleteAllClipsFrom(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position) 2766 { 2767 // Abort if track is locked 2768 if (timeline->trackIsLocked(trackId)) { 2769 timeline->flashLock(trackId); 2770 return false; 2771 } 2772 // Start undoable command 2773 std::function<bool(void)> undo = []() { return true; }; 2774 std::function<bool(void)> redo = []() { return true; }; 2775 std::unordered_set<int> items; 2776 if (timeline->isSubtitleTrack(trackId)) { 2777 // Subtitle track 2778 items = timeline->getSubtitleModel()->getItemsInRange(position, -1); 2779 } else { 2780 items = timeline->getTrackById_const(trackId)->getClipsInRange(position, -1); 2781 } 2782 if (items.size() == 0) { 2783 return false; 2784 } 2785 for (int id : items) { 2786 timeline->requestItemDeletion(id, undo, redo); 2787 } 2788 pCore->pushUndo(undo, redo, i18n("Delete clips on track")); 2789 return true; 2790 } 2791 2792 QDomDocument TimelineFunctions::extractClip(const std::shared_ptr<TimelineItemModel> &timeline, int cid, const QString &binId) 2793 { 2794 int tid = timeline->getClipTrackId(cid); 2795 int pos = timeline->getClipPosition(cid); 2796 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(binId); 2797 QDomDocument sourceDoc; 2798 QDomDocument destDoc; 2799 if (!Xml::docContentFromFile(sourceDoc, clip->clipUrl(), false)) { 2800 return destDoc; 2801 } 2802 QDomElement container = destDoc.createElement(QStringLiteral("kdenlive-scene")); 2803 destDoc.appendChild(container); 2804 QDomElement bin = destDoc.createElement(QStringLiteral("bin")); 2805 container.appendChild(bin); 2806 bool isAudio = timeline->isAudioTrack(tid); 2807 container.setAttribute(QStringLiteral("offset"), pos); 2808 container.setAttribute(QStringLiteral("documentid"), QStringLiteral("000000")); 2809 // Process producers 2810 QList<int> processedProducers; 2811 QString blackBg; 2812 QMap<QString, int> producerMap; 2813 QMap<QString, double> producerSpeed; 2814 QMap<QString, int> producerSpeedResource; 2815 QDomNodeList producers = sourceDoc.elementsByTagName(QLatin1String("producer")); 2816 for (int i = 0; i < producers.count(); ++i) { 2817 QDomElement currentProd = producers.item(i).toElement(); 2818 bool ok; 2819 int clipId = Xml::getXmlProperty(currentProd, QLatin1String("kdenlive:id")).toInt(&ok); 2820 if (!ok) { 2821 // Check if this is a black bg track 2822 if (Xml::hasXmlProperty(currentProd, QLatin1String("kdenlive:playlistid"))) { 2823 // This is the black bg track 2824 blackBg = currentProd.attribute(QStringLiteral("id")); 2825 continue; 2826 } 2827 const QString resource = Xml::getXmlProperty(currentProd, QLatin1String("resource")); 2828 qDebug() << "===== CLIP NOT FOUND: " << resource; 2829 if (producerSpeedResource.contains(resource)) { 2830 clipId = producerSpeedResource.value(resource); 2831 qDebug() << "===== GOT PREVIOUS ID: " << clipId; 2832 QString baseProducerId; 2833 int baseProducerClipId = 0; 2834 QMapIterator<QString, int> m(producerMap); 2835 while (m.hasNext()) { 2836 m.next(); 2837 if (m.value() == clipId) { 2838 baseProducerId = m.key(); 2839 baseProducerClipId = m.value(); 2840 qDebug() << "=== FOUND PRODUCER FOR ID: " << m.key(); 2841 break; 2842 } 2843 } 2844 if (!baseProducerId.isEmpty()) { 2845 producerSpeed.insert(currentProd.attribute(QLatin1String("id")), producerSpeed.value(baseProducerId)); 2846 producerMap.insert(currentProd.attribute(QLatin1String("id")), baseProducerClipId); 2847 qDebug() << "/// INSERTING PRODUCERMAP: " << currentProd.attribute(QLatin1String("id")) << "=" << baseProducerClipId; 2848 } 2849 // Producer already processed; 2850 continue; 2851 } else { 2852 clipId = pCore->projectItemModel()->getFreeClipId(); 2853 } 2854 Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), QString::number(clipId)); 2855 qDebug() << "=== UNKNOWN CLIP FOUND: " << Xml::getXmlProperty(currentProd, QLatin1String("resource")); 2856 } 2857 producerMap.insert(currentProd.attribute(QLatin1String("id")), clipId); 2858 qDebug() << "/// INSERTING SOURCE PRODUCERMAP: " << currentProd.attribute(QLatin1String("id")) << "=" << clipId; 2859 QString mltService = Xml::getXmlProperty(currentProd, QStringLiteral("mlt_service")); 2860 if (mltService == QLatin1String("timewarp")) { 2861 // Speed producer 2862 double speed = Xml::getXmlProperty(currentProd, QStringLiteral("warp_speed")).toDouble(); 2863 Xml::setXmlProperty(currentProd, QStringLiteral("mlt_service"), QStringLiteral("avformat")); 2864 producerSpeedResource.insert(Xml::getXmlProperty(currentProd, QLatin1String("resource")), clipId); 2865 qDebug() << "===== CLIP SPEED RESOURCE: " << Xml::getXmlProperty(currentProd, QLatin1String("resource")) << " = " << clipId; 2866 QString resource = Xml::getXmlProperty(currentProd, QStringLiteral("warp_resource")); 2867 Xml::setXmlProperty(currentProd, QStringLiteral("resource"), resource); 2868 producerSpeed.insert(currentProd.attribute(QLatin1String("id")), speed); 2869 } 2870 if (processedProducers.contains(clipId)) { 2871 // Producer already processed 2872 continue; 2873 } 2874 processedProducers << clipId; 2875 // This could be a timeline track producer, reset custom audio/video setting 2876 Xml::removeXmlProperty(currentProd, QLatin1String("set.test_audio")); 2877 Xml::removeXmlProperty(currentProd, QLatin1String("set.test_image")); 2878 bin.appendChild(destDoc.importNode(currentProd, true)); 2879 } 2880 // Same for chains 2881 QDomNodeList chains = sourceDoc.elementsByTagName(QStringLiteral("chain")); 2882 for (int i = 0; i < chains.count(); ++i) { 2883 QDomElement currentProd = chains.item(i).toElement(); 2884 bool ok; 2885 int clipId = Xml::getXmlProperty(currentProd, QLatin1String("kdenlive:id")).toInt(&ok); 2886 if (!ok) { 2887 const QString resource = Xml::getXmlProperty(currentProd, QLatin1String("resource")); 2888 qDebug() << "===== CLIP NOT FOUND: " << resource; 2889 if (producerSpeedResource.contains(resource)) { 2890 clipId = producerSpeedResource.value(resource); 2891 qDebug() << "===== GOT PREVIOUS ID: " << clipId; 2892 QString baseProducerId; 2893 int baseProducerClipId = 0; 2894 QMapIterator<QString, int> m(producerMap); 2895 while (m.hasNext()) { 2896 m.next(); 2897 if (m.value() == clipId) { 2898 baseProducerId = m.key(); 2899 baseProducerClipId = m.value(); 2900 qDebug() << "=== FOUND PRODUCER FOR ID: " << m.key(); 2901 break; 2902 } 2903 } 2904 if (!baseProducerId.isEmpty()) { 2905 producerSpeed.insert(currentProd.attribute(QLatin1String("id")), producerSpeed.value(baseProducerId)); 2906 producerMap.insert(currentProd.attribute(QLatin1String("id")), baseProducerClipId); 2907 qDebug() << "/// INSERTING PRODUCERMAP: " << currentProd.attribute(QLatin1String("id")) << "=" << baseProducerClipId; 2908 } 2909 // Producer already processed; 2910 continue; 2911 } else { 2912 clipId = pCore->projectItemModel()->getFreeClipId(); 2913 } 2914 Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), QString::number(clipId)); 2915 qDebug() << "=== UNKNOWN CLIP FOUND: " << Xml::getXmlProperty(currentProd, QLatin1String("resource")); 2916 } 2917 producerMap.insert(currentProd.attribute(QLatin1String("id")), clipId); 2918 qDebug() << "/// INSERTING SOURCE PRODUCERMAP: " << currentProd.attribute(QLatin1String("id")) << "=" << clipId; 2919 QString mltService = Xml::getXmlProperty(currentProd, QStringLiteral("mlt_service")); 2920 if (mltService == QLatin1String("timewarp")) { 2921 // Speed producer 2922 double speed = Xml::getXmlProperty(currentProd, QStringLiteral("warp_speed")).toDouble(); 2923 Xml::setXmlProperty(currentProd, QStringLiteral("mlt_service"), QStringLiteral("avformat")); 2924 producerSpeedResource.insert(Xml::getXmlProperty(currentProd, QLatin1String("resource")), clipId); 2925 qDebug() << "===== CLIP SPEED RESOURCE: " << Xml::getXmlProperty(currentProd, QLatin1String("resource")) << " = " << clipId; 2926 QString resource = Xml::getXmlProperty(currentProd, QStringLiteral("warp_resource")); 2927 Xml::setXmlProperty(currentProd, QStringLiteral("resource"), resource); 2928 producerSpeed.insert(currentProd.attribute(QLatin1String("id")), speed); 2929 } 2930 if (processedProducers.contains(clipId)) { 2931 // Producer already processed 2932 continue; 2933 } 2934 processedProducers << clipId; 2935 // This could be a timeline track producer, reset custom audio/video setting 2936 Xml::removeXmlProperty(currentProd, QLatin1String("set.test_audio")); 2937 Xml::removeXmlProperty(currentProd, QLatin1String("set.test_image")); 2938 bin.appendChild(destDoc.importNode(currentProd, true)); 2939 } 2940 // Check for audio tracks 2941 QMap<QString, bool> tracksType; 2942 int audioTracks = 0; 2943 int videoTracks = 0; 2944 QDomNodeList tracks = sourceDoc.elementsByTagName(QLatin1String("track")); 2945 for (int i = 0; i < tracks.count(); ++i) { 2946 QDomElement currentTrack = tracks.item(i).toElement(); 2947 if (currentTrack.attribute(QLatin1String("hide")) == QLatin1String("video")) { 2948 // Audio track 2949 tracksType.insert(currentTrack.attribute(QLatin1String("producer")), true); 2950 audioTracks++; 2951 } else { 2952 // Video track 2953 if (!blackBg.isEmpty() && blackBg == currentTrack.attribute(QLatin1String("producer"))) { 2954 continue; 2955 } 2956 tracksType.insert(currentTrack.attribute(QLatin1String("producer")), false); 2957 videoTracks++; 2958 } 2959 } 2960 int track = 1; 2961 if (isAudio) { 2962 container.setAttribute(QStringLiteral("masterAudioTrack"), 0); 2963 } else { 2964 track = audioTracks; 2965 container.setAttribute(QStringLiteral("masterTrack"), track); 2966 } 2967 // Process playlists 2968 QDomNodeList playlists = sourceDoc.elementsByTagName(QLatin1String("playlist")); 2969 for (int i = 0; i < playlists.count(); ++i) { 2970 QDomElement currentPlay = playlists.item(i).toElement(); 2971 int position = 0; 2972 bool audioTrack = tracksType.value(currentPlay.attribute("id")); 2973 if (audioTrack != isAudio) { 2974 continue; 2975 } 2976 QDomNodeList elements = currentPlay.childNodes(); 2977 for (int j = 0; j < elements.count(); ++j) { 2978 QDomElement currentElement = elements.item(j).toElement(); 2979 if (currentElement.tagName() == QLatin1String("blank")) { 2980 position += currentElement.attribute(QLatin1String("length")).toInt(); 2981 continue; 2982 } 2983 if (currentElement.tagName() == QLatin1String("entry")) { 2984 QDomElement clipElement = destDoc.createElement(QStringLiteral("clip")); 2985 container.appendChild(clipElement); 2986 int in = currentElement.attribute(QLatin1String("in")).toInt(); 2987 int out = currentElement.attribute(QLatin1String("out")).toInt(); 2988 const QString originalProducer = currentElement.attribute(QLatin1String("producer")); 2989 clipElement.setAttribute(QLatin1String("binid"), producerMap.value(originalProducer)); 2990 clipElement.setAttribute(QLatin1String("in"), in); 2991 clipElement.setAttribute(QLatin1String("out"), out); 2992 clipElement.setAttribute(QLatin1String("position"), position + pos); 2993 clipElement.setAttribute(QLatin1String("track"), track); 2994 // clipElement.setAttribute(QStringLiteral("state"), (int)m_currentState); 2995 clipElement.setAttribute(QStringLiteral("state"), audioTrack ? 2 : 1); 2996 if (audioTrack) { 2997 clipElement.setAttribute(QLatin1String("audioTrack"), 1); 2998 int mirror = audioTrack + videoTracks - track - 1; 2999 if (track <= videoTracks) { 3000 clipElement.setAttribute(QLatin1String("mirrorTrack"), mirror); 3001 } else { 3002 clipElement.setAttribute(QLatin1String("mirrorTrack"), -1); 3003 } 3004 } 3005 if (producerSpeed.contains(originalProducer)) { 3006 clipElement.setAttribute(QStringLiteral("speed"), producerSpeed.value(originalProducer)); 3007 } else { 3008 clipElement.setAttribute(QStringLiteral("speed"), 1); 3009 } 3010 position += (out - in + 1); 3011 QDomNodeList effects = currentElement.elementsByTagName(QLatin1String("filter")); 3012 if (effects.count() == 0) { 3013 continue; 3014 } 3015 QDomElement effectsList = destDoc.createElement(QStringLiteral("effects")); 3016 clipElement.appendChild(effectsList); 3017 effectsList.setAttribute(QStringLiteral("parentIn"), in); 3018 for (int k = 0; k < effects.count(); ++k) { 3019 QDomElement effect = effects.item(k).toElement(); 3020 QString filterId = Xml::getXmlProperty(effect, QLatin1String("kdenlive_id")); 3021 QDomElement clipEffect = destDoc.createElement(QStringLiteral("effect")); 3022 effectsList.appendChild(clipEffect); 3023 clipEffect.setAttribute(QStringLiteral("id"), filterId); 3024 QDomNodeList properties = effect.childNodes(); 3025 if (effect.hasAttribute(QStringLiteral("in"))) { 3026 clipEffect.setAttribute(QStringLiteral("in"), effect.attribute(QStringLiteral("in"))); 3027 } 3028 if (effect.hasAttribute(QStringLiteral("out"))) { 3029 clipEffect.setAttribute(QStringLiteral("out"), effect.attribute(QStringLiteral("out"))); 3030 } 3031 for (int l = 0; l < properties.count(); ++l) { 3032 QDomElement prop = properties.item(l).toElement(); 3033 const QString propName = prop.attribute(QLatin1String("name")); 3034 if (propName == QLatin1String("mlt_service") || propName == QLatin1String("kdenlive_id")) { 3035 continue; 3036 } 3037 Xml::setXmlProperty(clipEffect, propName, prop.text()); 3038 } 3039 } 3040 } 3041 } 3042 track++; 3043 } 3044 if (!isAudio) { 3045 // Compositions 3046 QDomNodeList compositions = sourceDoc.elementsByTagName(QLatin1String("transition")); 3047 for (int i = 0; i < compositions.count(); ++i) { 3048 QDomElement currentCompo = compositions.item(i).toElement(); 3049 if (Xml::getXmlProperty(currentCompo, QLatin1String("internal_added")).toInt() > 0) { 3050 // Track compositing, discard 3051 continue; 3052 } 3053 QDomElement composition = destDoc.createElement(QStringLiteral("composition")); 3054 container.appendChild(composition); 3055 int in = currentCompo.attribute(QLatin1String("in")).toInt(); 3056 int out = currentCompo.attribute(QLatin1String("out")).toInt(); 3057 const QString compoId = Xml::getXmlProperty(currentCompo, QLatin1String("kdenlive_id")); 3058 composition.setAttribute(QLatin1String("position"), in + pos); 3059 composition.setAttribute(QLatin1String("in"), in); 3060 composition.setAttribute(QLatin1String("out"), out); 3061 composition.setAttribute(QLatin1String("composition"), compoId); 3062 int a_track = Xml::getXmlProperty(currentCompo, QLatin1String("a_track")).toInt(); 3063 int b_track = Xml::getXmlProperty(currentCompo, QLatin1String("b_track")).toInt(); 3064 if (!blackBg.isEmpty()) { 3065 a_track--; 3066 b_track--; 3067 } 3068 composition.setAttribute(QLatin1String("a_track"), a_track); 3069 composition.setAttribute(QLatin1String("track"), b_track); 3070 QDomNodeList properties = currentCompo.childNodes(); 3071 for (int l = 0; l < properties.count(); ++l) { 3072 QDomElement prop = properties.item(l).toElement(); 3073 const QString &propName = prop.attribute(QLatin1String("name")); 3074 Xml::setXmlProperty(composition, propName, prop.text()); 3075 } 3076 } 3077 } 3078 qDebug() << "=== GOT CONVERTED DOCUMENT\n\n" << destDoc.toString(); 3079 return destDoc; 3080 } 3081 3082 int TimelineFunctions::spacerMinPos() 3083 { 3084 return spacerMinPosition; 3085 }