File indexing completed on 2024-04-28 04:52:29
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(i18nc("@info:status", "Cannot cut overlapping subtitles"), 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 findPerfectTracks = [](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 int sourceOffset = 0; 1930 if (neededTracksBelow < 0) { 1931 sourceOffset = neededTracksBelow; 1932 } else if (neededTracksAbove < 0) { 1933 sourceOffset = neededTracksAbove; 1934 } 1935 1936 sourceTrackId += qMax(0, sourceTracks.count() - targetTracks.count() - sourceOffset); 1937 1938 if (existingTracksBelow < neededTracksBelow) { 1939 qDebug() << "// UPDATING BELOW TID IX TO:" << neededTracksBelow; 1940 // not enough tracks below, try to paste on upper track 1941 targetTrackId = targetTracks.at(qMin(neededTracksBelow, targetTracks.length() - 1)); 1942 return; 1943 } 1944 1945 if (existingTracksAbove < neededTracksAbove) { 1946 // not enough tracks above, try to paste on lower track 1947 qDebug() << "// UPDATING ABOVE TID IX TO:" << (targetTracks.size() - neededTracksAbove); 1948 targetTrackId = targetTracks.at(qMax(0, targetTracks.size() - neededTracksAbove - 1)); 1949 return; 1950 } 1951 1952 // enough tracks above and below, keep the current 1953 // ensure it is one of the existing tracks 1954 targetTrackId = qBound(targetTracks.first(), targetTrackId, targetTracks.last()); 1955 }; 1956 1957 // Find destination master track 1958 // Check we have enough tracks above/below 1959 if (requestedVideoTracks > 0) { 1960 findPerfectTracks(sourceMasterTrack, sourceTracks.videoIds, trackId, timelineTracks.videoIds); 1961 1962 // Find top-most video track that requires an audio mirror 1963 int topAudioOffset = sourceTracks.videoIds.indexOf(topAudioMirror) - sourceTracks.videoIds.indexOf(sourceMasterTrack); 1964 // Check if we have enough video tracks with mirror at paste track position 1965 if (requestedAudioTracks > 0 && timelineTracks.audioIds.size() <= (timelineTracks.videoIds.indexOf(trackId) + topAudioOffset)) { 1966 int updatedPos = sourceTracks.audioIds.size() - topAudioOffset - 1; 1967 if (updatedPos < 0 || updatedPos >= timelineTracks.videoIds.size()) { 1968 pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500); 1969 semaphore.release(1); 1970 return false; 1971 } 1972 trackId = timelineTracks.videoIds.at(updatedPos); 1973 } 1974 } else if (requestedAudioTracks > 0) { 1975 // Audio only 1976 sourceMasterTrack = copiedItems.documentElement().attribute(QStringLiteral("masterAudioTrack")).toInt(); 1977 findPerfectTracks(sourceMasterTrack, sourceTracks.audioIds, trackId, timelineTracks.audioIds); 1978 } 1979 tracksMap.clear(); 1980 bool audioMaster = false; 1981 int targetMasterIx = timelineTracks.videoIds.indexOf(trackId); 1982 if (targetMasterIx == -1) { 1983 targetMasterIx = timelineTracks.audioIds.indexOf(trackId); 1984 audioMaster = true; 1985 } 1986 1987 int masterOffset = targetMasterIx - sourceMasterTrack; 1988 for (int tk : qAsConst(sourceTracks.videoIds)) { 1989 int newPos = masterOffset + tk; 1990 if (newPos < 0 || newPos >= timelineTracks.videoIds.size()) { 1991 pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500); 1992 semaphore.release(1); 1993 return false; 1994 } 1995 tracksMap.insert(tk, timelineTracks.videoIds.at(newPos)); 1996 // qDebug() << "/// MAPPING SOURCE TRACK: "<<tk<<" TO PROJECT TK: "<<timelineTracks.videoIds.at(newPos)<<" = 1997 // "<<timeline->getTrackMltIndex(timelineTracks.videoIds.at(newPos)); 1998 } 1999 bool audioOffsetCalculated = false; 2000 int audioOffset = 0; 2001 for (const auto &mirror : audioMirrors) { 2002 int videoIx = tracksMap.value(mirror.second); 2003 int mirrorIx = timeline->getMirrorAudioTrackId(videoIx); 2004 if (mirrorIx > 0) { 2005 tracksMap.insert(mirror.first, mirrorIx); 2006 if (!audioOffsetCalculated) { 2007 int oldPosition = mirror.first; 2008 int currentPosition = timeline->getTrackPosition(tracksMap.value(oldPosition)); 2009 audioOffset = currentPosition - oldPosition; 2010 audioOffsetCalculated = true; 2011 } 2012 } 2013 } 2014 if (!audioOffsetCalculated && audioMaster) { 2015 audioOffset = masterOffset; 2016 audioOffsetCalculated = true; 2017 } else if (audioMirrors.size() == 0) { 2018 // We are passing ungrouped audio clips, calculate offset 2019 int sourceAudioTracks = copiedItems.documentElement().attribute(QStringLiteral("audioTracks")).toInt(); 2020 if (sourceAudioTracks > 0) { 2021 audioOffset = timelineTracks.audioIds.count() - sourceAudioTracks; 2022 } 2023 } 2024 for (int oldPos : qAsConst(singleAudioTracks)) { 2025 if (tracksMap.contains(oldPos)) { 2026 continue; 2027 } 2028 int offsetId = oldPos + audioOffset; 2029 if (offsetId < 0 || offsetId >= timelineTracks.audioIds.size()) { 2030 pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500); 2031 semaphore.release(1); 2032 return false; 2033 } 2034 tracksMap.insert(oldPos, timelineTracks.audioIds.at(offsetId)); 2035 } 2036 std::function<void(const QString &)> callBack = [timeline, copiedItems, position, inPos, duration](const QString &binId) { 2037 waitingBinIds.removeAll(binId); 2038 if (waitingBinIds.isEmpty()) { 2039 TimelineFunctions::pasteTimelineClips(timeline, copiedItems, position, inPos, duration); 2040 } 2041 }; 2042 bool clipsImported = false; 2043 int updatedPosition = 0; 2044 int pasteDuration = copiedItems.documentElement().attribute(QStringLiteral("duration")).toInt(); 2045 if (docId == pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))) { 2046 // Check that the bin clips exists in case we try to paste in a copy of original project 2047 QDomNodeList binClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("producer")); 2048 QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Pasted clips")); 2049 for (int i = 0; i < binClips.count(); ++i) { 2050 QDomElement currentProd = binClips.item(i).toElement(); 2051 QString clipId = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:id")); 2052 if (clipId.isEmpty()) { 2053 // Invalid clip, maybe black track from a sequence, ignore 2054 continue; 2055 } 2056 QString clipHash = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:file_hash")); 2057 if (!pCore->projectItemModel()->validateClip(clipId, clipHash)) { 2058 // This clip is different in project and in paste data, create a copy 2059 QString updatedId = QString::number(pCore->projectItemModel()->getFreeClipId()); 2060 Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), updatedId); 2061 mappedIds.insert(clipId, updatedId); 2062 if (folderId.isEmpty()) { 2063 // Folder does not exist 2064 const QString rootId = pCore->projectItemModel()->getRootFolder()->clipId(); 2065 folderId = QString::number(pCore->projectItemModel()->getFreeFolderId()); 2066 pCore->projectItemModel()->requestAddFolder(folderId, i18n("Pasted clips"), rootId, undo, redo); 2067 } 2068 waitingBinIds << updatedId; 2069 clipsImported = true; 2070 pCore->projectItemModel()->requestAddBinClip(updatedId, currentProd, folderId, undo, redo, callBack); 2071 } 2072 } 2073 updatedPosition = position + pasteDuration; 2074 } 2075 2076 if (!docId.isEmpty() && docId != pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))) { 2077 // paste from another document, import bin clips 2078 2079 // Check if the fps matches 2080 QString currentFps = QString::number(pCore->getCurrentFps()); 2081 QString sourceFps = copiedItems.documentElement().attribute(QStringLiteral("fps")); 2082 double ratio = 1.; 2083 if (currentFps != sourceFps && !sourceFps.isEmpty()) { 2084 if (KMessageBox::questionTwoActions( 2085 pCore->window(), 2086 i18n("The source project has a different framerate (%1fps) than your current project.<br/>Clips or keyframes might be messed up.", 2087 sourceFps), 2088 i18n("Pasting Warning"), KGuiItem(i18n("Paste")), KStandardGuiItem::cancel()) != KMessageBox::PrimaryAction) { 2089 semaphore.release(1); 2090 return false; 2091 } 2092 ratio = pCore->getCurrentFps() / sourceFps.toDouble(); 2093 copiedItems.documentElement().setAttribute(QStringLiteral("fps-ratio"), ratio); 2094 } 2095 2096 // Folder in the project for the pasted clips 2097 QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Pasted clips")); 2098 if (folderId.isEmpty()) { 2099 // Folder does not exist 2100 const QString rootId = pCore->projectItemModel()->getRootFolder()->clipId(); 2101 folderId = QString::number(pCore->projectItemModel()->getFreeFolderId()); 2102 pCore->projectItemModel()->requestAddFolder(folderId, i18n("Pasted clips"), rootId, undo, redo); 2103 } 2104 updatedPosition = position + (pasteDuration * ratio); 2105 2106 auto disableProxy = [](QDomElement &producer) { 2107 const QString proxy = Xml::getXmlProperty(producer, QStringLiteral("kdenlive:proxy")); 2108 if (proxy.length() < 4) { 2109 return; 2110 } 2111 const QString resource = Xml::getXmlProperty(producer, QStringLiteral("kdenlive:originalurl")); 2112 if (!resource.isEmpty()) { 2113 Xml::setXmlProperty(producer, QStringLiteral("resource"), resource); 2114 Xml::setXmlProperty(producer, QStringLiteral("kdenlive:proxy"), QStringLiteral("-")); 2115 } 2116 }; 2117 2118 auto useFreeBinId = [](QDomElement &producer, const QString &clipId, QMap<QString, QString> &mappedIds) { 2119 if (!pCore->projectItemModel()->isIdFree(clipId)) { 2120 QString updatedId = QString::number(pCore->projectItemModel()->getFreeClipId()); 2121 Xml::setXmlProperty(producer, QStringLiteral("kdenlive:id"), updatedId); 2122 mappedIds.insert(clipId, updatedId); 2123 return updatedId; 2124 } 2125 return clipId; 2126 }; 2127 2128 auto pasteClip = [disableProxy, callBack, useFreeBinId](const QDomNodeList &clips, int ratio, const QString &folderId, bool &clipsImported, Fun &undo, 2129 Fun &redo){ 2130 for (int i = 0; i < clips.count(); ++i) { 2131 QDomElement currentProd = clips.item(i).toElement(); 2132 QString clipId = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:id")); 2133 if (clipId.isEmpty()) { 2134 // Not a bin clip 2135 continue; 2136 } 2137 2138 // Adjust duration in case of different fps on source and target 2139 if (ratio != 1.) { 2140 int out = currentProd.attribute(QStringLiteral("out")).toInt() * ratio; 2141 int length = Xml::getXmlProperty(currentProd, QStringLiteral("length")).toInt() * ratio; 2142 currentProd.setAttribute(QStringLiteral("out"), out); 2143 Xml::setXmlProperty(currentProd, QStringLiteral("length"), QString::number(length)); 2144 } 2145 2146 // Check if we already have a clip with same hash in pasted clips folder 2147 QString clipHash = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:file_hash")); 2148 QString existingId = pCore->projectItemModel()->validateClipInFolder(folderId, clipHash); 2149 if (!existingId.isEmpty()) { 2150 mappedIds.insert(clipId, existingId); 2151 continue; 2152 } 2153 clipId = useFreeBinId(currentProd, clipId, mappedIds); 2154 2155 // Disable proxy if any when pasting to another document 2156 disableProxy(currentProd); 2157 2158 waitingBinIds << clipId; 2159 clipsImported = true; 2160 bool insert = pCore->projectItemModel()->requestAddBinClip(clipId, currentProd, folderId, undo, redo, callBack); 2161 if (!insert) { 2162 return false; 2163 } 2164 } 2165 return true; 2166 }; 2167 2168 QDomNodeList binClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("producer")); 2169 if (!pasteClip(binClips, ratio, folderId, clipsImported, undo, redo)) { 2170 pCore->displayMessage(i18n("Could not add bin clip"), ErrorMessage, 500); 2171 undo(); 2172 semaphore.release(1); 2173 return false; 2174 } 2175 2176 QDomNodeList chainClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("chain")); 2177 if (!pasteClip(chainClips, ratio, folderId, clipsImported, undo, redo)) { 2178 pCore->displayMessage(i18n("Could not add bin clip"), ErrorMessage, 500); 2179 undo(); 2180 semaphore.release(1); 2181 return false; 2182 } 2183 2184 auto remapClipIds = [](QDomNodeList &elements, const QMap<QString, QString> &map) { 2185 int max = elements.count(); 2186 for (int i = 0; i < max; i++) { 2187 QDomElement e = elements.item(i).toElement(); 2188 const QString currentId = Xml::getXmlProperty(e, QStringLiteral("kdenlive:id")); 2189 if (map.contains(currentId)) { 2190 Xml::setXmlProperty(e, QStringLiteral("kdenlive:id"), map.value(currentId)); 2191 } 2192 } 2193 }; 2194 2195 QDomNodeList sequenceClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("mlt")); 2196 for (int i = 0; i < sequenceClips.count(); ++i) { 2197 QDomElement currentProd = sequenceClips.item(i).toElement(); 2198 QString clipId = currentProd.attribute(QStringLiteral("kdenlive:id")); 2199 const QString uuid = currentProd.attribute(QStringLiteral("kdenlive:uuid")); 2200 int duration = currentProd.attribute(QStringLiteral("kdenlive:duration")).toInt(); 2201 const QString clipname = currentProd.attribute(QStringLiteral("kdenlive:clipname")); 2202 if (clipId.isEmpty()) { 2203 // Not a bin clip 2204 continue; 2205 } 2206 2207 QDomDocument doc; 2208 doc.appendChild(doc.importNode(currentProd, true)); 2209 clipId = useFreeBinId(currentProd, clipId, mappedIds); 2210 2211 // update all bin ids 2212 QDomNodeList prods = doc.documentElement().elementsByTagName(QStringLiteral("producer")); 2213 remapClipIds(prods, mappedIds); 2214 QDomNodeList chains = doc.documentElement().elementsByTagName(QStringLiteral("chain")); 2215 remapClipIds(chains, mappedIds); 2216 QDomNodeList entries = doc.documentElement().elementsByTagName(QStringLiteral("entry")); 2217 remapClipIds(entries, mappedIds); 2218 2219 waitingBinIds << clipId; 2220 clipsImported = true; 2221 std::shared_ptr<Mlt::Producer> xmlProd(new Mlt::Producer(pCore->getProjectProfile(), "xml-string", doc.toString().toUtf8().constData())); 2222 if (!xmlProd->is_valid()) { 2223 qDebug() << ":::: CANNOT IMPORT SEQUENCE: " << clipId; 2224 continue; 2225 } 2226 xmlProd->set("kdenlive:id", clipId.toUtf8().constData()); 2227 xmlProd->set("kdenlive:producer_type", ClipType::Timeline); 2228 xmlProd->set("kdenlive:uuid", uuid.toUtf8().constData()); 2229 xmlProd->set("kdenlive:duration", xmlProd->frames_to_time(duration)); 2230 xmlProd->set("kdenlive:clipname", clipname.toUtf8().constData()); 2231 xmlProd->set("_kdenlive_processed", 1); 2232 Mlt::Service s(*xmlProd.get()); 2233 Mlt::Tractor tractor(s); 2234 std::shared_ptr<Mlt::Producer> prod(new Mlt::Producer(tractor.cut())); 2235 prod->set("id", uuid.toUtf8().constData()); 2236 prod->set("kdenlive:id", clipId.toUtf8().constData()); 2237 prod->set("kdenlive:producer_type", ClipType::Timeline); 2238 prod->set("kdenlive:uuid", uuid.toUtf8().constData()); 2239 prod->set("kdenlive:duration", xmlProd->frames_to_time(duration)); 2240 prod->set("kdenlive:clipname", clipname.toUtf8().constData()); 2241 prod->set("_kdenlive_processed", 1); 2242 bool insert = pCore->projectItemModel()->requestAddBinClip(clipId, prod, folderId, undo, redo, callBack); 2243 if (!insert) { 2244 pCore->displayMessage(i18n("Could not add bin clip"), ErrorMessage, 500); 2245 undo(); 2246 semaphore.release(1); 2247 return false; 2248 } 2249 } 2250 } 2251 2252 if (!clipsImported) { 2253 // Clips from same document, directly proceed to pasting 2254 bool result = TimelineFunctions::pasteTimelineClips(timeline, copiedItems, position, undo, redo, false, inPos, duration); 2255 if (result && updatedPosition > 0) { 2256 pCore->seekMonitor(Kdenlive::ProjectMonitor, updatedPosition); 2257 } 2258 return result; 2259 } 2260 qDebug() << "++++++++++++\nWAITIND FOR BIN INSERTION: " << waitingBinIds << "\n\n+++++++++++++"; 2261 return true; 2262 } 2263 2264 bool TimelineFunctions::pasteTimelineClips(const std::shared_ptr<TimelineItemModel> &timeline, const QDomDocument &copiedItems, int position, int inPos, 2265 int duration) 2266 { 2267 std::function<bool(void)> timeline_undo = []() { return true; }; 2268 std::function<bool(void)> timeline_redo = []() { return true; }; 2269 return TimelineFunctions::pasteTimelineClips(timeline, copiedItems, position, timeline_undo, timeline_redo, true, inPos, duration); 2270 } 2271 2272 bool TimelineFunctions::pasteTimelineClips(const std::shared_ptr<TimelineItemModel> &timeline, QDomDocument copiedItems, int position, Fun &timeline_undo, 2273 Fun &timeline_redo, bool pushToStack, int inPos, int duration) 2274 { 2275 // Wait until all bin clips are inserted 2276 QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip")); 2277 QDomNodeList compositions = copiedItems.documentElement().elementsByTagName(QStringLiteral("composition")); 2278 QDomNodeList subtitles = copiedItems.documentElement().elementsByTagName(QStringLiteral("subtitle")); 2279 int offset = copiedItems.documentElement().attribute(QStringLiteral("offset")).toInt(); 2280 bool res = true; 2281 std::unordered_map<int, int> correspondingIds; 2282 double ratio = 1.0; 2283 if (copiedItems.documentElement().hasAttribute(QStringLiteral("fps-ratio"))) { 2284 ratio = copiedItems.documentElement().attribute(QStringLiteral("fps-ratio")).toDouble(); 2285 offset *= ratio; 2286 } 2287 2288 QDomElement documentMixes = copiedItems.createElement(QStringLiteral("mixes")); 2289 for (int i = 0; i < clips.count(); i++) { 2290 QDomElement prod = clips.at(i).toElement(); 2291 QString originalId = prod.attribute(QStringLiteral("binid")); 2292 if (mappedIds.contains(originalId)) { 2293 // Map id 2294 originalId = mappedIds.value(originalId); 2295 } 2296 if (!pCore->projectItemModel()->hasClip(originalId)) { 2297 // Clip import was not successful, continue 2298 pCore->displayMessage(i18n("All clips were not successfully copied"), ErrorMessage, 500); 2299 continue; 2300 } 2301 int in = prod.attribute(QStringLiteral("in")).toInt(); 2302 int out = prod.attribute(QStringLiteral("out")).toInt(); 2303 int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt()); 2304 if (!timeline->isTrack(curTrackId)) { 2305 // Something is broken 2306 pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), ErrorMessage, 500); 2307 timeline_undo(); 2308 semaphore.release(1); 2309 return false; 2310 } 2311 int pos = prod.attribute(QStringLiteral("position")).toInt(); 2312 if (ratio != 1.0) { 2313 in = in * ratio; 2314 out = out * ratio; 2315 pos = pos * ratio; 2316 } 2317 int newIn = in; 2318 int newOut = out; 2319 if ((inPos > 0 && pos + (out - in) < inPos + offset) || (duration > -1 && (pos > inPos + duration + offset))) { 2320 // Clip outside paste range 2321 continue; 2322 } 2323 if (inPos > 0) { 2324 pos -= inPos; 2325 if (pos < offset) { 2326 newIn = in + (offset - pos); 2327 pos = offset; 2328 } 2329 } 2330 if (duration > -1) { 2331 if (pos + (out - in) > inPos + duration + offset) { 2332 newOut = out - (pos + (out - in) - (inPos + duration + offset)); 2333 } 2334 } 2335 2336 pos -= offset; 2337 double speed = prod.attribute(QStringLiteral("speed")).toDouble(); 2338 bool warp_pitch = false; 2339 if (!qFuzzyCompare(speed, 1.)) { 2340 warp_pitch = prod.attribute(QStringLiteral("warp_pitch")).toInt(); 2341 } 2342 int audioStream = prod.attribute(QStringLiteral("audioStream")).toInt(); 2343 int newId; 2344 bool created = timeline->requestClipCreation(originalId, newId, timeline->getTrackById_const(curTrackId)->trackType(), audioStream, speed, warp_pitch, 2345 timeline_undo, timeline_redo); 2346 if (!created) { 2347 // Something is broken 2348 pCore->displayMessage(i18n("Could not paste items in timeline"), ErrorMessage, 500); 2349 timeline_undo(); 2350 semaphore.release(1); 2351 return false; 2352 } 2353 if (prod.hasAttribute(QStringLiteral("timemap"))) { 2354 // This is a timeremap 2355 timeline->m_allClips[newId]->useTimeRemapProducer(true, timeline_undo, timeline_redo); 2356 if (timeline->m_allClips[newId]->m_producer->parent().type() == mlt_service_chain_type) { 2357 Mlt::Chain fromChain(timeline->m_allClips[newId]->m_producer->parent()); 2358 int count = fromChain.link_count(); 2359 for (int i = 0; i < count; i++) { 2360 QScopedPointer<Mlt::Link> fromLink(fromChain.link(i)); 2361 if (fromLink && fromLink->is_valid() && fromLink->get("mlt_service")) { 2362 if (fromLink->get("mlt_service") == QLatin1String("timeremap")) { 2363 // Found a timeremap effect, read params 2364 fromLink->set("time_map", prod.attribute(QStringLiteral("timemap")).toUtf8().constData()); 2365 fromLink->set("pitch", prod.attribute(QStringLiteral("timepitch")).toInt()); 2366 fromLink->set("image_mode", prod.attribute(QStringLiteral("timeblend")).toUtf8().constData()); 2367 break; 2368 } 2369 } 2370 } 2371 } 2372 } 2373 if (timeline->m_allClips[newId]->m_endlessResize) { 2374 out = out - in; 2375 in = 0; 2376 timeline->m_allClips[newId]->m_producer->set("length", out + 1); 2377 timeline->m_allClips[newId]->m_producer->set("out", out); 2378 } 2379 timeline->m_allClips[newId]->setInOut(in, out); 2380 int targetId = prod.attribute(QStringLiteral("id")).toInt(); 2381 int targetPlaylist = prod.attribute(QStringLiteral("playlist")).toInt(); 2382 if (targetPlaylist > 0) { 2383 timeline->m_allClips[newId]->setSubPlaylistIndex(targetPlaylist, curTrackId); 2384 } 2385 correspondingIds[targetId] = newId; 2386 std::shared_ptr<EffectStackModel> destStack = timeline->getClipEffectStackModel(newId); 2387 destStack->fromXml(prod.firstChildElement(QStringLiteral("effects")), timeline_undo, timeline_redo); 2388 if (newIn != in) { 2389 int newSize = out - newIn + 1; 2390 res = res && timeline->requestItemResize(newId, newSize, false, true, timeline_undo, timeline_redo); 2391 if (res) { 2392 std::shared_ptr<EffectStackModel> sourceStack = timeline->getClipEffectStackModel(newId); 2393 sourceStack->cleanFadeEffects(true, timeline_undo, timeline_redo); 2394 } 2395 // TODO manage mixes 2396 } 2397 if (newOut != out) { 2398 int newSize = newOut - newIn; 2399 res = res && timeline->requestItemResize(newId, newSize, true, true, timeline_undo, timeline_redo); 2400 if (res) { 2401 std::shared_ptr<EffectStackModel> sourceStack = timeline->getClipEffectStackModel(newId); 2402 sourceStack->cleanFadeEffects(false, timeline_undo, timeline_redo); 2403 } 2404 // TODO manage mixes 2405 } 2406 res = res && timeline->getTrackById(curTrackId)->requestClipInsertion(newId, position + pos, true, true, timeline_undo, timeline_redo); 2407 // paste effects 2408 if (!res) { 2409 qDebug() << "=== COULD NOT PASTE CLIP: " << newId << " ON TRACK: " << curTrackId << " AT: " << position; 2410 break; 2411 } 2412 // Mixes (same track transitions) 2413 if (prod.hasChildNodes()) { 2414 // TODO: adjust position/duration with inPos / duration 2415 QDomNodeList mixes = prod.elementsByTagName(QLatin1String("mix")); 2416 if (!mixes.isEmpty()) { 2417 QDomElement mix = mixes.at(0).toElement(); 2418 if (mix.tagName() == QLatin1String("mix")) { 2419 mix.setAttribute(QStringLiteral("tid"), curTrackId); 2420 documentMixes.appendChild(mix); 2421 } 2422 } 2423 } 2424 } 2425 // Process mix insertion 2426 QDomNodeList mixes = documentMixes.childNodes(); 2427 for (int k = 0; k < mixes.count(); k++) { 2428 QDomElement mix = mixes.at(k).toElement(); 2429 int originalFirstClipId = mix.attribute(QLatin1String("firstClip")).toInt(); 2430 int originalSecondClipId = mix.attribute(QLatin1String("secondClip")).toInt(); 2431 if (correspondingIds.count(originalFirstClipId) > 0 && correspondingIds.count(originalSecondClipId) > 0) { 2432 QVector<QPair<QString, QVariant>> params; 2433 QDomNodeList paramsXml = mix.elementsByTagName(QLatin1String("param")); 2434 for (int j = 0; j < paramsXml.count(); j++) { 2435 QDomElement e = paramsXml.at(j).toElement(); 2436 params.append({e.attribute(QLatin1String("name")), e.text()}); 2437 } 2438 std::pair<QString, QVector<QPair<QString, QVariant>>> mixParams = {mix.attribute(QLatin1String("asset")), params}; 2439 MixInfo mixData; 2440 mixData.firstClipId = correspondingIds[originalFirstClipId]; 2441 mixData.secondClipId = correspondingIds[originalSecondClipId]; 2442 mixData.firstClipInOut.second = mix.attribute(QLatin1String("mixEnd")).toInt() * ratio; 2443 mixData.secondClipInOut.first = mix.attribute(QLatin1String("mixStart")).toInt() * ratio; 2444 mixData.mixOffset = mix.attribute(QLatin1String("mixOffset")).toInt() * ratio; 2445 std::pair<int, int> tracks = {mix.attribute(QLatin1String("a_track")).toInt(), mix.attribute(QLatin1String("b_track")).toInt()}; 2446 if (tracks.first == tracks.second) { 2447 tracks = {0, 1}; 2448 } 2449 timeline->getTrackById_const(mix.attribute(QLatin1String("tid")).toInt())->createMix(mixData, mixParams, tracks, true); 2450 } 2451 } 2452 // Compositions 2453 if (res) { 2454 for (int i = 0; res && i < compositions.count(); i++) { 2455 QDomElement prod = compositions.at(i).toElement(); 2456 QString originalId = prod.attribute(QStringLiteral("composition")); 2457 int in = prod.attribute(QStringLiteral("in")).toInt() * ratio; 2458 int out = prod.attribute(QStringLiteral("out")).toInt() * ratio; 2459 int pos = prod.attribute(QStringLiteral("position")).toInt() * ratio - offset; 2460 int newPos = pos; 2461 if (inPos > 0) { 2462 newPos -= inPos; 2463 } 2464 int compoDuration = out - in + 1; 2465 int compoDuration2 = out - in + 1; 2466 if (newPos < 0) { 2467 // resize composition 2468 compoDuration += newPos; 2469 newPos = 0; 2470 } 2471 if (duration > -1 && (newPos + compoDuration > inPos + duration)) { 2472 compoDuration2 = inPos + duration - newPos; 2473 } 2474 if (compoDuration2 <= 0) { 2475 continue; 2476 } 2477 int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt()); 2478 int trackOffset = Xml::getXmlProperty(prod, QStringLiteral("b_track")).toInt() - Xml::getXmlProperty(prod, QStringLiteral("a_track")).toInt(); 2479 // Add 1 to account for the black track 2480 int aTrackPos = timeline->getTrackPosition(curTrackId) - trackOffset + 1; 2481 int atrackId = -1; 2482 if (aTrackPos > 0 && aTrackPos < timeline->getTracksCount()) { 2483 atrackId = timeline->getTrackIndexFromPosition(aTrackPos - 1); 2484 } 2485 if (atrackId > -1 && !timeline->isAudioTrack(atrackId)) { 2486 // Ok, track found 2487 } else { 2488 aTrackPos = 0; 2489 } 2490 2491 int newId; 2492 auto transProps = std::make_unique<Mlt::Properties>(); 2493 QDomNodeList props = prod.elementsByTagName(QStringLiteral("property")); 2494 for (int j = 0; j < props.count(); j++) { 2495 transProps->set(props.at(j).toElement().attribute(QStringLiteral("name")).toUtf8().constData(), 2496 props.at(j).toElement().text().toUtf8().constData()); 2497 } 2498 res = res && timeline->requestCompositionCreation(originalId, out - in + 1, std::move(transProps), newId, timeline_undo, timeline_redo); 2499 if (newPos != pos) { 2500 // transition start resized 2501 timeline->requestItemResize(newId, compoDuration, false, true, timeline_undo, timeline_redo, false); 2502 } 2503 if (compoDuration != compoDuration2) { 2504 timeline->requestItemResize(newId, compoDuration2, true, true, timeline_undo, timeline_redo, false); 2505 } 2506 res = res && timeline->requestCompositionMove(newId, curTrackId, aTrackPos, position + newPos, true, true, timeline_undo, timeline_redo); 2507 } 2508 } 2509 if (res && !subtitles.isEmpty()) { 2510 auto subModel = timeline->getSubtitleModel(); 2511 if (!subModel) { 2512 // This timeline doesn't yet have subtitles, initiate 2513 pCore->window()->slotShowSubtitles(true); 2514 subModel = timeline->getSubtitleModel(); 2515 } 2516 for (int i = 0; res && i < subtitles.count(); i++) { 2517 QDomElement prod = subtitles.at(i).toElement(); 2518 int in = prod.attribute(QStringLiteral("in")).toInt() * ratio - offset; 2519 int out = prod.attribute(QStringLiteral("out")).toInt() * ratio - offset; 2520 QString text = prod.attribute(QStringLiteral("text")); 2521 res = res && subModel->addSubtitle(GenTime(position + in, pCore->getCurrentFps()), GenTime(position + out, pCore->getCurrentFps()), text, 2522 timeline_undo, timeline_redo); 2523 } 2524 } 2525 if (!res) { 2526 timeline_undo(); 2527 pCore->displayMessage(i18n("Could not paste items in timeline"), ErrorMessage, 500); 2528 semaphore.release(1); 2529 return false; 2530 } 2531 // Rebuild groups 2532 const QString groupsData = copiedItems.documentElement().firstChildElement(QStringLiteral("groups")).text(); 2533 if (!groupsData.isEmpty()) { 2534 timeline->m_groups->fromJsonWithOffset(groupsData, tracksMap, position - offset, ratio, timeline_undo, timeline_redo); 2535 } 2536 // Ensure to clear selection in undo/redo too. 2537 Fun unselect = [timeline]() { 2538 qDebug() << "starting undo or redo. Selection " << timeline->m_currentSelection.size(); 2539 timeline->requestClearSelection(); 2540 qDebug() << "after Selection " << timeline->m_currentSelection.size(); 2541 return true; 2542 }; 2543 PUSH_FRONT_LAMBDA(unselect, timeline_undo); 2544 PUSH_FRONT_LAMBDA(unselect, timeline_redo); 2545 // UPDATE_UNDO_REDO_NOLOCK(timeline_redo, timeline_undo, undo, redo); 2546 if (pushToStack) { 2547 pCore->pushUndo(timeline_undo, timeline_redo, i18n("Paste timeline clips")); 2548 } 2549 semaphore.release(1); 2550 return true; 2551 } 2552 2553 bool TimelineFunctions::requestDeleteBlankAt(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position, bool affectAllTracks) 2554 { 2555 // Check we have blank at position 2556 int startPos = -1; 2557 int endPos = -1; 2558 if (affectAllTracks) { 2559 for (const auto &track : timeline->m_allTracks) { 2560 if (!track->isLocked()) { 2561 if (!track->isBlankAt(position)) { 2562 return false; 2563 } 2564 startPos = track->getBlankStart(position) - 1; 2565 endPos = track->getBlankEnd(position) + 2; 2566 if (startPos > -1) { 2567 std::unordered_set<int> clips = timeline->getItemsInRange(trackId, startPos, endPos); 2568 if (clips.size() == 2) { 2569 auto it = clips.begin(); 2570 int firstCid = *it; 2571 ++it; 2572 int lastCid = *it; 2573 if (timeline->m_groups->isInGroup(firstCid)) { 2574 int groupId = timeline->m_groups->getRootId(firstCid); 2575 std::unordered_set<int> all_children = timeline->m_groups->getLeaves(groupId); 2576 if (all_children.find(lastCid) != all_children.end()) { 2577 return false; 2578 } 2579 } 2580 } 2581 } 2582 } 2583 } 2584 // check subtitle track 2585 if (timeline->getSubtitleModel() && !timeline->getSubtitleModel()->isLocked()) { 2586 if (!timeline->getSubtitleModel()->isBlankAt(position)) { 2587 return false; 2588 } 2589 startPos = timeline->getSubtitleModel()->getBlankStart(position) - 1; 2590 endPos = timeline->getSubtitleModel()->getBlankEnd(position) + 1; 2591 if (startPos > -1) { 2592 std::unordered_set<int> clips = timeline->getItemsInRange(trackId, startPos, endPos); 2593 if (clips.size() == 2) { 2594 auto it = clips.begin(); 2595 int firstCid = *it; 2596 ++it; 2597 int lastCid = *it; 2598 if (timeline->m_groups->isInGroup(firstCid)) { 2599 int groupId = timeline->m_groups->getRootId(firstCid); 2600 std::unordered_set<int> all_children = timeline->m_groups->getLeaves(groupId); 2601 if (all_children.find(lastCid) != all_children.end()) { 2602 return false; 2603 } 2604 } 2605 } 2606 } 2607 } 2608 } else { 2609 // Check we have a blank and that it is in not between 2 grouped clips 2610 if (timeline->trackIsLocked(trackId)) { 2611 timeline->flashLock(trackId); 2612 return false; 2613 } 2614 if (timeline->isSubtitleTrack(trackId)) { 2615 // Subtitle track 2616 if (!timeline->getSubtitleModel()->isBlankAt(position)) { 2617 return false; 2618 } 2619 startPos = timeline->getSubtitleModel()->getBlankStart(position) - 1; 2620 endPos = timeline->getSubtitleModel()->getBlankEnd(position) + 1; 2621 } else { 2622 if (!timeline->getTrackById_const(trackId)->isBlankAt(position)) { 2623 return false; 2624 } 2625 startPos = timeline->getTrackById_const(trackId)->getBlankStart(position) - 1; 2626 endPos = timeline->getTrackById_const(trackId)->getBlankEnd(position) + 2; 2627 } 2628 if (startPos > -1) { 2629 std::unordered_set<int> clips = timeline->getItemsInRange(trackId, startPos, endPos); 2630 if (clips.size() == 2) { 2631 auto it = clips.begin(); 2632 int firstCid = *it; 2633 ++it; 2634 int lastCid = *it; 2635 if (timeline->m_groups->isInGroup(firstCid)) { 2636 int groupId = timeline->m_groups->getRootId(firstCid); 2637 std::unordered_set<int> all_children = timeline->m_groups->getLeaves(groupId); 2638 if (all_children.find(lastCid) != all_children.end()) { 2639 return false; 2640 } 2641 } 2642 } 2643 } 2644 } 2645 std::pair<int, int> spacerOp = requestSpacerStartOperation(timeline, affectAllTracks ? -1 : trackId, position); 2646 int cid = spacerOp.first; 2647 if (cid == -1 || spacerOp.second == -1) { 2648 return false; 2649 } 2650 int start = timeline->getItemPosition(cid); 2651 int spaceStart = start - spacerOp.second; 2652 if (spaceStart >= start) { 2653 return false; 2654 } 2655 // Start undoable command 2656 std::function<bool(void)> undo = []() { return true; }; 2657 std::function<bool(void)> redo = []() { return true; }; 2658 requestSpacerEndOperation(timeline, cid, start, spaceStart, affectAllTracks ? -1 : trackId, KdenliveSettings::lockedGuides() ? -1 : position, undo, redo); 2659 return true; 2660 } 2661 2662 bool TimelineFunctions::requestDeleteAllBlanksFrom(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position) 2663 { 2664 // Abort if track is locked 2665 if (timeline->trackIsLocked(trackId)) { 2666 timeline->flashLock(trackId); 2667 return false; 2668 } 2669 // Start undoable command 2670 std::function<bool(void)> undo = []() { return true; }; 2671 std::function<bool(void)> redo = []() { return true; }; 2672 if (timeline->isSubtitleTrack(trackId)) { 2673 // Subtitle track 2674 int blankStart = timeline->getSubtitleModel()->getNextBlankStart(position); 2675 if (blankStart == -1) { 2676 return false; 2677 } 2678 while (blankStart != -1) { 2679 std::pair<int, int> spacerOp = requestSpacerStartOperation(timeline, trackId, blankStart, true); 2680 int cid = spacerOp.first; 2681 if (cid == -1) { 2682 break; 2683 } 2684 int start = timeline->getItemPosition(cid); 2685 // Start undoable command 2686 std::function<bool(void)> local_undo = []() { return true; }; 2687 std::function<bool(void)> local_redo = []() { return true; }; 2688 if (blankStart < start) { 2689 if (!requestSpacerEndOperation(timeline, cid, start, blankStart, trackId, KdenliveSettings::lockedGuides() ? -1 : position, local_undo, 2690 local_redo, false)) { 2691 // Failed to remove blank, maybe blocked because of a group. Pass to the next one 2692 blankStart = start; 2693 } else { 2694 UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); 2695 } 2696 } else { 2697 if (timeline->getSubtitleModel()->isBlankAt(blankStart)) { 2698 blankStart = timeline->getSubtitleModel()->getBlankEnd(blankStart) + 1; 2699 if (blankStart == 1) { 2700 break; 2701 } 2702 } else { 2703 blankStart = start + timeline->getItemPlaytime(cid) + 1; 2704 } 2705 } 2706 int nextBlank = timeline->getSubtitleModel()->getNextBlankStart(blankStart); 2707 if (nextBlank == blankStart) { 2708 blankStart = timeline->getSubtitleModel()->getBlankEnd(blankStart) + 1; 2709 nextBlank = timeline->getSubtitleModel()->getNextBlankStart(blankStart); 2710 if (nextBlank == blankStart) { 2711 break; 2712 } 2713 } 2714 if (nextBlank < blankStart) { 2715 // Done 2716 break; 2717 } 2718 blankStart = nextBlank; 2719 } 2720 } else { 2721 int blankStart = timeline->getTrackById_const(trackId)->getNextBlankStart(position); 2722 if (blankStart == -1) { 2723 return false; 2724 } 2725 while (blankStart != -1) { 2726 std::pair<int, int> spacerOp = requestSpacerStartOperation(timeline, trackId, blankStart, true); 2727 int cid = spacerOp.first; 2728 if (cid == -1) { 2729 break; 2730 } 2731 int start = timeline->getItemPosition(cid); 2732 // Start undoable command 2733 std::function<bool(void)> local_undo = []() { return true; }; 2734 std::function<bool(void)> local_redo = []() { return true; }; 2735 if (blankStart < start) { 2736 if (!requestSpacerEndOperation(timeline, cid, start, blankStart, trackId, KdenliveSettings::lockedGuides() ? -1 : position, local_undo, 2737 local_redo, false)) { 2738 // Failed to remove blank, maybe blocked because of a group. Pass to the next one 2739 blankStart = start; 2740 } else { 2741 UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); 2742 } 2743 } else { 2744 if (timeline->getTrackById_const(trackId)->isBlankAt(blankStart)) { 2745 blankStart = timeline->getTrackById_const(trackId)->getBlankEnd(blankStart) + 1; 2746 } else { 2747 blankStart = start + timeline->getItemPlaytime(cid); 2748 } 2749 } 2750 int nextBlank = timeline->getTrackById_const(trackId)->getNextBlankStart(blankStart); 2751 if (nextBlank == blankStart) { 2752 blankStart = timeline->getTrackById_const(trackId)->getBlankEnd(blankStart) + 1; 2753 nextBlank = timeline->getTrackById_const(trackId)->getNextBlankStart(blankStart); 2754 if (nextBlank == blankStart) { 2755 break; 2756 } 2757 } 2758 if (nextBlank < blankStart) { 2759 // Done 2760 break; 2761 } 2762 blankStart = nextBlank; 2763 } 2764 } 2765 pCore->pushUndo(undo, redo, i18n("Remove space on track")); 2766 return true; 2767 } 2768 2769 bool TimelineFunctions::requestDeleteAllClipsFrom(const std::shared_ptr<TimelineItemModel> &timeline, int trackId, int position) 2770 { 2771 // Abort if track is locked 2772 if (timeline->trackIsLocked(trackId)) { 2773 timeline->flashLock(trackId); 2774 return false; 2775 } 2776 // Start undoable command 2777 std::function<bool(void)> undo = []() { return true; }; 2778 std::function<bool(void)> redo = []() { return true; }; 2779 std::unordered_set<int> items; 2780 if (timeline->isSubtitleTrack(trackId)) { 2781 // Subtitle track 2782 items = timeline->getSubtitleModel()->getItemsInRange(position, -1); 2783 } else { 2784 items = timeline->getTrackById_const(trackId)->getClipsInRange(position, -1); 2785 } 2786 if (items.size() == 0) { 2787 return false; 2788 } 2789 for (int id : items) { 2790 timeline->requestItemDeletion(id, undo, redo); 2791 } 2792 pCore->pushUndo(undo, redo, i18n("Delete clips on track")); 2793 return true; 2794 } 2795 2796 QDomDocument TimelineFunctions::extractClip(const std::shared_ptr<TimelineItemModel> &timeline, int cid, const QString &binId) 2797 { 2798 int tid = timeline->getClipTrackId(cid); 2799 int pos = timeline->getClipPosition(cid); 2800 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(binId); 2801 QDomDocument sourceDoc; 2802 QDomDocument destDoc; 2803 if (!Xml::docContentFromFile(sourceDoc, clip->clipUrl(), false)) { 2804 return destDoc; 2805 } 2806 QDomElement container = destDoc.createElement(QStringLiteral("kdenlive-scene")); 2807 destDoc.appendChild(container); 2808 QDomElement bin = destDoc.createElement(QStringLiteral("bin")); 2809 container.appendChild(bin); 2810 bool isAudio = timeline->isAudioTrack(tid); 2811 container.setAttribute(QStringLiteral("offset"), pos); 2812 container.setAttribute(QStringLiteral("documentid"), QStringLiteral("000000")); 2813 // Process producers 2814 QList<int> processedProducers; 2815 QString blackBg; 2816 QMap<QString, int> producerMap; 2817 QMap<QString, double> producerSpeed; 2818 QMap<QString, int> producerSpeedResource; 2819 QDomNodeList producers = sourceDoc.elementsByTagName(QLatin1String("producer")); 2820 for (int i = 0; i < producers.count(); ++i) { 2821 QDomElement currentProd = producers.item(i).toElement(); 2822 bool ok; 2823 int clipId = Xml::getXmlProperty(currentProd, QLatin1String("kdenlive:id")).toInt(&ok); 2824 if (!ok) { 2825 // Check if this is a black bg track 2826 if (Xml::hasXmlProperty(currentProd, QLatin1String("kdenlive:playlistid"))) { 2827 // This is the black bg track 2828 blackBg = currentProd.attribute(QStringLiteral("id")); 2829 continue; 2830 } 2831 const QString resource = Xml::getXmlProperty(currentProd, QLatin1String("resource")); 2832 qDebug() << "===== CLIP NOT FOUND: " << resource; 2833 if (producerSpeedResource.contains(resource)) { 2834 clipId = producerSpeedResource.value(resource); 2835 qDebug() << "===== GOT PREVIOUS ID: " << clipId; 2836 QString baseProducerId; 2837 int baseProducerClipId = 0; 2838 QMapIterator<QString, int> m(producerMap); 2839 while (m.hasNext()) { 2840 m.next(); 2841 if (m.value() == clipId) { 2842 baseProducerId = m.key(); 2843 baseProducerClipId = m.value(); 2844 qDebug() << "=== FOUND PRODUCER FOR ID: " << m.key(); 2845 break; 2846 } 2847 } 2848 if (!baseProducerId.isEmpty()) { 2849 producerSpeed.insert(currentProd.attribute(QLatin1String("id")), producerSpeed.value(baseProducerId)); 2850 producerMap.insert(currentProd.attribute(QLatin1String("id")), baseProducerClipId); 2851 qDebug() << "/// INSERTING PRODUCERMAP: " << currentProd.attribute(QLatin1String("id")) << "=" << baseProducerClipId; 2852 } 2853 // Producer already processed; 2854 continue; 2855 } else { 2856 clipId = pCore->projectItemModel()->getFreeClipId(); 2857 } 2858 Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), QString::number(clipId)); 2859 qDebug() << "=== UNKNOWN CLIP FOUND: " << Xml::getXmlProperty(currentProd, QLatin1String("resource")); 2860 } 2861 producerMap.insert(currentProd.attribute(QLatin1String("id")), clipId); 2862 qDebug() << "/// INSERTING SOURCE PRODUCERMAP: " << currentProd.attribute(QLatin1String("id")) << "=" << clipId; 2863 QString mltService = Xml::getXmlProperty(currentProd, QStringLiteral("mlt_service")); 2864 if (mltService == QLatin1String("timewarp")) { 2865 // Speed producer 2866 double speed = Xml::getXmlProperty(currentProd, QStringLiteral("warp_speed")).toDouble(); 2867 Xml::setXmlProperty(currentProd, QStringLiteral("mlt_service"), QStringLiteral("avformat")); 2868 producerSpeedResource.insert(Xml::getXmlProperty(currentProd, QLatin1String("resource")), clipId); 2869 qDebug() << "===== CLIP SPEED RESOURCE: " << Xml::getXmlProperty(currentProd, QLatin1String("resource")) << " = " << clipId; 2870 QString resource = Xml::getXmlProperty(currentProd, QStringLiteral("warp_resource")); 2871 Xml::setXmlProperty(currentProd, QStringLiteral("resource"), resource); 2872 producerSpeed.insert(currentProd.attribute(QLatin1String("id")), speed); 2873 } 2874 if (processedProducers.contains(clipId)) { 2875 // Producer already processed 2876 continue; 2877 } 2878 processedProducers << clipId; 2879 // This could be a timeline track producer, reset custom audio/video setting 2880 Xml::removeXmlProperty(currentProd, QLatin1String("set.test_audio")); 2881 Xml::removeXmlProperty(currentProd, QLatin1String("set.test_image")); 2882 bin.appendChild(destDoc.importNode(currentProd, true)); 2883 } 2884 // Same for chains 2885 QDomNodeList chains = sourceDoc.elementsByTagName(QStringLiteral("chain")); 2886 for (int i = 0; i < chains.count(); ++i) { 2887 QDomElement currentProd = chains.item(i).toElement(); 2888 bool ok; 2889 int clipId = Xml::getXmlProperty(currentProd, QLatin1String("kdenlive:id")).toInt(&ok); 2890 if (!ok) { 2891 const QString resource = Xml::getXmlProperty(currentProd, QLatin1String("resource")); 2892 qDebug() << "===== CLIP NOT FOUND: " << resource; 2893 if (producerSpeedResource.contains(resource)) { 2894 clipId = producerSpeedResource.value(resource); 2895 qDebug() << "===== GOT PREVIOUS ID: " << clipId; 2896 QString baseProducerId; 2897 int baseProducerClipId = 0; 2898 QMapIterator<QString, int> m(producerMap); 2899 while (m.hasNext()) { 2900 m.next(); 2901 if (m.value() == clipId) { 2902 baseProducerId = m.key(); 2903 baseProducerClipId = m.value(); 2904 qDebug() << "=== FOUND PRODUCER FOR ID: " << m.key(); 2905 break; 2906 } 2907 } 2908 if (!baseProducerId.isEmpty()) { 2909 producerSpeed.insert(currentProd.attribute(QLatin1String("id")), producerSpeed.value(baseProducerId)); 2910 producerMap.insert(currentProd.attribute(QLatin1String("id")), baseProducerClipId); 2911 qDebug() << "/// INSERTING PRODUCERMAP: " << currentProd.attribute(QLatin1String("id")) << "=" << baseProducerClipId; 2912 } 2913 // Producer already processed; 2914 continue; 2915 } else { 2916 clipId = pCore->projectItemModel()->getFreeClipId(); 2917 } 2918 Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), QString::number(clipId)); 2919 qDebug() << "=== UNKNOWN CLIP FOUND: " << Xml::getXmlProperty(currentProd, QLatin1String("resource")); 2920 } 2921 producerMap.insert(currentProd.attribute(QLatin1String("id")), clipId); 2922 qDebug() << "/// INSERTING SOURCE PRODUCERMAP: " << currentProd.attribute(QLatin1String("id")) << "=" << clipId; 2923 QString mltService = Xml::getXmlProperty(currentProd, QStringLiteral("mlt_service")); 2924 if (mltService == QLatin1String("timewarp")) { 2925 // Speed producer 2926 double speed = Xml::getXmlProperty(currentProd, QStringLiteral("warp_speed")).toDouble(); 2927 Xml::setXmlProperty(currentProd, QStringLiteral("mlt_service"), QStringLiteral("avformat")); 2928 producerSpeedResource.insert(Xml::getXmlProperty(currentProd, QLatin1String("resource")), clipId); 2929 qDebug() << "===== CLIP SPEED RESOURCE: " << Xml::getXmlProperty(currentProd, QLatin1String("resource")) << " = " << clipId; 2930 QString resource = Xml::getXmlProperty(currentProd, QStringLiteral("warp_resource")); 2931 Xml::setXmlProperty(currentProd, QStringLiteral("resource"), resource); 2932 producerSpeed.insert(currentProd.attribute(QLatin1String("id")), speed); 2933 } 2934 if (processedProducers.contains(clipId)) { 2935 // Producer already processed 2936 continue; 2937 } 2938 processedProducers << clipId; 2939 // This could be a timeline track producer, reset custom audio/video setting 2940 Xml::removeXmlProperty(currentProd, QLatin1String("set.test_audio")); 2941 Xml::removeXmlProperty(currentProd, QLatin1String("set.test_image")); 2942 bin.appendChild(destDoc.importNode(currentProd, true)); 2943 } 2944 // Check for audio tracks 2945 QMap<QString, bool> tracksType; 2946 int audioTracks = 0; 2947 int videoTracks = 0; 2948 QDomNodeList tracks = sourceDoc.elementsByTagName(QLatin1String("track")); 2949 for (int i = 0; i < tracks.count(); ++i) { 2950 QDomElement currentTrack = tracks.item(i).toElement(); 2951 if (currentTrack.attribute(QLatin1String("hide")) == QLatin1String("video")) { 2952 // Audio track 2953 tracksType.insert(currentTrack.attribute(QLatin1String("producer")), true); 2954 audioTracks++; 2955 } else { 2956 // Video track 2957 if (!blackBg.isEmpty() && blackBg == currentTrack.attribute(QLatin1String("producer"))) { 2958 continue; 2959 } 2960 tracksType.insert(currentTrack.attribute(QLatin1String("producer")), false); 2961 videoTracks++; 2962 } 2963 } 2964 int track = 1; 2965 if (isAudio) { 2966 container.setAttribute(QStringLiteral("masterAudioTrack"), 0); 2967 } else { 2968 track = audioTracks; 2969 container.setAttribute(QStringLiteral("masterTrack"), track); 2970 } 2971 // Process playlists 2972 QDomNodeList playlists = sourceDoc.elementsByTagName(QLatin1String("playlist")); 2973 for (int i = 0; i < playlists.count(); ++i) { 2974 QDomElement currentPlay = playlists.item(i).toElement(); 2975 int position = 0; 2976 bool audioTrack = tracksType.value(currentPlay.attribute("id")); 2977 if (audioTrack != isAudio) { 2978 continue; 2979 } 2980 QDomNodeList elements = currentPlay.childNodes(); 2981 for (int j = 0; j < elements.count(); ++j) { 2982 QDomElement currentElement = elements.item(j).toElement(); 2983 if (currentElement.tagName() == QLatin1String("blank")) { 2984 position += currentElement.attribute(QLatin1String("length")).toInt(); 2985 continue; 2986 } 2987 if (currentElement.tagName() == QLatin1String("entry")) { 2988 QDomElement clipElement = destDoc.createElement(QStringLiteral("clip")); 2989 container.appendChild(clipElement); 2990 int in = currentElement.attribute(QLatin1String("in")).toInt(); 2991 int out = currentElement.attribute(QLatin1String("out")).toInt(); 2992 const QString originalProducer = currentElement.attribute(QLatin1String("producer")); 2993 clipElement.setAttribute(QLatin1String("binid"), producerMap.value(originalProducer)); 2994 clipElement.setAttribute(QLatin1String("in"), in); 2995 clipElement.setAttribute(QLatin1String("out"), out); 2996 clipElement.setAttribute(QLatin1String("position"), position + pos); 2997 clipElement.setAttribute(QLatin1String("track"), track); 2998 // clipElement.setAttribute(QStringLiteral("state"), (int)m_currentState); 2999 clipElement.setAttribute(QStringLiteral("state"), audioTrack ? 2 : 1); 3000 if (audioTrack) { 3001 clipElement.setAttribute(QLatin1String("audioTrack"), 1); 3002 int mirror = audioTrack + videoTracks - track - 1; 3003 if (track <= videoTracks) { 3004 clipElement.setAttribute(QLatin1String("mirrorTrack"), mirror); 3005 } else { 3006 clipElement.setAttribute(QLatin1String("mirrorTrack"), -1); 3007 } 3008 } 3009 if (producerSpeed.contains(originalProducer)) { 3010 clipElement.setAttribute(QStringLiteral("speed"), producerSpeed.value(originalProducer)); 3011 } else { 3012 clipElement.setAttribute(QStringLiteral("speed"), 1); 3013 } 3014 position += (out - in + 1); 3015 QDomNodeList effects = currentElement.elementsByTagName(QLatin1String("filter")); 3016 if (effects.count() == 0) { 3017 continue; 3018 } 3019 QDomElement effectsList = destDoc.createElement(QStringLiteral("effects")); 3020 clipElement.appendChild(effectsList); 3021 effectsList.setAttribute(QStringLiteral("parentIn"), in); 3022 for (int k = 0; k < effects.count(); ++k) { 3023 QDomElement effect = effects.item(k).toElement(); 3024 QString filterId = Xml::getXmlProperty(effect, QLatin1String("kdenlive_id")); 3025 QDomElement clipEffect = destDoc.createElement(QStringLiteral("effect")); 3026 effectsList.appendChild(clipEffect); 3027 clipEffect.setAttribute(QStringLiteral("id"), filterId); 3028 QDomNodeList properties = effect.childNodes(); 3029 if (effect.hasAttribute(QStringLiteral("in"))) { 3030 clipEffect.setAttribute(QStringLiteral("in"), effect.attribute(QStringLiteral("in"))); 3031 } 3032 if (effect.hasAttribute(QStringLiteral("out"))) { 3033 clipEffect.setAttribute(QStringLiteral("out"), effect.attribute(QStringLiteral("out"))); 3034 } 3035 for (int l = 0; l < properties.count(); ++l) { 3036 QDomElement prop = properties.item(l).toElement(); 3037 const QString propName = prop.attribute(QLatin1String("name")); 3038 if (propName == QLatin1String("mlt_service") || propName == QLatin1String("kdenlive_id")) { 3039 continue; 3040 } 3041 Xml::setXmlProperty(clipEffect, propName, prop.text()); 3042 } 3043 } 3044 } 3045 } 3046 track++; 3047 } 3048 if (!isAudio) { 3049 // Compositions 3050 QDomNodeList compositions = sourceDoc.elementsByTagName(QLatin1String("transition")); 3051 for (int i = 0; i < compositions.count(); ++i) { 3052 QDomElement currentCompo = compositions.item(i).toElement(); 3053 if (Xml::getXmlProperty(currentCompo, QLatin1String("internal_added")).toInt() > 0) { 3054 // Track compositing, discard 3055 continue; 3056 } 3057 QDomElement composition = destDoc.createElement(QStringLiteral("composition")); 3058 container.appendChild(composition); 3059 int in = currentCompo.attribute(QLatin1String("in")).toInt(); 3060 int out = currentCompo.attribute(QLatin1String("out")).toInt(); 3061 const QString compoId = Xml::getXmlProperty(currentCompo, QLatin1String("kdenlive_id")); 3062 composition.setAttribute(QLatin1String("position"), in + pos); 3063 composition.setAttribute(QLatin1String("in"), in); 3064 composition.setAttribute(QLatin1String("out"), out); 3065 composition.setAttribute(QLatin1String("composition"), compoId); 3066 int a_track = Xml::getXmlProperty(currentCompo, QLatin1String("a_track")).toInt(); 3067 int b_track = Xml::getXmlProperty(currentCompo, QLatin1String("b_track")).toInt(); 3068 if (!blackBg.isEmpty()) { 3069 a_track--; 3070 b_track--; 3071 } 3072 composition.setAttribute(QLatin1String("a_track"), a_track); 3073 composition.setAttribute(QLatin1String("track"), b_track); 3074 QDomNodeList properties = currentCompo.childNodes(); 3075 for (int l = 0; l < properties.count(); ++l) { 3076 QDomElement prop = properties.item(l).toElement(); 3077 const QString &propName = prop.attribute(QLatin1String("name")); 3078 Xml::setXmlProperty(composition, propName, prop.text()); 3079 } 3080 } 3081 } 3082 qDebug() << "=== GOT CONVERTED DOCUMENT\n\n" << destDoc.toString(); 3083 return destDoc; 3084 } 3085 3086 int TimelineFunctions::spacerMinPos() 3087 { 3088 return spacerMinPosition; 3089 }