File indexing completed on 2024-05-12 08:54:42
0001 /* 0002 SPDX-FileCopyrightText: 2017 Nicolas Carion 0003 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0004 */ 0005 0006 #include "timelinemodel.hpp" 0007 #include "assets/model/assetparametermodel.hpp" 0008 #include "bin/model/markerlistmodel.hpp" 0009 #include "bin/model/markersortmodel.h" 0010 #include "bin/model/subtitlemodel.hpp" 0011 #include "bin/projectclip.h" 0012 #include "bin/projectitemmodel.h" 0013 #include "clipmodel.hpp" 0014 #include "compositionmodel.hpp" 0015 #include "core.h" 0016 #include "doc/docundostack.hpp" 0017 #include "doc/kdenlivedoc.h" 0018 #include "effects/effectsrepository.hpp" 0019 #include "effects/effectstack/model/effectstackmodel.hpp" 0020 #include "groupsmodel.hpp" 0021 #include "kdenlivesettings.h" 0022 #include "profiles/profilemodel.hpp" 0023 #include "snapmodel.hpp" 0024 #include "timeline2/view/previewmanager.h" 0025 #include "timelinefunctions.hpp" 0026 0027 #include "monitor/monitormanager.h" 0028 0029 #include <KLocalizedString> 0030 #include <QCryptographicHash> 0031 #include <QDebug> 0032 #include <QModelIndex> 0033 #include <QThread> 0034 #include <mlt++/MltConsumer.h> 0035 #include <mlt++/MltField.h> 0036 #include <mlt++/MltProfile.h> 0037 #include <mlt++/MltTractor.h> 0038 #include <mlt++/MltTransition.h> 0039 #include <queue> 0040 #include <set> 0041 0042 #include "macros.hpp" 0043 #include <localeHandling.h> 0044 0045 #ifdef CRASH_AUTO_TEST 0046 #include "logger.hpp" 0047 #pragma GCC diagnostic push 0048 #pragma GCC diagnostic ignored "-Wunused-parameter" 0049 #pragma GCC diagnostic ignored "-Wsign-conversion" 0050 #pragma GCC diagnostic ignored "-Wfloat-equal" 0051 #pragma GCC diagnostic ignored "-Wshadow" 0052 #pragma GCC diagnostic ignored "-Wpedantic" 0053 #include <rttr/registration> 0054 #pragma GCC diagnostic pop 0055 RTTR_REGISTRATION 0056 { 0057 using namespace rttr; 0058 registration::class_<TimelineModel>("TimelineModel") 0059 .method("setTrackLockedState", &TimelineModel::setTrackLockedState)(parameter_names("trackId", "lock")) 0060 .method("requestClipMove", select_overload<bool(int, int, int, bool, bool, bool, bool, bool)>(&TimelineModel::requestClipMove))( 0061 parameter_names("clipId", "trackId", "position", "moveMirrorTracks", "updateView", "logUndo", "invalidateTimeline", "revertMove")) 0062 .method("requestCompositionMove", select_overload<bool(int, int, int, bool, bool)>(&TimelineModel::requestCompositionMove))( 0063 parameter_names("compoId", "trackId", "position", "updateView", "logUndo")) 0064 .method("requestClipInsertion", select_overload<bool(const QString &, int, int, int &, bool, bool, bool)>(&TimelineModel::requestClipInsertion))( 0065 parameter_names("binClipId", "trackId", "position", "id", "logUndo", "refreshView", "useTargets")) 0066 .method("requestItemDeletion", select_overload<bool(int, bool)>(&TimelineModel::requestItemDeletion))(parameter_names("clipId", "logUndo")) 0067 .method("requestGroupMove", select_overload<bool(int, int, int, int, bool, bool, bool, bool)>(&TimelineModel::requestGroupMove))( 0068 parameter_names("itemId", "groupId", "delta_track", "delta_pos", "moveMirrorTracks", "updateView", "logUndo", "revertMove")) 0069 .method("requestGroupDeletion", select_overload<bool(int, bool)>(&TimelineModel::requestGroupDeletion))(parameter_names("clipId", "logUndo")) 0070 .method("requestItemResize", select_overload<int(int, int, bool, bool, int, bool)>(&TimelineModel::requestItemResize))( 0071 parameter_names("itemId", "size", "right", "logUndo", "snapDistance", "allowSingleResize")) 0072 .method("requestClipsGroup", select_overload<int(const std::unordered_set<int> &, bool, GroupType)>(&TimelineModel::requestClipsGroup))( 0073 parameter_names("itemIds", "logUndo", "type")) 0074 .method("requestClipUngroup", select_overload<bool(int, bool)>(&TimelineModel::requestClipUngroup))(parameter_names("itemId", "logUndo")) 0075 .method("requestClipsUngroup", &TimelineModel::requestClipsUngroup)(parameter_names("itemIds", "logUndo")) 0076 .method("requestTrackInsertion", select_overload<bool(int, int &, const QString &, bool)>(&TimelineModel::requestTrackInsertion))( 0077 parameter_names("pos", "id", "trackName", "audioTrack")) 0078 .method("requestTrackDeletion", select_overload<bool(int)>(&TimelineModel::requestTrackDeletion))(parameter_names("trackId")) 0079 .method("requestClearSelection", select_overload<bool(bool)>(&TimelineModel::requestClearSelection))(parameter_names("onDeletion")) 0080 .method("requestAddToSelection", &TimelineModel::requestAddToSelection)(parameter_names("itemId", "clear", "singleSelect")) 0081 .method("requestRemoveFromSelection", &TimelineModel::requestRemoveFromSelection)(parameter_names("itemId")) 0082 .method("requestSetSelection", select_overload<bool(const std::unordered_set<int> &)>(&TimelineModel::requestSetSelection))(parameter_names("itemIds")) 0083 .method("requestFakeClipMove", select_overload<bool(int, int, int, bool, bool, bool)>(&TimelineModel::requestFakeClipMove))( 0084 parameter_names("clipId", "trackId", "position", "updateView", "logUndo", "invalidateTimeline")) 0085 .method("requestFakeGroupMove", select_overload<bool(int, int, int, int, bool, bool)>(&TimelineModel::requestFakeGroupMove))( 0086 parameter_names("clipId", "groupId", "delta_track", "delta_pos", "updateView", "logUndo")) 0087 .method("suggestClipMove", 0088 &TimelineModel::suggestClipMove)(parameter_names("clipId", "trackId", "position", "cursorPosition", "snapDistance", "moveMirrorTracks")) 0089 .method("suggestCompositionMove", 0090 &TimelineModel::suggestCompositionMove)(parameter_names("compoId", "trackId", "position", "cursorPosition", "snapDistance")) 0091 // .method("addSnap", &TimelineModel::addSnap)(parameter_names("pos")) 0092 // .method("removeSnap", &TimelineModel::addSnap)(parameter_names("pos")) 0093 // .method("requestCompositionInsertion", select_overload<bool(const QString &, int, int, int, std::unique_ptr<Mlt::Properties>, int &, bool)>( 0094 // &TimelineModel::requestCompositionInsertion))( 0095 // parameter_names("transitionId", "trackId", "position", "length", "transProps", "id", "logUndo")) 0096 .method("requestClipTimeWarp", select_overload<bool(int, double, bool, bool)>(&TimelineModel::requestClipTimeWarp))( 0097 parameter_names("clipId", "speed", "pitchCompensate", "changeDuration")); 0098 } 0099 #else 0100 #define TRACE_CONSTR(...) 0101 #define TRACE_STATIC(...) 0102 #define TRACE_RES(...) 0103 #define TRACE(...) 0104 #endif 0105 0106 int TimelineModel::seekDuration = 30000; 0107 0108 TimelineModel::TimelineModel(const QUuid &uuid, std::weak_ptr<DocUndoStack> undo_stack) 0109 : QAbstractItemModel_shared_from_this() 0110 , m_blockRefresh(false) 0111 , m_uuid(uuid) 0112 , m_tractor(new Mlt::Tractor(pCore->getProjectProfile())) 0113 , m_masterStack(nullptr) 0114 , m_masterService(nullptr) 0115 , m_timelinePreview(nullptr) 0116 , m_snaps(new SnapModel()) 0117 , m_undoStack(std::move(undo_stack)) 0118 , m_blackClip(new Mlt::Producer(pCore->getProjectProfile(), "color:black")) 0119 , m_lock(QReadWriteLock::Recursive) 0120 , m_timelineEffectsEnabled(true) 0121 , m_id(getNextId()) 0122 , m_overlayTrackCount(-1) 0123 , m_videoTarget(-1) 0124 , m_editMode(TimelineMode::NormalEdit) 0125 , m_closing(false) 0126 , m_softDelete(false) 0127 { 0128 // Create black background track 0129 m_blackClip->set("kdenlive:playlistid", "black_track"); 0130 m_blackClip->set("mlt_type", "producer"); 0131 m_blackClip->set("aspect_ratio", 1); 0132 m_blackClip->set("length", INT_MAX); 0133 m_blackClip->set("mlt_image_format", "rgba"); 0134 m_blackClip->set("set.test_audio", 0); 0135 m_blackClip->set_in_and_out(0, TimelineModel::seekDuration); 0136 m_tractor->insert_track(*m_blackClip, 0); 0137 if (uuid != pCore->currentDoc()->uuid()) { 0138 // This is not the main tractor 0139 m_tractor->set("id", uuid.toString().toUtf8().constData()); 0140 } 0141 0142 TRACE_CONSTR(this); 0143 } 0144 0145 void TimelineModel::prepareClose(bool softDelete) 0146 { 0147 requestClearSelection(true); 0148 QWriteLocker locker(&m_lock); 0149 // Unlock all tracks to allow deleting clip from tracks 0150 m_closing = true; 0151 m_blockRefresh = true; 0152 if (softDelete) { 0153 m_softDelete = true; 0154 } 0155 if (!m_softDelete) { 0156 auto it = m_allTracks.begin(); 0157 while (it != m_allTracks.end()) { 0158 (*it)->unlock(); 0159 ++it; 0160 } 0161 m_subtitleModel.reset(); 0162 } else { 0163 auto it = m_allTracks.begin(); 0164 while (it != m_allTracks.end()) { 0165 (*it)->m_softDelete = true; 0166 ++it; 0167 } 0168 } 0169 // m_subtitleModel->removeAllSubtitles(); 0170 } 0171 0172 TimelineModel::~TimelineModel() 0173 { 0174 m_closing = true; 0175 if (!m_softDelete) { 0176 qDebug() << "::::::==\n\nCLOSING TIMELINE MODEL\n\n::::::::"; 0177 QScopedPointer<Mlt::Service> service(m_tractor->field()); 0178 QScopedPointer<Mlt::Field> field(m_tractor->field()); 0179 field->lock(); 0180 // Make sure all previous track compositing is removed 0181 while (service != nullptr && service->is_valid()) { 0182 if (service->type() == mlt_service_transition_type) { 0183 Mlt::Transition t(mlt_transition(service->get_service())); 0184 service.reset(service->producer()); 0185 // remove all compositing 0186 field->disconnect_service(t); 0187 t.disconnect_all_producers(); 0188 } else { 0189 service.reset(service->producer()); 0190 } 0191 } 0192 field->unlock(); 0193 m_allTracks.clear(); 0194 if (pCore && pCore->currentDoc() && !pCore->currentDoc()->closing) { 0195 // If we are not closing the project, unregister this timeline clips from bin 0196 for (const auto &clip : m_allClips) { 0197 clip.second->deregisterClipToBin(m_uuid); 0198 } 0199 } 0200 } 0201 } 0202 0203 void TimelineModel::setMarkerModel(std::shared_ptr<MarkerListModel> markerModel) 0204 { 0205 if (m_guidesModel) { 0206 return; 0207 } 0208 m_guidesModel = markerModel; 0209 m_guidesModel->registerSnapModel(std::static_pointer_cast<SnapInterface>(m_snaps)); 0210 m_guidesFilterModel.reset(new MarkerSortModel(this)); 0211 m_guidesFilterModel->setSourceModel(m_guidesModel.get()); 0212 m_guidesFilterModel->setSortRole(MarkerListModel::PosRole); 0213 m_guidesFilterModel->sort(0, Qt::AscendingOrder); 0214 m_guidesModel->loadCategories(KdenliveSettings::guidesCategories()); 0215 } 0216 0217 int TimelineModel::getTracksCount() const 0218 { 0219 READ_LOCK(); 0220 int count = m_tractor->count(); 0221 if (m_overlayTrackCount > -1) { 0222 count -= m_overlayTrackCount; 0223 } 0224 Q_ASSERT(count >= 0); 0225 // don't count the black background track 0226 Q_ASSERT(count - 1 == static_cast<int>(m_allTracks.size())); 0227 return count - 1; 0228 } 0229 0230 QPair<int, int> TimelineModel::getAVtracksCount() const 0231 { 0232 QPair<int, int> tracks{0, 0}; 0233 auto it = m_allTracks.cbegin(); 0234 while (it != m_allTracks.cend()) { 0235 if ((*it)->isAudioTrack()) { 0236 tracks.first++; 0237 } else { 0238 tracks.second++; 0239 } 0240 ++it; 0241 } 0242 if (m_overlayTrackCount > -1) { 0243 tracks.first -= m_overlayTrackCount; 0244 } 0245 return tracks; 0246 } 0247 0248 QList<int> TimelineModel::getTracksIds(bool audio) const 0249 { 0250 QList<int> trackIds; 0251 auto it = m_allTracks.cbegin(); 0252 while (it != m_allTracks.cend()) { 0253 if ((*it)->isAudioTrack() == audio) { 0254 trackIds.insert(0, (*it)->getId()); 0255 } 0256 ++it; 0257 } 0258 return trackIds; 0259 } 0260 0261 int TimelineModel::getTrackIndexFromPosition(int pos) const 0262 { 0263 Q_ASSERT(pos >= 0 && pos < int(m_allTracks.size())); 0264 READ_LOCK(); 0265 auto it = m_allTracks.cbegin(); 0266 while (pos > 0) { 0267 it++; 0268 pos--; 0269 } 0270 return (*it)->getId(); 0271 } 0272 0273 int TimelineModel::getClipsCount() const 0274 { 0275 READ_LOCK(); 0276 int size = int(m_allClips.size()); 0277 return size; 0278 } 0279 0280 int TimelineModel::getCompositionsCount() const 0281 { 0282 READ_LOCK(); 0283 int size = int(m_allCompositions.size()); 0284 return size; 0285 } 0286 0287 int TimelineModel::getClipTrackId(int clipId) const 0288 { 0289 READ_LOCK(); 0290 Q_ASSERT(m_allClips.count(clipId) > 0); 0291 const auto clip = m_allClips.at(clipId); 0292 return clip->getCurrentTrackId(); 0293 } 0294 0295 int TimelineModel::getCompositionTrackId(int compoId) const 0296 { 0297 Q_ASSERT(m_allCompositions.count(compoId) > 0); 0298 const auto trans = m_allCompositions.at(compoId); 0299 return trans->getCurrentTrackId(); 0300 } 0301 0302 int TimelineModel::getItemTrackId(int itemId) const 0303 { 0304 READ_LOCK(); 0305 Q_ASSERT(isItem(itemId)); 0306 if (isClip(itemId)) { 0307 return getClipTrackId(itemId); 0308 } 0309 if (isComposition(itemId)) { 0310 return getCompositionTrackId(itemId); 0311 } 0312 if (isSubTitle(itemId)) { 0313 // TODO: fix when introducing multiple subtitle tracks 0314 return -2; 0315 } 0316 return -1; 0317 } 0318 0319 bool TimelineModel::hasClipEndMix(int clipId) const 0320 { 0321 if (!isClip(clipId)) return false; 0322 int tid = getClipTrackId(clipId); 0323 if (tid < 0) return false; 0324 0325 return getTrackById_const(tid)->hasEndMix(clipId); 0326 } 0327 0328 int TimelineModel::getClipPosition(int clipId) const 0329 { 0330 READ_LOCK(); 0331 Q_ASSERT(m_allClips.count(clipId) > 0); 0332 const auto clip = m_allClips.at(clipId); 0333 int pos = clip->getPosition(); 0334 return pos; 0335 } 0336 0337 int TimelineModel::getClipEnd(int clipId) const 0338 { 0339 READ_LOCK(); 0340 Q_ASSERT(m_allClips.count(clipId) > 0); 0341 const auto clip = m_allClips.at(clipId); 0342 int pos = clip->getPosition() + clip->getPlaytime(); 0343 return pos; 0344 } 0345 0346 double TimelineModel::getClipSpeed(int clipId) const 0347 { 0348 READ_LOCK(); 0349 Q_ASSERT(m_allClips.count(clipId) > 0); 0350 return m_allClips.at(clipId)->getSpeed(); 0351 } 0352 0353 int TimelineModel::getClipSplitPartner(int clipId) const 0354 { 0355 READ_LOCK(); 0356 Q_ASSERT(m_allClips.count(clipId) > 0); 0357 return m_groups->getSplitPartner(clipId); 0358 } 0359 0360 int TimelineModel::getClipIn(int clipId) const 0361 { 0362 READ_LOCK(); 0363 Q_ASSERT(m_allClips.count(clipId) > 0); 0364 const auto clip = m_allClips.at(clipId); 0365 return clip->getIn(); 0366 } 0367 0368 QPoint TimelineModel::getClipInDuration(int clipId) const 0369 { 0370 READ_LOCK(); 0371 Q_ASSERT(m_allClips.count(clipId) > 0); 0372 const auto clip = m_allClips.at(clipId); 0373 return {clip->getIn(), clip->getPlaytime()}; 0374 } 0375 0376 PlaylistState::ClipState TimelineModel::getClipState(int clipId) const 0377 { 0378 READ_LOCK(); 0379 Q_ASSERT(m_allClips.count(clipId) > 0); 0380 const auto clip = m_allClips.at(clipId); 0381 return clip->clipState(); 0382 } 0383 0384 const QString TimelineModel::getClipBinId(int clipId) const 0385 { 0386 READ_LOCK(); 0387 Q_ASSERT(m_allClips.count(clipId) > 0); 0388 const auto clip = m_allClips.at(clipId); 0389 QString id = clip->binId(); 0390 return id; 0391 } 0392 0393 int TimelineModel::getClipPlaytime(int clipId) const 0394 { 0395 READ_LOCK(); 0396 Q_ASSERT(isClip(clipId)); 0397 const auto clip = m_allClips.at(clipId); 0398 int playtime = clip->getPlaytime(); 0399 return playtime; 0400 } 0401 0402 QSize TimelineModel::getClipFrameSize(int clipId) const 0403 { 0404 READ_LOCK(); 0405 Q_ASSERT(isClip(clipId)); 0406 const auto clip = m_allClips.at(clipId); 0407 return clip->getFrameSize(); 0408 } 0409 0410 int TimelineModel::getTrackClipsCount(int trackId) const 0411 { 0412 READ_LOCK(); 0413 Q_ASSERT(isTrack(trackId)); 0414 int count = getTrackById_const(trackId)->getClipsCount(); 0415 return count; 0416 } 0417 0418 int TimelineModel::getClipByStartPosition(int trackId, int position) const 0419 { 0420 READ_LOCK(); 0421 Q_ASSERT(isTrack(trackId)); 0422 return getTrackById_const(trackId)->getClipByStartPosition(position); 0423 } 0424 0425 int TimelineModel::getClipByPosition(int trackId, int position, int playlist) const 0426 { 0427 READ_LOCK(); 0428 if (isSubtitleTrack(trackId)) { 0429 return getSubtitleByPosition(position); 0430 } 0431 Q_ASSERT(isTrack(trackId)); 0432 return getTrackById_const(trackId)->getClipByPosition(position, playlist); 0433 } 0434 0435 int TimelineModel::getCompositionByPosition(int trackId, int position) const 0436 { 0437 READ_LOCK(); 0438 Q_ASSERT(isTrack(trackId)); 0439 return getTrackById_const(trackId)->getCompositionByPosition(position); 0440 } 0441 0442 int TimelineModel::getSubtitleByStartPosition(int position) const 0443 { 0444 READ_LOCK(); 0445 GenTime startTime(position, pCore->getCurrentFps()); 0446 auto findResult = 0447 std::find_if(std::begin(m_allSubtitles), std::end(m_allSubtitles), [&](const std::pair<int, GenTime> &pair) { return pair.second == startTime; }); 0448 if (findResult != std::end(m_allSubtitles)) { 0449 return findResult->first; 0450 } 0451 return -1; 0452 } 0453 0454 int TimelineModel::getSubtitleByPosition(int position) const 0455 { 0456 READ_LOCK(); 0457 GenTime startTime(position, pCore->getCurrentFps()); 0458 if (m_subtitleModel) { 0459 std::unordered_set<int> sids = m_subtitleModel->getItemsInRange(position, position); 0460 if (!sids.empty()) { 0461 return *sids.begin(); 0462 } 0463 } 0464 return -1; 0465 } 0466 0467 int TimelineModel::getTrackPosition(int trackId) const 0468 { 0469 READ_LOCK(); 0470 Q_ASSERT(isTrack(trackId)); 0471 auto it = m_allTracks.cbegin(); 0472 int pos = int(std::distance(it, static_cast<decltype(it)>(m_iteratorTable.at(trackId)))); 0473 return pos; 0474 } 0475 0476 int TimelineModel::getTrackMltIndex(int trackId) const 0477 { 0478 READ_LOCK(); 0479 // Because of the black track that we insert in first position, the mlt index is the position + 1 0480 return getTrackPosition(trackId) + 1; 0481 } 0482 0483 int TimelineModel::getTrackSortValue(int trackId, int separated) const 0484 { 0485 if (separated == 1) { 0486 // This will be A2, A1, V1, V2 0487 return getTrackPosition(trackId) + 1; 0488 } 0489 if (separated == 2) { 0490 // This will be A1, A2, V1, V2 0491 // Count audio/video tracks 0492 auto it = m_allTracks.cbegin(); 0493 int aCount = 0; 0494 int vCount = 0; 0495 int refPos = 0; 0496 bool isVideo = true; 0497 while (it != m_allTracks.cend()) { 0498 if ((*it)->isAudioTrack()) { 0499 if ((*it)->getId() == trackId) { 0500 refPos = aCount; 0501 isVideo = false; 0502 } 0503 aCount++; 0504 } else { 0505 // video track 0506 if ((*it)->getId() == trackId) { 0507 refPos = vCount; 0508 } 0509 vCount++; 0510 } 0511 ++it; 0512 } 0513 return isVideo ? aCount + refPos + 1 : aCount - refPos; 0514 } 0515 // This will be A1, V1, A2, V2 0516 auto it = m_allTracks.cend(); 0517 int aCount = 0; 0518 int vCount = 0; 0519 bool isAudio = false; 0520 int trackPos = 0; 0521 while (it != m_allTracks.begin()) { 0522 --it; 0523 bool audioTrack = (*it)->isAudioTrack(); 0524 if (audioTrack) { 0525 aCount++; 0526 } else { 0527 vCount++; 0528 } 0529 if (trackId == (*it)->getId()) { 0530 isAudio = audioTrack; 0531 trackPos = audioTrack ? aCount : vCount; 0532 } 0533 } 0534 if (isAudio) { 0535 if (aCount > vCount) { 0536 if (trackPos - 1 > aCount - vCount) { 0537 // We have more audio tracks than video tracks 0538 return (aCount - vCount + 1) + 2 * (trackPos - (aCount - vCount + 1)); 0539 } 0540 return trackPos; 0541 } 0542 return 2 * trackPos; 0543 } 0544 return 2 * (vCount + 1 - trackPos) + 1; 0545 } 0546 0547 QList<int> TimelineModel::getLowerTracksId(int trackId, TrackType type) const 0548 { 0549 READ_LOCK(); 0550 Q_ASSERT(isTrack(trackId)); 0551 QList<int> results; 0552 auto it = m_iteratorTable.at(trackId); 0553 while (it != m_allTracks.cbegin()) { 0554 --it; 0555 if (type == TrackType::AnyTrack) { 0556 results << (*it)->getId(); 0557 continue; 0558 } 0559 bool audioTrack = (*it)->isAudioTrack(); 0560 if (type == TrackType::AudioTrack && audioTrack) { 0561 results << (*it)->getId(); 0562 } else if (type == TrackType::VideoTrack && !audioTrack) { 0563 results << (*it)->getId(); 0564 } 0565 } 0566 return results; 0567 } 0568 0569 int TimelineModel::getPreviousVideoTrackIndex(int trackId) const 0570 { 0571 READ_LOCK(); 0572 Q_ASSERT(isTrack(trackId)); 0573 auto it = m_iteratorTable.at(trackId); 0574 while (it != m_allTracks.cbegin()) { 0575 --it; 0576 if (!(*it)->isAudioTrack()) { 0577 return (*it)->getId(); 0578 } 0579 } 0580 return 0; 0581 } 0582 0583 int TimelineModel::getPreviousVideoTrackPos(int trackId) const 0584 { 0585 READ_LOCK(); 0586 Q_ASSERT(isTrack(trackId)); 0587 auto it = m_iteratorTable.at(trackId); 0588 while (it != m_allTracks.cbegin()) { 0589 --it; 0590 if (!(*it)->isAudioTrack()) { 0591 return getTrackMltIndex((*it)->getId()); 0592 } 0593 } 0594 return 0; 0595 } 0596 0597 int TimelineModel::getMirrorVideoTrackId(int trackId) const 0598 { 0599 READ_LOCK(); 0600 Q_ASSERT(isTrack(trackId)); 0601 auto it = m_iteratorTable.at(trackId); 0602 if (!(*it)->isAudioTrack()) { 0603 // we expected an audio track... 0604 return -1; 0605 } 0606 int count = 0; 0607 while (it != m_allTracks.cend()) { 0608 if ((*it)->isAudioTrack()) { 0609 count++; 0610 } else { 0611 count--; 0612 if (count == 0) { 0613 return (*it)->getId(); 0614 } 0615 } 0616 ++it; 0617 } 0618 return -1; 0619 } 0620 0621 int TimelineModel::getMirrorTrackId(int trackId) const 0622 { 0623 if (isAudioTrack(trackId)) { 0624 return getMirrorVideoTrackId(trackId); 0625 } 0626 return getMirrorAudioTrackId(trackId); 0627 } 0628 0629 int TimelineModel::getMirrorAudioTrackId(int trackId) const 0630 { 0631 READ_LOCK(); 0632 Q_ASSERT(isTrack(trackId)); 0633 auto it = m_iteratorTable.at(trackId); 0634 if ((*it)->isAudioTrack()) { 0635 // we expected a video track... 0636 qWarning() << "requesting mirror audio track for audio track"; 0637 return -1; 0638 } 0639 int count = 0; 0640 while (it != m_allTracks.cbegin()) { 0641 if (!(*it)->isAudioTrack()) { 0642 count++; 0643 } else { 0644 count--; 0645 if (count == 0) { 0646 return (*it)->getId(); 0647 } 0648 } 0649 --it; 0650 } 0651 if ((*it)->isAudioTrack() && count == 1) { 0652 return (*it)->getId(); 0653 } 0654 return -1; 0655 } 0656 0657 void TimelineModel::setEditMode(TimelineMode::EditMode mode) 0658 { 0659 m_editMode = mode; 0660 } 0661 0662 TimelineMode::EditMode TimelineModel::editMode() const 0663 { 0664 return m_editMode; 0665 } 0666 0667 bool TimelineModel::normalEdit() const 0668 { 0669 return m_editMode == TimelineMode::NormalEdit; 0670 } 0671 0672 bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool invalidateTimeline, Fun &undo, Fun &redo) 0673 { 0674 Q_UNUSED(updateView); 0675 Q_UNUSED(invalidateTimeline); 0676 Q_UNUSED(undo); 0677 Q_UNUSED(redo); 0678 Q_ASSERT(isClip(clipId)); 0679 m_allClips[clipId]->setFakePosition(position); 0680 bool trackChanged = false; 0681 if (trackId > -1) { 0682 if (trackId != m_allClips[clipId]->getFakeTrackId()) { 0683 if (getTrackById_const(trackId)->trackType() == m_allClips[clipId]->clipState()) { 0684 m_allClips[clipId]->setFakeTrackId(trackId); 0685 trackChanged = true; 0686 } 0687 } 0688 } 0689 QModelIndex modelIndex = makeClipIndexFromID(clipId); 0690 if (modelIndex.isValid()) { 0691 QVector<int> roles{FakePositionRole}; 0692 if (trackChanged) { 0693 roles << FakeTrackIdRole; 0694 } 0695 notifyChange(modelIndex, modelIndex, roles); 0696 return true; 0697 } 0698 return false; 0699 } 0700 0701 bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool moveMirrorTracks, bool updateView, bool invalidateTimeline, bool finalMove, 0702 Fun &undo, Fun &redo, bool revertMove, bool groupMove, const QMap<int, int> &moving_clips, 0703 std::pair<MixInfo, MixInfo> mixData) 0704 { 0705 Q_UNUSED(moveMirrorTracks) 0706 if (trackId == -1) { 0707 qWarning() << "clip is not on a track"; 0708 return false; 0709 } 0710 Q_ASSERT(isClip(clipId)); 0711 if (m_allClips[clipId]->clipState() == PlaylistState::Disabled) { 0712 if (getTrackById_const(trackId)->trackType() == PlaylistState::AudioOnly && !m_allClips[clipId]->canBeAudio()) { 0713 qWarning() << "clip type mismatch 1"; 0714 return false; 0715 } 0716 if (getTrackById_const(trackId)->trackType() == PlaylistState::VideoOnly && !m_allClips[clipId]->canBeVideo()) { 0717 qWarning() << "clip type mismatch 2"; 0718 return false; 0719 } 0720 } else if (getTrackById_const(trackId)->trackType() != m_allClips[clipId]->clipState()) { 0721 // Move not allowed (audio / video mismatch) 0722 qWarning() << "clip type mismatch 3"; 0723 return false; 0724 } 0725 std::function<bool(void)> local_undo = []() { return true; }; 0726 std::function<bool(void)> local_redo = []() { return true; }; 0727 bool ok = true; 0728 int old_trackId = getClipTrackId(clipId); 0729 int previous_track = moving_clips.value(clipId, -1); 0730 if (old_trackId == -1) { 0731 // old_trackId = previous_track; 0732 } 0733 bool notifyViewOnly = false; 0734 Fun update_model = []() { return true; }; 0735 if (old_trackId == trackId) { 0736 // Move on same track, simply inform the view 0737 updateView = false; 0738 notifyViewOnly = true; 0739 update_model = [clipId, this, trackId, invalidateTimeline]() { 0740 QModelIndex modelIndex = makeClipIndexFromID(clipId); 0741 notifyChange(modelIndex, modelIndex, StartRole); 0742 if (invalidateTimeline && !getTrackById_const(trackId)->isAudioTrack()) { 0743 int in = getClipPosition(clipId); 0744 Q_EMIT invalidateZone(in, in + getClipPlaytime(clipId)); 0745 } 0746 return true; 0747 }; 0748 } 0749 Fun sync_mix = []() { return true; }; 0750 Fun simple_move_mix = []() { return true; }; 0751 Fun simple_restore_mix = []() { return true; }; 0752 QList<int> allowedClipMixes; 0753 if (!groupMove && old_trackId > -1) { 0754 mixData = getTrackById_const(old_trackId)->getMixInfo(clipId); 0755 } 0756 if (old_trackId == trackId && !finalMove && !revertMove) { 0757 if (mixData.first.firstClipId > -1 && !moving_clips.contains(mixData.first.firstClipId)) { 0758 // Mix at clip start, don't allow moving left 0759 if (position < (mixData.first.firstClipInOut.second - mixData.first.mixOffset) && 0760 (position + m_allClips[clipId]->getPlaytime() >= mixData.first.firstClipInOut.first)) { 0761 qDebug() << "==== ABORTING GROUP MOVE ON START MIX"; 0762 return false; 0763 } 0764 } 0765 if (mixData.second.firstClipId > -1 && !moving_clips.contains(mixData.second.secondClipId)) { 0766 // Mix at clip end, don't allow moving right 0767 if (position + getClipPlaytime(clipId) > mixData.second.secondClipInOut.first && position < mixData.second.secondClipInOut.second) { 0768 qDebug() << "==== ABORTING GROUP MOVE ON END MIX: " << position; 0769 return false; 0770 } 0771 } 0772 } 0773 bool hadMix = mixData.first.firstClipId > -1 || mixData.second.firstClipId > -1; 0774 if (!finalMove && !revertMove) { 0775 QVector<int> exceptions = {clipId}; 0776 if (mixData.first.firstClipId > -1) { 0777 exceptions << mixData.first.firstClipId; 0778 } 0779 if (mixData.second.secondClipId > -1) { 0780 exceptions << mixData.second.secondClipId; 0781 } 0782 if (!getTrackById_const(trackId)->isAvailableWithExceptions(position, getClipPlaytime(clipId), exceptions)) { 0783 // No space for clip insert operation, abort 0784 qWarning() << "No free space for clip move"; 0785 return false; 0786 } 0787 } 0788 if (old_trackId == -1 && isTrack(previous_track) && hadMix && previous_track != trackId) { 0789 // Clip is moved to another track 0790 bool mixGroupMove = false; 0791 if (mixData.first.firstClipId > 0) { 0792 allowedClipMixes << mixData.first.firstClipId; 0793 if (moving_clips.contains(mixData.first.firstClipId)) { 0794 allowedClipMixes << mixData.first.firstClipId; 0795 } else if (finalMove) { 0796 position += (mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first - mixData.first.mixOffset); 0797 removeMixWithUndo(clipId, local_undo, local_redo); 0798 } 0799 } 0800 if (mixData.second.firstClipId > 0) { 0801 allowedClipMixes << mixData.second.secondClipId; 0802 if (moving_clips.contains(mixData.second.secondClipId)) { 0803 allowedClipMixes << mixData.second.secondClipId; 0804 } else if (finalMove) { 0805 removeMixWithUndo(mixData.second.secondClipId, local_undo, local_redo); 0806 } 0807 } 0808 if (m_groups->isInGroup(clipId) && mixData.first.firstClipId > 0) { 0809 int parentGroup = m_groups->getRootId(clipId); 0810 if (parentGroup > -1) { 0811 std::unordered_set<int> sub = m_groups->getLeaves(parentGroup); 0812 if (sub.count(mixData.first.firstClipId) > 0 && sub.count(mixData.first.secondClipId) > 0) { 0813 mixGroupMove = true; 0814 } 0815 } 0816 } 0817 if (mixGroupMove) { 0818 // We are moving a group on another track, delete and re-add 0819 // Get mix properties 0820 std::pair<int, int> tracks = getTrackById_const(previous_track)->getMixTracks(mixData.first.secondClipId); 0821 std::pair<QString, QVector<QPair<QString, QVariant>>> mixParams = getTrackById_const(previous_track)->getMixParams(mixData.first.secondClipId); 0822 simple_move_mix = [this, previous_track, trackId, finalMove, mixData, tracks, mixParams]() { 0823 // Insert mix on new track 0824 bool result = getTrackById_const(trackId)->createMix(mixData.first, mixParams, tracks, finalMove); 0825 // Remove mix on old track 0826 getTrackById_const(previous_track)->removeMix(mixData.first); 0827 return result; 0828 }; 0829 simple_restore_mix = [this, previous_track, trackId, finalMove, mixData, tracks, mixParams]() { 0830 bool result = getTrackById_const(previous_track)->createMix(mixData.first, mixParams, tracks, finalMove); 0831 // Remove mix on old track 0832 getTrackById_const(trackId)->removeMix(mixData.first); 0833 0834 return result; 0835 }; 0836 } 0837 } else if (finalMove && !groupMove && isTrack(old_trackId) && hadMix) { 0838 // Clip has a mix 0839 if (mixData.first.firstClipId > -1) { 0840 if (old_trackId == trackId) { 0841 int mixCut = m_allClips[clipId]->getMixCutPosition(); 0842 // We are moving a clip on same track 0843 if (position > mixData.first.secondClipInOut.first - mixCut || position < mixData.first.firstClipInOut.first) { 0844 position += m_allClips[clipId]->getMixDuration() - mixCut; 0845 removeMixWithUndo(clipId, local_undo, local_redo); 0846 } 0847 } else { 0848 // Clip moved to another track, delete mix 0849 position += (m_allClips[clipId]->getMixDuration() - m_allClips[clipId]->getMixCutPosition()); 0850 removeMixWithUndo(clipId, local_undo, local_redo); 0851 } 0852 } 0853 if (mixData.second.firstClipId > -1) { 0854 // We have a mix at clip end 0855 if (old_trackId == trackId) { 0856 int mixEnd = m_allClips[mixData.second.secondClipId]->getPosition() + m_allClips[mixData.second.secondClipId]->getMixDuration(); 0857 if (position > mixEnd || position < m_allClips[mixData.second.secondClipId]->getPosition()) { 0858 // Moved outside mix zone 0859 removeMixWithUndo(mixData.second.secondClipId, local_undo, local_redo); 0860 } 0861 } else { 0862 // Clip moved to another track, delete mix 0863 // Mix will be deleted by syncronizeMixes operation, only 0864 // re-add it on undo 0865 removeMixWithUndo(mixData.second.secondClipId, local_undo, local_redo); 0866 } 0867 } 0868 } else if (finalMove && groupMove && isTrack(old_trackId) && hadMix && old_trackId == trackId) { 0869 // Group move on same track with mix 0870 if (mixData.first.firstClipId > -1) { 0871 // Mix on clip start, check if mix is still in range 0872 if (!moving_clips.contains(mixData.first.firstClipId)) { 0873 int mixCut = m_allClips[clipId]->getMixCutPosition(); 0874 // We are moving a clip on same track 0875 if (position > mixData.first.secondClipInOut.first - mixCut || position < mixData.first.firstClipInOut.first) { 0876 // Mix will be deleted, recreate on undo 0877 position += m_allClips[mixData.first.secondClipId]->getMixDuration() - m_allClips[mixData.first.secondClipId]->getMixCutPosition(); 0878 removeMixWithUndo(mixData.first.secondClipId, local_undo, local_redo); 0879 } 0880 } else { 0881 allowedClipMixes << mixData.first.firstClipId; 0882 } 0883 } 0884 if (mixData.second.firstClipId > -1) { 0885 // Mix on clip end, check if mix is still in range 0886 if (!moving_clips.contains(mixData.second.secondClipId)) { 0887 int mixEnd = m_allClips[mixData.second.secondClipId]->getPosition() + m_allClips[mixData.second.secondClipId]->getMixDuration(); 0888 if (mixEnd > position + m_allClips[clipId]->getPlaytime() || position > mixEnd) { 0889 // Mix will be deleted, recreate on undo 0890 removeMixWithUndo(mixData.second.secondClipId, local_undo, local_redo); 0891 } 0892 } else { 0893 allowedClipMixes << mixData.second.secondClipId; 0894 } 0895 } 0896 } else { 0897 } 0898 if (old_trackId != -1) { 0899 if (notifyViewOnly) { 0900 PUSH_LAMBDA(update_model, local_undo); 0901 } 0902 ok = getTrackById(old_trackId)->requestClipDeletion(clipId, updateView, finalMove, local_undo, local_redo, groupMove, false, allowedClipMixes); 0903 if (!ok) { 0904 bool undone = local_undo(); 0905 Q_ASSERT(undone); 0906 qWarning() << "clip deletion failed"; 0907 return false; 0908 } else { 0909 } 0910 } 0911 ok = ok && getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, finalMove, local_undo, local_redo, groupMove, old_trackId == -1, 0912 allowedClipMixes); 0913 0914 if (!ok) { 0915 qWarning() << "clip insertion failed"; 0916 bool undone = local_undo(); 0917 Q_ASSERT(undone); 0918 return false; 0919 } 0920 0921 sync_mix(); 0922 update_model(); 0923 simple_move_mix(); 0924 0925 if (finalMove) { 0926 PUSH_LAMBDA(simple_restore_mix, undo); 0927 PUSH_LAMBDA(simple_move_mix, local_redo); 0928 // PUSH_LAMBDA(sync_mix, local_redo); 0929 } 0930 if (notifyViewOnly) { 0931 PUSH_LAMBDA(update_model, local_redo); 0932 } 0933 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 0934 return true; 0935 } 0936 0937 bool TimelineModel::mixClip(int idToMove, const QString &mixId, int delta) 0938 { 0939 std::unordered_set<int> initialSelection = getCurrentSelection(); 0940 if (idToMove == -1 && initialSelection.empty()) { 0941 pCore->displayMessage(i18n("Select a clip to apply the mix"), ErrorMessage, 500); 0942 return false; 0943 } 0944 struct mixStructure 0945 { 0946 int clipId; 0947 std::pair<int, int> clips; 0948 std::pair<int, int> spaces; 0949 std::pair<int, int> durations; 0950 int mixPosition; 0951 int selectedTrack; 0952 }; 0953 QList<mixStructure> clipsToMixList; 0954 std::vector<int> clipIds; 0955 std::unordered_set<int> selectionToSort; 0956 if (idToMove != -1) { 0957 selectionToSort = {idToMove}; 0958 // Check for audio / video partner 0959 if (isClip(idToMove) && m_groups->isInGroup(idToMove)) { 0960 int partnerId = getClipSplitPartner(idToMove); 0961 if (partnerId > -1) { 0962 selectionToSort.insert(partnerId); 0963 } 0964 } 0965 } else { 0966 selectionToSort = initialSelection; 0967 } 0968 // Sort selection chronologically to limit reordering 0969 std::vector<int> ordered(selectionToSort.begin(), selectionToSort.end()); 0970 std::sort(ordered.begin(), ordered.end(), [this](int cidA, int cidB) { 0971 if (!isClip(cidA) || !isClip(cidB)) { 0972 return false; 0973 } 0974 return m_allClips[cidA]->getPosition() < m_allClips[cidB]->getPosition(); 0975 }); 0976 clipIds = ordered; 0977 0978 int noSpaceInClip = 0; 0979 int mixDuration = pCore->getDurationFromString(KdenliveSettings::mix_duration()); 0980 std::function<bool(void)> undo = []() { return true; }; 0981 std::function<bool(void)> redo = []() { return true; }; 0982 0983 auto processMix = [this, &undo, &redo, mixDuration, mixId](mixStructure mInfo) { 0984 if (mInfo.spaces.first > -1) { 0985 if (mInfo.spaces.second > -1) { 0986 // Both clips have limited durations 0987 mInfo.durations.first = qMin(mixDuration / 2, mInfo.spaces.first); 0988 mInfo.durations.first = qMin(mInfo.durations.first, mInfo.spaces.first); 0989 mInfo.durations.second = qMin(mixDuration - mixDuration / 2, mInfo.spaces.second); 0990 int offset = mixDuration - (mInfo.durations.first + mInfo.durations.second); 0991 if (offset > 0) { 0992 if (mInfo.spaces.first > mInfo.durations.first) { 0993 mInfo.durations.first = qMin(mInfo.spaces.first, mInfo.durations.first + offset); 0994 } else if (mInfo.spaces.second > mInfo.durations.second) { 0995 mInfo.durations.second = qMin(mInfo.spaces.second, mInfo.durations.second + offset); 0996 } 0997 } 0998 } else { 0999 mInfo.durations.first = qMin(mixDuration - mixDuration / 2, mInfo.spaces.first); 1000 mInfo.durations.second = mixDuration - mInfo.durations.second; 1001 } 1002 } else { 1003 if (mInfo.spaces.second > -1) { 1004 mInfo.durations.second = qMin(mixDuration - mixDuration / 2, mInfo.spaces.second); 1005 mInfo.durations.first = mixDuration - mInfo.durations.second; 1006 } else { 1007 mInfo.durations.first = mixDuration / 2; 1008 mInfo.durations.second = mixDuration - mInfo.durations.first; 1009 } 1010 } 1011 return requestClipMix(mixId, mInfo.clips, mInfo.durations, mInfo.selectedTrack, mInfo.mixPosition, true, true, true, undo, redo, false); 1012 }; 1013 1014 for (int s : clipIds) { 1015 if (!isClip(s)) { 1016 continue; 1017 } 1018 mixStructure mixInfo; 1019 mixInfo.clipId = s; 1020 mixInfo.clips = {-1, -1}; 1021 mixInfo.spaces = {0, 0}; 1022 mixInfo.durations = {0, 0}; 1023 mixInfo.mixPosition = 0; 1024 mixInfo.selectedTrack = getClipTrackId(s); 1025 if (mixInfo.selectedTrack == -1 || !isTrack(mixInfo.selectedTrack) || !getTrackById_const(mixInfo.selectedTrack)->shouldReceiveTimelineOp()) { 1026 continue; 1027 } 1028 mixInfo.mixPosition = getItemPosition(s); 1029 int clipDuration = getItemPlaytime(s); 1030 // Check if clip already has a mix 1031 if (delta > -1 && getTrackById_const(mixInfo.selectedTrack)->hasStartMix(s)) { 1032 if (getTrackById_const(mixInfo.selectedTrack)->hasEndMix(s)) { 1033 continue; 1034 } 1035 mixInfo.clips.second = getTrackById_const(mixInfo.selectedTrack)->getClipByPosition(mixInfo.mixPosition + clipDuration + 1); 1036 // Check if previous clip was selected, and not next clip. In that case we stop processing 1037 int previousClip = getTrackById_const(mixInfo.selectedTrack)->getClipByPosition(mixInfo.mixPosition - 1); 1038 if (std::find(clipIds.begin(), clipIds.end(), previousClip) != clipIds.end() && 1039 std::find(clipIds.begin(), clipIds.end(), mixInfo.clips.second) == clipIds.end()) { 1040 continue; 1041 } 1042 } else if (delta < 1 && getTrackById_const(mixInfo.selectedTrack)->hasEndMix(s)) { 1043 mixInfo.clips.first = getTrackById_const(mixInfo.selectedTrack)->getClipByPosition(mixInfo.mixPosition - 1); 1044 // Check if next clip was selected, and not previous clip. In that case we stop processing 1045 int nextClip = getTrackById_const(mixInfo.selectedTrack)->getClipByPosition(mixInfo.mixPosition + clipDuration + 1); 1046 if (std::find(clipIds.begin(), clipIds.end(), nextClip) != clipIds.end() && 1047 std::find(clipIds.begin(), clipIds.end(), mixInfo.clips.first) == clipIds.end()) { 1048 continue; 1049 } 1050 if (mixInfo.clips.first > -1 && getTrackById_const(mixInfo.selectedTrack)->hasEndMix(mixInfo.clips.first)) { 1051 // Could happen if 2 clips before are mixed to full length 1052 mixInfo.clips.first = -1; 1053 } 1054 } else { 1055 if (delta < 1) { 1056 mixInfo.clips.first = getTrackById_const(mixInfo.selectedTrack)->getClipByPosition(mixInfo.mixPosition - 1); 1057 } 1058 if (delta > -1) { 1059 mixInfo.clips.second = getTrackById_const(mixInfo.selectedTrack)->getClipByPosition(mixInfo.mixPosition + clipDuration + 1); 1060 } 1061 } 1062 if (mixInfo.clips.first > -1 && mixInfo.clips.second > -1) { 1063 // We have a clip before and a clip after, check if both are selected 1064 if (std::find(clipIds.begin(), clipIds.end(), mixInfo.clips.second) == clipIds.end()) { 1065 if (std::find(clipIds.begin(), clipIds.end(), mixInfo.clips.first) != clipIds.end()) { 1066 mixInfo.clips.second = -1; 1067 } 1068 } else if (std::find(clipIds.begin(), clipIds.end(), mixInfo.clips.first) == clipIds.end()) { 1069 mixInfo.clips.first = -1; 1070 } 1071 } 1072 if (mixInfo.clips.second != -1) { 1073 // Mix at end of selected clip 1074 // Make sure we have enough space in clip to resize 1075 // mixInfo.spaces.first is the maximum frames we have to expand first clip on the right 1076 mixInfo.spaces.first = m_allClips[s]->m_endlessResize 1077 ? m_allClips[mixInfo.clips.second]->getPlaytime() 1078 : qMin(m_allClips[mixInfo.clips.second]->getPlaytime(), m_allClips[s]->getMaxDuration() - m_allClips[s]->getOut() - 1); 1079 // mixInfo.spaces.second is the maximum frames we have to expand second clip on the left 1080 mixInfo.spaces.second = m_allClips[mixInfo.clips.second]->m_endlessResize 1081 ? m_allClips[s]->getPlaytime() 1082 : qMin(m_allClips[s]->getPlaytime(), m_allClips[mixInfo.clips.second]->getIn()); 1083 if (getTrackById_const(mixInfo.selectedTrack)->hasStartMix(s)) { 1084 int spaceBeforeMix = m_allClips[mixInfo.clips.second]->getPosition() - (m_allClips[s]->getPosition() + m_allClips[s]->getMixDuration()); 1085 mixInfo.spaces.second = mixInfo.spaces.second == -1 ? spaceBeforeMix : qMin(mixInfo.spaces.second, spaceBeforeMix); 1086 } 1087 if (getTrackById_const(mixInfo.selectedTrack)->hasEndMix(mixInfo.clips.second)) { 1088 MixInfo mixData = getTrackById_const(mixInfo.selectedTrack)->getMixInfo(mixInfo.clips.second).second; 1089 if (mixData.secondClipId > -1) { 1090 int spaceAfterMix = m_allClips[mixInfo.clips.second]->getPlaytime() - m_allClips[mixData.secondClipId]->getMixDuration(); 1091 mixInfo.spaces.first = mixInfo.spaces.first == -1 ? spaceAfterMix : qMin(mixInfo.spaces.first, spaceAfterMix); 1092 } 1093 } 1094 if (mixInfo.spaces.first > -1 && mixInfo.spaces.second > -1 && (mixInfo.spaces.first + mixInfo.spaces.second < 3)) { 1095 noSpaceInClip = 2; 1096 continue; 1097 } 1098 mixInfo.mixPosition += clipDuration; 1099 mixInfo.clips.first = s; 1100 mixInfo.clipId = s; 1101 processMix(mixInfo); 1102 clipsToMixList << mixInfo; 1103 continue; 1104 } else { 1105 if (mixInfo.clips.first == -1) { 1106 // No clip to mix, abort 1107 continue; 1108 } 1109 // Make sure we have enough space in clip to resize 1110 // mixInfo.spaces.first is the maximum frames we have to expand first clip on the right 1111 mixInfo.spaces.first = 1112 m_allClips[mixInfo.clips.first]->m_endlessResize 1113 ? m_allClips[s]->getPlaytime() 1114 : qMin(m_allClips[s]->getPlaytime(), m_allClips[mixInfo.clips.first]->getMaxDuration() - m_allClips[mixInfo.clips.first]->getOut() - 1); 1115 // mixInfo.spaces.second is the maximum frames we have to expand second clip on the left 1116 mixInfo.spaces.second = m_allClips[s]->m_endlessResize ? m_allClips[mixInfo.clips.first]->getPlaytime() 1117 : qMin(m_allClips[mixInfo.clips.first]->getPlaytime(), m_allClips[s]->getIn()); 1118 if (getTrackById_const(mixInfo.selectedTrack)->hasStartMix(mixInfo.clips.first)) { 1119 int spaceBeforeMix = 1120 m_allClips[s]->getPosition() - (m_allClips[mixInfo.clips.first]->getPosition() + m_allClips[mixInfo.clips.first]->getMixDuration()); 1121 mixInfo.spaces.second = mixInfo.spaces.second == -1 ? spaceBeforeMix : qMin(mixInfo.spaces.second, spaceBeforeMix); 1122 } 1123 if (getTrackById_const(mixInfo.selectedTrack)->hasEndMix(s)) { 1124 MixInfo mixData = getTrackById_const(mixInfo.selectedTrack)->getMixInfo(s).second; 1125 if (mixData.secondClipId > -1) { 1126 int spaceAfterMix = m_allClips[s]->getPlaytime() - m_allClips[mixData.secondClipId]->getMixDuration(); 1127 mixInfo.spaces.first = mixInfo.spaces.first == -1 ? spaceAfterMix : qMin(mixInfo.spaces.first, spaceAfterMix); 1128 } 1129 } 1130 if (mixInfo.spaces.first > -1 && mixInfo.spaces.second > -1 && (mixInfo.spaces.first + mixInfo.spaces.second < 3)) { 1131 noSpaceInClip = 1; 1132 continue; 1133 } 1134 // Create Mix at start of selected clip 1135 mixInfo.clips.second = s; 1136 mixInfo.clipId = s; 1137 processMix(mixInfo); 1138 clipsToMixList << mixInfo; 1139 continue; 1140 } 1141 } 1142 if (clipsToMixList.isEmpty()) { 1143 if (noSpaceInClip > 0) { 1144 pCore->displayMessage(i18n("Not enough frames at clip %1 to apply the mix", noSpaceInClip == 1 ? i18n("start") : i18n("end")), ErrorMessage, 500); 1145 } else { 1146 pCore->displayMessage(i18n("Select a clip to apply the mix"), ErrorMessage, 500); 1147 } 1148 return false; 1149 } 1150 pCore->pushUndo(undo, redo, i18n("Create mix")); 1151 // Reselect clips 1152 if (!initialSelection.empty()) { 1153 requestSetSelection(initialSelection); 1154 } 1155 return true; 1156 } 1157 1158 bool TimelineModel::requestClipMix(const QString &mixId, std::pair<int, int> clipIds, std::pair<int, int> mixDurations, int trackId, int position, 1159 bool updateView, bool invalidateTimeline, bool finalMove, Fun &undo, Fun &redo, bool groupMove) 1160 { 1161 if (trackId == -1) { 1162 return false; 1163 } 1164 Q_ASSERT(isClip(clipIds.first)); 1165 std::function<bool(void)> local_undo = []() { return true; }; 1166 std::function<bool(void)> local_redo = []() { return true; }; 1167 bool ok = true; 1168 Fun update_model = []() { return true; }; 1169 // Move on same track, simply inform the view 1170 updateView = false; 1171 bool notifyViewOnly = true; 1172 update_model = [clipIds, this, trackId, position, invalidateTimeline, mixDurations]() { 1173 QModelIndex modelIndex = makeClipIndexFromID(clipIds.second); 1174 notifyChange(modelIndex, modelIndex, {StartRole, DurationRole}); 1175 QModelIndex modelIndex2 = makeClipIndexFromID(clipIds.first); 1176 notifyChange(modelIndex2, modelIndex2, DurationRole); 1177 if (invalidateTimeline && !getTrackById_const(trackId)->isAudioTrack()) { 1178 Q_EMIT invalidateZone(position - mixDurations.second, position + mixDurations.first); 1179 } 1180 return true; 1181 }; 1182 if (notifyViewOnly) { 1183 PUSH_LAMBDA(update_model, local_undo); 1184 } 1185 ok = getTrackById(trackId)->requestClipMix(mixId, clipIds, mixDurations, updateView, finalMove, local_undo, local_redo, groupMove); 1186 if (!ok) { 1187 qWarning() << "mix failed, reverting"; 1188 bool undone = local_undo(); 1189 Q_ASSERT(undone); 1190 return false; 1191 } 1192 update_model(); 1193 if (notifyViewOnly) { 1194 PUSH_LAMBDA(update_model, local_redo); 1195 } 1196 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 1197 return ok; 1198 } 1199 1200 bool TimelineModel::requestFakeClipMove(int clipId, int trackId, int position, bool updateView, bool logUndo, bool invalidateTimeline) 1201 { 1202 QWriteLocker locker(&m_lock); 1203 TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline) 1204 Q_ASSERT(m_allClips.count(clipId) > 0); 1205 if (m_groups->isInGroup(clipId)) { 1206 // element is in a group. 1207 int groupId = m_groups->getRootId(clipId); 1208 int current_trackId = getClipTrackId(clipId); 1209 int track_pos1 = getTrackPosition(trackId); 1210 int track_pos2 = getTrackPosition(current_trackId); 1211 int delta_track = track_pos1 - track_pos2; 1212 int delta_pos = position - m_allClips[clipId]->getPosition(); 1213 bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); 1214 TRACE_RES(res); 1215 return res; 1216 } 1217 std::function<bool(void)> undo = []() { return true; }; 1218 std::function<bool(void)> redo = []() { return true; }; 1219 bool res = requestFakeClipMove(clipId, trackId, position, updateView, invalidateTimeline, undo, redo); 1220 if (res && logUndo) { 1221 PUSH_UNDO(undo, redo, i18n("Move clip")); 1222 } 1223 TRACE_RES(res); 1224 return res; 1225 } 1226 1227 bool TimelineModel::requestClipMove(int clipId, int trackId, int position, bool moveMirrorTracks, bool updateView, bool logUndo, bool invalidateTimeline, 1228 bool revertMove) 1229 { 1230 QWriteLocker locker(&m_lock); 1231 TRACE(clipId, trackId, position, updateView, logUndo, invalidateTimeline); 1232 Q_ASSERT(m_allClips.count(clipId) > 0); 1233 if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { 1234 TRACE_RES(true); 1235 return true; 1236 } 1237 if (m_groups->isInGroup(clipId) && moveMirrorTracks) { 1238 // element is in a group. 1239 int groupId = m_groups->getRootId(clipId); 1240 int current_trackId = getClipTrackId(clipId); 1241 int track_pos1 = getTrackPosition(trackId); 1242 int track_pos2 = getTrackPosition(current_trackId); 1243 int delta_track = track_pos1 - track_pos2; 1244 int delta_pos = position - m_allClips[clipId]->getPosition(); 1245 return requestGroupMove(clipId, groupId, delta_track, delta_pos, moveMirrorTracks, updateView, logUndo, revertMove); 1246 } 1247 std::function<bool(void)> undo = []() { return true; }; 1248 std::function<bool(void)> redo = []() { return true; }; 1249 bool res = requestClipMove(clipId, trackId, position, moveMirrorTracks, updateView, invalidateTimeline, logUndo, undo, redo, revertMove); 1250 if (res && logUndo) { 1251 PUSH_UNDO(undo, redo, i18n("Move clip")); 1252 } 1253 TRACE_RES(res); 1254 return res; 1255 } 1256 1257 std::shared_ptr<SubtitleModel> TimelineModel::getSubtitleModel() 1258 { 1259 return m_subtitleModel; 1260 } 1261 1262 int TimelineModel::cutSubtitle(int position, Fun &undo, Fun &redo) 1263 { 1264 if (m_subtitleModel) { 1265 return m_subtitleModel->cutSubtitle(position, undo, redo); 1266 } 1267 return -1; 1268 } 1269 1270 bool TimelineModel::requestSubtitleMove(int clipId, int position, bool updateView, bool logUndo, bool finalMove) 1271 { 1272 QWriteLocker locker(&m_lock); 1273 Q_ASSERT(m_allSubtitles.count(clipId) > 0); 1274 GenTime oldPos = m_allSubtitles.at(clipId); 1275 GenTime newPos(position, pCore->getCurrentFps()); 1276 if (oldPos == newPos) { 1277 return true; 1278 } 1279 if (m_groups->isInGroup(clipId)) { 1280 // element is in a group. 1281 int groupId = m_groups->getRootId(clipId); 1282 int delta_pos = position - oldPos.frames(pCore->getCurrentFps()); 1283 return requestGroupMove(clipId, groupId, 0, delta_pos, false, updateView, logUndo); 1284 } 1285 std::function<bool(void)> undo = []() { return true; }; 1286 std::function<bool(void)> redo = []() { return true; }; 1287 bool res = requestSubtitleMove(clipId, position, updateView, logUndo, logUndo, finalMove, undo, redo); 1288 if (res && logUndo) { 1289 PUSH_UNDO(undo, redo, i18n("Move subtitle")); 1290 } 1291 return res; 1292 } 1293 1294 bool TimelineModel::requestSubtitleMove(int clipId, int position, bool updateView, bool first, bool last, bool finalMove, Fun &undo, Fun &redo) 1295 { 1296 QWriteLocker locker(&m_lock); 1297 GenTime oldPos = m_allSubtitles.at(clipId); 1298 GenTime newPos(position, pCore->getCurrentFps()); 1299 Fun local_redo = [this, clipId, newPos, reloadSubFile = last && finalMove, updateView]() { 1300 return m_subtitleModel->moveSubtitle(clipId, newPos, reloadSubFile, updateView); 1301 }; 1302 Fun local_undo = [this, oldPos, clipId, reloadSubFile = first && finalMove, updateView]() { 1303 return m_subtitleModel->moveSubtitle(clipId, oldPos, reloadSubFile, updateView); 1304 }; 1305 bool res = local_redo(); 1306 if (res) { 1307 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 1308 } else { 1309 local_undo(); 1310 } 1311 return res; 1312 } 1313 1314 bool TimelineModel::requestClipMoveAttempt(int clipId, int trackId, int position) 1315 { 1316 QWriteLocker locker(&m_lock); 1317 Q_ASSERT(m_allClips.count(clipId) > 0); 1318 if (m_allClips[clipId]->getPosition() == position && getClipTrackId(clipId) == trackId) { 1319 return true; 1320 } 1321 std::function<bool(void)> undo = []() { return true; }; 1322 std::function<bool(void)> redo = []() { return true; }; 1323 bool res = true; 1324 if (m_groups->isInGroup(clipId)) { 1325 // element is in a group. 1326 int groupId = m_groups->getRootId(clipId); 1327 int current_trackId = getClipTrackId(clipId); 1328 int track_pos1 = getTrackPosition(trackId); 1329 int track_pos2 = getTrackPosition(current_trackId); 1330 int delta_track = track_pos1 - track_pos2; 1331 int delta_pos = position - m_allClips[clipId]->getPosition(); 1332 res = requestGroupMove(clipId, groupId, delta_track, delta_pos, false, false, undo, redo, false, false); 1333 } else { 1334 res = requestClipMove(clipId, trackId, position, true, false, false, false, undo, redo); 1335 } 1336 if (res) { 1337 undo(); 1338 } 1339 return res; 1340 } 1341 1342 QVariantList TimelineModel::suggestItemMove(int itemId, int trackId, int position, int cursorPosition, int snapDistance) 1343 { 1344 if (isClip(itemId)) { 1345 return suggestClipMove(itemId, trackId, position, cursorPosition, snapDistance); 1346 } 1347 if (isComposition(itemId)) { 1348 return suggestCompositionMove(itemId, trackId, position, cursorPosition, snapDistance); 1349 } 1350 if (isSubTitle(itemId)) { 1351 return {suggestSubtitleMove(itemId, position, cursorPosition, snapDistance), -1}; 1352 } 1353 return QVariantList(); 1354 } 1355 1356 int TimelineModel::adjustFrame(int frame, int trackId) 1357 { 1358 if (m_editMode == TimelineMode::InsertEdit && isTrack(trackId)) { 1359 frame = qMin(frame, getTrackById_const(trackId)->trackDuration()); 1360 } 1361 return frame; 1362 } 1363 1364 int TimelineModel::suggestSubtitleMove(int subId, int position, int cursorPosition, int snapDistance) 1365 { 1366 QWriteLocker locker(&m_lock); 1367 Q_ASSERT(isSubTitle(subId)); 1368 int currentPos = getSubtitlePosition(subId); 1369 if (currentPos == position || m_subtitleModel->isLocked()) { 1370 return position; 1371 } 1372 int newPos = position; 1373 if (snapDistance > 0) { 1374 int offset = 0; 1375 std::vector<int> ignored_pts; 1376 // For snapping, we must ignore all in/outs of the clips of the group being moved 1377 std::unordered_set<int> all_items = {subId}; 1378 if (m_groups->isInGroup(subId)) { 1379 int groupId = m_groups->getRootId(subId); 1380 all_items = m_groups->getLeaves(groupId); 1381 } 1382 for (int current_clipId : all_items) { 1383 if (isClip(current_clipId)) { 1384 m_allClips[current_clipId]->allSnaps(ignored_pts, offset); 1385 } else if (isComposition(current_clipId) || isSubTitle(current_clipId)) { 1386 // Composition or subtitle 1387 int in = getItemPosition(current_clipId) - offset; 1388 ignored_pts.push_back(in); 1389 ignored_pts.push_back(in + getItemPlaytime(current_clipId)); 1390 } 1391 } 1392 int snapped = getBestSnapPos(currentPos, position - currentPos, ignored_pts, cursorPosition, snapDistance); 1393 if (snapped >= 0) { 1394 newPos = snapped; 1395 } 1396 } 1397 // m_subtitleModel->moveSubtitle(GenTime(currentPos, pCore->getCurrentFps()), GenTime(position, pCore->getCurrentFps())); 1398 if (requestSubtitleMove(subId, newPos, true, false)) { 1399 return newPos; 1400 } 1401 return currentPos; 1402 } 1403 1404 QVariantList TimelineModel::suggestClipMove(int clipId, int trackId, int position, int cursorPosition, int snapDistance, bool moveMirrorTracks) 1405 { 1406 QWriteLocker locker(&m_lock); 1407 TRACE(clipId, trackId, position, cursorPosition, snapDistance); 1408 Q_ASSERT(isClip(clipId)); 1409 Q_ASSERT(isTrack(trackId)); 1410 int currentPos = m_editMode == TimelineMode::NormalEdit ? getClipPosition(clipId) : m_allClips[clipId]->getFakePosition(); 1411 int offset = m_editMode == TimelineMode::NormalEdit ? 0 : getClipPosition(clipId) - currentPos; 1412 int sourceTrackId = (m_editMode != TimelineMode::NormalEdit) ? m_allClips[clipId]->getFakeTrackId() : getClipTrackId(clipId); 1413 if (sourceTrackId > -1 && getTrackById_const(trackId)->isAudioTrack() != getTrackById_const(sourceTrackId)->isAudioTrack()) { 1414 // Trying move on incompatible track type, stay on same track 1415 trackId = sourceTrackId; 1416 } 1417 if (currentPos == position && sourceTrackId == trackId) { 1418 TRACE_RES(position); 1419 return {position, trackId}; 1420 } 1421 if (m_editMode == TimelineMode::InsertEdit) { 1422 int maxPos = getTrackById_const(trackId)->trackDuration(); 1423 if (m_allClips[clipId]->getCurrentTrackId() == trackId) { 1424 maxPos -= m_allClips[clipId]->getPlaytime(); 1425 } 1426 position = qMin(position, maxPos); 1427 } 1428 bool after = position > currentPos; 1429 if (snapDistance > 0) { 1430 std::vector<int> ignored_pts; 1431 // For snapping, we must ignore all in/outs of the clips of the group being moved 1432 std::unordered_set<int> all_items = {clipId}; 1433 if (m_groups->isInGroup(clipId)) { 1434 int groupId = m_groups->getRootId(clipId); 1435 all_items = m_groups->getLeaves(groupId); 1436 } 1437 for (int current_clipId : all_items) { 1438 if (isClip(current_clipId)) { 1439 m_allClips[current_clipId]->allSnaps(ignored_pts, offset); 1440 } else if (isComposition(current_clipId) || isSubTitle(current_clipId)) { 1441 // Composition 1442 int in = getItemPosition(current_clipId) - offset; 1443 ignored_pts.push_back(in); 1444 ignored_pts.push_back(in + getItemPlaytime(current_clipId)); 1445 } 1446 } 1447 int snapped = getBestSnapPos(currentPos, position - currentPos, ignored_pts, cursorPosition, snapDistance); 1448 if (snapped >= 0) { 1449 position = snapped; 1450 } 1451 } 1452 position = qMax(0, position); 1453 if (currentPos == position && sourceTrackId == trackId) { 1454 TRACE_RES(position); 1455 return {position, trackId}; 1456 } 1457 bool isInGroup = m_groups->isInGroup(clipId); 1458 if (sourceTrackId == trackId) { 1459 // Same track move, check if there is a mix and limit move 1460 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(trackId)->getMixInfo(clipId); 1461 if (mixData.first.firstClipId > -1) { 1462 // Clip has start mix 1463 int clipDuration = m_allClips[clipId]->getPlaytime(); 1464 // ensure we don't move into clip 1465 bool allowMove = false; 1466 if (isInGroup) { 1467 // Check if in same group as clip mix 1468 int groupId = m_groups->getRootId(clipId); 1469 if (groupId == m_groups->getRootId(mixData.first.firstClipId)) { 1470 allowMove = true; 1471 } 1472 } 1473 if (!allowMove && position + clipDuration > mixData.first.firstClipInOut.first && position < mixData.first.firstClipInOut.second) { 1474 // Abort move 1475 return {currentPos, sourceTrackId}; 1476 } 1477 } 1478 if (mixData.second.firstClipId > -1) { 1479 // Clip has end mix 1480 int clipDuration = m_allClips[clipId]->getPlaytime(); 1481 bool allowMove = false; 1482 if (isInGroup) { 1483 // Check if in same group as clip mix 1484 int groupId = m_groups->getRootId(clipId); 1485 if (groupId == m_groups->getRootId(mixData.second.secondClipId)) { 1486 allowMove = true; 1487 } 1488 } 1489 if (!allowMove && position + clipDuration > mixData.second.secondClipInOut.first && position < mixData.second.secondClipInOut.second) { 1490 // Abort move 1491 return {currentPos, sourceTrackId}; 1492 } 1493 } 1494 } 1495 if (moveMirrorTracks && m_singleSelectionMode) { 1496 // If we are in single selection mode, only move active clip 1497 moveMirrorTracks = false; 1498 } 1499 // we check if move is possible 1500 bool possible = (m_editMode == TimelineMode::NormalEdit) ? requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false) 1501 : requestFakeClipMove(clipId, trackId, position, true, false, false); 1502 1503 if (possible) { 1504 TRACE_RES(position); 1505 if (m_editMode != TimelineMode::NormalEdit) { 1506 trackId = m_allClips[clipId]->getFakeTrackId(); 1507 } 1508 return {position, trackId}; 1509 } 1510 if (sourceTrackId == -1) { 1511 // not clear what to do here, if the current move doesn't work. We could try to find empty space, but it might end up being far away... 1512 TRACE_RES(currentPos); 1513 return {currentPos, -1}; 1514 } 1515 // Find best possible move 1516 if (!isInGroup) { 1517 // Try same track move 1518 if (trackId != sourceTrackId && sourceTrackId != -1) { 1519 trackId = sourceTrackId; 1520 possible = requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false); 1521 if (!possible) { 1522 qWarning() << "can't move clip" << clipId << "on track" << trackId << "at" << position; 1523 } else { 1524 TRACE_RES(position); 1525 return {position, trackId}; 1526 } 1527 } 1528 1529 int blank_length = getTrackById_const(trackId)->getBlankSizeNearClip(clipId, after); 1530 if (blank_length < INT_MAX) { 1531 if (after) { 1532 position = currentPos + blank_length; 1533 } else { 1534 position = currentPos - blank_length; 1535 } 1536 } else { 1537 TRACE_RES(currentPos); 1538 return {currentPos, sourceTrackId}; 1539 } 1540 possible = requestClipMove(clipId, trackId, position, moveMirrorTracks, true, false, false); 1541 TRACE_RES(possible ? position : currentPos); 1542 if (possible) { 1543 return {position, trackId}; 1544 } 1545 return {currentPos, sourceTrackId}; 1546 } 1547 if (trackId != sourceTrackId) { 1548 // Try same track move 1549 possible = requestClipMove(clipId, sourceTrackId, position, moveMirrorTracks, true, false, false); 1550 if (possible) { 1551 return {position, sourceTrackId}; 1552 } 1553 return {currentPos, sourceTrackId}; 1554 } 1555 // find best pos for groups 1556 int groupId = m_groups->getRootId(clipId); 1557 std::unordered_set<int> all_items = m_groups->getLeaves(groupId); 1558 QMap<int, int> trackPosition; 1559 1560 // First pass, sort clips by track and keep only the first / last depending on move direction 1561 for (int current_clipId : all_items) { 1562 int clipTrack = getItemTrackId(current_clipId); 1563 if (clipTrack == -1) { 1564 continue; 1565 } 1566 int in = getItemPosition(current_clipId); 1567 if (trackPosition.contains(clipTrack)) { 1568 if (after) { 1569 // keep only last clip position for track 1570 int out = in + getItemPlaytime(current_clipId); 1571 if (trackPosition.value(clipTrack) < out) { 1572 trackPosition.insert(clipTrack, out); 1573 } 1574 } else { 1575 // keep only first clip position for track 1576 if (trackPosition.value(clipTrack) > in) { 1577 trackPosition.insert(clipTrack, in); 1578 } 1579 } 1580 } else { 1581 trackPosition.insert(clipTrack, after ? in + getItemPlaytime(current_clipId) - 1 : in); 1582 } 1583 } 1584 1585 // Now check space on each track 1586 QMapIterator<int, int> i(trackPosition); 1587 int blank_length = 0; 1588 while (i.hasNext()) { 1589 i.next(); 1590 int track_space; 1591 if (!after) { 1592 // Check space before the position 1593 if (isSubtitleTrack(i.key())) { 1594 track_space = i.value(); 1595 } else { 1596 track_space = i.value() - getTrackById_const(i.key())->getBlankStart(i.value() - 1); 1597 } 1598 if (blank_length == 0 || blank_length > track_space) { 1599 blank_length = track_space; 1600 } 1601 } else { 1602 // Check space after the position 1603 if (isSubtitleTrack(i.key())) { 1604 continue; 1605 } 1606 track_space = getTrackById(i.key())->getBlankEnd(i.value() + 1) - i.value(); 1607 if (blank_length == 0 || blank_length > track_space) { 1608 blank_length = track_space; 1609 } 1610 } 1611 } 1612 if (snapDistance > 0) { 1613 if (blank_length > 10 * snapDistance) { 1614 blank_length = 0; 1615 } 1616 } else if (blank_length / pCore->getProjectProfile().fps() > 5) { 1617 blank_length = 0; 1618 } 1619 if (blank_length != 0) { 1620 int updatedPos = currentPos + (after ? blank_length : -blank_length); 1621 possible = requestClipMove(clipId, trackId, updatedPos, moveMirrorTracks, true, false, false); 1622 if (possible) { 1623 TRACE_RES(updatedPos); 1624 return {updatedPos, trackId}; 1625 } 1626 } 1627 TRACE_RES(currentPos); 1628 return {currentPos, sourceTrackId}; 1629 } 1630 1631 QVariantList TimelineModel::suggestCompositionMove(int compoId, int trackId, int position, int cursorPosition, int snapDistance) 1632 { 1633 QWriteLocker locker(&m_lock); 1634 TRACE(compoId, trackId, position, cursorPosition, snapDistance); 1635 Q_ASSERT(isComposition(compoId)); 1636 Q_ASSERT(isTrack(trackId)); 1637 int currentPos = getCompositionPosition(compoId); 1638 int currentTrack = getCompositionTrackId(compoId); 1639 if (getTrackById_const(trackId)->isAudioTrack()) { 1640 // Trying move on incompatible track type, stay on same track 1641 trackId = currentTrack; 1642 } 1643 if (currentPos == position && currentTrack == trackId) { 1644 TRACE_RES(position); 1645 return {position, trackId}; 1646 } 1647 1648 if (snapDistance > 0) { 1649 // For snapping, we must ignore all in/outs of the clips of the group being moved 1650 std::vector<int> ignored_pts; 1651 if (m_groups->isInGroup(compoId)) { 1652 int groupId = m_groups->getRootId(compoId); 1653 auto all_items = m_groups->getLeaves(groupId); 1654 for (int current_compoId : all_items) { 1655 // TODO: fix for composition 1656 int in = getItemPosition(current_compoId); 1657 ignored_pts.push_back(in); 1658 ignored_pts.push_back(in + getItemPlaytime(current_compoId)); 1659 } 1660 } else { 1661 int in = currentPos; 1662 int out = in + getCompositionPlaytime(compoId); 1663 ignored_pts.push_back(in); 1664 ignored_pts.push_back(out); 1665 } 1666 int snapped = getBestSnapPos(currentPos, position - currentPos, ignored_pts, cursorPosition, snapDistance); 1667 if (snapped >= 0) { 1668 position = snapped; 1669 } 1670 } 1671 position = qMax(0, position); 1672 if (currentPos == position && currentTrack == trackId) { 1673 TRACE_RES(position); 1674 return {position, trackId}; 1675 } 1676 // we check if move is possible 1677 bool possible = requestCompositionMove(compoId, trackId, position, true, false); 1678 if (possible) { 1679 TRACE_RES(position); 1680 return {position, trackId}; 1681 } 1682 TRACE_RES(currentPos); 1683 return {currentPos, currentTrack}; 1684 } 1685 1686 bool TimelineModel::requestClipCreation(const QString &binClipId, int &id, PlaylistState::ClipState state, int audioStream, double speed, bool warp_pitch, 1687 Fun &undo, Fun &redo) 1688 { 1689 QString bid = binClipId; 1690 if (binClipId.contains(QLatin1Char('/'))) { 1691 bid = binClipId.section(QLatin1Char('/'), 0, 0); 1692 } 1693 if (!pCore->projectItemModel()->hasClip(bid)) { 1694 qWarning() << "master clip not found"; 1695 return false; 1696 } 1697 std::shared_ptr<ProjectClip> master = pCore->projectItemModel()->getClipByBinID(bid); 1698 if (!master->statusReady() || !master->isCompatible(state)) { 1699 qWarning() << "clip not ready or not compatible" << state << master->statusReady(); 1700 return false; 1701 } 1702 int clipId = TimelineModel::getNextId(); 1703 id = clipId; 1704 qDebug() << "======\nCREATING TIMELINE OBJECT: " << clipId << "\n========================"; 1705 Fun local_undo = deregisterClip_lambda(clipId); 1706 ClipModel::construct(shared_from_this(), bid, clipId, state, audioStream, speed, warp_pitch); 1707 auto clip = m_allClips[clipId]; 1708 Fun local_redo = [clip, this, state, audioStream, speed, warp_pitch]() { 1709 // We capture a shared_ptr to the clip, which means that as long as this undo object lives, the clip object is not deleted. To insert it back it is 1710 // sufficient to register it. 1711 registerClip(clip, true); 1712 clip->refreshProducerFromBin(-1, state, audioStream, speed, warp_pitch); 1713 return true; 1714 }; 1715 1716 if (binClipId.contains(QLatin1Char('/'))) { 1717 int in = binClipId.section(QLatin1Char('/'), 1, 1).toInt(); 1718 int out = binClipId.section(QLatin1Char('/'), 2, 2).toInt(); 1719 int initLength = m_allClips[clipId]->getPlaytime(); 1720 bool res = true; 1721 if (in != 0) { 1722 initLength -= in; 1723 res = requestItemResize(clipId, initLength, false, true, local_undo, local_redo); 1724 } 1725 int updatedDuration = out - in + 1; // +1: e.g. in=100, out=101 is 2 frames long 1726 res = res && requestItemResize(clipId, updatedDuration, true, true, local_undo, local_redo); 1727 if (!res) { 1728 bool undone = local_undo(); 1729 Q_ASSERT(undone); 1730 return false; 1731 } 1732 } 1733 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 1734 return true; 1735 } 1736 1737 bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets) 1738 { 1739 QWriteLocker locker(&m_lock); 1740 TRACE(binClipId, trackId, position, id, logUndo, refreshView, useTargets); 1741 Fun undo = []() { return true; }; 1742 Fun redo = []() { return true; }; 1743 QVector<int> allowedTracks; 1744 if (useTargets) { 1745 auto it = m_allTracks.cbegin(); 1746 while (it != m_allTracks.cend()) { 1747 int target_track = (*it)->getId(); 1748 if (getTrackById_const(target_track)->shouldReceiveTimelineOp()) { 1749 allowedTracks << target_track; 1750 } 1751 ++it; 1752 } 1753 } 1754 if (useTargets && allowedTracks.isEmpty()) { 1755 pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage, 500); 1756 return false; 1757 } 1758 bool result = requestClipInsertion(binClipId, trackId, position, id, logUndo, refreshView, useTargets, undo, redo, allowedTracks); 1759 if (result && logUndo) { 1760 PUSH_UNDO(undo, redo, i18n("Insert Clip")); 1761 } 1762 TRACE_RES(result); 1763 return result; 1764 } 1765 1766 bool TimelineModel::requestClipInsertion(const QString &binClipId, int trackId, int position, int &id, bool logUndo, bool refreshView, bool useTargets, 1767 Fun &undo, Fun &redo, const QVector<int> &allowedTracks) 1768 { 1769 Fun local_undo = []() { return true; }; 1770 Fun local_redo = []() { return true; }; 1771 bool res = false; 1772 ClipType::ProducerType type = ClipType::Unknown; 1773 // binClipId id is in the form: A2/10/50 1774 // A2 means audio only insertion for bin clip with id 2 1775 // 10 is in point 1776 // 50 is out point 1777 QString binIdWithInOut = binClipId; 1778 // bid is the A2 part 1779 QString bid = binClipId.section(QLatin1Char('/'), 0, 0); 1780 // dropType indicates if we want a normal drop (disabled), audio only or video only drop 1781 PlaylistState::ClipState dropType = PlaylistState::Disabled; 1782 if (bid.startsWith(QLatin1Char('A'))) { 1783 dropType = PlaylistState::AudioOnly; 1784 bid.remove(0, 1); 1785 binIdWithInOut.remove(0, 1); 1786 } else if (bid.startsWith(QLatin1Char('V'))) { 1787 dropType = PlaylistState::VideoOnly; 1788 bid.remove(0, 1); 1789 binIdWithInOut.remove(0, 1); 1790 } 1791 if (!pCore->projectItemModel()->hasClip(bid)) { 1792 qWarning() << "no clip found in bin for" << bid; 1793 return false; 1794 } 1795 1796 bool audioDrop = false; 1797 if (!useTargets) { 1798 audioDrop = getTrackById_const(trackId)->isAudioTrack(); 1799 if (audioDrop) { 1800 if (dropType == PlaylistState::VideoOnly) { 1801 return false; 1802 } 1803 } else if (dropType == PlaylistState::AudioOnly) { 1804 return false; 1805 } 1806 } 1807 1808 std::shared_ptr<ProjectClip> master = pCore->projectItemModel()->getClipByBinID(bid); 1809 type = master->clipType(); 1810 // Ensure we don't insert a timeline clip onto itself 1811 if (type == ClipType::Timeline && !master->canBeDropped(m_uuid)) { 1812 // Abort insert 1813 pCore->displayMessage(i18n("You cannot insert a sequence containing itself"), ErrorMessage); 1814 return false; 1815 } 1816 if (useTargets && m_audioTarget.isEmpty() && m_videoTarget == -1) { 1817 useTargets = false; 1818 } 1819 if ((dropType == PlaylistState::Disabled || dropType == PlaylistState::AudioOnly) && 1820 (type == ClipType::AV || type == ClipType::Playlist || type == ClipType::Timeline)) { 1821 bool useAudioTarget = false; 1822 if (useTargets && !m_audioTarget.isEmpty() && m_videoTarget == -1) { 1823 // If audio target is set but no video target, only insert audio 1824 useAudioTarget = true; 1825 } else if (useTargets && (getTrackById_const(trackId)->isLocked() || !allowedTracks.contains(trackId))) { 1826 // Video target set but locked 1827 useAudioTarget = true; 1828 } 1829 if (useAudioTarget) { 1830 // Find first possible audio target 1831 QList<int> audioTargetTracks = m_audioTarget.keys(); 1832 trackId = -1; 1833 for (int tid : qAsConst(audioTargetTracks)) { 1834 if (tid > -1 && !getTrackById_const(tid)->isLocked() && allowedTracks.contains(tid)) { 1835 trackId = tid; 1836 break; 1837 } 1838 } 1839 } 1840 if (trackId == -1) { 1841 if (!allowedTracks.isEmpty()) { 1842 // No active tracks, aborting 1843 return true; 1844 } 1845 pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage); 1846 return false; 1847 } 1848 int audioStream = -1; 1849 QList<int> keys = m_binAudioTargets.keys(); 1850 if (!useTargets) { 1851 // Drag and drop, calculate target tracks 1852 if (audioDrop) { 1853 if (keys.count() > 1) { 1854 // Dropping a clip with several audio streams 1855 int tracksBelow = getLowerTracksId(trackId, TrackType::AudioTrack).count(); 1856 if (tracksBelow < keys.count() - 1) { 1857 // We don't have enough audio tracks below, check above 1858 QList<int> audioTrackIds = getTracksIds(true); 1859 if (audioTrackIds.count() < keys.count()) { 1860 // Not enough audio tracks 1861 pCore->displayMessage(i18n("Not enough audio tracks for all streams (%1)", keys.count()), ErrorMessage); 1862 return false; 1863 } 1864 trackId = audioTrackIds.at(audioTrackIds.count() - keys.count()); 1865 } 1866 } 1867 if (keys.isEmpty()) { 1868 pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage); 1869 return false; 1870 } 1871 audioStream = keys.first(); 1872 } else { 1873 // Dropping video, ensure we have enough audio tracks for its streams 1874 int mirror = getMirrorTrackId(trackId); 1875 QList<int> audioTids = {}; 1876 if (mirror > -1) { 1877 audioTids = getLowerTracksId(mirror, TrackType::AudioTrack); 1878 } 1879 if (audioTids.count() < keys.count() - 1 || (mirror == -1 && !keys.isEmpty())) { 1880 // Check if project has enough audio tracks 1881 if (keys.count() > getTracksIds(true).count()) { 1882 // Not enough audio tracks in the project 1883 pCore->displayMessage(i18n("Not enough audio tracks for all streams (%1)", keys.count()), ErrorMessage); 1884 return false; 1885 } else { 1886 // Check if all audio tracks are locked. In that case allow inserting video only 1887 QList<int> audioTracks = getTracksIds(true); 1888 auto is_unlocked = [&](int tid) { return !getTrackById_const(tid)->isLocked(); }; 1889 bool hasUnlockedAudio = std::any_of(audioTracks.begin(), audioTracks.end(), is_unlocked); 1890 if (hasUnlockedAudio) { 1891 pCore->displayMessage(i18n("No available track for insert operation"), ErrorMessage); 1892 return false; 1893 } else { 1894 keys.clear(); 1895 } 1896 } 1897 } 1898 } 1899 } else if (audioDrop) { 1900 // Drag & drop, use our first audio target 1901 audioStream = m_audioTarget.first(); 1902 } else { 1903 // Using target tracks 1904 if (m_audioTarget.contains(trackId)) { 1905 audioStream = m_audioTarget.value(trackId); 1906 } 1907 } 1908 1909 res = requestClipCreation(binIdWithInOut, id, getTrackById_const(trackId)->trackType(), audioStream, 1.0, false, local_undo, local_redo); 1910 res = res && requestClipMove(id, trackId, position, true, refreshView, logUndo, logUndo, local_undo, local_redo); 1911 // Get mirror track 1912 int mirror = dropType == PlaylistState::Disabled ? getMirrorTrackId(trackId) : -1; 1913 if (mirror > -1 && getTrackById_const(mirror)->isLocked() && !useTargets) { 1914 mirror = -1; 1915 } 1916 QList<int> target_track; 1917 if (audioDrop) { 1918 if (m_videoTarget > -1 && !getTrackById_const(m_videoTarget)->isLocked() && dropType != PlaylistState::AudioOnly) { 1919 target_track << m_videoTarget; 1920 } 1921 } else if (useTargets) { 1922 QList<int> targetIds = m_audioTarget.keys(); 1923 targetIds.removeAll(trackId); 1924 for (int &ix : targetIds) { 1925 if (!getTrackById_const(ix)->isLocked() && allowedTracks.contains(ix)) { 1926 target_track << ix; 1927 } 1928 } 1929 } 1930 1931 bool canMirrorDrop = !useTargets && ((mirror > -1 && (audioDrop || !keys.isEmpty())) || keys.count() > 1); 1932 QMap<int, int> dropTargets; 1933 if (res && (canMirrorDrop || !target_track.isEmpty()) && master->hasAudioAndVideo()) { 1934 if (!useTargets) { 1935 int streamsCount = 0; 1936 target_track.clear(); 1937 QList<int> audioTids; 1938 if (!audioDrop) { 1939 // insert audio mirror track 1940 if (mirror > -1) { 1941 target_track << mirror; 1942 audioTids = getLowerTracksId(mirror, TrackType::AudioTrack); 1943 } 1944 } else { 1945 audioTids = getLowerTracksId(trackId, TrackType::AudioTrack); 1946 } 1947 // First audio stream already inserted in target_track or in timeline 1948 streamsCount = m_binAudioTargets.count() - 1; 1949 while (streamsCount > 0 && !audioTids.isEmpty()) { 1950 target_track << audioTids.takeFirst(); 1951 streamsCount--; 1952 } 1953 QList<int> aTargets = m_binAudioTargets.keys(); 1954 if (audioDrop) { 1955 aTargets.removeAll(audioStream); 1956 } 1957 std::sort(aTargets.begin(), aTargets.end()); 1958 for (int i = 0; i < target_track.count() && i < aTargets.count(); ++i) { 1959 dropTargets.insert(target_track.at(i), aTargets.at(i)); 1960 } 1961 if (audioDrop && mirror > -1) { 1962 target_track << mirror; 1963 } 1964 } 1965 if (target_track.isEmpty() && useTargets) { 1966 // No available track for splitting, abort 1967 pCore->displayMessage(i18n("No available track for split operation"), ErrorMessage); 1968 res = false; 1969 } 1970 if (!res) { 1971 bool undone = local_undo(); 1972 Q_ASSERT(undone); 1973 id = -1; 1974 return false; 1975 } 1976 // Process all mirror insertions 1977 std::function<bool(void)> audio_undo = []() { return true; }; 1978 std::function<bool(void)> audio_redo = []() { return true; }; 1979 std::unordered_set<int> createdMirrors = {id}; 1980 int mirrorAudioStream = -1; 1981 for (int &target_ix : target_track) { 1982 bool currentDropIsAudio = !audioDrop; 1983 if (!useTargets && m_binAudioTargets.count() > 1 && dropTargets.contains(target_ix)) { 1984 // Audio clip dropped first but has other streams 1985 currentDropIsAudio = true; 1986 mirrorAudioStream = dropTargets.value(target_ix); 1987 if (mirrorAudioStream == audioStream) { 1988 continue; 1989 } 1990 } else if (currentDropIsAudio) { 1991 if (!useTargets) { 1992 mirrorAudioStream = dropTargets.value(target_ix); 1993 } else { 1994 mirrorAudioStream = m_audioTarget.value(target_ix); 1995 } 1996 } 1997 int newId; 1998 res = requestClipCreation(binIdWithInOut, newId, currentDropIsAudio ? PlaylistState::AudioOnly : PlaylistState::VideoOnly, 1999 currentDropIsAudio ? mirrorAudioStream : -1, 1.0, false, audio_undo, audio_redo); 2000 if (res) { 2001 res = requestClipMove(newId, target_ix, position, true, true, true, true, audio_undo, audio_redo); 2002 // use lazy evaluation to group only if move was successful 2003 if (!res) { 2004 pCore->displayMessage(i18n("Audio split failed: no viable track"), ErrorMessage); 2005 bool undone = audio_undo(); 2006 Q_ASSERT(undone); 2007 break; 2008 } else { 2009 createdMirrors.insert(newId); 2010 } 2011 } else { 2012 pCore->displayMessage(i18n("Audio split failed: impossible to create audio clip"), ErrorMessage); 2013 bool undone = audio_undo(); 2014 Q_ASSERT(undone); 2015 break; 2016 } 2017 } 2018 if (res) { 2019 requestClipsGroup(createdMirrors, audio_undo, audio_redo, GroupType::AVSplit); 2020 UPDATE_UNDO_REDO(audio_redo, audio_undo, local_undo, local_redo); 2021 } 2022 } 2023 } else { 2024 std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(bid); 2025 if (dropType == PlaylistState::Disabled) { 2026 dropType = getTrackById_const(trackId)->trackType(); 2027 } else if (dropType != getTrackById_const(trackId)->trackType()) { 2028 return false; 2029 } 2030 QString normalisedBinId = binClipId; 2031 if (normalisedBinId.startsWith(QLatin1Char('A')) || normalisedBinId.startsWith(QLatin1Char('V'))) { 2032 normalisedBinId.remove(0, 1); 2033 } 2034 res = requestClipCreation(normalisedBinId, id, dropType, binClip->getProducerIntProperty(QStringLiteral("audio_index")), 1.0, false, local_undo, 2035 local_redo); 2036 res = res && requestClipMove(id, trackId, position, true, refreshView, logUndo, logUndo, local_undo, local_redo); 2037 } 2038 if (!res) { 2039 bool undone = local_undo(); 2040 Q_ASSERT(undone); 2041 id = -1; 2042 return false; 2043 } 2044 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 2045 return true; 2046 } 2047 2048 bool TimelineModel::requestItemDeletion(int itemId, Fun &undo, Fun &redo, bool logUndo) 2049 { 2050 QWriteLocker locker(&m_lock); 2051 if (m_groups->isInGroup(itemId)) { 2052 return requestGroupDeletion(itemId, undo, redo); 2053 } 2054 if (isClip(itemId)) { 2055 return requestClipDeletion(itemId, undo, redo, logUndo); 2056 } 2057 if (isComposition(itemId)) { 2058 return requestCompositionDeletion(itemId, undo, redo); 2059 } 2060 if (isSubTitle(itemId)) { 2061 return requestSubtitleDeletion(itemId, undo, redo, true, true); 2062 } 2063 Q_ASSERT(false); 2064 return false; 2065 } 2066 2067 bool TimelineModel::requestItemDeletion(int itemId, bool logUndo) 2068 { 2069 QWriteLocker locker(&m_lock); 2070 TRACE(itemId, logUndo); 2071 Q_ASSERT(isItem(itemId)); 2072 QString actionLabel; 2073 if (m_groups->isInGroup(itemId) && !m_singleSelectionMode) { 2074 actionLabel = i18n("Remove group"); 2075 } else { 2076 if (isClip(itemId)) { 2077 actionLabel = i18n("Delete Clip"); 2078 } else if (isComposition(itemId)) { 2079 actionLabel = i18n("Delete Composition"); 2080 } else if (isSubTitle(itemId)) { 2081 actionLabel = i18n("Delete Subtitle"); 2082 } 2083 } 2084 Fun undo = []() { return true; }; 2085 Fun redo = []() { return true; }; 2086 2087 bool res = true; 2088 if (m_singleSelectionMode) { 2089 auto selection = getCurrentSelection(); 2090 // Ungroup all items first 2091 for (int id : selection) { 2092 // Ungroup item before deletion 2093 requestRemoveFromGroup(id, undo, redo); 2094 } 2095 // loop deletion 2096 for (int id : selection) { 2097 res = res && requestItemDeletion(id, undo, redo, logUndo); 2098 ; 2099 } 2100 } else { 2101 res = requestItemDeletion(itemId, undo, redo, logUndo); 2102 } 2103 if (res && logUndo) { 2104 PUSH_UNDO(undo, redo, actionLabel); 2105 } 2106 TRACE_RES(res); 2107 return res; 2108 } 2109 2110 bool TimelineModel::requestClipDeletion(int clipId, Fun &undo, Fun &redo, bool logUndo) 2111 { 2112 int trackId = getClipTrackId(clipId); 2113 if (trackId != -1) { 2114 bool res = true; 2115 if (getTrackById_const(trackId)->hasStartMix(clipId)) { 2116 MixInfo mixData = getTrackById_const(trackId)->getMixInfo(clipId).first; 2117 if (isClip(mixData.firstClipId)) { 2118 res = getTrackById(trackId)->requestRemoveMix({mixData.firstClipId, clipId}, undo, redo); 2119 } 2120 } 2121 if (getTrackById_const(trackId)->hasEndMix(clipId)) { 2122 MixInfo mixData = getTrackById_const(trackId)->getMixInfo(clipId).second; 2123 if (isClip(mixData.secondClipId)) { 2124 res = getTrackById(trackId)->requestRemoveMix({clipId, mixData.secondClipId}, undo, redo); 2125 } 2126 } 2127 res = res && getTrackById(trackId)->requestClipDeletion(clipId, true, logUndo && !m_closing, undo, redo, false, true); 2128 if (!res) { 2129 undo(); 2130 return false; 2131 } 2132 } 2133 auto operation = deregisterClip_lambda(clipId); 2134 auto clip = m_allClips[clipId]; 2135 Fun reverse = [this, clip]() { 2136 // We capture a shared_ptr to the clip, which means that as long as this undo object lives, 2137 // the clip object is not deleted. To insert it back it is sufficient to register it. 2138 registerClip(clip, true); 2139 return true; 2140 }; 2141 if (operation()) { 2142 UPDATE_UNDO_REDO(operation, reverse, undo, redo); 2143 return true; 2144 } 2145 undo(); 2146 return false; 2147 } 2148 2149 bool TimelineModel::requestSubtitleDeletion(int clipId, Fun &undo, Fun &redo, bool first, bool last) 2150 { 2151 GenTime startTime = m_allSubtitles.at(clipId); 2152 SubtitledTime sub = m_subtitleModel->getSubtitle(startTime); 2153 Fun operation = [this, clipId, last]() { return m_subtitleModel->removeSubtitle(clipId, false, last); }; 2154 GenTime start = sub.start(); 2155 GenTime end = sub.end(); 2156 QString text = sub.subtitle(); 2157 Fun reverse = [this, clipId, start, end, text, first]() { return m_subtitleModel->addSubtitle(clipId, start, end, text, false, first); }; 2158 if (operation()) { 2159 UPDATE_UNDO_REDO(operation, reverse, undo, redo); 2160 return true; 2161 } 2162 return false; 2163 } 2164 2165 bool TimelineModel::requestCompositionDeletion(int compositionId, Fun &undo, Fun &redo) 2166 { 2167 int trackId = getCompositionTrackId(compositionId); 2168 if (trackId != -1) { 2169 bool res = getTrackById(trackId)->requestCompositionDeletion(compositionId, true, true, undo, redo, true); 2170 if (!res) { 2171 undo(); 2172 return false; 2173 } else { 2174 Fun unplant_op = [this, compositionId]() { 2175 unplantComposition(compositionId); 2176 return true; 2177 }; 2178 unplant_op(); 2179 PUSH_LAMBDA(unplant_op, redo); 2180 } 2181 } 2182 Fun operation = deregisterComposition_lambda(compositionId); 2183 auto composition = m_allCompositions[compositionId]; 2184 int new_in = composition->getPosition(); 2185 int new_out = new_in + composition->getPlaytime(); 2186 Fun reverse = [this, composition, compositionId, trackId, new_in, new_out]() { 2187 // We capture a shared_ptr to the composition, which means that as long as this undo object lives, 2188 // the composition object is not deleted. To insert it back it is sufficient to register it. 2189 registerComposition(composition); 2190 composition->setCurrentTrackId(trackId, true); 2191 replantCompositions(compositionId, false); 2192 checkRefresh(new_in, new_out); 2193 return true; 2194 }; 2195 if (operation()) { 2196 Fun update_monitor = [this, new_in, new_out]() { 2197 checkRefresh(new_in, new_out); 2198 return true; 2199 }; 2200 update_monitor(); 2201 PUSH_LAMBDA(update_monitor, operation); 2202 UPDATE_UNDO_REDO(operation, reverse, undo, redo); 2203 return true; 2204 } 2205 undo(); 2206 return false; 2207 } 2208 2209 std::unordered_set<int> TimelineModel::getItemsInRange(int trackId, int start, int end, bool listCompositions) 2210 { 2211 Q_UNUSED(listCompositions) 2212 2213 std::unordered_set<int> allClips; 2214 if (isSubtitleTrack(trackId) || trackId == -1) { 2215 // Subtitles 2216 if (m_subtitleModel) { 2217 std::unordered_set<int> subs = m_subtitleModel->getItemsInRange(start, end); 2218 allClips.insert(subs.begin(), subs.end()); 2219 } 2220 } 2221 if (trackId == -1) { 2222 for (const auto &track : m_allTracks) { 2223 if (track->isLocked()) { 2224 continue; 2225 } 2226 std::unordered_set<int> clipTracks = getItemsInRange(track->getId(), start, end, listCompositions); 2227 allClips.insert(clipTracks.begin(), clipTracks.end()); 2228 } 2229 } else if (trackId >= 0) { 2230 std::unordered_set<int> clipTracks = getTrackById(trackId)->getClipsInRange(start, end); 2231 allClips.insert(clipTracks.begin(), clipTracks.end()); 2232 if (listCompositions) { 2233 std::unordered_set<int> compoTracks = getTrackById(trackId)->getCompositionsInRange(start, end); 2234 allClips.insert(compoTracks.begin(), compoTracks.end()); 2235 } 2236 } 2237 return allClips; 2238 } 2239 2240 bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) 2241 { 2242 TRACE(clipId, groupId, delta_track, delta_pos, updateView, logUndo); 2243 std::function<bool(void)> undo = []() { return true; }; 2244 std::function<bool(void)> redo = []() { return true; }; 2245 bool res = requestFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); 2246 if (res && logUndo) { 2247 PUSH_UNDO(undo, redo, i18n("Move group")); 2248 } 2249 TRACE_RES(res); 2250 return res; 2251 } 2252 2253 bool TimelineModel::requestFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, 2254 bool allowViewRefresh) 2255 { 2256 Q_UNUSED(updateView); 2257 Q_UNUSED(finalMove); 2258 Q_UNUSED(undo); 2259 Q_UNUSED(redo); 2260 Q_UNUSED(allowViewRefresh); 2261 QWriteLocker locker(&m_lock); 2262 Q_ASSERT(m_allGroups.count(groupId) > 0); 2263 bool ok = true; 2264 auto all_items = m_groups->getLeaves(groupId); 2265 Q_ASSERT(all_items.size() > 1); 2266 Fun local_undo = []() { return true; }; 2267 Fun local_redo = []() { return true; }; 2268 2269 // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. 2270 // This way, we ensure that no conflict will arise with clips inside the group being moved 2271 2272 // Check if there is a track move 2273 2274 // First, remove clips 2275 bool hasAudio = false; 2276 bool hasVideo = false; 2277 std::unordered_map<int, int> old_track_ids, old_position, old_forced_track; 2278 for (int item : all_items) { 2279 int old_trackId = getItemTrackId(item); 2280 old_track_ids[item] = old_trackId; 2281 if (old_trackId != -1) { 2282 if (isClip(item)) { 2283 old_position[item] = m_allClips[item]->getPosition(); 2284 if (!hasAudio && getTrackById_const(old_trackId)->isAudioTrack()) { 2285 hasAudio = true; 2286 } else if (!hasVideo && !getTrackById_const(old_trackId)->isAudioTrack()) { 2287 hasVideo = true; 2288 } 2289 } else { 2290 hasVideo = true; 2291 old_position[item] = m_allCompositions[item]->getPosition(); 2292 old_forced_track[item] = m_allCompositions[item]->getForcedTrack(); 2293 } 2294 } 2295 } 2296 2297 // Second step, calculate delta 2298 int audio_delta, video_delta; 2299 audio_delta = video_delta = delta_track; 2300 2301 if (getTrackById(old_track_ids[clipId])->isAudioTrack()) { 2302 // Master clip is audio, so reverse delta for video clips 2303 if (hasAudio) { 2304 video_delta = -delta_track; 2305 } else { 2306 video_delta = 0; 2307 } 2308 } else { 2309 if (hasVideo) { 2310 audio_delta = -delta_track; 2311 } else { 2312 audio_delta = 0; 2313 } 2314 } 2315 bool trackChanged = false; 2316 if (delta_track != 0) { 2317 // Ensure the track move is possible (not outside our current tracks) 2318 for (int item : all_items) { 2319 int current_track_id = old_track_ids[item]; 2320 int current_track_position = getTrackPosition(current_track_id); 2321 bool audioTrack = getTrackById_const(current_track_id)->isAudioTrack(); 2322 int d = audioTrack ? audio_delta : video_delta; 2323 int target_track_position = current_track_position + d; 2324 bool brokenMove = target_track_position < 0 || target_track_position >= getTracksCount(); 2325 if (!brokenMove) { 2326 int target_id = getTrackIndexFromPosition(target_track_position); 2327 brokenMove = audioTrack != getTrackById_const(target_id)->isAudioTrack(); 2328 } 2329 if (brokenMove) { 2330 if (isClip(item)) { 2331 int lastTid = m_allClips[item]->getFakeTrackId(); 2332 int originalTid = m_allClips[item]->getCurrentTrackId(); 2333 int last_position = getTrackPosition(lastTid); 2334 int original_position = getTrackPosition(originalTid); 2335 int lastDelta = last_position - original_position; 2336 if (audioTrack) { 2337 if (qAbs(audio_delta) > qAbs(lastDelta)) { 2338 audio_delta = lastDelta; 2339 } 2340 if (video_delta != 0) { 2341 video_delta = -lastDelta; 2342 } 2343 } else { 2344 if (qAbs(video_delta) > qAbs(lastDelta)) { 2345 video_delta = lastDelta; 2346 } 2347 if (audio_delta != 0) { 2348 audio_delta = -lastDelta; 2349 } 2350 } 2351 }; 2352 } 2353 } 2354 } 2355 2356 // Reverse sort. We need to insert from left to right to avoid confusing the view 2357 for (int item : all_items) { 2358 int current_track_id = old_track_ids[item]; 2359 int current_track_position = getTrackPosition(current_track_id); 2360 int d = getTrackById_const(current_track_id)->isAudioTrack() ? audio_delta : video_delta; 2361 int target_track_position = current_track_position + d; 2362 if (target_track_position >= 0 && target_track_position < getTracksCount()) { 2363 auto it = m_allTracks.cbegin(); 2364 std::advance(it, target_track_position); 2365 int target_track = (*it)->getId(); 2366 int target_position = old_position[item] + delta_pos; 2367 if (isClip(item)) { 2368 m_allClips[item]->setFakePosition(target_position); 2369 if (m_allClips[item]->getFakeTrackId() != target_track) { 2370 trackChanged = true; 2371 } 2372 m_allClips[item]->setFakeTrackId(target_track); 2373 } else { 2374 } 2375 } else { 2376 ok = false; 2377 } 2378 if (!ok) { 2379 bool undone = local_undo(); 2380 Q_ASSERT(undone); 2381 return false; 2382 } 2383 } 2384 QModelIndex modelIndex; 2385 QVector<int> roles{FakePositionRole}; 2386 if (trackChanged) { 2387 roles << FakeTrackIdRole; 2388 } 2389 for (int item : all_items) { 2390 if (isClip(item)) { 2391 modelIndex = makeClipIndexFromID(item); 2392 } else { 2393 modelIndex = makeCompositionIndexFromID(item); 2394 } 2395 notifyChange(modelIndex, modelIndex, roles); 2396 } 2397 return true; 2398 } 2399 2400 bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool moveMirrorTracks, bool updateView, bool logUndo, 2401 bool revertMove) 2402 { 2403 QWriteLocker locker(&m_lock); 2404 TRACE(itemId, groupId, delta_track, delta_pos, updateView, logUndo); 2405 std::function<bool(void)> undo = []() { return true; }; 2406 std::function<bool(void)> redo = []() { return true; }; 2407 bool res = requestGroupMove(itemId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo, revertMove, moveMirrorTracks); 2408 if (res && logUndo) { 2409 PUSH_UNDO(undo, redo, i18n("Move group")); 2410 } 2411 TRACE_RES(res); 2412 return res; 2413 } 2414 2415 bool TimelineModel::requestGroupMove(int itemId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo, 2416 bool revertMove, bool moveMirrorTracks, bool allowViewRefresh, const QVector<int> &allowedTracks) 2417 { 2418 QWriteLocker locker(&m_lock); 2419 Q_ASSERT(m_allGroups.count(groupId) > 0); 2420 Q_ASSERT(isItem(itemId)); 2421 if (getGroupElements(groupId).count(itemId) == 0) { 2422 // this group doesn't contain the clip, abort 2423 return false; 2424 } 2425 bool ok = true; 2426 auto all_items = m_groups->getLeaves(groupId); 2427 Q_ASSERT(all_items.size() > 1); 2428 Fun local_undo = []() { return true; }; 2429 Fun local_redo = []() { return true; }; 2430 std::vector<std::pair<int, int>> sorted_clips; 2431 QVector<int> sorted_clips_ids; 2432 std::vector<std::pair<int, std::pair<int, int>>> sorted_compositions; 2433 std::vector<std::pair<int, GenTime>> sorted_subtitles; 2434 int lowerTrack = -1; 2435 int upperTrack = -1; 2436 QVector<int> tracksWithMix; 2437 2438 // Separate clips from compositions to sort and check source tracks 2439 QMap<std::pair<int, int>, int> mixesToDelete; 2440 // Mixes might be deleted while moving clips to another track, so store them before attempting a move 2441 QMap<int, std::pair<MixInfo, MixInfo>> mixDataArray; 2442 for (int affectedItemId : all_items) { 2443 if (delta_track != 0 && !isSubTitle(affectedItemId)) { 2444 // Check if an upper / lower move is possible 2445 const int trackPos = getTrackPosition(getItemTrackId(affectedItemId)); 2446 if (lowerTrack == -1 || lowerTrack > trackPos) { 2447 lowerTrack = trackPos; 2448 } 2449 if (upperTrack == -1 || upperTrack < trackPos) { 2450 upperTrack = trackPos; 2451 } 2452 } 2453 if (isClip(affectedItemId)) { 2454 sorted_clips.emplace_back(affectedItemId, m_allClips[affectedItemId]->getPosition()); 2455 sorted_clips_ids.push_back(affectedItemId); 2456 int current_track_id = getClipTrackId(affectedItemId); 2457 // Check if we have a mix in the group 2458 if (getTrackById_const(current_track_id)->hasMix(affectedItemId)) { 2459 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(current_track_id)->getMixInfo(affectedItemId); 2460 mixDataArray.insert(affectedItemId, mixData); 2461 if (delta_track != 0) { 2462 if (mixData.first.firstClipId > -1 && all_items.find(mixData.first.firstClipId) == all_items.end()) { 2463 // First part of the mix is not moving, delete start mix 2464 mixesToDelete.insert({mixData.first.firstClipId, affectedItemId}, current_track_id); 2465 } 2466 if (mixData.second.firstClipId > -1 && all_items.find(mixData.second.secondClipId) == all_items.end()) { 2467 // First part of the mix is not moving, delete start mix 2468 mixesToDelete.insert({affectedItemId, mixData.second.secondClipId}, current_track_id); 2469 } 2470 } else if (!tracksWithMix.contains(current_track_id)) { 2471 // There is a mix, prepare for update 2472 tracksWithMix << current_track_id; 2473 } 2474 } 2475 } else if (isComposition(affectedItemId)) { 2476 sorted_compositions.push_back( 2477 {affectedItemId, {m_allCompositions[affectedItemId]->getPosition(), getTrackMltIndex(m_allCompositions[affectedItemId]->getCurrentTrackId())}}); 2478 } else if (isSubTitle(affectedItemId)) { 2479 sorted_subtitles.emplace_back(affectedItemId, m_allSubtitles.at(affectedItemId)); 2480 } 2481 } 2482 2483 if (!sorted_subtitles.empty() && m_subtitleModel->isLocked()) { 2484 // Group with a locked subtitle, abort 2485 return false; 2486 } 2487 2488 // Sort clips first 2489 std::sort(sorted_clips.begin(), sorted_clips.end(), [delta_pos](const std::pair<int, int> &clipId1, const std::pair<int, int> &clipId2) { 2490 return delta_pos > 0 ? clipId2.second < clipId1.second : clipId1.second < clipId2.second; 2491 }); 2492 2493 // Sort subtitles 2494 std::sort(sorted_subtitles.begin(), sorted_subtitles.end(), [delta_pos](const std::pair<int, GenTime> &clipId1, const std::pair<int, GenTime> &clipId2) { 2495 return delta_pos > 0 ? clipId2.second < clipId1.second : clipId1.second < clipId2.second; 2496 }); 2497 2498 // Sort compositions. We need to delete in the move direction from top to bottom 2499 std::sort(sorted_compositions.begin(), sorted_compositions.end(), 2500 [delta_track, delta_pos](const std::pair<int, std::pair<int, int>> &clipId1, const std::pair<int, std::pair<int, int>> &clipId2) { 2501 const int p1 = delta_track < 0 ? clipId1.second.second : delta_track > 0 ? -clipId1.second.second : clipId1.second.first; 2502 const int p2 = delta_track < 0 ? clipId2.second.second : delta_track > 0 ? -clipId2.second.second : clipId2.second.first; 2503 return delta_track == 0 ? (delta_pos > 0 ? p2 < p1 : p1 < p2) : p1 < p2; 2504 }); 2505 2506 // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. 2507 // This way, we ensure that no conflict will arise with clips inside the group being moved 2508 2509 Fun update_model = [this, finalMove]() { 2510 if (finalMove) { 2511 updateDuration(); 2512 } 2513 return true; 2514 }; 2515 // Check that we don't move subtitles before 0 2516 if (!sorted_subtitles.empty() && sorted_subtitles.front().second.frames(pCore->getCurrentFps()) + delta_pos < 0) { 2517 delta_pos = -sorted_subtitles.front().second.frames(pCore->getCurrentFps()); 2518 if (delta_pos == 0) { 2519 return false; 2520 } 2521 } 2522 2523 // Check if there is a track move 2524 // Second step, reinsert clips at correct positions 2525 int audio_delta, video_delta; 2526 audio_delta = video_delta = delta_track; 2527 bool masterIsAudio = delta_track != 0 ? getTrackById_const(getItemTrackId(itemId))->isAudioTrack() : false; 2528 if (delta_track < 0) { 2529 if (!masterIsAudio) { 2530 // Case 1, dragging a video clip down 2531 bool lowerTrackIsAudio = getTrackById_const(getTrackIndexFromPosition(lowerTrack))->isAudioTrack(); 2532 int lowerPos = lowerTrackIsAudio ? lowerTrack - delta_track : lowerTrack + delta_track; 2533 if (lowerPos < 0) { 2534 // No space below 2535 delta_track = 0; 2536 } else if (!lowerTrackIsAudio) { 2537 // Moving a group of video clips 2538 if (getTrackById_const(getTrackIndexFromPosition(lowerPos))->isAudioTrack()) { 2539 // Moving to a non matching track (video on audio track) 2540 delta_track = 0; 2541 } 2542 } 2543 } else if (lowerTrack + delta_track < 0) { 2544 // Case 2, dragging an audio clip down 2545 delta_track = 0; 2546 } 2547 } else if (delta_track > 0) { 2548 if (!masterIsAudio) { 2549 // Case 1, dragging a video clip up 2550 int upperPos = upperTrack + delta_track; 2551 if (upperPos >= getTracksCount()) { 2552 // Moving above top track, not allowed 2553 delta_track = 0; 2554 } else if (getTrackById_const(getTrackIndexFromPosition(upperPos))->isAudioTrack()) { 2555 // Trying to move to a non matching track (video clip on audio track) 2556 delta_track = 0; 2557 } 2558 } else { 2559 bool upperTrackIsAudio = getTrackById_const(getTrackIndexFromPosition(upperTrack))->isAudioTrack(); 2560 if (!upperTrackIsAudio) { 2561 // Dragging an audio clip up, check that upper video clip has an available video track 2562 int targetPos = upperTrack - delta_track; 2563 if (moveMirrorTracks && (targetPos < 0 || getTrackById_const(getTrackIndexFromPosition(targetPos))->isAudioTrack())) { 2564 delta_track = 0; 2565 } 2566 } else { 2567 int targetPos = upperTrack + delta_track; 2568 if (targetPos >= getTracksCount() || !getTrackById_const(getTrackIndexFromPosition(targetPos))->isAudioTrack()) { 2569 // Trying to drag audio above topmost track or on video track 2570 delta_track = 0; 2571 } 2572 } 2573 } 2574 } 2575 if (delta_track != 0 && !revertMove) { 2576 // Ensure destination tracks are empty 2577 for (const std::pair<int, int> &item : sorted_clips) { 2578 int currentTrack = getClipTrackId(item.first); 2579 int trackOffset = delta_track; 2580 // Adjust delta_track depending on master 2581 if (getTrackById_const(currentTrack)->isAudioTrack()) { 2582 if (!masterIsAudio) { 2583 trackOffset = -delta_track; 2584 } 2585 } else if (masterIsAudio) { 2586 trackOffset = -delta_track; 2587 } 2588 int newTrackPosition = getTrackPosition(currentTrack) + trackOffset; 2589 if (newTrackPosition < 0 || newTrackPosition >= int(m_allTracks.size())) { 2590 if (!moveMirrorTracks && item.first != itemId) { 2591 continue; 2592 } 2593 delta_track = 0; 2594 break; 2595 } 2596 int newItemTrackId = getTrackIndexFromPosition(newTrackPosition); 2597 int newIn = item.second + delta_pos; 2598 if (!getTrackById_const(newItemTrackId)->isAvailableWithExceptions(newIn, getClipPlaytime(item.first) - 1, sorted_clips_ids)) { 2599 if (!moveMirrorTracks && item.first != itemId) { 2600 continue; 2601 } 2602 delta_track = 0; 2603 break; 2604 } 2605 } 2606 } 2607 if (delta_track == 0 && delta_pos == 0) { 2608 return false; 2609 } 2610 bool updateSubtitles = updateView; 2611 if (delta_track == 0 && updateView) { 2612 updateView = false; 2613 allowViewRefresh = false; 2614 update_model = [sorted_clips, sorted_compositions, finalMove, this]() { 2615 QModelIndex modelIndex; 2616 QVector<int> roles{StartRole}; 2617 for (const std::pair<int, int> &item : sorted_clips) { 2618 modelIndex = makeClipIndexFromID(item.first); 2619 notifyChange(modelIndex, modelIndex, roles); 2620 } 2621 for (const std::pair<int, std::pair<int, int>> &item : sorted_compositions) { 2622 modelIndex = makeCompositionIndexFromID(item.first); 2623 notifyChange(modelIndex, modelIndex, roles); 2624 } 2625 if (finalMove) { 2626 updateDuration(); 2627 } 2628 return true; 2629 }; 2630 } 2631 2632 std::unordered_map<int, int> old_track_ids, old_position, old_forced_track; 2633 QMap<int, int> oldTrackIds; 2634 // Check for mixes 2635 for (const std::pair<int, int> &item : sorted_clips) { 2636 // Keep track of old track for mixes 2637 oldTrackIds.insert(item.first, getClipTrackId(item.first)); 2638 } 2639 // First delete mixes that have to 2640 if (finalMove && !mixesToDelete.isEmpty()) { 2641 QMapIterator<std::pair<int, int>, int> i(mixesToDelete); 2642 while (i.hasNext()) { 2643 i.next(); 2644 // Delete mix 2645 getTrackById(i.value())->requestRemoveMix(i.key(), local_undo, local_redo); 2646 } 2647 } 2648 2649 // First, remove clips 2650 if (delta_track != 0) { 2651 // We delete our clips only if changing track 2652 for (const std::pair<int, int> &item : sorted_clips) { 2653 int old_trackId = getClipTrackId(item.first); 2654 old_track_ids[item.first] = old_trackId; 2655 if (old_trackId != -1) { 2656 bool updateThisView = allowViewRefresh; 2657 ok = ok && getTrackById(old_trackId)->requestClipDeletion(item.first, updateThisView, finalMove, local_undo, local_redo, true, false); 2658 old_position[item.first] = item.second; 2659 if (!ok) { 2660 bool undone = local_undo(); 2661 Q_ASSERT(undone); 2662 return false; 2663 } 2664 } 2665 } 2666 for (const std::pair<int, std::pair<int, int>> &item : sorted_compositions) { 2667 int old_trackId = getCompositionTrackId(item.first); 2668 if (old_trackId != -1) { 2669 old_track_ids[item.first] = old_trackId; 2670 old_position[item.first] = item.second.first; 2671 old_forced_track[item.first] = m_allCompositions[item.first]->getForcedTrack(); 2672 } 2673 } 2674 if (masterIsAudio) { 2675 // Master clip is audio, so reverse delta for video clips 2676 video_delta = -delta_track; 2677 } else { 2678 audio_delta = -delta_track; 2679 } 2680 } 2681 2682 Fun sync_mix = [this, tracksWithMix, finalMove]() { 2683 if (!finalMove) { 2684 return true; 2685 } 2686 for (int tid : tracksWithMix) { 2687 getTrackById_const(tid)->syncronizeMixes(finalMove); 2688 } 2689 return true; 2690 }; 2691 // We need to insert depending on the move direction to avoid confusing the view 2692 // std::reverse(std::begin(sorted_clips), std::end(sorted_clips)); 2693 bool updateThisView = allowViewRefresh; 2694 if (delta_track == 0) { 2695 // Special case, we are moving on same track, avoid too many calculations 2696 // First pass, check for collisions and suggest better delta 2697 QVector<int> processedTracks; 2698 for (const std::pair<int, int> &item : sorted_clips) { 2699 int current_track_id = getClipTrackId(item.first); 2700 if (processedTracks.contains(current_track_id)) { 2701 // We only check the first clip for each track since they are sorted depending on the move direction 2702 continue; 2703 } 2704 processedTracks << current_track_id; 2705 if (!allowedTracks.isEmpty() && !allowedTracks.contains(current_track_id)) { 2706 continue; 2707 } 2708 int current_in = item.second; 2709 int playtime = getClipPlaytime(item.first); 2710 int target_position = current_in + delta_pos; 2711 int subPlaylist = -1; 2712 if (delta_pos < 0) { 2713 if (getTrackById_const(current_track_id)->hasStartMix(item.first)) { 2714 subPlaylist = m_allClips[item.first]->getSubPlaylistIndex(); 2715 } 2716 if (!getTrackById_const(current_track_id)->isAvailable(target_position, qMin(qAbs(delta_pos), playtime), subPlaylist)) { 2717 if (!getTrackById_const(current_track_id)->isBlankAt(current_in - 1)) { 2718 // No move possible, abort 2719 bool undone = local_undo(); 2720 Q_ASSERT(undone); 2721 return false; 2722 } 2723 int newStart = getTrackById_const(current_track_id)->getBlankStart(current_in - 1, subPlaylist); 2724 delta_pos = qMax(delta_pos, newStart - current_in); 2725 } 2726 } else { 2727 int moveEnd = target_position + playtime; 2728 int moveStart = qMax(current_in + playtime, target_position); 2729 if (getTrackById_const(current_track_id)->hasEndMix(item.first)) { 2730 subPlaylist = m_allClips[item.first]->getSubPlaylistIndex(); 2731 } 2732 if (!getTrackById_const(current_track_id)->isAvailable(moveStart, moveEnd - moveStart, subPlaylist)) { 2733 int newStart = getTrackById_const(current_track_id)->getBlankEnd(current_in + playtime, subPlaylist); 2734 if (newStart == current_in + playtime) { 2735 // No move possible, abort 2736 bool undone = local_undo(); 2737 Q_ASSERT(undone); 2738 return false; 2739 } 2740 delta_pos = qMin(delta_pos, newStart - (current_in + playtime)); 2741 } 2742 } 2743 } 2744 PUSH_LAMBDA(sync_mix, local_undo); 2745 for (const std::pair<int, int> &item : sorted_clips) { 2746 int current_track_id = getClipTrackId(item.first); 2747 if (!allowedTracks.isEmpty() && !allowedTracks.contains(current_track_id)) { 2748 continue; 2749 } 2750 int current_in = item.second; 2751 int target_position = current_in + delta_pos; 2752 ok = requestClipMove(item.first, current_track_id, target_position, moveMirrorTracks, updateThisView, finalMove, finalMove, local_undo, local_redo, 2753 revertMove, true, oldTrackIds, 2754 mixDataArray.contains(item.first) ? mixDataArray.value(item.first) : std::pair<MixInfo, MixInfo>()); 2755 if (!ok) { 2756 qWarning() << "failed moving clip on track " << current_track_id; 2757 break; 2758 } 2759 } 2760 if (ok) { 2761 sync_mix(); 2762 PUSH_LAMBDA(sync_mix, local_redo); 2763 // Move compositions 2764 for (const std::pair<int, std::pair<int, int>> &item : sorted_compositions) { 2765 int current_track_id = getItemTrackId(item.first); 2766 if (!allowedTracks.isEmpty() && !allowedTracks.contains(current_track_id)) { 2767 continue; 2768 } 2769 int current_in = item.second.first; 2770 int target_position = current_in + delta_pos; 2771 ok = requestCompositionMove(item.first, current_track_id, m_allCompositions[item.first]->getForcedTrack(), target_position, updateThisView, 2772 finalMove, local_undo, local_redo); 2773 if (!ok) { 2774 break; 2775 } 2776 } 2777 } 2778 if (!ok) { 2779 bool undone = local_undo(); 2780 Q_ASSERT(undone); 2781 return false; 2782 } 2783 } else { 2784 // Track changed 2785 PUSH_LAMBDA(sync_mix, local_undo); 2786 for (const std::pair<int, int> &item : sorted_clips) { 2787 int current_track_id = old_track_ids[item.first]; 2788 int current_track_position = getTrackPosition(current_track_id); 2789 int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta; 2790 if (!moveMirrorTracks && item.first != itemId) { 2791 d = 0; 2792 } 2793 int target_track_position = current_track_position + d; 2794 if (target_track_position >= 0 && target_track_position < getTracksCount()) { 2795 auto it = m_allTracks.cbegin(); 2796 std::advance(it, target_track_position); 2797 int target_track = (*it)->getId(); 2798 int target_position = old_position[item.first] + delta_pos; 2799 ok = ok && requestClipMove(item.first, target_track, target_position, moveMirrorTracks, updateThisView, finalMove, finalMove, local_undo, 2800 local_redo, revertMove, true, oldTrackIds, 2801 mixDataArray.contains(item.first) ? mixDataArray.value(item.first) : std::pair<MixInfo, MixInfo>()); 2802 } else { 2803 ok = false; 2804 } 2805 if (!ok) { 2806 bool undone = local_undo(); 2807 Q_ASSERT(undone); 2808 return false; 2809 } 2810 } 2811 sync_mix(); 2812 PUSH_LAMBDA(sync_mix, local_redo); 2813 for (const std::pair<int, std::pair<int, int>> &item : sorted_compositions) { 2814 int current_track_id = old_track_ids[item.first]; 2815 int current_track_position = getTrackPosition(current_track_id); 2816 int d = getTrackById(current_track_id)->isAudioTrack() ? audio_delta : video_delta; 2817 int target_track_position = current_track_position + d; 2818 2819 if (target_track_position >= 0 && target_track_position < getTracksCount()) { 2820 auto it = m_allTracks.cbegin(); 2821 std::advance(it, target_track_position); 2822 int target_track = (*it)->getId(); 2823 int target_position = old_position[item.first] + delta_pos; 2824 ok = ok && requestCompositionMove(item.first, target_track, old_forced_track[item.first], target_position, updateThisView, finalMove, 2825 local_undo, local_redo); 2826 } else { 2827 qWarning() << "aborting move tried on track" << target_track_position; 2828 ok = false; 2829 } 2830 if (!ok) { 2831 bool undone = local_undo(); 2832 Q_ASSERT(undone); 2833 return false; 2834 } 2835 } 2836 } 2837 // Move subtitles 2838 if (!sorted_subtitles.empty()) { 2839 std::vector<std::pair<int, GenTime>>::iterator ptr; 2840 auto last = std::prev(sorted_subtitles.end()); 2841 2842 for (ptr = sorted_subtitles.begin(); ptr < sorted_subtitles.end(); ptr++) { 2843 ok = requestSubtitleMove((*ptr).first, (*ptr).second.frames(pCore->getCurrentFps()) + delta_pos, updateSubtitles, ptr == sorted_subtitles.begin(), 2844 ptr == last, finalMove, local_undo, local_redo); 2845 if (!ok) { 2846 bool undone = local_undo(); 2847 Q_ASSERT(undone); 2848 return false; 2849 } 2850 } 2851 } 2852 update_model(); 2853 PUSH_LAMBDA(update_model, local_redo); 2854 PUSH_LAMBDA(update_model, local_undo); 2855 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 2856 return true; 2857 } 2858 2859 bool TimelineModel::requestGroupDeletion(int clipId, bool logUndo) 2860 { 2861 QWriteLocker locker(&m_lock); 2862 TRACE(clipId, logUndo); 2863 if (!m_groups->isInGroup(clipId)) { 2864 TRACE_RES(false); 2865 return false; 2866 } 2867 bool res = requestItemDeletion(clipId, logUndo); 2868 TRACE_RES(res); 2869 return res; 2870 } 2871 2872 bool TimelineModel::requestGroupDeletion(int clipId, Fun &undo, Fun &redo) 2873 { 2874 // we do a breadth first exploration of the group tree, ungroup (delete) every inner node, and then delete all the leaves. 2875 std::queue<int> group_queue; 2876 group_queue.push(m_groups->getRootId(clipId)); 2877 std::unordered_set<int> all_items; 2878 std::unordered_set<int> all_compositions; 2879 // Subtitles MUST BE SORTED BY REVERSE timeline id to preserve the qml model index on undo!! 2880 std::set<int> all_subtitles; 2881 while (!group_queue.empty()) { 2882 int current_group = group_queue.front(); 2883 bool isSelection = m_currentSelection.count(current_group); 2884 2885 group_queue.pop(); 2886 Q_ASSERT(isGroup(current_group)); 2887 auto children = m_groups->getDirectChildren(current_group); 2888 int one_child = -1; // we need the id on any of the indices of the elements of the group 2889 for (int c : children) { 2890 if (isClip(c)) { 2891 all_items.insert(c); 2892 one_child = c; 2893 } else if (isComposition(c)) { 2894 all_compositions.insert(c); 2895 one_child = c; 2896 } else if (isSubTitle(c)) { 2897 all_subtitles.insert(c); 2898 one_child = c; 2899 } else { 2900 Q_ASSERT(isGroup(c)); 2901 one_child = c; 2902 group_queue.push(c); 2903 } 2904 } 2905 if (one_child != -1) { 2906 if (m_groups->getType(current_group) == GroupType::Selection) { 2907 Q_ASSERT(isSelection); 2908 // in the case of a selection group, we delete the group but don't log it in the undo object 2909 Fun tmp_undo = []() { return true; }; 2910 Fun tmp_redo = []() { return true; }; 2911 m_groups->ungroupItem(one_child, tmp_undo, tmp_redo); 2912 } else { 2913 bool res = m_groups->ungroupItem(one_child, undo, redo); 2914 if (!res) { 2915 undo(); 2916 return false; 2917 } 2918 } 2919 } 2920 if (isSelection) { 2921 requestClearSelection(true); 2922 } 2923 } 2924 for (int clip : all_items) { 2925 bool res = requestClipDeletion(clip, undo, redo); 2926 if (!res) { 2927 // Undo is processed in requestClipDeletion 2928 return false; 2929 } 2930 } 2931 for (int compo : all_compositions) { 2932 bool res = requestCompositionDeletion(compo, undo, redo); 2933 if (!res) { 2934 undo(); 2935 return false; 2936 } 2937 } 2938 std::set<int>::reverse_iterator rit; 2939 for (rit = all_subtitles.rbegin(); rit != all_subtitles.rend(); ++rit) { 2940 bool res = requestSubtitleDeletion(*rit, undo, redo, rit == all_subtitles.rbegin(), rit == std::prev(all_subtitles.rend())); 2941 if (!res) { 2942 undo(); 2943 return false; 2944 } 2945 } 2946 return true; 2947 } 2948 2949 const QVariantList TimelineModel::getGroupData(int itemId) 2950 { 2951 QWriteLocker locker(&m_lock); 2952 if (!m_groups->isInGroup(itemId)) { 2953 return {itemId, getItemPosition(itemId), getItemPlaytime(itemId)}; 2954 } 2955 int groupId = m_groups->getRootId(itemId); 2956 QVariantList result; 2957 std::unordered_set<int> items = m_groups->getLeaves(groupId); 2958 for (int id : items) { 2959 result << id << getItemPosition(id) << getItemPlaytime(id); 2960 } 2961 return result; 2962 } 2963 2964 void TimelineModel::processGroupResize(QVariantList startPosList, QVariantList endPosList, bool right) 2965 { 2966 Q_ASSERT(startPosList.size() == endPosList.size()); 2967 QMap<int, QPair<int, int>> startData; 2968 QMap<int, QPair<int, int>> endData; 2969 while (!startPosList.isEmpty()) { 2970 int id = startPosList.takeFirst().toInt(); 2971 int in = startPosList.takeFirst().toInt(); 2972 int duration = startPosList.takeFirst().toInt(); 2973 startData.insert(id, {in, duration}); 2974 id = endPosList.takeFirst().toInt(); 2975 in = endPosList.takeFirst().toInt(); 2976 duration = endPosList.takeFirst().toInt(); 2977 endData.insert(id, {in, duration}); 2978 } 2979 QMapIterator<int, QPair<int, int>> i(startData); 2980 QList<int> changedItems; 2981 Fun undo = []() { return true; }; 2982 Fun redo = []() { return true; }; 2983 bool result = true; 2984 QVector<int> mixTracks; 2985 QList<int> ids = startData.keys(); 2986 for (auto &id : ids) { 2987 if (isClip(id)) { 2988 int tid = m_allClips[id]->getCurrentTrackId(); 2989 if (tid > -1 && getTrackById(tid)->hasMix(id)) { 2990 if (!mixTracks.contains(tid)) { 2991 mixTracks << tid; 2992 } 2993 if (right) { 2994 if (getTrackById_const(tid)->hasEndMix(id)) { 2995 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(id); 2996 QPair<int, int> endPos = endData.value(id); 2997 if (endPos.first + endPos.second <= mixData.second.secondClipInOut.first) { 2998 Fun sync_mix_undo = [this, tid, mixData]() { 2999 getTrackById_const(tid)->createMix(mixData.second, getTrackById_const(tid)->isAudioTrack()); 3000 return true; 3001 }; 3002 PUSH_LAMBDA(sync_mix_undo, undo); 3003 } 3004 } 3005 } else if (getTrackById_const(tid)->hasStartMix(id)) { 3006 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(id); 3007 QPair<int, int> endPos = endData.value(id); 3008 if (endPos.first >= mixData.first.firstClipInOut.second) { 3009 Fun sync_mix_undo = [this, tid, mixData]() { 3010 getTrackById_const(tid)->createMix(mixData.first, getTrackById_const(tid)->isAudioTrack()); 3011 return true; 3012 }; 3013 PUSH_LAMBDA(sync_mix_undo, undo); 3014 } 3015 } 3016 } 3017 } 3018 } 3019 Fun sync_mix = []() { return true; }; 3020 if (!mixTracks.isEmpty()) { 3021 sync_mix = [this, mixTracks]() { 3022 for (auto &tid : mixTracks) { 3023 getTrackById_const(tid)->syncronizeMixes(true); 3024 } 3025 return true; 3026 }; 3027 PUSH_LAMBDA(sync_mix, undo); 3028 } 3029 while (i.hasNext()) { 3030 i.next(); 3031 QPair<int, int> startItemPos = i.value(); 3032 QPair<int, int> endItemPos = endData.value(i.key()); 3033 if (startItemPos.first != endItemPos.first || startItemPos.second != endItemPos.second) { 3034 // Revert individual items to original position 3035 requestItemResize(i.key(), startItemPos.second, right, false, 0, true); 3036 changedItems << i.key(); 3037 } 3038 } 3039 for (int id : qAsConst(changedItems)) { 3040 QPair<int, int> endItemPos = endData.value(id); 3041 int duration = endItemPos.second; 3042 result = result & requestItemResize(id, duration, right, true, undo, redo, false); 3043 if (!result) { 3044 break; 3045 } 3046 } 3047 if (result) { 3048 sync_mix(); 3049 PUSH_LAMBDA(sync_mix, redo); 3050 PUSH_UNDO(undo, redo, i18n("Resize group")); 3051 } else { 3052 undo(); 3053 } 3054 } 3055 3056 const std::vector<int> TimelineModel::getBoundaries(int itemId) 3057 { 3058 std::vector<int> boundaries; 3059 std::unordered_set<int> items; 3060 if (m_groups->isInGroup(itemId)) { 3061 int groupId = m_groups->getRootId(itemId); 3062 items = m_groups->getLeaves(groupId); 3063 } else { 3064 items.insert(itemId); 3065 } 3066 for (int id : items) { 3067 if (isItem(id)) { 3068 int pos = getItemPosition(id); 3069 boundaries.push_back(pos); 3070 pos += getItemPlaytime(id); 3071 boundaries.push_back(pos); 3072 } 3073 } 3074 return boundaries; 3075 } 3076 3077 int TimelineModel::requestClipResizeAndTimeWarp(int itemId, int size, bool right, int snapDistance, bool allowSingleResize, double speed) 3078 { 3079 Q_UNUSED(snapDistance) 3080 QWriteLocker locker(&m_lock); 3081 TRACE(itemId, size, right, true, snapDistance, allowSingleResize); 3082 Q_ASSERT(isClip(itemId)); 3083 if (size <= 0) { 3084 TRACE_RES(-1); 3085 return -1; 3086 } 3087 int in = getItemPosition(itemId); 3088 int out = in + getItemPlaytime(itemId); 3089 // size = requestItemResizeInfo(itemId, in, out, size, right, snapDistance); 3090 Fun undo = []() { return true; }; 3091 Fun redo = []() { return true; }; 3092 std::unordered_set<int> all_items; 3093 if (!allowSingleResize && m_groups->isInGroup(itemId)) { 3094 int groupId = m_groups->getRootId(itemId); 3095 std::unordered_set<int> items; 3096 if (m_groups->getType(groupId) == GroupType::AVSplit) { 3097 // Only resize group elements if it is an avsplit 3098 items = m_groups->getLeaves(groupId); 3099 } else { 3100 all_items.insert(itemId); 3101 } 3102 for (int id : items) { 3103 if (id == itemId) { 3104 all_items.insert(id); 3105 continue; 3106 } 3107 int start = getItemPosition(id); 3108 int end = in + getItemPlaytime(id); 3109 if (right) { 3110 if (out == end) { 3111 all_items.insert(id); 3112 } 3113 } else if (start == in) { 3114 all_items.insert(id); 3115 } 3116 } 3117 } else { 3118 all_items.insert(itemId); 3119 } 3120 bool result = true; 3121 for (int id : all_items) { 3122 int tid = getItemTrackId(id); 3123 if (tid > -1 && getTrackById_const(tid)->isLocked()) { 3124 continue; 3125 } 3126 // First delete clip, then timewarp, resize and reinsert 3127 int pos = getItemPosition(id); 3128 int invalidateIn = pos; 3129 int invalidateOut = invalidateIn + getClipPlaytime(id); 3130 if (!right) { 3131 pos += getItemPlaytime(id) - size; 3132 } 3133 bool hasVideo = false; 3134 if (tid != -1 && !getTrackById_const(tid)->isAudioTrack()) { 3135 hasVideo = true; 3136 } 3137 int trackDuration = getTrackById_const(tid)->trackDuration(); 3138 result = getTrackById(tid)->requestClipDeletion(id, true, false, undo, redo, false, false); 3139 bool pitchCompensate = m_allClips[id]->getIntProperty(QStringLiteral("warp_pitch")); 3140 result = result && requestClipTimeWarp(id, speed, pitchCompensate, true, undo, redo); 3141 result = result && requestItemResize(id, size, true, true, undo, redo); 3142 result = result && getTrackById(tid)->requestClipInsertion(id, pos, true, false, undo, redo, false, false); 3143 if (!result) { 3144 break; 3145 } 3146 bool durationChanged = false; 3147 if (trackDuration != getTrackById_const(tid)->trackDuration()) { 3148 durationChanged = true; 3149 getTrackById(tid)->adjustStackLength(trackDuration, getTrackById_const(tid)->trackDuration(), undo, redo); 3150 } 3151 if (right) { 3152 invalidateOut = qMax(invalidateOut, invalidateIn + getClipPlaytime(id)); 3153 } else { 3154 invalidateIn = qMin(invalidateIn, invalidateOut - getClipPlaytime(id)); 3155 } 3156 Fun view_redo = [this, invalidateIn, invalidateOut, hasVideo, durationChanged]() { 3157 if (hasVideo) { 3158 Q_EMIT invalidateZone(invalidateIn, invalidateOut); 3159 } 3160 if (durationChanged) { 3161 // last clip in playlist updated 3162 updateDuration(); 3163 } 3164 return true; 3165 }; 3166 view_redo(); 3167 PUSH_LAMBDA(view_redo, redo); 3168 PUSH_LAMBDA(view_redo, undo); 3169 } 3170 if (!result) { 3171 bool undone = undo(); 3172 Q_ASSERT(undone); 3173 } else { 3174 PUSH_UNDO(undo, redo, i18n("Resize clip speed")); 3175 } 3176 int res = result ? size : -1; 3177 TRACE_RES(res); 3178 return res; 3179 } 3180 3181 int TimelineModel::requestItemResizeInfo(int itemId, int in, int out, int size, bool right, int snapDistance) 3182 { 3183 int trackId = getItemTrackId(itemId); 3184 bool checkMix = trackId != -1; 3185 Fun temp_undo = []() { return true; }; 3186 Fun temp_redo = []() { return true; }; 3187 bool skipSnaps = snapDistance <= 0; 3188 bool sizeUpdated = false; 3189 if (checkMix && right && size > out - in && isClip(itemId)) { 3190 int playlist = -1; 3191 if (getTrackById_const(trackId)->hasEndMix(itemId)) { 3192 playlist = m_allClips[itemId]->getSubPlaylistIndex(); 3193 } 3194 int targetPos = in + size - 1; 3195 if (!getTrackById_const(trackId)->isBlankAt(targetPos, playlist)) { 3196 int updatedSize = getTrackById_const(trackId)->getBlankEnd(out, playlist) - in + 1; 3197 if (!skipSnaps && size - updatedSize > snapDistance) { 3198 skipSnaps = true; 3199 } 3200 size = updatedSize; 3201 sizeUpdated = true; 3202 } 3203 } else if (checkMix && !right && size > (out - in) && isClip(itemId)) { 3204 int targetPos = out - size; 3205 int playlist = -1; 3206 if (getTrackById_const(trackId)->hasStartMix(itemId)) { 3207 playlist = m_allClips[itemId]->getSubPlaylistIndex(); 3208 } 3209 if (!getTrackById_const(trackId)->isBlankAt(targetPos, playlist)) { 3210 int updatedSize = out - getTrackById_const(trackId)->getBlankStart(in - 1, playlist); 3211 if (!skipSnaps && size - updatedSize > snapDistance) { 3212 skipSnaps = true; 3213 } 3214 size = updatedSize; 3215 sizeUpdated = true; 3216 } 3217 } 3218 int proposed_size = size; 3219 if (!skipSnaps) { 3220 int timelinePos = pCore->getMonitorPosition(); 3221 m_snaps->addPoint(timelinePos); 3222 proposed_size = m_snaps->proposeSize(in, out, getBoundaries(itemId), size, right, snapDistance); 3223 m_snaps->removePoint(timelinePos); 3224 } 3225 if (proposed_size > 0 && (!skipSnaps || sizeUpdated)) { 3226 // only test move if proposed_size is valid 3227 bool success = false; 3228 if (isClip(itemId)) { 3229 bool hasMix = getTrackById_const(trackId)->hasMix(itemId); 3230 success = m_allClips[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false, hasMix); 3231 } else if (isComposition(itemId)) { 3232 success = m_allCompositions[itemId]->requestResize(proposed_size, right, temp_undo, temp_redo, false); 3233 } else if (isSubTitle(itemId)) { 3234 // TODO: don't allow subtitle overlap? 3235 success = true; 3236 } 3237 // undo temp move 3238 temp_undo(); 3239 if (success) { 3240 size = proposed_size; 3241 } 3242 } 3243 return size; 3244 } 3245 3246 bool TimelineModel::trackIsBlankAt(int tid, int pos, int playlist) const 3247 { 3248 if (pos > getTrackById_const(tid)->trackDuration() - 1) { 3249 return true; 3250 } 3251 return getTrackById_const(tid)->isBlankAt(pos, playlist); 3252 } 3253 3254 bool TimelineModel::trackIsAvailable(int tid, int pos, int duration, int playlist) const 3255 { 3256 return getTrackById_const(tid)->isAvailable(pos, duration, playlist); 3257 } 3258 3259 int TimelineModel::getClipStartAt(int tid, int pos, int playlist) const 3260 { 3261 return getTrackById_const(tid)->getClipStart(pos, playlist); 3262 } 3263 3264 int TimelineModel::getClipEndAt(int tid, int pos, int playlist) const 3265 { 3266 return getTrackById_const(tid)->getClipEnd(pos, playlist); 3267 } 3268 3269 int TimelineModel::requestItemSpeedChange(int itemId, int size, bool right, int snapDistance) 3270 { 3271 Q_ASSERT(isClip(itemId)); 3272 QWriteLocker locker(&m_lock); 3273 TRACE(itemId, size, right, snapDistance); 3274 Q_ASSERT(isItem(itemId)); 3275 if (size <= 0) { 3276 TRACE_RES(-1); 3277 return -1; 3278 } 3279 int in = getItemPosition(itemId); 3280 int out = in + getItemPlaytime(itemId); 3281 3282 if (right && size > out - in) { 3283 int targetPos = in + size - 1; 3284 int trackId = getItemTrackId(itemId); 3285 if (!getTrackById_const(trackId)->isBlankAt(targetPos) || !getItemsInRange(trackId, out + 1, targetPos, false).empty()) { 3286 size = getTrackById_const(trackId)->getBlankEnd(out + 1) - in; 3287 } 3288 } else if (!right && size > (out - in)) { 3289 int targetPos = out - size; 3290 int trackId = getItemTrackId(itemId); 3291 if (!getTrackById_const(trackId)->isBlankAt(targetPos) || !getItemsInRange(trackId, targetPos, in - 1, false).empty()) { 3292 size = out - getTrackById_const(trackId)->getBlankStart(in - 1); 3293 } 3294 } 3295 int timelinePos = pCore->getMonitorPosition(); 3296 m_snaps->addPoint(timelinePos); 3297 int proposed_size = m_snaps->proposeSize(in, out, getBoundaries(itemId), size, right, snapDistance); 3298 m_snaps->removePoint(timelinePos); 3299 return proposed_size > 0 ? proposed_size : size; 3300 } 3301 3302 bool TimelineModel::removeMixWithUndo(int cid, Fun &undo, Fun &redo) 3303 { 3304 int tid = getItemTrackId(cid); 3305 if (isTrack(tid)) { 3306 MixInfo mixData = getTrackById_const(tid)->getMixInfo(cid).first; 3307 if (mixData.firstClipId > -1 && mixData.secondClipId > -1) { 3308 bool res = getTrackById(tid)->requestRemoveMix({mixData.firstClipId, mixData.secondClipId}, undo, redo); 3309 return res; 3310 } 3311 } 3312 return true; 3313 } 3314 3315 bool TimelineModel::removeMix(int cid) 3316 { 3317 Fun undo = []() { return true; }; 3318 Fun redo = []() { return true; }; 3319 bool res = removeMixWithUndo(cid, undo, redo); 3320 if (res) { 3321 PUSH_UNDO(undo, redo, i18n("Remove mix")); 3322 } else { 3323 pCore->displayMessage(i18n("Removing mix failed"), ErrorMessage, 500); 3324 } 3325 return res; 3326 } 3327 3328 int TimelineModel::requestItemResize(int itemId, int size, bool right, bool logUndo, int snapDistance, bool allowSingleResize) 3329 { 3330 QWriteLocker locker(&m_lock); 3331 TRACE(itemId, size, right, logUndo, snapDistance, allowSingleResize) 3332 Q_ASSERT(isItem(itemId)); 3333 if (size <= 0) { 3334 TRACE_RES(-1) 3335 return -1; 3336 } 3337 int in = 0; 3338 int offset = getItemPlaytime(itemId); 3339 int tid = getItemTrackId(itemId); 3340 int out = offset; 3341 qDebug() << "======= REQUESTING NEW CLIP SIZE: " << size << ", ON TID: " << tid; 3342 if (tid != -1 || !isClip(itemId)) { 3343 in = qMax(0, getItemPosition(itemId)); 3344 out += in; 3345 size = requestItemResizeInfo(itemId, in, out, size, right, snapDistance); 3346 } 3347 qDebug() << "======= ADJUSTED NEW CLIP SIZE: " << size << " FROM " << offset; 3348 offset -= size; 3349 Fun undo = []() { return true; }; 3350 Fun redo = []() { return true; }; 3351 Fun adjust_mix = []() { return true; }; 3352 Fun sync_end_mix = []() { return true; }; 3353 Fun sync_end_mix_undo = []() { return true; }; 3354 std::unordered_set<int> all_items; 3355 QList<int> tracksWithMixes; 3356 all_items.insert(itemId); 3357 int resizedCount = 0; 3358 if (logUndo && isClip(itemId)) { 3359 if (tid > -1) { 3360 if (right) { 3361 if (getTrackById_const(tid)->hasEndMix(itemId)) { 3362 tracksWithMixes << tid; 3363 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId); 3364 int resizeOut = in + size; 3365 int clipMixCut = mixData.second.secondClipInOut.first + m_allClips[mixData.second.secondClipId]->getMixDuration() - 3366 m_allClips[mixData.second.secondClipId]->getMixCutPosition(); 3367 int clipMixStart = mixData.second.secondClipInOut.first; 3368 if (resizeOut < clipMixCut || resizeOut <= clipMixStart) { 3369 // Clip resized outside of mix zone, mix will be deleted 3370 bool res = removeMixWithUndo(mixData.second.secondClipId, undo, redo); 3371 if (res) { 3372 size = m_allClips[itemId]->getPlaytime(); 3373 resizedCount++; 3374 } else { 3375 return -1; 3376 } 3377 } else { 3378 // Mix was resized, update cut position 3379 int currentMixDuration = m_allClips[mixData.second.secondClipId]->getMixDuration(); 3380 int currentMixCut = m_allClips[mixData.second.secondClipId]->getMixCutPosition(); 3381 adjust_mix = [this, tid, mixData, currentMixCut, itemId]() { 3382 MixInfo secondMixData = getTrackById_const(tid)->getMixInfo(itemId).second; 3383 int mixOffset = mixData.second.firstClipInOut.second - secondMixData.firstClipInOut.second; 3384 getTrackById_const(tid)->setMixDuration(secondMixData.secondClipId, 3385 secondMixData.firstClipInOut.second - secondMixData.secondClipInOut.first, 3386 currentMixCut - mixOffset); 3387 QModelIndex ix = makeClipIndexFromID(secondMixData.secondClipId); 3388 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 3389 return true; 3390 }; 3391 Fun adjust_mix_undo = [this, tid, mixData, currentMixCut, currentMixDuration]() { 3392 getTrackById_const(tid)->setMixDuration(mixData.second.secondClipId, currentMixDuration, currentMixCut); 3393 QModelIndex ix = makeClipIndexFromID(mixData.second.secondClipId); 3394 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 3395 return true; 3396 }; 3397 PUSH_LAMBDA(adjust_mix_undo, undo); 3398 } 3399 } 3400 if (getTrackById_const(tid)->hasStartMix(itemId)) { 3401 // Resize mix if necessary 3402 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId); 3403 if (in + size <= mixData.first.firstClipInOut.second) { 3404 // Resized smaller than mix, adjust 3405 int updatedSize = in + size - mixData.first.firstClipInOut.first; 3406 // Mix was resized, update cut position 3407 int currentMixDuration = m_allClips[itemId]->getMixDuration(); 3408 int currentMixCut = m_allClips[itemId]->getMixCutPosition(); 3409 Fun adjust_mix1 = [this, tid, currentMixDuration, currentMixCut, itemId, 3410 mixOffset = mixData.first.firstClipInOut.second - (in + size)]() { 3411 getTrackById_const(tid)->setMixDuration(itemId, currentMixDuration - mixOffset, currentMixCut - mixOffset); 3412 QModelIndex ix = makeClipIndexFromID(itemId); 3413 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 3414 return true; 3415 }; 3416 Fun adjust_mix_undo = [this, tid, itemId, currentMixCut, currentMixDuration]() { 3417 getTrackById_const(tid)->setMixDuration(itemId, currentMixDuration, currentMixCut); 3418 QModelIndex ix = makeClipIndexFromID(itemId); 3419 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 3420 return true; 3421 }; 3422 PUSH_LAMBDA(adjust_mix1, adjust_mix); 3423 PUSH_LAMBDA(adjust_mix_undo, undo); 3424 requestItemResize(mixData.first.firstClipId, updatedSize, true, logUndo, undo, redo); 3425 resizedCount++; 3426 } 3427 } 3428 } else { 3429 // Resized left side 3430 if (getTrackById_const(tid)->hasStartMix(itemId)) { 3431 tracksWithMixes << tid; 3432 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId); 3433 if (out - size >= mixData.first.firstClipInOut.second) { 3434 // Moved outside mix, delete 3435 bool res = removeMixWithUndo(mixData.first.secondClipId, undo, redo); 3436 if (res) { 3437 size = m_allClips[itemId]->getPlaytime(); 3438 resizedCount++; 3439 } else { 3440 return -1; 3441 } 3442 } else { 3443 // Mix was resized, update cut position 3444 int currentMixDuration = m_allClips[mixData.first.secondClipId]->getMixDuration(); 3445 int currentMixCut = m_allClips[mixData.first.secondClipId]->getMixCutPosition(); 3446 adjust_mix = [this, tid, currentMixCut, itemId]() { 3447 MixInfo firstMixData = getTrackById_const(tid)->getMixInfo(itemId).first; 3448 if (firstMixData.firstClipId > -1 && firstMixData.secondClipId > -1) { 3449 getTrackById_const(tid)->setMixDuration(firstMixData.secondClipId, 3450 firstMixData.firstClipInOut.second - firstMixData.secondClipInOut.first, currentMixCut); 3451 QModelIndex ix = makeClipIndexFromID(firstMixData.secondClipId); 3452 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 3453 } 3454 return true; 3455 }; 3456 Fun adjust_mix_undo = [this, tid, mixData, currentMixCut, currentMixDuration]() { 3457 getTrackById_const(tid)->setMixDuration(mixData.first.secondClipId, currentMixDuration, currentMixCut); 3458 QModelIndex ix = makeClipIndexFromID(mixData.first.secondClipId); 3459 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 3460 return true; 3461 }; 3462 PUSH_LAMBDA(adjust_mix_undo, undo); 3463 } 3464 } 3465 if (getTrackById_const(tid)->hasEndMix(itemId)) { 3466 // Resize mix if necessary 3467 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId); 3468 if (out - size >= mixData.second.secondClipInOut.first) { 3469 // Resized smaller than mix, adjust 3470 int updatedClipSize = mixData.second.secondClipInOut.second - (out - size); 3471 int updatedMixDuration = mixData.second.firstClipInOut.second - (out - size); 3472 // Mix was resized, update cut position 3473 int currentMixDuration = m_allClips[mixData.second.secondClipId]->getMixDuration(); 3474 int currentMixCut = m_allClips[mixData.second.secondClipId]->getMixCutPosition(); 3475 Fun adjust_mix1 = [this, tid, currentMixCut, secondId = mixData.second.secondClipId, updatedMixDuration]() { 3476 getTrackById_const(tid)->setMixDuration(secondId, updatedMixDuration, currentMixCut); 3477 QModelIndex ix = makeClipIndexFromID(secondId); 3478 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 3479 return true; 3480 }; 3481 Fun adjust_mix_undo = [this, tid, secondId = mixData.second.secondClipId, currentMixCut, currentMixDuration]() { 3482 getTrackById_const(tid)->setMixDuration(secondId, currentMixDuration, currentMixCut); 3483 QModelIndex ix = makeClipIndexFromID(secondId); 3484 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 3485 return true; 3486 }; 3487 PUSH_LAMBDA(adjust_mix1, adjust_mix); 3488 PUSH_LAMBDA(adjust_mix_undo, undo); 3489 requestItemResize(mixData.second.secondClipId, updatedClipSize, false, logUndo, undo, redo); 3490 resizedCount++; 3491 } 3492 } 3493 } 3494 } 3495 } 3496 if (!allowSingleResize && m_groups->isInGroup(itemId)) { 3497 int groupId = m_groups->getRootId(itemId); 3498 std::unordered_set<int> items = m_groups->getLeaves(groupId); 3499 /*if (m_groups->getType(groupId) == GroupType::AVSplit) { 3500 // Only resize group elements if it is an avsplit 3501 items = m_groups->getLeaves(groupId); 3502 }*/ 3503 for (int id : items) { 3504 if (id == itemId) { 3505 continue; 3506 } 3507 int start = getItemPosition(id); 3508 int end = start + getItemPlaytime(id); 3509 bool resizeMix = false; 3510 if (right) { 3511 if (out == end) { 3512 all_items.insert(id); 3513 resizeMix = true; 3514 } 3515 } else if (start == in) { 3516 all_items.insert(id); 3517 resizeMix = true; 3518 } 3519 if (logUndo && resizeMix && isClip(id)) { 3520 int trackId = getItemTrackId(id); 3521 if (trackId > -1) { 3522 if (right) { 3523 if (getTrackById_const(trackId)->hasEndMix(id)) { 3524 if (!tracksWithMixes.contains(trackId)) { 3525 tracksWithMixes << trackId; 3526 } 3527 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(trackId)->getMixInfo(id); 3528 if (end - offset <= mixData.second.secondClipInOut.first + m_allClips[mixData.second.secondClipId]->getMixDuration() - 3529 m_allClips[mixData.second.secondClipId]->getMixCutPosition()) { 3530 // Resized outside mix 3531 removeMixWithUndo(mixData.second.secondClipId, undo, redo); 3532 resizedCount++; 3533 } else { 3534 // Mix was resized, update cut position 3535 int currentMixDuration = m_allClips[mixData.second.secondClipId]->getMixDuration(); 3536 int currentMixCut = m_allClips[mixData.second.secondClipId]->getMixCutPosition(); 3537 Fun adjust_mix2 = [this, trackId, mixData, currentMixCut, id]() { 3538 MixInfo secondMixData = getTrackById_const(trackId)->getMixInfo(id).second; 3539 int offset = mixData.second.firstClipInOut.second - secondMixData.firstClipInOut.second; 3540 getTrackById_const(trackId)->setMixDuration(secondMixData.secondClipId, 3541 secondMixData.firstClipInOut.second - secondMixData.secondClipInOut.first, 3542 currentMixCut - offset); 3543 QModelIndex ix = makeClipIndexFromID(secondMixData.secondClipId); 3544 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 3545 return true; 3546 }; 3547 Fun adjust_mix_undo = [this, trackId, mixData, currentMixCut, currentMixDuration]() { 3548 getTrackById_const(trackId)->setMixDuration(mixData.second.secondClipId, currentMixDuration, currentMixCut); 3549 QModelIndex ix = makeClipIndexFromID(mixData.second.secondClipId); 3550 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 3551 return true; 3552 }; 3553 PUSH_LAMBDA(adjust_mix2, adjust_mix); 3554 PUSH_LAMBDA(adjust_mix_undo, undo); 3555 } 3556 } 3557 } else if (getTrackById_const(trackId)->hasStartMix(id)) { 3558 if (!tracksWithMixes.contains(trackId)) { 3559 tracksWithMixes << trackId; 3560 } 3561 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(trackId)->getMixInfo(id); 3562 if (start + offset >= mixData.first.firstClipInOut.second) { 3563 // Moved outside mix, remove 3564 removeMixWithUndo(mixData.first.secondClipId, undo, redo); 3565 resizedCount++; 3566 } 3567 } 3568 } 3569 } 3570 } 3571 } 3572 bool result = true; 3573 int finalPos = right ? in + size : out - size; 3574 int finalSize; 3575 for (int id : all_items) { 3576 int trackId = getItemTrackId(id); 3577 if (trackId > -1 && getTrackById_const(trackId)->isLocked()) { 3578 continue; 3579 } 3580 if (isSubtitleTrack(trackId) && m_subtitleModel && m_subtitleModel->isLocked()) { 3581 continue; 3582 } 3583 if (right) { 3584 finalSize = finalPos - qMax(0, getItemPosition(id)); 3585 } else { 3586 int currentDuration = getItemPlaytime(id); 3587 if (trackId == -1) { 3588 finalSize = qMax(0, getItemPosition(id)) + currentDuration - finalPos; 3589 } else { 3590 finalSize = qMax(0, getItemPosition(id)) + currentDuration - qMax(0, finalPos); 3591 if (finalSize == currentDuration) { 3592 continue; 3593 } 3594 } 3595 } 3596 if (finalSize < 1) { 3597 // Abort resize 3598 result = false; 3599 } 3600 result = result && requestItemResize(id, finalSize, right, logUndo, undo, redo); 3601 if (!result) { 3602 break; 3603 } 3604 if (id == itemId) { 3605 size = finalSize; 3606 } 3607 resizedCount++; 3608 } 3609 result = result && resizedCount != 0; 3610 if (!result) { 3611 qDebug() << "resize aborted" << result; 3612 bool undone = undo(); 3613 Q_ASSERT(undone); 3614 } else if (logUndo) { 3615 if (isClip(itemId)) { 3616 adjust_mix(); 3617 PUSH_LAMBDA(adjust_mix, redo); 3618 PUSH_UNDO(undo, redo, i18n("Resize clip")) 3619 } else if (isComposition(itemId)) { 3620 PUSH_UNDO(undo, redo, i18n("Resize composition")) 3621 } else if (isSubTitle(itemId)) { 3622 PUSH_UNDO(undo, redo, i18n("Resize subtitle")) 3623 } 3624 } 3625 int res = result ? size : -1; 3626 TRACE_RES(res) 3627 return res; 3628 } 3629 3630 bool TimelineModel::requestItemResize(int itemId, int &size, bool right, bool logUndo, Fun &undo, Fun &redo, bool blockUndo) 3631 { 3632 Q_UNUSED(blockUndo) 3633 Fun local_undo = []() { return true; }; 3634 Fun local_redo = []() { return true; }; 3635 bool result = false; 3636 if (isClip(itemId)) { 3637 bool hasMix = false; 3638 int tid = m_allClips[itemId]->getCurrentTrackId(); 3639 if (tid > -1) { 3640 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId); 3641 if (right && mixData.second.firstClipId > -1) { 3642 hasMix = true; 3643 if (mixData.second.firstClipInOut.first + size < mixData.second.secondClipInOut.first) { 3644 // Resize is outside mix zone, remove mix 3645 removeMixWithUndo(mixData.second.secondClipId, undo, redo); 3646 } else { 3647 size = qMin(size, mixData.second.secondClipInOut.second - mixData.second.firstClipInOut.first); 3648 } 3649 } else if (!right && mixData.first.firstClipId > -1) { 3650 hasMix = true; 3651 // We have a mix at clip start, limit size to previous clip start 3652 size = qMin(size, mixData.first.secondClipInOut.second - mixData.first.firstClipInOut.first); 3653 int currentMixDuration = mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first; 3654 int mixDuration = mixData.first.firstClipInOut.second - (mixData.first.secondClipInOut.second - size); 3655 Fun local_update = [this, itemId, tid, mixData, mixDuration] { 3656 getTrackById_const(tid)->setMixDuration(itemId, qMax(1, mixDuration), mixData.first.mixOffset); 3657 QModelIndex ix = makeClipIndexFromID(itemId); 3658 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 3659 return true; 3660 }; 3661 Fun local_update_undo = [this, itemId, tid, mixData, currentMixDuration] { 3662 if (getTrackById_const(tid)->hasStartMix(itemId)) { 3663 getTrackById_const(tid)->setMixDuration(itemId, currentMixDuration, mixData.first.mixOffset); 3664 QModelIndex ix = makeClipIndexFromID(itemId); 3665 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 3666 } 3667 return true; 3668 }; 3669 local_update(); 3670 if (logUndo) { 3671 UPDATE_UNDO_REDO(local_update, local_update_undo, local_undo, local_redo); 3672 } 3673 } else { 3674 hasMix = mixData.second.firstClipId > -1 || mixData.first.firstClipId > -1; 3675 } 3676 } 3677 result = m_allClips[itemId]->requestResize(size, right, local_undo, local_redo, logUndo, hasMix); 3678 } else if (isComposition(itemId)) { 3679 result = m_allCompositions[itemId]->requestResize(size, right, local_undo, local_redo, logUndo); 3680 } else if (isSubTitle(itemId)) { 3681 result = m_subtitleModel->requestResize(itemId, size, right, local_undo, local_redo, logUndo); 3682 } 3683 if (result) { 3684 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 3685 } 3686 return result; 3687 } 3688 3689 int TimelineModel::requestItemRippleResize(const std::shared_ptr<TimelineItemModel> &timeline, int itemId, int size, bool right, bool logUndo, bool moveGuides, 3690 int snapDistance, bool allowSingleResize) 3691 { 3692 QWriteLocker locker(&m_lock); 3693 TRACE(itemId, size, right, logUndo, snapDistance, allowSingleResize) 3694 Q_ASSERT(isItem(itemId)); 3695 if (size <= 0) { 3696 TRACE_RES(-1) 3697 return -1; 3698 } 3699 int in = 0; 3700 int offset = getItemPlaytime(itemId); 3701 int tid = getItemTrackId(itemId); 3702 int out = offset; 3703 qDebug() << "======= REQUESTING NEW CLIP SIZE (RIPPLE): " << size; 3704 if (tid != -1 || !isClip(itemId)) { 3705 in = qMax(0, getItemPosition(itemId)); 3706 out += in; 3707 // m_snaps->addPoint(cursorPos); 3708 int proposed_size = m_snaps->proposeSize(in, out, getBoundaries(itemId), size, true, snapDistance); 3709 // m_snaps->removePoint(cursorPos); 3710 if (proposed_size > -1) { 3711 size = proposed_size; 3712 } 3713 } 3714 qDebug() << "======= ADJUSTED NEW CLIP SIZE (RIPPLE): " << size; 3715 offset -= size; 3716 Fun undo = []() { return true; }; 3717 Fun redo = []() { return true; }; 3718 Fun sync_mix = []() { return true; }; 3719 Fun adjust_mix = []() { return true; }; 3720 Fun sync_end_mix = []() { return true; }; 3721 Fun sync_end_mix_undo = []() { return true; }; 3722 PUSH_LAMBDA(sync_mix, undo); 3723 std::unordered_set<int> all_items; 3724 QList<int> tracksWithMixes; 3725 all_items.insert(itemId); 3726 if (logUndo && isClip(itemId)) { 3727 /*if (tid > -1) { 3728 if (right) { 3729 if (getTrackById_const(tid)->hasEndMix(itemId)) { 3730 tracksWithMixes << tid; 3731 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId); 3732 if (in + size <= mixData.second.secondClipInOut.first + m_allClips[mixData.second.secondClipId]->getMixDuration() - 3733 m_allClips[mixData.second.secondClipId]->getMixCutPosition()) { 3734 // Clip resized outside of mix zone, mix will be deleted 3735 bool res = removeMixWithUndo(mixData.second.secondClipId, undo, redo); 3736 if (res) { 3737 size = m_allClips[itemId]->getPlaytime(); 3738 } else { 3739 return -1; 3740 } 3741 } else { 3742 // Mix was resized, update cut position 3743 int currentMixDuration = m_allClips[mixData.second.secondClipId]->getMixDuration(); 3744 int currentMixCut = m_allClips[mixData.second.secondClipId]->getMixCutPosition(); 3745 adjust_mix = [this, tid, mixData, currentMixCut, itemId]() { 3746 MixInfo secondMixData = getTrackById_const(tid)->getMixInfo(itemId).second; 3747 int offset = mixData.second.firstClipInOut.second - secondMixData.firstClipInOut.second; 3748 getTrackById_const(tid)->setMixDuration(secondMixData.secondClipId, secondMixData.firstClipInOut.second - 3749 secondMixData.secondClipInOut.first, currentMixCut - offset); QModelIndex ix = makeClipIndexFromID(secondMixData.secondClipId); Q_EMIT dataChanged(ix, 3750 ix, {TimelineModel::MixRole,TimelineModel::MixCutRole}); return true; 3751 }; 3752 Fun adjust_mix_undo = [this, tid, mixData, currentMixCut, currentMixDuration]() { 3753 getTrackById_const(tid)->setMixDuration(mixData.second.secondClipId, currentMixDuration, currentMixCut); 3754 QModelIndex ix = makeClipIndexFromID(mixData.second.secondClipId); 3755 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole}); 3756 return true; 3757 }; 3758 PUSH_LAMBDA(adjust_mix_undo, undo); 3759 } 3760 } 3761 } else if (getTrackById_const(tid)->hasStartMix(itemId)) { 3762 tracksWithMixes << tid; 3763 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId); 3764 if (out - size >= mixData.first.firstClipInOut.second) { 3765 // Moved outside mix, delete 3766 Fun sync_mix_undo = [this, tid, mixData]() { 3767 getTrackById_const(tid)->createMix(mixData.first, getTrackById_const(tid)->isAudioTrack()); 3768 getTrackById_const(tid)->syncronizeMixes(true); 3769 return true; 3770 }; 3771 bool switchPlaylist = getTrackById_const(tid)->hasEndMix(itemId) == false && m_allClips[itemId]->getSubPlaylistIndex() == 1; 3772 if (switchPlaylist) { 3773 sync_end_mix = [this, tid, mixData]() { 3774 return getTrackById_const(tid)->switchPlaylist(mixData.first.secondClipId, m_allClips[mixData.first.secondClipId]->getPosition(), 1, 3775 0); 3776 }; 3777 sync_end_mix_undo = [this, tid, mixData]() { 3778 return getTrackById_const(tid)->switchPlaylist(mixData.first.secondClipId, m_allClips[mixData.first.secondClipId]->getPosition(), 0, 3779 1); 3780 }; 3781 } 3782 PUSH_LAMBDA(sync_mix_undo, undo); 3783 3784 } 3785 } 3786 }*/ 3787 } 3788 if (!allowSingleResize && m_groups->isInGroup(itemId)) { 3789 int groupId = m_groups->getRootId(itemId); 3790 std::unordered_set<int> items = m_groups->getLeaves(groupId); 3791 /*if (m_groups->getType(groupId) == GroupType::AVSplit) { 3792 // Only resize group elements if it is an avsplit 3793 items = m_groups->getLeaves(groupId); 3794 }*/ 3795 for (int id : items) { 3796 if (id == itemId) { 3797 continue; 3798 } 3799 int start = getItemPosition(id); 3800 int end = start + getItemPlaytime(id); 3801 bool resizeMix = false; 3802 if (right) { 3803 if (out == end) { 3804 all_items.insert(id); 3805 resizeMix = true; 3806 } 3807 } else if (start == in) { 3808 all_items.insert(id); 3809 resizeMix = true; 3810 } 3811 if (logUndo && resizeMix && isClip(id)) { 3812 int trackId = getItemTrackId(id); 3813 if (trackId > -1) { 3814 if (right) { 3815 if (getTrackById_const(trackId)->hasEndMix(id)) { 3816 if (!tracksWithMixes.contains(trackId)) { 3817 tracksWithMixes << trackId; 3818 } 3819 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(trackId)->getMixInfo(id); 3820 if (end - offset <= mixData.second.secondClipInOut.first + m_allClips[mixData.second.secondClipId]->getMixDuration() - 3821 m_allClips[mixData.second.secondClipId]->getMixCutPosition()) { 3822 // Resized outside mix 3823 removeMixWithUndo(mixData.second.secondClipId, undo, redo); 3824 Fun sync_mix_undo = [this, trackId, mixData]() { 3825 getTrackById_const(trackId)->createMix(mixData.second, getTrackById_const(trackId)->isAudioTrack()); 3826 getTrackById_const(trackId)->syncronizeMixes(true); 3827 return true; 3828 }; 3829 bool switchPlaylist = getTrackById_const(trackId)->hasEndMix(mixData.second.secondClipId) == false && 3830 m_allClips[mixData.second.secondClipId]->getSubPlaylistIndex() == 1; 3831 if (switchPlaylist) { 3832 Fun sync_end_mix2 = [this, trackId, mixData]() { 3833 return getTrackById_const(trackId)->switchPlaylist(mixData.second.secondClipId, mixData.second.secondClipInOut.first, 1, 3834 0); 3835 }; 3836 Fun sync_end_mix_undo2 = [this, trackId, mixData]() { 3837 return getTrackById_const(trackId)->switchPlaylist(mixData.second.secondClipId, 3838 m_allClips[mixData.second.secondClipId]->getPosition(), 0, 1); 3839 }; 3840 PUSH_LAMBDA(sync_end_mix2, sync_end_mix); 3841 PUSH_LAMBDA(sync_end_mix_undo2, sync_end_mix_undo); 3842 } 3843 PUSH_LAMBDA(sync_mix_undo, undo); 3844 } else { 3845 // Mix was resized, update cut position 3846 int currentMixDuration = m_allClips[mixData.second.secondClipId]->getMixDuration(); 3847 int currentMixCut = m_allClips[mixData.second.secondClipId]->getMixCutPosition(); 3848 Fun adjust_mix2 = [this, trackId, mixData, currentMixCut, id]() { 3849 MixInfo secondMixData = getTrackById_const(trackId)->getMixInfo(id).second; 3850 int offset = mixData.second.firstClipInOut.second - secondMixData.firstClipInOut.second; 3851 getTrackById_const(trackId)->setMixDuration(secondMixData.secondClipId, 3852 secondMixData.firstClipInOut.second - secondMixData.secondClipInOut.first, 3853 currentMixCut - offset); 3854 QModelIndex ix = makeClipIndexFromID(secondMixData.secondClipId); 3855 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 3856 return true; 3857 }; 3858 Fun adjust_mix_undo = [this, trackId, mixData, currentMixCut, currentMixDuration]() { 3859 getTrackById_const(trackId)->setMixDuration(mixData.second.secondClipId, currentMixDuration, currentMixCut); 3860 QModelIndex ix = makeClipIndexFromID(mixData.second.secondClipId); 3861 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 3862 return true; 3863 }; 3864 PUSH_LAMBDA(adjust_mix2, adjust_mix); 3865 PUSH_LAMBDA(adjust_mix_undo, undo); 3866 } 3867 } 3868 } else if (getTrackById_const(trackId)->hasStartMix(id)) { 3869 if (!tracksWithMixes.contains(trackId)) { 3870 tracksWithMixes << trackId; 3871 } 3872 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(trackId)->getMixInfo(id); 3873 if (start + offset >= mixData.first.firstClipInOut.second) { 3874 // Moved outside mix, remove 3875 Fun sync_mix_undo = [this, trackId, mixData]() { 3876 getTrackById_const(trackId)->createMix(mixData.first, getTrackById_const(trackId)->isAudioTrack()); 3877 getTrackById_const(trackId)->syncronizeMixes(true); 3878 return true; 3879 }; 3880 bool switchPlaylist = getTrackById_const(trackId)->hasEndMix(id) == false && m_allClips[id]->getSubPlaylistIndex() == 1; 3881 if (switchPlaylist) { 3882 Fun sync_end_mix2 = [this, trackId, mixData]() { 3883 return getTrackById_const(trackId)->switchPlaylist(mixData.first.secondClipId, 3884 m_allClips[mixData.first.secondClipId]->getPosition(), 1, 0); 3885 }; 3886 Fun sync_end_mix_undo2 = [this, trackId, mixData]() { 3887 return getTrackById_const(trackId)->switchPlaylist(mixData.first.secondClipId, 3888 m_allClips[mixData.first.secondClipId]->getPosition(), 0, 1); 3889 }; 3890 PUSH_LAMBDA(sync_end_mix2, sync_end_mix); 3891 PUSH_LAMBDA(sync_end_mix_undo2, sync_end_mix_undo); 3892 } 3893 PUSH_LAMBDA(sync_mix_undo, undo); 3894 } 3895 } 3896 } 3897 } 3898 } 3899 } 3900 if (logUndo && !tracksWithMixes.isEmpty()) { 3901 sync_mix = [this, tracksWithMixes]() { 3902 for (auto &t : tracksWithMixes) { 3903 getTrackById_const(t)->syncronizeMixes(true); 3904 } 3905 return true; 3906 }; 3907 } 3908 bool result = true; 3909 int finalPos = right ? in + size : out - size; 3910 int finalSize; 3911 int resizedCount = 0; 3912 for (int id : all_items) { 3913 int trackId = getItemTrackId(id); 3914 if (trackId > -1 && getTrackById_const(trackId)->isLocked()) { 3915 continue; 3916 } 3917 if (isSubtitleTrack(trackId) && m_subtitleModel && m_subtitleModel->isLocked()) { 3918 continue; 3919 } 3920 if (right) { 3921 finalSize = finalPos - qMax(0, getItemPosition(id)); 3922 } else { 3923 finalSize = qMax(0, getItemPosition(id)) + getItemPlaytime(id) - finalPos; 3924 } 3925 result = result && requestItemRippleResize(timeline, id, finalSize, right, logUndo, moveGuides, undo, redo); 3926 resizedCount++; 3927 } 3928 result = result && resizedCount != 0; 3929 if (!result) { 3930 qDebug() << "resize aborted" << result; 3931 bool undone = undo(); 3932 Q_ASSERT(undone); 3933 } else if (logUndo) { 3934 if (isClip(itemId)) { 3935 sync_end_mix(); 3936 sync_mix(); 3937 adjust_mix(); 3938 PUSH_LAMBDA(sync_end_mix, redo); 3939 PUSH_LAMBDA(adjust_mix, redo); 3940 PUSH_LAMBDA(sync_mix, redo); 3941 PUSH_LAMBDA(undo, sync_end_mix_undo); 3942 PUSH_UNDO(sync_end_mix_undo, redo, i18n("Ripple resize clip")) 3943 } else if (isComposition(itemId)) { 3944 PUSH_UNDO(undo, redo, i18n("Ripple resize composition")) 3945 } else if (isSubTitle(itemId)) { 3946 PUSH_UNDO(undo, redo, i18n("Ripple resize subtitle")) 3947 } 3948 } 3949 int res = result ? size : -1; 3950 TRACE_RES(res) 3951 return res; 3952 } 3953 3954 bool TimelineModel::requestItemRippleResize(const std::shared_ptr<TimelineItemModel> &timeline, int itemId, int size, bool right, bool logUndo, bool moveGuides, 3955 Fun &undo, Fun &redo, bool blockUndo) 3956 { 3957 Q_UNUSED(blockUndo) 3958 Q_UNUSED(moveGuides) 3959 Fun local_undo = []() { return true; }; 3960 Fun local_redo = []() { return true; }; 3961 bool result = false; 3962 if (isClip(itemId)) { 3963 bool hasMix = false; 3964 int tid = m_allClips[itemId]->getCurrentTrackId(); 3965 if (tid > -1) { 3966 /*std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(itemId); 3967 if (right && mixData.second.firstClipId > -1) { 3968 hasMix = true; 3969 size = qMin(size, mixData.second.secondClipInOut.second - mixData.second.firstClipInOut.first); 3970 } else if (!right && mixData.first.firstClipId > -1) { 3971 hasMix = true; 3972 // We have a mix at clip start, limit size to previous clip start 3973 size = qMin(size, mixData.first.secondClipInOut.second - mixData.first.firstClipInOut.first); 3974 int currentMixDuration = mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first; 3975 int mixDuration = mixData.first.firstClipInOut.second - (mixData.first.secondClipInOut.second - size); 3976 Fun local_update = [this, itemId, tid, mixData, mixDuration] { 3977 getTrackById_const(tid)->setMixDuration(itemId, qMax(1, mixDuration), mixData.first.mixOffset); 3978 QModelIndex ix = makeClipIndexFromID(itemId); 3979 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole}); 3980 return true; 3981 }; 3982 Fun local_update_undo = [this, itemId, tid, mixData, currentMixDuration] { 3983 getTrackById_const(tid)->setMixDuration(itemId, currentMixDuration, mixData.first.mixOffset); 3984 QModelIndex ix = makeClipIndexFromID(itemId); 3985 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole,TimelineModel::MixCutRole}); 3986 return true; 3987 }; 3988 local_update(); 3989 if (logUndo) { 3990 UPDATE_UNDO_REDO(local_update, local_update_undo, local_undo, local_redo); 3991 } 3992 } else { 3993 hasMix = mixData.second.firstClipId > -1 || mixData.first.firstClipId > -1; 3994 }*/ 3995 } 3996 bool affectAllTracks = false; 3997 size = m_allClips[itemId]->getMaxDuration() > 0 ? qBound(1, size, m_allClips[itemId]->getMaxDuration()) : qMax(1, size); 3998 int delta = size - m_allClips[itemId]->getPlaytime(); 3999 qDebug() << "requestItemRippleResize logUndo: " << logUndo << " size: " << size << " playtime: " << m_allClips[itemId]->getPlaytime() 4000 << " delta: " << delta; 4001 auto spacerOperation = [this, itemId, affectAllTracks, &local_undo, &local_redo, delta, right, timeline](int position) { 4002 int trackId = getItemTrackId(itemId); 4003 if (right && getTrackById_const(trackId)->isLastClip(getItemPosition(itemId))) { 4004 return true; 4005 } 4006 std::pair<int, int> spacerOp = TimelineFunctions::requestSpacerStartOperation(timeline, affectAllTracks ? -1 : trackId, position + 1, true, true); 4007 int cid = spacerOp.first; 4008 if (cid == -1) { 4009 return false; 4010 } 4011 int endPos = getItemPosition(cid) + delta; 4012 // Start undoable command 4013 TimelineFunctions::requestSpacerEndOperation(timeline, cid, getItemPosition(cid), endPos, affectAllTracks ? -1 : trackId, 4014 KdenliveSettings::lockedGuides() ? -1 : position, local_undo, local_redo, false); 4015 return true; 4016 }; 4017 if (delta > 0) { 4018 if (right) { 4019 int position = getItemPosition(itemId) + getItemPlaytime(itemId); 4020 if (!spacerOperation(position)) { 4021 return false; 4022 } 4023 } else { 4024 int position = getItemPosition(itemId); 4025 if (!spacerOperation(position)) { 4026 return false; 4027 } 4028 } 4029 } 4030 4031 result = m_allClips[itemId]->requestResize(size, right, local_undo, local_redo, logUndo, hasMix); 4032 if (!result && delta > 0) { 4033 local_undo(); 4034 } 4035 if (result && delta < 0) { 4036 if (right) { 4037 int position = getItemPosition(itemId) + getItemPlaytime(itemId) - delta; 4038 if (!spacerOperation(position)) { 4039 return false; 4040 } 4041 } else { 4042 int position = getItemPosition(itemId) + delta; 4043 if (!spacerOperation(position)) { 4044 return false; 4045 } 4046 } 4047 } 4048 4049 } else if (isComposition(itemId)) { 4050 return false; 4051 // TODO? Does it make sense? 4052 // result = m_allCompositions[itemId]->requestResize(size, right, local_undo, local_redo, logUndo); 4053 } else if (isSubTitle(itemId)) { 4054 return false; 4055 // TODO? 4056 // result = m_subtitleModel->requestResize(itemId, size, right, local_undo, local_redo, logUndo); 4057 } 4058 if (result) { 4059 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 4060 } 4061 return result; 4062 } 4063 4064 int TimelineModel::requestSlipSelection(int offset, bool logUndo) 4065 { 4066 QWriteLocker locker(&m_lock); 4067 TRACE(offset, logUndo) 4068 4069 Fun undo = []() { return true; }; 4070 Fun redo = []() { return true; }; 4071 bool result = true; 4072 int slipCount = 0; 4073 for (auto id : getCurrentSelection()) { 4074 int tid = getItemTrackId(id); 4075 if (tid > -1 && getTrackById_const(tid)->isLocked()) { 4076 continue; 4077 } 4078 if (!isClip(id)) { 4079 continue; 4080 } 4081 result = result && requestClipSlip(id, offset, logUndo, undo, redo); 4082 slipCount++; 4083 } 4084 result = result && slipCount != 0; 4085 if (!result) { 4086 bool undone = undo(); 4087 Q_ASSERT(undone); 4088 } else if (logUndo) { 4089 PUSH_UNDO(undo, redo, i18ncp("Undo/Redo menu text", "Slip clip", "Slip clips", slipCount)); 4090 } 4091 int res = result ? offset : 0; 4092 TRACE_RES(res) 4093 return res; 4094 } 4095 4096 int TimelineModel::requestClipSlip(int itemId, int offset, bool logUndo, bool allowSingleResize) 4097 { 4098 QWriteLocker locker(&m_lock); 4099 TRACE(itemId, offset, logUndo, allowSingleResize) 4100 Q_ASSERT(isClip(itemId)); 4101 Fun undo = []() { return true; }; 4102 Fun redo = []() { return true; }; 4103 std::unordered_set<int> all_items; 4104 all_items.insert(itemId); 4105 if (!allowSingleResize && m_groups->isInGroup(itemId)) { 4106 int groupId = m_groups->getRootId(itemId); 4107 all_items = m_groups->getLeaves(groupId); 4108 } 4109 bool result = true; 4110 int slipCount = 0; 4111 for (int id : all_items) { 4112 int tid = getItemTrackId(id); 4113 if (tid > -1 && getTrackById_const(tid)->isLocked()) { 4114 continue; 4115 } 4116 result = result && requestClipSlip(id, offset, logUndo, undo, redo); 4117 slipCount++; 4118 } 4119 result = result && slipCount != 0; 4120 if (!result) { 4121 bool undone = undo(); 4122 Q_ASSERT(undone); 4123 } else if (logUndo) { 4124 PUSH_UNDO(undo, redo, i18n("Slip clip")) 4125 } 4126 int res = result ? offset : 0; 4127 TRACE_RES(res) 4128 return res; 4129 } 4130 4131 bool TimelineModel::requestClipSlip(int itemId, int offset, bool logUndo, Fun &undo, Fun &redo, bool blockUndo) 4132 { 4133 Q_UNUSED(blockUndo) 4134 Fun local_undo = []() { return true; }; 4135 Fun local_redo = []() { return true; }; 4136 bool result = false; 4137 if (isClip(itemId)) { 4138 result = m_allClips[itemId]->requestSlip(offset, local_undo, local_redo, logUndo); 4139 } 4140 if (result) { 4141 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 4142 } 4143 return result; 4144 } 4145 4146 int TimelineModel::requestClipsGroup(const std::unordered_set<int> &ids, bool logUndo, GroupType type) 4147 { 4148 QWriteLocker locker(&m_lock); 4149 TRACE(ids, logUndo, type); 4150 if (type == GroupType::Selection || type == GroupType::Leaf) { 4151 // Selections shouldn't be done here. Call requestSetSelection instead 4152 TRACE_RES(-1); 4153 return -1; 4154 } 4155 Fun undo = []() { return true; }; 4156 Fun redo = []() { return true; }; 4157 int result = requestClipsGroup(ids, undo, redo, type); 4158 if (result > -1 && logUndo) { 4159 PUSH_UNDO(undo, redo, i18n("Group clips")); 4160 } 4161 TRACE_RES(result); 4162 return result; 4163 } 4164 4165 int TimelineModel::requestClipsGroup(const std::unordered_set<int> &ids, Fun &undo, Fun &redo, GroupType type) 4166 { 4167 QWriteLocker locker(&m_lock); 4168 if (type != GroupType::Selection) { 4169 requestClearSelection(); 4170 } 4171 int clipsCount = 0; 4172 QList<int> tracks; 4173 for (int id : ids) { 4174 if (isClip(id)) { 4175 int trackId = getClipTrackId(id); 4176 if (trackId == -1) { 4177 return -1; 4178 } 4179 tracks << trackId; 4180 clipsCount++; 4181 } else if (isComposition(id)) { 4182 if (getCompositionTrackId(id) == -1) { 4183 return -1; 4184 } 4185 } else if (isSubTitle(id)) { 4186 } else if (!isGroup(id)) { 4187 return -1; 4188 } 4189 } 4190 if (type == GroupType::Selection && ids.size() == 1) { 4191 // only one element selected, no group created 4192 return -1; 4193 } 4194 if (ids.size() == 2 && clipsCount == 2 && type == GroupType::Normal) { 4195 // Check if we are grouping an AVSplit 4196 auto it = ids.begin(); 4197 int firstId = *it; 4198 std::advance(it, 1); 4199 int secondId = *it; 4200 bool isAVGroup = false; 4201 if (getClipBinId(firstId) == getClipBinId(secondId)) { 4202 if (getClipState(firstId) == PlaylistState::AudioOnly) { 4203 if (getClipState(secondId) == PlaylistState::VideoOnly) { 4204 isAVGroup = true; 4205 } 4206 } else if (getClipState(secondId) == PlaylistState::AudioOnly) { 4207 isAVGroup = true; 4208 } 4209 } 4210 if (isAVGroup) { 4211 type = GroupType::AVSplit; 4212 } 4213 } 4214 int groupId = m_groups->groupItems(ids, undo, redo, type); 4215 if (type != GroupType::Selection) { 4216 // we make sure that the undo and the redo are going to unselect before doing anything else 4217 Fun unselect = [this]() { return requestClearSelection(); }; 4218 PUSH_FRONT_LAMBDA(unselect, undo); 4219 PUSH_FRONT_LAMBDA(unselect, redo); 4220 } 4221 return groupId; 4222 } 4223 4224 bool TimelineModel::requestClipsUngroup(const std::unordered_set<int> &itemIds, bool logUndo) 4225 { 4226 QWriteLocker locker(&m_lock); 4227 TRACE(itemIds, logUndo); 4228 Fun undo = []() { return true; }; 4229 Fun redo = []() { return true; }; 4230 bool result = true; 4231 requestClearSelection(); 4232 std::unordered_set<int> roots; 4233 std::transform(itemIds.begin(), itemIds.end(), std::inserter(roots, roots.begin()), [&](int id) { return m_groups->getRootId(id); }); 4234 for (int root : roots) { 4235 if (isGroup(root)) { 4236 result = result && requestClipUngroup(root, undo, redo); 4237 } 4238 } 4239 if (!result) { 4240 bool undone = undo(); 4241 Q_ASSERT(undone); 4242 } 4243 if (result && logUndo) { 4244 PUSH_UNDO(undo, redo, i18n("Ungroup clips")); 4245 } 4246 TRACE_RES(result); 4247 return result; 4248 } 4249 4250 bool TimelineModel::requestClipUngroup(int itemId, bool logUndo) 4251 { 4252 QWriteLocker locker(&m_lock); 4253 TRACE(itemId, logUndo); 4254 requestClearSelection(); 4255 Fun undo = []() { return true; }; 4256 Fun redo = []() { return true; }; 4257 bool result = true; 4258 result = requestClipUngroup(itemId, undo, redo); 4259 if (result && logUndo) { 4260 PUSH_UNDO(undo, redo, i18n("Ungroup clips")); 4261 } 4262 TRACE_RES(result); 4263 return result; 4264 } 4265 4266 bool TimelineModel::requestClipUngroup(int itemId, Fun &undo, Fun &redo) 4267 { 4268 QWriteLocker locker(&m_lock); 4269 bool isSelection = m_groups->getType(m_groups->getRootId(itemId)) == GroupType::Selection; 4270 if (!isSelection) { 4271 requestClearSelection(); 4272 } 4273 bool res = m_groups->ungroupItem(itemId, undo, redo); 4274 if (res && !isSelection) { 4275 // we make sure that the undo and the redo are going to unselect before doing anything else 4276 Fun unselect = [this]() { return requestClearSelection(); }; 4277 PUSH_FRONT_LAMBDA(unselect, undo); 4278 PUSH_FRONT_LAMBDA(unselect, redo); 4279 } 4280 return res; 4281 } 4282 4283 bool TimelineModel::requestRemoveFromGroup(int itemId, Fun &undo, Fun &redo) 4284 { 4285 QWriteLocker locker(&m_lock); 4286 GroupType type = m_groups->getType(m_groups->getRootId(itemId)); 4287 bool isSelection = type == GroupType::Selection; 4288 if (!isSelection) { 4289 requestClearSelection(); 4290 } 4291 std::unordered_set<int> items = getGroupElements(itemId); 4292 bool res = m_groups->ungroupItem(itemId, undo, redo); 4293 if (res && items.size() > 1) { 4294 items.erase(itemId); 4295 res = m_groups->groupItems(items, undo, redo, type); 4296 } 4297 4298 if (res && !isSelection) { 4299 // we make sure that the undo and the redo are going to unselect before doing anything else 4300 Fun unselect = [this]() { return requestClearSelection(); }; 4301 PUSH_FRONT_LAMBDA(unselect, undo); 4302 PUSH_FRONT_LAMBDA(unselect, redo); 4303 } 4304 return res; 4305 } 4306 4307 bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack) 4308 { 4309 QWriteLocker locker(&m_lock); 4310 TRACE(position, id, trackName, audioTrack); 4311 Fun undo = []() { return true; }; 4312 Fun redo = []() { return true; }; 4313 bool result = requestTrackInsertion(position, id, trackName, audioTrack, undo, redo); 4314 if (result) { 4315 PUSH_UNDO(undo, redo, i18nc("@action", "Insert Track")); 4316 } 4317 TRACE_RES(result); 4318 return result; 4319 } 4320 4321 bool TimelineModel::requestTrackInsertion(int position, int &id, const QString &trackName, bool audioTrack, Fun &undo, Fun &redo, bool addCompositing) 4322 { 4323 // TODO: make sure we disable overlayTrack before inserting a track 4324 if (position == -1) { 4325 position = int(m_allTracks.size()); 4326 } 4327 if (position < 0 || position > int(m_allTracks.size())) { 4328 return false; 4329 } 4330 int previousId = -1; 4331 if (position < int(m_allTracks.size())) { 4332 previousId = getTrackIndexFromPosition(position); 4333 } 4334 int trackId = TimelineModel::getNextId(); 4335 id = trackId; 4336 Fun local_undo = deregisterTrack_lambda(trackId); 4337 TrackModel::construct(shared_from_this(), trackId, position, trackName, audioTrack, addCompositing); 4338 // Adjust compositions that were affecting track at previous pos 4339 QList<std::shared_ptr<CompositionModel>> updatedCompositions; 4340 if (previousId > -1) { 4341 for (auto &compo : m_allCompositions) { 4342 if (position > 0 && compo.second->getATrack() == position && compo.second->getForcedTrack() == -1) { 4343 updatedCompositions << compo.second; 4344 } 4345 } 4346 } 4347 Fun local_update = [position, updatedCompositions]() { 4348 for (auto &compo : updatedCompositions) { 4349 compo->setATrack(position + 1, -1); 4350 } 4351 return true; 4352 }; 4353 Fun local_update_undo = [position, updatedCompositions]() { 4354 for (auto &compo : updatedCompositions) { 4355 compo->setATrack(position, -1); 4356 } 4357 return true; 4358 }; 4359 4360 Fun local_name_update = [position, audioTrack, this]() { 4361 if (KdenliveSettings::audiotracksbelow() == 0) { 4362 _resetView(); 4363 } else { 4364 if (audioTrack) { 4365 for (int i = 0; i <= position && i < int(m_allTracks.size()); i++) { 4366 QModelIndex ix = makeTrackIndexFromID(getTrackIndexFromPosition(i)); 4367 Q_EMIT dataChanged(ix, ix, {TimelineModel::TrackTagRole}); 4368 } 4369 } else { 4370 for (int i = position; i < int(m_allTracks.size()); i++) { 4371 QModelIndex ix = makeTrackIndexFromID(getTrackIndexFromPosition(i)); 4372 Q_EMIT dataChanged(ix, ix, {TimelineModel::TrackTagRole}); 4373 } 4374 } 4375 } 4376 return true; 4377 }; 4378 4379 local_update(); 4380 local_name_update(); 4381 Fun rebuild_compositing = [this]() { 4382 buildTrackCompositing(true); 4383 return true; 4384 }; 4385 if (addCompositing) { 4386 buildTrackCompositing(true); 4387 } 4388 auto track = getTrackById(trackId); 4389 Fun local_redo = [track, position, local_update, addCompositing, this]() { 4390 // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is 4391 // sufficient to register it. 4392 registerTrack(track, position, true); 4393 local_update(); 4394 if (addCompositing) { 4395 buildTrackCompositing(true); 4396 } 4397 return true; 4398 }; 4399 if (addCompositing) { 4400 PUSH_LAMBDA(local_update_undo, local_undo); 4401 PUSH_LAMBDA(rebuild_compositing, local_undo); 4402 } 4403 PUSH_LAMBDA(local_name_update, local_undo); 4404 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 4405 PUSH_LAMBDA(local_name_update, redo); 4406 return true; 4407 } 4408 4409 bool TimelineModel::requestTrackDeletion(int trackId) 4410 { 4411 // TODO: make sure we disable overlayTrack before deleting a track 4412 QWriteLocker locker(&m_lock); 4413 TRACE(trackId); 4414 Fun undo = []() { return true; }; 4415 Fun redo = []() { return true; }; 4416 bool result = requestTrackDeletion(trackId, undo, redo); 4417 if (result) { 4418 if (m_videoTarget == trackId) { 4419 m_videoTarget = -1; 4420 } 4421 if (m_audioTarget.contains(trackId)) { 4422 m_audioTarget.remove(trackId); 4423 } 4424 PUSH_UNDO(undo, redo, i18n("Delete Track")); 4425 } 4426 TRACE_RES(result); 4427 return result; 4428 } 4429 4430 bool TimelineModel::requestTrackDeletion(int trackId, Fun &undo, Fun &redo) 4431 { 4432 Q_ASSERT(isTrack(trackId)); 4433 if (m_allTracks.size() < 2) { 4434 pCore->displayMessage(i18n("Cannot delete last track in timeline"), ErrorMessage, 500); 4435 return false; 4436 } 4437 // Discard running jobs 4438 pCore->taskManager.discardJobs(ObjectId(KdenliveObjectType::TimelineTrack, trackId, m_uuid)); 4439 4440 std::vector<int> clips_to_delete; 4441 for (const auto &it : getTrackById(trackId)->m_allClips) { 4442 clips_to_delete.push_back(it.first); 4443 } 4444 Fun local_undo = []() { return true; }; 4445 Fun local_redo = []() { return true; }; 4446 for (int clip : clips_to_delete) { 4447 bool res = true; 4448 while (res && m_groups->isInGroup(clip)) { 4449 res = requestClipUngroup(clip, local_undo, local_redo); 4450 } 4451 if (res) { 4452 res = requestClipDeletion(clip, local_undo, local_redo); 4453 } 4454 if (!res) { 4455 bool u = local_undo(); 4456 Q_ASSERT(u); 4457 return false; 4458 } 4459 } 4460 std::vector<int> compositions_to_delete; 4461 for (const auto &it : getTrackById(trackId)->m_allCompositions) { 4462 compositions_to_delete.push_back(it.first); 4463 } 4464 for (int compo : compositions_to_delete) { 4465 bool res = true; 4466 while (res && m_groups->isInGroup(compo)) { 4467 res = requestClipUngroup(compo, local_undo, local_redo); 4468 } 4469 if (res) { 4470 res = requestCompositionDeletion(compo, local_undo, local_redo); 4471 } 4472 if (!res) { 4473 bool u = local_undo(); 4474 Q_ASSERT(u); 4475 return false; 4476 } 4477 } 4478 int old_position = getTrackPosition(trackId); 4479 int previousTrack = getPreviousVideoTrackPos(trackId); 4480 auto operation = deregisterTrack_lambda(trackId); 4481 std::shared_ptr<TrackModel> track = getTrackById(trackId); 4482 bool audioTrack = track->isAudioTrack(); 4483 QList<std::shared_ptr<CompositionModel>> updatedCompositions; 4484 for (auto &compo : m_allCompositions) { 4485 if (compo.second->getATrack() == old_position + 1 && compo.second->getForcedTrack() == -1) { 4486 updatedCompositions << compo.second; 4487 } 4488 } 4489 Fun reverse = [this, track, old_position, updatedCompositions]() { 4490 // We capture a shared_ptr to the track, which means that as long as this undo object lives, the track object is not deleted. To insert it back it is 4491 // sufficient to register it. 4492 registerTrack(track, old_position); 4493 for (auto &compo : updatedCompositions) { 4494 compo->setATrack(old_position + 1, -1); 4495 } 4496 return true; 4497 }; 4498 Fun local_update = [previousTrack, updatedCompositions]() { 4499 for (auto &compo : updatedCompositions) { 4500 compo->setATrack(previousTrack, -1); 4501 } 4502 return true; 4503 }; 4504 Fun rebuild_compositing = [this]() { 4505 buildTrackCompositing(true); 4506 return true; 4507 }; 4508 Fun local_name_update = [old_position, audioTrack, this]() { 4509 if (audioTrack) { 4510 for (int i = 0; i < qMin(old_position + 1, getTracksCount()); i++) { 4511 QModelIndex ix = makeTrackIndexFromID(getTrackIndexFromPosition(i)); 4512 Q_EMIT dataChanged(ix, ix, {TimelineModel::TrackTagRole}); 4513 } 4514 } else { 4515 for (int i = old_position; i < getTracksCount(); i++) { 4516 QModelIndex ix = makeTrackIndexFromID(getTrackIndexFromPosition(i)); 4517 Q_EMIT dataChanged(ix, ix, {TimelineModel::TrackTagRole}); 4518 } 4519 } 4520 return true; 4521 }; 4522 if (operation()) { 4523 local_update(); 4524 rebuild_compositing(); 4525 local_name_update(); 4526 PUSH_LAMBDA(rebuild_compositing, local_undo); 4527 PUSH_LAMBDA(local_name_update, local_undo); 4528 UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo); 4529 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 4530 PUSH_LAMBDA(local_update, redo); 4531 PUSH_LAMBDA(rebuild_compositing, redo); 4532 PUSH_LAMBDA(local_name_update, redo); 4533 return true; 4534 } 4535 local_undo(); 4536 return false; 4537 } 4538 4539 void TimelineModel::registerTrack(std::shared_ptr<TrackModel> track, int pos, bool doInsert, bool singleOperation) 4540 { 4541 int id = track->getId(); 4542 if (pos == -1) { 4543 pos = static_cast<int>(m_allTracks.size()); 4544 } 4545 Q_ASSERT(pos >= 0); 4546 Q_ASSERT(pos <= static_cast<int>(m_allTracks.size())); 4547 4548 // effective insertion (MLT operation), add 1 to account for black background track 4549 if (doInsert) { 4550 if (!singleOperation) { 4551 m_tractor->block(); 4552 } 4553 int error = m_tractor->insert_track(*track, pos + 1); 4554 if (!singleOperation) { 4555 m_tractor->unblock(); 4556 } 4557 Q_ASSERT(error == 0); // we might need better error handling... 4558 } 4559 4560 // we now insert in the list 4561 auto posIt = m_allTracks.begin(); 4562 std::advance(posIt, pos); 4563 beginInsertRows(QModelIndex(), pos, pos); 4564 auto it = m_allTracks.insert(posIt, std::move(track)); 4565 // it now contains the iterator to the inserted element, we store it 4566 Q_ASSERT(m_iteratorTable.count(id) == 0); // check that id is not used (shouldn't happen) 4567 m_iteratorTable[id] = it; 4568 endInsertRows(); 4569 int cache = int(QThread::idealThreadCount()) + int(m_allTracks.size() + 1) * 2; 4570 mlt_service_cache_set_size(nullptr, "producer_avformat", qMax(4, cache)); 4571 } 4572 4573 void TimelineModel::registerClip(const std::shared_ptr<ClipModel> &clip, bool registerProducer) 4574 { 4575 int id = clip->getId(); 4576 Q_ASSERT(m_allClips.count(id) == 0); 4577 m_allClips[id] = clip; 4578 clip->registerClipToBin(clip->getProducer(), registerProducer); 4579 m_groups->createGroupItem(id); 4580 clip->setTimelineEffectsEnabled(m_timelineEffectsEnabled); 4581 } 4582 4583 void TimelineModel::registerSubtitle(int id, GenTime startTime, bool temporary) 4584 { 4585 Q_ASSERT(m_allSubtitles.count(id) == 0); 4586 m_allSubtitles.emplace(id, startTime); 4587 if (!temporary) { 4588 m_groups->createGroupItem(id); 4589 } 4590 } 4591 4592 int TimelineModel::positionForIndex(int id) 4593 { 4594 return int(std::distance(m_allSubtitles.begin(), m_allSubtitles.find(id))); 4595 } 4596 4597 void TimelineModel::deregisterSubtitle(int id, bool temporary) 4598 { 4599 Q_ASSERT(m_allSubtitles.count(id) > 0); 4600 if (!temporary && m_subtitleModel->isSelected(id)) { 4601 requestClearSelection(true); 4602 } 4603 m_allSubtitles.erase(id); 4604 if (!temporary) { 4605 m_groups->destructGroupItem(id); 4606 } 4607 } 4608 4609 void TimelineModel::registerGroup(int groupId) 4610 { 4611 Q_ASSERT(m_allGroups.count(groupId) == 0); 4612 m_allGroups.insert(groupId); 4613 } 4614 4615 Fun TimelineModel::deregisterTrack_lambda(int id) 4616 { 4617 return [this, id]() { 4618 if (!m_closing) { 4619 Q_EMIT checkTrackDeletion(id); 4620 } 4621 auto it = m_iteratorTable[id]; // iterator to the element 4622 int index = getTrackPosition(id); // compute index in list 4623 if (!m_closing) { 4624 // send update to the model 4625 beginRemoveRows(QModelIndex(), index, index); 4626 } 4627 // melt operation, add 1 to account for black background track 4628 m_tractor->remove_track(static_cast<int>(index + 1)); 4629 // actual deletion of object 4630 m_allTracks.erase(it); 4631 // clean table 4632 m_iteratorTable.erase(id); 4633 if (!m_closing) { 4634 // Finish operation 4635 endRemoveRows(); 4636 int cache = int(QThread::idealThreadCount()) + int(m_allTracks.size() + 1) * 2; 4637 mlt_service_cache_set_size(nullptr, "producer_avformat", qMax(4, cache)); 4638 } 4639 return true; 4640 }; 4641 } 4642 4643 Fun TimelineModel::deregisterClip_lambda(int clipId) 4644 { 4645 return [this, clipId]() { 4646 // Clear effect stack 4647 Q_EMIT requestClearAssetView(clipId); 4648 if (!m_closing) { 4649 Q_EMIT checkItemDeletion(clipId); 4650 } 4651 Q_ASSERT(m_allClips.count(clipId) > 0); 4652 Q_ASSERT(getClipTrackId(clipId) == -1); // clip must be deleted from its track at this point 4653 Q_ASSERT(!m_groups->isInGroup(clipId)); // clip must be ungrouped at this point 4654 auto clip = m_allClips[clipId]; 4655 m_allClips.erase(clipId); 4656 clip->deregisterClipToBin(m_uuid); 4657 m_groups->destructGroupItem(clipId); 4658 return true; 4659 }; 4660 } 4661 4662 void TimelineModel::deregisterGroup(int id) 4663 { 4664 Q_ASSERT(m_allGroups.count(id) > 0); 4665 m_allGroups.erase(id); 4666 } 4667 4668 std::shared_ptr<TrackModel> TimelineModel::getTrackById(int trackId) 4669 { 4670 Q_ASSERT(m_iteratorTable.count(trackId) > 0); 4671 return *m_iteratorTable[trackId]; 4672 } 4673 4674 const std::shared_ptr<TrackModel> TimelineModel::getTrackById_const(int trackId) const 4675 { 4676 Q_ASSERT(m_iteratorTable.count(trackId) > 0); 4677 return *m_iteratorTable.at(trackId); 4678 } 4679 4680 bool TimelineModel::addTrackEffect(int trackId, const QString &effectId) 4681 { 4682 if (trackId == -1) { 4683 if (m_masterStack == nullptr || m_masterStack->appendEffect(effectId) == false) { 4684 QString effectName = EffectsRepository::get()->getName(effectId); 4685 pCore->displayMessage(i18n("Cannot add effect %1 to master track", effectName), InformationMessage, 500); 4686 return false; 4687 } 4688 } else { 4689 Q_ASSERT(m_iteratorTable.count(trackId) > 0); 4690 if ((*m_iteratorTable.at(trackId))->addEffect(effectId) == false) { 4691 QString effectName = EffectsRepository::get()->getName(effectId); 4692 pCore->displayMessage(i18n("Cannot add effect %1 to selected track", effectName), InformationMessage, 500); 4693 return false; 4694 } 4695 } 4696 return true; 4697 } 4698 4699 bool TimelineModel::copyTrackEffect(int trackId, const QString &sourceId) 4700 { 4701 QStringList source = sourceId.split(QLatin1Char(',')); 4702 Q_ASSERT(source.count() == 4); 4703 int itemType = source.at(0).toInt(); 4704 int itemId = source.at(1).toInt(); 4705 int itemRow = source.at(2).toInt(); 4706 const QUuid uuid(source.at(3)); 4707 std::shared_ptr<EffectStackModel> effectStack = pCore->getItemEffectStack(uuid, itemType, itemId); 4708 4709 if (trackId == -1) { 4710 QWriteLocker locker(&m_lock); 4711 if (m_masterStack == nullptr || m_masterStack->copyEffect(effectStack->getEffectStackRow(itemRow), PlaylistState::Disabled) == 4712 false) { // We use "disabled" in a hacky way to accept video and audio on master 4713 pCore->displayMessage(i18n("Cannot paste effect to master track"), InformationMessage, 500); 4714 return false; 4715 } 4716 } else { 4717 Q_ASSERT(m_iteratorTable.count(trackId) > 0); 4718 if ((*m_iteratorTable.at(trackId))->copyEffect(effectStack, itemRow) == false) { 4719 pCore->displayMessage(i18n("Cannot paste effect to selected track"), InformationMessage, 500); 4720 return false; 4721 } 4722 } 4723 return true; 4724 } 4725 4726 std::shared_ptr<ClipModel> TimelineModel::getClipPtr(int clipId) const 4727 { 4728 Q_ASSERT(m_allClips.count(clipId) > 0); 4729 return m_allClips.at(clipId); 4730 } 4731 4732 QVariantList TimelineModel::addClipEffect(int clipId, const QString &effectId, bool notify) 4733 { 4734 Q_ASSERT(m_allClips.count(clipId) > 0); 4735 bool result = false; 4736 QVariantList affectedClips; 4737 std::unordered_set<int> items; 4738 if (m_singleSelectionMode && m_currentSelection.count(clipId)) { 4739 // only operate on the selected item(s) 4740 items = m_currentSelection; 4741 } else if (m_groups->isInGroup(clipId)) { 4742 int parentGroup = m_groups->getRootId(clipId); 4743 if (parentGroup > -1) { 4744 items = m_groups->getLeaves(parentGroup); 4745 } 4746 } else { 4747 items = {clipId}; 4748 } 4749 Fun undo = []() { return true; }; 4750 Fun redo = []() { return true; }; 4751 for (auto &s : items) { 4752 if (isClip(s)) { 4753 if (m_allClips.at(s)->addEffectWithUndo(effectId, undo, redo)) { 4754 result = true; 4755 affectedClips << s; 4756 } 4757 } 4758 } 4759 if (result) { 4760 pCore->pushUndo(undo, redo, i18n("Add effect %1", EffectsRepository::get()->getName(effectId))); 4761 } else if (notify) { 4762 QString effectName = EffectsRepository::get()->getName(effectId); 4763 pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), ErrorMessage, 500); 4764 } 4765 return affectedClips; 4766 } 4767 4768 bool TimelineModel::removeFade(int clipId, bool fromStart) 4769 { 4770 Q_ASSERT(m_allClips.count(clipId) > 0); 4771 return m_allClips.at(clipId)->removeFade(fromStart); 4772 } 4773 4774 std::shared_ptr<EffectStackModel> TimelineModel::getClipEffectStack(int itemId) 4775 { 4776 Q_ASSERT(m_allClips.count(itemId)); 4777 return m_allClips.at(itemId)->m_effectStack; 4778 } 4779 4780 bool TimelineModel::adjustEffectLength(int clipId, const QString &effectId, int duration, int initialDuration) 4781 { 4782 Q_ASSERT(m_allClips.count(clipId)); 4783 Fun undo = []() { return true; }; 4784 Fun redo = []() { return true; }; 4785 bool res = m_allClips.at(clipId)->adjustEffectLength(effectId, duration, initialDuration, undo, redo); 4786 if (res && initialDuration > 0) { 4787 PUSH_UNDO(undo, redo, i18n("Adjust Fade")); 4788 } 4789 return res; 4790 } 4791 4792 std::shared_ptr<CompositionModel> TimelineModel::getCompositionPtr(int compoId) const 4793 { 4794 Q_ASSERT(m_allCompositions.count(compoId) > 0); 4795 return m_allCompositions.at(compoId); 4796 } 4797 4798 int TimelineModel::getNextId() 4799 { 4800 return KdenliveDoc::next_id++; 4801 } 4802 4803 bool TimelineModel::isClip(int id) const 4804 { 4805 return m_allClips.count(id) > 0; 4806 } 4807 4808 bool TimelineModel::isComposition(int id) const 4809 { 4810 return m_allCompositions.count(id) > 0; 4811 } 4812 4813 bool TimelineModel::isSubTitle(int id) const 4814 { 4815 return m_allSubtitles.count(id) > 0; 4816 } 4817 4818 bool TimelineModel::isItem(int id) const 4819 { 4820 return isClip(id) || isComposition(id) || isSubTitle(id); 4821 } 4822 4823 bool TimelineModel::isTrack(int id) const 4824 { 4825 return m_iteratorTable.count(id) > 0; 4826 } 4827 4828 bool TimelineModel::isGroup(int id) const 4829 { 4830 return m_allGroups.count(id) > 0; 4831 } 4832 4833 bool TimelineModel::isInGroup(int id) const 4834 { 4835 return m_groups->isInGroup(id); 4836 } 4837 4838 void TimelineModel::updateDuration() 4839 { 4840 if (m_closing) { 4841 return; 4842 } 4843 int current = m_blackClip->get_playtime() - TimelineModel::seekDuration - 1; 4844 int duration = 0; 4845 for (const auto &tck : m_iteratorTable) { 4846 auto track = (*tck.second); 4847 if (track->isAudioTrack()) { 4848 if (track->isMute()) { 4849 continue; 4850 } 4851 } else if (track->isHidden()) { 4852 continue; 4853 } 4854 duration = qMax(duration, track->trackDuration()); 4855 } 4856 if (m_subtitleModel) { 4857 duration = qMax(duration, m_subtitleModel->trackDuration()); 4858 } 4859 if (duration != current) { 4860 // update black track length 4861 std::unique_ptr<Mlt::Field> field(m_tractor->field()); 4862 field->lock(); 4863 m_blackClip->set("out", duration + TimelineModel::seekDuration); 4864 field->unlock(); 4865 Q_EMIT durationUpdated(m_uuid); 4866 if (m_masterStack) { 4867 Q_EMIT m_masterStack->dataChanged(QModelIndex(), QModelIndex(), {}); 4868 } 4869 } 4870 } 4871 4872 int TimelineModel::duration() const 4873 { 4874 int duration = 0; 4875 auto it = m_allTracks.cbegin(); 4876 while (it != m_allTracks.cend()) { 4877 if ((*it)->isAudioTrack()) { 4878 if ((*it)->isMute()) { 4879 // Muted audio track 4880 ++it; 4881 continue; 4882 } 4883 } else if ((*it)->isHidden()) { 4884 // Hidden video track 4885 ++it; 4886 continue; 4887 } 4888 int trackDuration = (*it)->getTrackService()->get_playtime(); 4889 duration = qMax(duration, trackDuration); 4890 ++it; 4891 } 4892 if (m_subtitleModel && !m_subtitleModel->isDisabled()) { 4893 duration = qMax(duration, m_subtitleModel->trackDuration()); 4894 } 4895 return duration; 4896 } 4897 4898 std::unordered_set<int> TimelineModel::getGroupElements(int clipId) 4899 { 4900 int groupId = m_groups->getRootId(clipId); 4901 return m_groups->getLeaves(groupId); 4902 } 4903 4904 bool TimelineModel::requestReset(Fun &undo, Fun &redo) 4905 { 4906 std::vector<int> all_ids; 4907 for (const auto &track : m_iteratorTable) { 4908 all_ids.push_back(track.first); 4909 } 4910 bool ok = true; 4911 for (int trackId : all_ids) { 4912 ok = ok && requestTrackDeletion(trackId, undo, redo); 4913 } 4914 return ok; 4915 } 4916 4917 void TimelineModel::setUndoStack(std::weak_ptr<DocUndoStack> undo_stack) 4918 { 4919 m_undoStack = std::move(undo_stack); 4920 } 4921 4922 int TimelineModel::suggestSnapPoint(int pos, int snapDistance) 4923 { 4924 int cursorPosition = pCore->getMonitorPosition(); 4925 m_snaps->addPoint(cursorPosition); 4926 int snapped = m_snaps->getClosestPoint(pos); 4927 m_snaps->removePoint(cursorPosition); 4928 return (qAbs(snapped - pos) < snapDistance ? snapped : pos); 4929 } 4930 4931 int TimelineModel::getBestSnapPos(int referencePos, int diff, std::vector<int> pts, int cursorPosition, int snapDistance) 4932 { 4933 if (!pts.empty()) { 4934 if (m_editMode == TimelineMode::NormalEdit) { 4935 m_snaps->ignore(pts); 4936 } 4937 } else { 4938 return -1; 4939 } 4940 // Sort and remove duplicates 4941 std::sort(pts.begin(), pts.end()); 4942 pts.erase(std::unique(pts.begin(), pts.end()), pts.end()); 4943 m_snaps->addPoint(cursorPosition); 4944 int closest = -1; 4945 int lowestDiff = snapDistance + 1; 4946 for (int point : pts) { 4947 int snapped = m_snaps->getClosestPoint(point + diff); 4948 int currentDiff = qAbs(point + diff - snapped); 4949 if (currentDiff < lowestDiff) { 4950 lowestDiff = currentDiff; 4951 closest = snapped - (point - referencePos); 4952 if (lowestDiff < 2) { 4953 break; 4954 } 4955 } 4956 } 4957 if (m_editMode == TimelineMode::NormalEdit) { 4958 m_snaps->unIgnore(); 4959 } 4960 m_snaps->removePoint(cursorPosition); 4961 return closest; 4962 } 4963 4964 int TimelineModel::getNextSnapPos(int pos, std::vector<int> &snaps, std::vector<int> &ignored) 4965 { 4966 QVector<int> tracks; 4967 // Get active tracks 4968 auto it = m_allTracks.cbegin(); 4969 while (it != m_allTracks.cend()) { 4970 if ((*it)->shouldReceiveTimelineOp()) { 4971 tracks << (*it)->getId(); 4972 } 4973 ++it; 4974 } 4975 bool hasSubtitles = m_subtitleModel && !m_allSubtitles.empty(); 4976 bool filterOutSubtitles = false; 4977 if (hasSubtitles) { 4978 // If subtitle track is locked or hidden, don't snap to it 4979 if (m_subtitleModel->isLocked() || !KdenliveSettings::showSubtitles()) { 4980 filterOutSubtitles = true; 4981 } 4982 } 4983 if ((tracks.isEmpty() || tracks.count() == int(m_allTracks.size())) && !filterOutSubtitles) { 4984 // No active track, use all possible snap points 4985 m_snaps->ignore(ignored); 4986 int next = m_snaps->getNextPoint(pos); 4987 m_snaps->unIgnore(); 4988 return next; 4989 } 4990 for (auto num : ignored) { 4991 snaps.erase(std::remove(snaps.begin(), snaps.end(), num), snaps.end()); 4992 } 4993 // Build snap points for selected tracks 4994 for (const auto &cp : m_allClips) { 4995 // Check if clip is on a target track 4996 if (tracks.contains(cp.second->getCurrentTrackId())) { 4997 auto clip = (cp.second); 4998 clip->allSnaps(snaps); 4999 } 5000 } 5001 // Subtitle snaps 5002 if (hasSubtitles && !filterOutSubtitles) { 5003 // Add subtitle snaps 5004 m_subtitleModel->allSnaps(snaps); 5005 } 5006 // sort snaps 5007 std::sort(snaps.begin(), snaps.end()); 5008 for (auto i : snaps) { 5009 if (int(i) > pos) { 5010 return int(i); 5011 } 5012 } 5013 return pos; 5014 } 5015 5016 int TimelineModel::getPreviousSnapPos(int pos, std::vector<int> &snaps, std::vector<int> &ignored) 5017 { 5018 QVector<int> tracks; 5019 // Get active tracks 5020 auto it = m_allTracks.cbegin(); 5021 while (it != m_allTracks.cend()) { 5022 if ((*it)->shouldReceiveTimelineOp()) { 5023 tracks << (*it)->getId(); 5024 } 5025 ++it; 5026 } 5027 bool hasSubtitles = m_subtitleModel && !m_allSubtitles.empty(); 5028 bool filterOutSubtitles = false; 5029 if (hasSubtitles) { 5030 // If subtitle track is locked or hidden, don't snap to it 5031 if (m_subtitleModel->isLocked() || !KdenliveSettings::showSubtitles()) { 5032 filterOutSubtitles = true; 5033 } 5034 } 5035 if ((tracks.isEmpty() || tracks.count() == int(m_allTracks.size())) && !filterOutSubtitles) { 5036 // No active track, use all possible snap points 5037 m_snaps->ignore(ignored); 5038 int previous = m_snaps->getPreviousPoint(pos); 5039 m_snaps->unIgnore(); 5040 return previous; 5041 } 5042 // Build snap points for selected tracks 5043 for (auto num : ignored) { 5044 snaps.erase(std::remove(snaps.begin(), snaps.end(), num), snaps.end()); 5045 } 5046 for (const auto &cp : m_allClips) { 5047 // Check if clip is on a target track 5048 if (tracks.contains(cp.second->getCurrentTrackId())) { 5049 auto clip = (cp.second); 5050 clip->allSnaps(snaps); 5051 } 5052 } 5053 // Subtitle snaps 5054 if (hasSubtitles && !filterOutSubtitles) { 5055 // Add subtitle snaps 5056 m_subtitleModel->allSnaps(snaps); 5057 } 5058 // sort snaps 5059 std::sort(snaps.begin(), snaps.end()); 5060 // sort descending 5061 std::reverse(snaps.begin(), snaps.end()); 5062 for (auto i : snaps) { 5063 if (int(i) < pos) { 5064 return int(i); 5065 } 5066 } 5067 return 0; 5068 } 5069 5070 void TimelineModel::addSnap(int pos) 5071 { 5072 TRACE(pos); 5073 return m_snaps->addPoint(pos); 5074 } 5075 5076 void TimelineModel::removeSnap(int pos) 5077 { 5078 TRACE(pos); 5079 return m_snaps->removePoint(pos); 5080 } 5081 5082 void TimelineModel::registerComposition(const std::shared_ptr<CompositionModel> &composition) 5083 { 5084 int id = composition->getId(); 5085 Q_ASSERT(m_allCompositions.count(id) == 0); 5086 m_allCompositions[id] = composition; 5087 m_groups->createGroupItem(id); 5088 } 5089 5090 bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int position, int length, std::unique_ptr<Mlt::Properties> transProps, 5091 int &id, bool logUndo) 5092 { 5093 QWriteLocker locker(&m_lock); 5094 // TRACE(transitionId, trackId, position, length, transProps.get(), id, logUndo); 5095 Fun undo = []() { return true; }; 5096 Fun redo = []() { return true; }; 5097 bool result = requestCompositionInsertion(transitionId, trackId, -1, position, length, std::move(transProps), id, undo, redo, logUndo); 5098 if (result && logUndo) { 5099 PUSH_UNDO(undo, redo, i18n("Insert Composition")); 5100 } 5101 // TRACE_RES(result); 5102 return result; 5103 } 5104 5105 bool TimelineModel::requestCompositionCreation(const QString &transitionId, int length, std::unique_ptr<Mlt::Properties> transProps, int &id, Fun &undo, 5106 Fun &redo, bool finalMove, const QString &originalDecimalPoint) 5107 { 5108 Q_UNUSED(finalMove) 5109 int compositionId = TimelineModel::getNextId(); 5110 id = compositionId; 5111 Fun local_undo = deregisterComposition_lambda(compositionId); 5112 CompositionModel::construct(shared_from_this(), transitionId, originalDecimalPoint, length, compositionId, std::move(transProps)); 5113 auto composition = m_allCompositions[compositionId]; 5114 Fun local_redo = [composition, this]() { 5115 // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it 5116 // back it is sufficient to register it. 5117 registerComposition(composition); 5118 return true; 5119 }; 5120 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 5121 return true; 5122 } 5123 5124 bool TimelineModel::requestCompositionInsertion(const QString &transitionId, int trackId, int compositionTrack, int position, int length, 5125 std::unique_ptr<Mlt::Properties> transProps, int &id, Fun &undo, Fun &redo, bool finalMove, 5126 const QString &originalDecimalPoint) 5127 { 5128 int compositionId = TimelineModel::getNextId(); 5129 id = compositionId; 5130 Fun local_undo = deregisterComposition_lambda(compositionId); 5131 CompositionModel::construct(shared_from_this(), transitionId, originalDecimalPoint, length, compositionId, std::move(transProps)); 5132 auto composition = m_allCompositions[compositionId]; 5133 Fun local_redo = [composition, this]() { 5134 // We capture a shared_ptr to the composition, which means that as long as this undo object lives, the composition object is not deleted. To insert it 5135 // back it is sufficient to register it. 5136 registerComposition(composition); 5137 return true; 5138 }; 5139 bool res = requestCompositionMove(compositionId, trackId, compositionTrack, position, true, finalMove, local_undo, local_redo); 5140 if (res) { 5141 res = requestItemResize(compositionId, length, true, true, local_undo, local_redo, true); 5142 } 5143 if (!res) { 5144 bool undone = local_undo(); 5145 Q_ASSERT(undone); 5146 id = -1; 5147 return false; 5148 } 5149 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 5150 return true; 5151 } 5152 5153 Fun TimelineModel::deregisterComposition_lambda(int compoId) 5154 { 5155 return [this, compoId]() { 5156 Q_ASSERT(m_allCompositions.count(compoId) > 0); 5157 Q_ASSERT(!m_groups->isInGroup(compoId)); // composition must be ungrouped at this point 5158 requestClearSelection(true); 5159 Q_EMIT requestClearAssetView(compoId); 5160 m_allCompositions.erase(compoId); 5161 m_groups->destructGroupItem(compoId); 5162 return true; 5163 }; 5164 } 5165 5166 int TimelineModel::getSubtitlePosition(int subId) const 5167 { 5168 Q_ASSERT(m_allSubtitles.count(subId) > 0); 5169 return m_allSubtitles.at(subId).frames(pCore->getCurrentFps()); 5170 } 5171 5172 int TimelineModel::getCompositionPosition(int compoId) const 5173 { 5174 Q_ASSERT(m_allCompositions.count(compoId) > 0); 5175 const auto trans = m_allCompositions.at(compoId); 5176 return trans->getPosition(); 5177 } 5178 5179 int TimelineModel::getCompositionEnd(int compoId) const 5180 { 5181 Q_ASSERT(m_allCompositions.count(compoId) > 0); 5182 const auto trans = m_allCompositions.at(compoId); 5183 return trans->getPosition() + trans->getPlaytime(); 5184 } 5185 5186 int TimelineModel::getCompositionPlaytime(int compoId) const 5187 { 5188 READ_LOCK(); 5189 Q_ASSERT(m_allCompositions.count(compoId) > 0); 5190 const auto trans = m_allCompositions.at(compoId); 5191 int playtime = trans->getPlaytime(); 5192 return playtime; 5193 } 5194 5195 int TimelineModel::getItemPosition(int itemId) const 5196 { 5197 if (isClip(itemId)) { 5198 return getClipPosition(itemId); 5199 } 5200 if (isComposition(itemId)) { 5201 return getCompositionPosition(itemId); 5202 } 5203 if (isSubTitle(itemId)) { 5204 return getSubtitlePosition(itemId); 5205 } 5206 return -1; 5207 } 5208 5209 int TimelineModel::getClipSubPlaylistIndex(int cid) const 5210 { 5211 Q_ASSERT(isClip(cid)); 5212 return m_allClips.at(cid)->getSubPlaylistIndex(); 5213 } 5214 5215 const QString TimelineModel::getClipName(int cid) const 5216 { 5217 Q_ASSERT(isClip(cid)); 5218 return m_allClips.at(cid)->clipName(); 5219 } 5220 5221 int TimelineModel::getItemEnd(int itemId) const 5222 { 5223 if (isClip(itemId)) { 5224 return getClipEnd(itemId); 5225 } 5226 if (isComposition(itemId)) { 5227 return getCompositionEnd(itemId); 5228 } 5229 if (isSubTitle(itemId)) { 5230 return m_subtitleModel->getSubtitleEnd(itemId); 5231 } 5232 return -1; 5233 } 5234 5235 int TimelineModel::getItemPlaytime(int itemId) const 5236 { 5237 if (isClip(itemId)) { 5238 return getClipPlaytime(itemId); 5239 } 5240 if (isComposition(itemId)) { 5241 return getCompositionPlaytime(itemId); 5242 } 5243 if (isSubTitle(itemId)) { 5244 return m_subtitleModel->getSubtitlePlaytime(itemId); 5245 } 5246 return -1; 5247 } 5248 5249 int TimelineModel::getTrackCompositionsCount(int trackId) const 5250 { 5251 Q_ASSERT(isTrack(trackId)); 5252 return getTrackById_const(trackId)->getCompositionsCount(); 5253 } 5254 5255 bool TimelineModel::requestCompositionMove(int compoId, int trackId, int position, bool updateView, bool logUndo) 5256 { 5257 QWriteLocker locker(&m_lock); 5258 Q_ASSERT(isComposition(compoId)); 5259 if (m_allCompositions[compoId]->getPosition() == position && getCompositionTrackId(compoId) == trackId) { 5260 return true; 5261 } 5262 if (m_groups->isInGroup(compoId)) { 5263 // element is in a group. 5264 int groupId = m_groups->getRootId(compoId); 5265 int current_trackId = getCompositionTrackId(compoId); 5266 int track_pos1 = getTrackPosition(trackId); 5267 int track_pos2 = getTrackPosition(current_trackId); 5268 int delta_track = track_pos1 - track_pos2; 5269 int delta_pos = position - m_allCompositions[compoId]->getPosition(); 5270 return requestGroupMove(compoId, groupId, delta_track, delta_pos, true, updateView, logUndo); 5271 } 5272 std::function<bool(void)> undo = []() { return true; }; 5273 std::function<bool(void)> redo = []() { return true; }; 5274 int min = getCompositionPosition(compoId); 5275 int max = min + getCompositionPlaytime(compoId); 5276 int tk = getCompositionTrackId(compoId); 5277 bool res = requestCompositionMove(compoId, trackId, m_allCompositions[compoId]->getForcedTrack(), position, updateView, logUndo, undo, redo); 5278 if (tk > -1) { 5279 min = qMin(min, getCompositionPosition(compoId)); 5280 max = qMax(max, getCompositionPosition(compoId)); 5281 } else { 5282 min = getCompositionPosition(compoId); 5283 max = min + getCompositionPlaytime(compoId); 5284 } 5285 5286 if (res && logUndo) { 5287 PUSH_UNDO(undo, redo, i18n("Move composition")); 5288 checkRefresh(min, max); 5289 } 5290 return res; 5291 } 5292 5293 bool TimelineModel::isAudioTrack(int trackId) const 5294 { 5295 READ_LOCK(); 5296 Q_ASSERT(isTrack(trackId)); 5297 auto it = m_iteratorTable.at(trackId); 5298 return (*it)->isAudioTrack(); 5299 } 5300 5301 bool TimelineModel::isSubtitleTrack(int trackId) const 5302 { 5303 return trackId == -2; 5304 } 5305 5306 bool TimelineModel::requestCompositionMove(int compoId, int trackId, int compositionTrack, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo) 5307 { 5308 QWriteLocker locker(&m_lock); 5309 Q_ASSERT(isComposition(compoId)); 5310 Q_ASSERT(isTrack(trackId)); 5311 if (compositionTrack == -1 || (compositionTrack > 0 && trackId == getTrackIndexFromPosition(compositionTrack - 1))) { 5312 compositionTrack = getPreviousVideoTrackPos(trackId); 5313 } 5314 if (compositionTrack == -1) { 5315 // it doesn't make sense to insert a composition on the last track 5316 qDebug() << "Move failed because of last track"; 5317 return false; 5318 } 5319 5320 Fun local_undo = []() { return true; }; 5321 Fun local_redo = []() { return true; }; 5322 bool ok = true; 5323 int old_trackId = getCompositionTrackId(compoId); 5324 bool notifyViewOnly = false; 5325 Fun update_model = []() { return true; }; 5326 if (updateView && old_trackId == trackId) { 5327 // Move on same track, only send view update 5328 updateView = false; 5329 notifyViewOnly = true; 5330 update_model = [compoId, this]() { 5331 QModelIndex modelIndex = makeCompositionIndexFromID(compoId); 5332 notifyChange(modelIndex, modelIndex, StartRole); 5333 return true; 5334 }; 5335 } 5336 if (old_trackId != -1) { 5337 Fun delete_operation = []() { return true; }; 5338 Fun delete_reverse = []() { return true; }; 5339 if (old_trackId != trackId) { 5340 delete_operation = [this, compoId]() { 5341 bool res = unplantComposition(compoId); 5342 if (res) m_allCompositions[compoId]->setATrack(-1, -1); 5343 return res; 5344 }; 5345 int oldAtrack = m_allCompositions[compoId]->getATrack(); 5346 delete_reverse = [this, compoId, oldAtrack, updateView]() { 5347 m_allCompositions[compoId]->setATrack(oldAtrack, oldAtrack < 1 ? -1 : getTrackIndexFromPosition(oldAtrack - 1)); 5348 return replantCompositions(compoId, updateView); 5349 }; 5350 } 5351 ok = delete_operation(); 5352 if (!ok) qDebug() << "Move failed because of first delete operation"; 5353 5354 if (ok) { 5355 if (notifyViewOnly) { 5356 PUSH_LAMBDA(update_model, local_undo); 5357 } 5358 UPDATE_UNDO_REDO(delete_operation, delete_reverse, local_undo, local_redo); 5359 ok = getTrackById(old_trackId)->requestCompositionDeletion(compoId, updateView, finalMove, local_undo, local_redo, false); 5360 } 5361 if (!ok) { 5362 qDebug() << "Move failed because of first deletion request"; 5363 bool undone = local_undo(); 5364 Q_ASSERT(undone); 5365 return false; 5366 } 5367 } 5368 ok = getTrackById(trackId)->requestCompositionInsertion(compoId, position, updateView, finalMove, local_undo, local_redo); 5369 if (!ok) qDebug() << "Move failed because of second insertion request"; 5370 if (ok) { 5371 Fun insert_operation = []() { return true; }; 5372 Fun insert_reverse = []() { return true; }; 5373 if (old_trackId != trackId) { 5374 insert_operation = [this, compoId, compositionTrack, updateView]() { 5375 m_allCompositions[compoId]->setATrack(compositionTrack, compositionTrack < 1 ? -1 : getTrackIndexFromPosition(compositionTrack - 1)); 5376 return replantCompositions(compoId, updateView); 5377 }; 5378 insert_reverse = [this, compoId]() { 5379 bool res = unplantComposition(compoId); 5380 if (res) m_allCompositions[compoId]->setATrack(-1, -1); 5381 return res; 5382 }; 5383 } 5384 ok = insert_operation(); 5385 if (!ok) qDebug() << "Move failed because of second insert operation"; 5386 if (ok) { 5387 if (notifyViewOnly) { 5388 PUSH_LAMBDA(update_model, local_redo); 5389 } 5390 UPDATE_UNDO_REDO(insert_operation, insert_reverse, local_undo, local_redo); 5391 } 5392 } 5393 if (!ok) { 5394 bool undone = local_undo(); 5395 Q_ASSERT(undone); 5396 return false; 5397 } 5398 update_model(); 5399 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 5400 return true; 5401 } 5402 5403 bool TimelineModel::replantCompositions(int currentCompo, bool updateView) 5404 { 5405 // We ensure that the compositions are planted in a decreasing order of a_track, and increasing order of b_track. 5406 // For that, there is no better option than to disconnect every composition and then reinsert everything in the correct order. 5407 std::vector<std::pair<int, int>> compos; 5408 for (const auto &compo : m_allCompositions) { 5409 int trackId = compo.second->getCurrentTrackId(); 5410 if (trackId == -1 || compo.second->getATrack() == -1) { 5411 continue; 5412 } 5413 // Note: we need to retrieve the position of the track, that is its melt index. 5414 int trackPos = getTrackMltIndex(trackId); 5415 compos.emplace_back(trackPos, compo.first); 5416 if (compo.first != currentCompo) { 5417 unplantComposition(compo.first); 5418 } 5419 } 5420 // sort by decreasing b_track 5421 std::sort(compos.begin(), compos.end(), [&](const std::pair<int, int> &a, const std::pair<int, int> &b) { 5422 if (m_allCompositions[a.second]->getATrack() == m_allCompositions[b.second]->getATrack()) { 5423 return a.first < b.first; 5424 } 5425 return m_allCompositions[a.second]->getATrack() > m_allCompositions[b.second]->getATrack(); 5426 }); 5427 // replant 5428 QScopedPointer<Mlt::Field> field(m_tractor->field()); 5429 field->lock(); 5430 5431 // Unplant track compositing 5432 mlt_service nextservice = mlt_service_get_producer(field->get_service()); 5433 mlt_properties properties = MLT_SERVICE_PROPERTIES(nextservice); 5434 QString resource = mlt_properties_get(properties, "mlt_service"); 5435 5436 mlt_service_type mlt_type = mlt_service_identify(nextservice); 5437 QList<Mlt::Transition *> trackCompositions; 5438 while (mlt_type == mlt_service_transition_type) { 5439 Mlt::Transition transition(reinterpret_cast<mlt_transition>(nextservice)); 5440 nextservice = mlt_service_producer(nextservice); 5441 int internal = transition.get_int("internal_added"); 5442 if (internal > 0 && resource != QLatin1String("mix")) { 5443 trackCompositions << new Mlt::Transition(transition); 5444 field->disconnect_service(transition); 5445 transition.disconnect_all_producers(); 5446 } 5447 if (nextservice == nullptr) { 5448 break; 5449 } 5450 mlt_type = mlt_service_identify(nextservice); 5451 properties = MLT_SERVICE_PROPERTIES(nextservice); 5452 resource = mlt_properties_get(properties, "mlt_service"); 5453 } 5454 // Sort track compositing 5455 std::sort(trackCompositions.begin(), trackCompositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); }); 5456 5457 for (const auto &compo : compos) { 5458 int aTrack = m_allCompositions[compo.second]->getATrack(); 5459 Q_ASSERT(aTrack != -1 && aTrack < m_tractor->count()); 5460 5461 Mlt::Transition &transition = *m_allCompositions[compo.second].get(); 5462 transition.set_tracks(aTrack, compo.first); 5463 int ret = field->plant_transition(transition, aTrack, compo.first); 5464 5465 mlt_service consumer = mlt_service_consumer(transition.get_service()); 5466 Q_ASSERT(consumer != nullptr); 5467 if (ret != 0) { 5468 field->unlock(); 5469 return false; 5470 } 5471 } 5472 // Replant last tracks compositing 5473 while (!trackCompositions.isEmpty()) { 5474 Mlt::Transition *firstTr = trackCompositions.takeFirst(); 5475 field->plant_transition(*firstTr, firstTr->get_a_track(), firstTr->get_b_track()); 5476 } 5477 field->unlock(); 5478 if (updateView) { 5479 QModelIndex modelIndex = makeCompositionIndexFromID(currentCompo); 5480 notifyChange(modelIndex, modelIndex, ItemATrack); 5481 } 5482 return true; 5483 } 5484 5485 bool TimelineModel::unplantComposition(int compoId) 5486 { 5487 Mlt::Transition &transition = *m_allCompositions[compoId].get(); 5488 mlt_service consumer = mlt_service_consumer(transition.get_service()); 5489 Q_ASSERT(consumer != nullptr); 5490 QScopedPointer<Mlt::Field> field(m_tractor->field()); 5491 field->lock(); 5492 field->disconnect_service(transition); 5493 int ret = transition.disconnect_all_producers(); 5494 5495 mlt_service nextservice = mlt_service_get_producer(transition.get_service()); 5496 // mlt_service consumer = mlt_service_consumer(transition.get_service()); 5497 Q_ASSERT(nextservice == nullptr); 5498 // Q_ASSERT(consumer == nullptr); 5499 field->unlock(); 5500 return ret != 0; 5501 } 5502 5503 bool TimelineModel::checkConsistency(const std::vector<int> &guideSnaps) 5504 { 5505 // We store all in/outs of clips to check snap points 5506 std::map<int, int> snaps; 5507 for (const auto &tck : m_iteratorTable) { 5508 auto track = (*tck.second); 5509 // Check parent/children link for tracks 5510 if (auto ptr = track->m_parent.lock()) { 5511 if (ptr.get() != this) { 5512 qWarning() << "Wrong parent for track" << tck.first; 5513 return false; 5514 } 5515 } else { 5516 qWarning() << "NULL parent for track" << tck.first; 5517 return false; 5518 } 5519 // check consistency of track 5520 if (!track->checkConsistency()) { 5521 qWarning() << "Consistency check failed for track" << tck.first; 5522 return false; 5523 } 5524 } 5525 // Check parent/children link for clips 5526 for (const auto &cp : m_allClips) { 5527 auto clip = (cp.second); 5528 // Check parent/children link for tracks 5529 if (auto ptr = clip->m_parent.lock()) { 5530 if (ptr.get() != this) { 5531 qWarning() << "Wrong parent for clip" << cp.first; 5532 return false; 5533 } 5534 } else { 5535 qWarning() << "NULL parent for clip" << cp.first; 5536 return false; 5537 } 5538 if (getClipTrackId(cp.first) != -1) { 5539 snaps[clip->getPosition()] += 1; 5540 snaps[clip->getPosition() + clip->getPlaytime()] += 1; 5541 if (clip->getMixDuration() > 0) { 5542 snaps[clip->getPosition() + clip->getMixDuration() - clip->getMixCutPosition()] += 1; 5543 } 5544 } 5545 if (!clip->checkConsistency()) { 5546 qWarning() << "Consistency check failed for clip" << cp.first; 5547 return false; 5548 } 5549 } 5550 for (const auto &cp : m_allCompositions) { 5551 auto clip = (cp.second); 5552 // Check parent/children link for tracks 5553 if (auto ptr = clip->m_parent.lock()) { 5554 if (ptr.get() != this) { 5555 qWarning() << "Wrong parent for compo" << cp.first; 5556 return false; 5557 } 5558 } else { 5559 qWarning() << "NULL parent for compo" << cp.first; 5560 return false; 5561 } 5562 if (getCompositionTrackId(cp.first) != -1) { 5563 snaps[clip->getPosition()] += 1; 5564 snaps[clip->getPosition() + clip->getPlaytime()] += 1; 5565 } 5566 } 5567 for (auto p : guideSnaps) { 5568 snaps[p] += 1; 5569 } 5570 5571 // Check snaps 5572 auto stored_snaps = m_snaps->_snaps(); 5573 if (snaps.size() != stored_snaps.size()) { 5574 qWarning() << "Wrong number of snaps" << snaps.size() << stored_snaps.size(); 5575 return false; 5576 } 5577 for (auto i = snaps.begin(), j = stored_snaps.begin(); i != snaps.end(); ++i, ++j) { 5578 if (*i != *j) { 5579 qWarning() << "Wrong snap info at point" << (*i).first; 5580 return false; 5581 } 5582 } 5583 5584 // We check consistency with bin model 5585 auto binClips = pCore->projectItemModel()->getAllClipIds(); 5586 // First step: all clips referenced by the bin model exist and are inserted 5587 for (const auto &binClip : binClips) { 5588 auto projClip = pCore->projectItemModel()->getClipByBinID(binClip); 5589 QList<int> referenced = projClip->m_registeredClipsByUuid.value(uuid()); 5590 for (int cid : referenced) { 5591 if (!isClip(cid)) { 5592 qWarning() << "Bin model registers a bad clip ID" << cid; 5593 qDebug() << ":::: GOT REF CLIPS FOR UUID: " << referenced; 5594 for (const auto &clip : m_allClips) { 5595 qWarning() << "Existing cids:" << clip.first; 5596 } 5597 return false; 5598 } 5599 } 5600 } 5601 5602 // Second step: all clips are referenced 5603 for (const auto &clip : m_allClips) { 5604 auto binId = clip.second->m_binClipId; 5605 auto projClip = pCore->projectItemModel()->getClipByBinID(binId); 5606 if (!projClip->m_registeredClipsByUuid.contains(uuid()) || !projClip->m_registeredClipsByUuid.value(uuid()).contains(clip.first)) { 5607 qWarning() << "Clip " << clip.first << "not registered in bin"; 5608 return false; 5609 } 5610 } 5611 5612 // We now check consistency of the compositions. For that, we list all compositions of the tractor, and see if we have a matching one in our 5613 // m_allCompositions 5614 std::unordered_set<int> remaining_compo; 5615 for (const auto &compo : m_allCompositions) { 5616 if (getCompositionTrackId(compo.first) != -1 && m_allCompositions[compo.first]->getATrack() != -1) { 5617 remaining_compo.insert(compo.first); 5618 5619 // check validity of the consumer 5620 Mlt::Transition &transition = *m_allCompositions[compo.first].get(); 5621 mlt_service consumer = mlt_service_consumer(transition.get_service()); 5622 Q_ASSERT(consumer != nullptr); 5623 } 5624 } 5625 QScopedPointer<Mlt::Field> field(m_tractor->field()); 5626 field->lock(); 5627 5628 mlt_service nextservice = mlt_service_get_producer(field->get_service()); 5629 mlt_service_type mlt_type = mlt_service_identify(nextservice); 5630 while (nextservice != nullptr) { 5631 if (mlt_type == mlt_service_transition_type) { 5632 auto tr = mlt_transition(nextservice); 5633 if (mlt_properties_get_int(MLT_TRANSITION_PROPERTIES(tr), "internal_added") > 0) { 5634 // Skip track compositing 5635 nextservice = mlt_service_producer(nextservice); 5636 continue; 5637 } 5638 int currentTrack = mlt_transition_get_b_track(tr); 5639 int currentATrack = mlt_transition_get_a_track(tr); 5640 if (currentTrack == currentATrack) { 5641 // Skip invalid transitions created by MLT on track deletion 5642 nextservice = mlt_service_producer(nextservice); 5643 continue; 5644 } 5645 5646 int currentIn = mlt_transition_get_in(tr); 5647 int currentOut = mlt_transition_get_out(tr); 5648 5649 int foundId = -1; 5650 // we iterate to try to find a matching compo 5651 for (int compoId : remaining_compo) { 5652 if (getTrackMltIndex(getCompositionTrackId(compoId)) == currentTrack && m_allCompositions[compoId]->getATrack() == currentATrack && 5653 m_allCompositions[compoId]->getIn() == currentIn && m_allCompositions[compoId]->getOut() == currentOut) { 5654 foundId = compoId; 5655 break; 5656 } 5657 } 5658 if (foundId == -1) { 5659 qWarning() << "No matching composition IN: " << currentIn << ", OUT: " << currentOut << ", TRACK: " << currentTrack << " / " << currentATrack 5660 << ", SERVICE: " << mlt_properties_get(MLT_TRANSITION_PROPERTIES(tr), "mlt_service") 5661 << "\nID: " << mlt_properties_get(MLT_TRANSITION_PROPERTIES(tr), "id"); 5662 field->unlock(); 5663 return false; 5664 } 5665 remaining_compo.erase(foundId); 5666 } 5667 nextservice = mlt_service_producer(nextservice); 5668 if (nextservice == nullptr) { 5669 break; 5670 } 5671 mlt_type = mlt_service_identify(nextservice); 5672 } 5673 field->unlock(); 5674 5675 if (!remaining_compo.empty()) { 5676 qWarning() << "Compositions have not been found:"; 5677 for (int compoId : remaining_compo) { 5678 qWarning() << compoId; 5679 } 5680 return false; 5681 } 5682 5683 // We check consistency of groups 5684 if (!m_groups->checkConsistency(true, true)) { 5685 qWarning() << "error in group consistency"; 5686 return false; 5687 } 5688 5689 // Check that the selection is in a valid state: 5690 if (m_currentSelection.size() == 1 && !isClip(*m_currentSelection.begin()) && !isComposition(*m_currentSelection.begin()) && 5691 !isSubTitle(*m_currentSelection.begin()) && !isGroup(*m_currentSelection.begin())) { 5692 qWarning() << "Selection is in inconsistent state"; 5693 return false; 5694 } 5695 return true; 5696 } 5697 5698 void TimelineModel::setTimelineEffectsEnabled(bool enabled) 5699 { 5700 m_timelineEffectsEnabled = enabled; 5701 // propagate info to clips 5702 for (const auto &clip : m_allClips) { 5703 clip.second->setTimelineEffectsEnabled(enabled); 5704 } 5705 5706 // TODO if we support track effects, they should be disabled here too 5707 } 5708 5709 std::shared_ptr<Mlt::Producer> TimelineModel::producer() 5710 { 5711 return std::make_shared<Mlt::Producer>(tractor()); 5712 } 5713 5714 const QString TimelineModel::sceneList(const QString &root, const QString &fullPath, const QString &filterData) 5715 { 5716 LocaleHandling::resetLocale(); 5717 QString playlist; 5718 Mlt::Consumer xmlConsumer(pCore->getProjectProfile(), "xml", fullPath.isEmpty() ? "kdenlive_playlist" : fullPath.toUtf8().constData()); 5719 if (!root.isEmpty()) { 5720 xmlConsumer.set("root", root.toUtf8().constData()); 5721 } 5722 if (!xmlConsumer.is_valid()) { 5723 return QString(); 5724 } 5725 xmlConsumer.set("store", "kdenlive"); 5726 xmlConsumer.set("time_format", "clock"); 5727 // Disabling meta creates cleaner files, but then we don't have access to metadata on the fly (meta channels, etc) 5728 // And we must use "avformat" instead of "avformat-novalidate" on project loading which causes a big delay on project opening 5729 // xmlConsumer.set("no_meta", 1); 5730 Mlt::Service s(m_tractor->get_service()); 5731 std::unique_ptr<Mlt::Filter> filter = nullptr; 5732 if (!filterData.isEmpty()) { 5733 filter = std::make_unique<Mlt::Filter>(pCore->getProjectProfile().get_profile(), QString("dynamictext:%1").arg(filterData).toUtf8().constData()); 5734 filter->set("fgcolour", "#ffffff"); 5735 filter->set("bgcolour", "#bb333333"); 5736 s.attach(*filter.get()); 5737 } 5738 xmlConsumer.connect(s); 5739 xmlConsumer.run(); 5740 if (filter) { 5741 s.detach(*filter.get()); 5742 } 5743 playlist = fullPath.isEmpty() ? QString::fromUtf8(xmlConsumer.get("kdenlive_playlist")) : fullPath; 5744 return playlist; 5745 } 5746 5747 void TimelineModel::checkRefresh(int start, int end) 5748 { 5749 if (m_blockRefresh) { 5750 return; 5751 } 5752 int currentPos = tractor()->position(); 5753 if (currentPos >= start && currentPos < end) { 5754 Q_EMIT requestMonitorRefresh(); 5755 } 5756 } 5757 5758 std::shared_ptr<AssetParameterModel> TimelineModel::getCompositionParameterModel(int compoId) const 5759 { 5760 READ_LOCK(); 5761 Q_ASSERT(isComposition(compoId)); 5762 return std::static_pointer_cast<AssetParameterModel>(m_allCompositions.at(compoId)); 5763 } 5764 5765 std::shared_ptr<EffectStackModel> TimelineModel::getClipEffectStackModel(int clipId) const 5766 { 5767 READ_LOCK(); 5768 Q_ASSERT(isClip(clipId)); 5769 return std::static_pointer_cast<EffectStackModel>(m_allClips.at(clipId)->m_effectStack); 5770 } 5771 5772 std::shared_ptr<EffectStackModel> TimelineModel::getClipMixStackModel(int clipId) const 5773 { 5774 READ_LOCK(); 5775 Q_ASSERT(isClip(clipId)); 5776 return std::static_pointer_cast<EffectStackModel>(m_allClips.at(clipId)->m_effectStack); 5777 } 5778 5779 std::shared_ptr<EffectStackModel> TimelineModel::getTrackEffectStackModel(int trackId) 5780 { 5781 READ_LOCK(); 5782 Q_ASSERT(isTrack(trackId)); 5783 return getTrackById(trackId)->m_effectStack; 5784 } 5785 5786 std::shared_ptr<EffectStackModel> TimelineModel::getMasterEffectStackModel() 5787 { 5788 READ_LOCK(); 5789 if (m_masterStack == nullptr) { 5790 m_masterService.reset(new Mlt::Service(*m_tractor.get())); 5791 m_masterStack = EffectStackModel::construct(m_masterService, ObjectId(KdenliveObjectType::Master, 0, m_uuid), m_undoStack); 5792 connect(m_masterStack.get(), &EffectStackModel::updateMasterZones, pCore.get(), &Core::updateMasterZones); 5793 } 5794 return m_masterStack; 5795 } 5796 5797 void TimelineModel::importMasterEffects(std::weak_ptr<Mlt::Service> service) 5798 { 5799 READ_LOCK(); 5800 if (m_masterStack == nullptr) { 5801 getMasterEffectStackModel(); 5802 } 5803 m_masterStack->importEffects(std::move(service), PlaylistState::Disabled, false, QString(), m_uuid); 5804 } 5805 5806 QStringList TimelineModel::extractCompositionLumas() const 5807 { 5808 QStringList urls; 5809 for (const auto &compo : m_allCompositions) { 5810 QString luma = compo.second->getProperty(QStringLiteral("resource")); 5811 if (luma.isEmpty()) { 5812 luma = compo.second->getProperty(QStringLiteral("luma")); 5813 } 5814 if (!luma.isEmpty()) { 5815 urls << QUrl::fromLocalFile(luma).toLocalFile(); 5816 } 5817 } 5818 urls.removeDuplicates(); 5819 return urls; 5820 } 5821 5822 QStringList TimelineModel::extractExternalEffectFiles() const 5823 { 5824 QStringList urls; 5825 for (const auto &clip : m_allClips) { 5826 urls << clip.second->externalFiles(); 5827 } 5828 return urls; 5829 } 5830 5831 void TimelineModel::adjustAssetRange(int clipId, int in, int out) 5832 { 5833 Q_UNUSED(clipId) 5834 Q_UNUSED(in) 5835 Q_UNUSED(out) 5836 // pCore->adjustAssetRange(clipId, in, out); 5837 } 5838 5839 bool TimelineModel::requestClipReload(int clipId, int forceDuration, Fun &local_undo, Fun &local_redo) 5840 { 5841 if (m_closing) { 5842 return false; 5843 } 5844 // in order to make the producer change effective, we need to unplant / replant the clip in its track 5845 int old_trackId = getClipTrackId(clipId); 5846 int oldPos = getClipPosition(clipId); 5847 int oldOut = getClipIn(clipId) + getClipPlaytime(clipId); 5848 int currentSubplaylist = m_allClips[clipId]->getSubPlaylistIndex(); 5849 int maxDuration = m_allClips[clipId]->getMaxDuration(); 5850 bool hasPitch = false; 5851 double speed = m_allClips[clipId]->getSpeed(); 5852 PlaylistState::ClipState state = m_allClips[clipId]->clipState(); 5853 if (!qFuzzyCompare(speed, 1.)) { 5854 hasPitch = m_allClips[clipId]->getIntProperty(QStringLiteral("warp_pitch")); 5855 } 5856 int audioStream = m_allClips[clipId]->getIntProperty(QStringLiteral("audio_index")); 5857 bool timeremap = m_allClips[clipId]->hasTimeRemap(); 5858 // Check if clip out is longer than actual producer duration (if user forced duration) 5859 std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(clipId)); 5860 bool clipIsShorter = oldOut > int(binClip->frameDuration()); 5861 bool refreshView = clipIsShorter || forceDuration > -1; 5862 if (old_trackId != -1) { 5863 if (clipIsShorter && forceDuration == -1 && binClip->hasLimitedDuration()) { 5864 // replacement clip is shorter, resize first 5865 int resizeDuration = int(binClip->frameDuration()); 5866 requestItemResize(clipId, resizeDuration, true, true, local_undo, local_redo); 5867 } 5868 getTrackById(old_trackId)->requestClipDeletion(clipId, refreshView, true, local_undo, local_redo, false, false); 5869 m_allClips[clipId]->refreshProducerFromBin(old_trackId, state, audioStream, 0, hasPitch, currentSubplaylist == 1, timeremap); 5870 if (forceDuration > -1) { 5871 m_allClips[clipId]->requestResize(forceDuration, true, local_undo, local_redo); 5872 } 5873 getTrackById(old_trackId)->requestClipInsertion(clipId, oldPos, refreshView, true, local_undo, local_redo, false, false); 5874 if (maxDuration != m_allClips[clipId]->getMaxDuration()) { 5875 QModelIndex ix = makeClipIndexFromID(clipId); 5876 Q_EMIT dataChanged(ix, ix, {TimelineModel::MaxDurationRole}); 5877 } 5878 } 5879 return clipIsShorter; 5880 } 5881 5882 void TimelineModel::replugClip(int clipId) 5883 { 5884 int old_trackId = getClipTrackId(clipId); 5885 if (old_trackId != -1) { 5886 getTrackById(old_trackId)->replugClip(clipId); 5887 } 5888 } 5889 5890 void TimelineModel::requestClipUpdate(int clipId, const QVector<int> &roles) 5891 { 5892 QModelIndex modelIndex = makeClipIndexFromID(clipId); 5893 if (roles.contains(TimelineModel::ReloadAudioThumbRole)) { 5894 m_allClips[clipId]->forceThumbReload = !m_allClips[clipId]->forceThumbReload; 5895 } 5896 if (roles.contains(TimelineModel::ResourceRole)) { 5897 int in = getClipPosition(clipId); 5898 Q_EMIT invalidateZone(in, in + getClipPlaytime(clipId)); 5899 } 5900 notifyChange(modelIndex, modelIndex, roles); 5901 } 5902 5903 bool TimelineModel::requestClipTimeWarp(int clipId, double speed, bool pitchCompensate, bool changeDuration, Fun &undo, Fun &redo) 5904 { 5905 QWriteLocker locker(&m_lock); 5906 Fun local_undo = []() { return true; }; 5907 Fun local_redo = []() { return true; }; 5908 int oldPos = getClipPosition(clipId); 5909 // in order to make the producer change effective, we need to unplant / replant the clip in its track 5910 bool success = true; 5911 int trackId = getClipTrackId(clipId); 5912 if (trackId != -1) { 5913 success = success && getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo, false, false); 5914 } 5915 if (success) { 5916 success = m_allClips[clipId]->useTimewarpProducer(speed, pitchCompensate, changeDuration, local_undo, local_redo); 5917 } 5918 if (trackId != -1) { 5919 success = success && getTrackById(trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo, false, false); 5920 } 5921 if (!success) { 5922 local_undo(); 5923 return false; 5924 } 5925 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 5926 return success; 5927 } 5928 5929 bool TimelineModel::requestClipTimeRemap(int clipId, bool enable) 5930 { 5931 if (!enable || !m_allClips[clipId]->hasTimeRemap()) { 5932 Fun undo = []() { return true; }; 5933 Fun redo = []() { return true; }; 5934 int splitId = m_groups->getSplitPartner(clipId); 5935 bool result = true; 5936 if (splitId > -1) { 5937 result = requestClipTimeRemap(splitId, enable, undo, redo); 5938 } 5939 result = result && requestClipTimeRemap(clipId, enable, undo, redo); 5940 if (result) { 5941 PUSH_UNDO(undo, redo, i18n("Enable time remap")); 5942 Q_EMIT refreshClipActions(); 5943 return true; 5944 } else { 5945 return false; 5946 } 5947 } 5948 return true; 5949 } 5950 5951 std::shared_ptr<Mlt::Producer> TimelineModel::getClipProducer(int clipId) 5952 { 5953 Q_ASSERT(m_allClips.count(clipId) > 0); 5954 return m_allClips[clipId]->getProducer(); 5955 } 5956 5957 bool TimelineModel::requestClipTimeRemap(int clipId, bool enable, Fun &undo, Fun &redo) 5958 { 5959 QWriteLocker locker(&m_lock); 5960 std::function<bool(void)> local_undo = []() { return true; }; 5961 std::function<bool(void)> local_redo = []() { return true; }; 5962 int oldPos = getClipPosition(clipId); 5963 // in order to make the producer change effective, we need to unplant / replant the clip in int track 5964 bool success = true; 5965 int trackId = getClipTrackId(clipId); 5966 int previousDuration = 0; 5967 qDebug() << "=== REQUEST REMAP: " << enable << "\n\nWWWWWWWWWWWWWWWWWWWWWWWWWWWW"; 5968 if (!enable && m_allClips[clipId]->hasTimeRemap()) { 5969 previousDuration = m_allClips[clipId]->getRemapInputDuration(); 5970 qDebug() << "==== CALCULATED INPIUT DURATION: " << previousDuration << "\n\nHHHHHHHHHHHHHH"; 5971 } 5972 if (trackId != -1) { 5973 success = success && getTrackById(trackId)->requestClipDeletion(clipId, true, true, local_undo, local_redo, false, false); 5974 } 5975 if (success) { 5976 success = m_allClips[clipId]->useTimeRemapProducer(enable, local_undo, local_redo); 5977 } 5978 if (trackId != -1) { 5979 success = success && getTrackById(trackId)->requestClipInsertion(clipId, oldPos, true, true, local_undo, local_redo, false, false); 5980 if (success && !enable && previousDuration > 0) { 5981 // Restore input duration 5982 requestItemResize(clipId, previousDuration, true, true, local_undo, local_redo); 5983 } 5984 } 5985 if (!success) { 5986 local_undo(); 5987 return false; 5988 } 5989 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 5990 return success; 5991 } 5992 5993 bool TimelineModel::requestClipTimeWarp(int clipId, double speed, bool pitchCompensate, bool changeDuration) 5994 { 5995 QWriteLocker locker(&m_lock); 5996 if (qFuzzyCompare(speed, m_allClips[clipId]->getSpeed()) && pitchCompensate == bool(m_allClips[clipId]->getIntProperty("warp_pitch"))) { 5997 return true; 5998 } 5999 TRACE(clipId, speed); 6000 Fun undo = []() { return true; }; 6001 Fun redo = []() { return true; }; 6002 // Get main clip info 6003 int trackId = getClipTrackId(clipId); 6004 bool result = true; 6005 if (trackId != -1) { 6006 // Check if clip has a split partner 6007 int splitId = m_groups->getSplitPartner(clipId); 6008 if (splitId > -1) { 6009 result = requestClipTimeWarp(splitId, speed / 100.0, pitchCompensate, changeDuration, undo, redo); 6010 } 6011 if (result) { 6012 result = requestClipTimeWarp(clipId, speed / 100.0, pitchCompensate, changeDuration, undo, redo); 6013 } 6014 if (!result) { 6015 pCore->displayMessage(i18n("Change speed failed"), ErrorMessage); 6016 undo(); 6017 TRACE_RES(false); 6018 return false; 6019 } 6020 } else { 6021 // If clip is not inserted on a track, we just change the producer 6022 result = m_allClips[clipId]->useTimewarpProducer(speed, pitchCompensate, changeDuration, undo, redo); 6023 } 6024 if (result) { 6025 PUSH_UNDO(undo, redo, i18n("Change clip speed")); 6026 } 6027 TRACE_RES(result); 6028 return result; 6029 } 6030 6031 const QString TimelineModel::getTrackTagById(int trackId) const 6032 { 6033 READ_LOCK(); 6034 Q_ASSERT(isTrack(trackId)); 6035 bool isAudio = getTrackById_const(trackId)->isAudioTrack(); 6036 int count = 1; 6037 int totalAudio = 2; 6038 auto it = m_allTracks.cbegin(); 6039 bool found = false; 6040 while ((isAudio || !found) && it != m_allTracks.cend()) { 6041 if ((*it)->isAudioTrack()) { 6042 totalAudio++; 6043 if (isAudio && !found) { 6044 count++; 6045 } 6046 } else if (!isAudio) { 6047 count++; 6048 } 6049 if ((*it)->getId() == trackId) { 6050 found = true; 6051 } 6052 it++; 6053 } 6054 return isAudio ? QStringLiteral("A%1").arg(totalAudio - count) : QStringLiteral("V%1").arg(count - 1); 6055 } 6056 6057 /*void TimelineModel::updateProfile(Mlt::Profile profile) 6058 { 6059 m_profile = profile; 6060 m_tractor->set_profile(m_profile.get_profile()); 6061 for (int i = 0; i < m_tractor->count(); i++) { 6062 std::shared_ptr<Mlt::Producer> tk(m_tractor->track(i)); 6063 tk->set_profile(m_profile.get_profile()); 6064 if (tk->type() == mlt_service_tractor_type) { 6065 Mlt::Tractor sub(*tk.get()); 6066 for (int j = 0; j < sub.count(); j++) { 6067 std::shared_ptr<Mlt::Producer> subtk(sub.track(j)); 6068 subtk->set_profile(m_profile.get_profile()); 6069 } 6070 } 6071 } 6072 m_blackClip->set_profile(m_profile.get_profile()); 6073 // Rebuild compositions since profile has changed 6074 buildTrackCompositing(true); 6075 }*/ 6076 6077 void TimelineModel::updateFieldOrderFilter(std::unique_ptr<ProfileModel> &ptr) 6078 { 6079 std::shared_ptr<Mlt::Filter> foFilter = nullptr; 6080 for (int i = 0; i < m_tractor->filter_count(); i++) { 6081 std::shared_ptr<Mlt::Filter> fl(m_tractor->filter(i)); 6082 if (!fl->is_valid()) { 6083 continue; 6084 } 6085 const QString filterService = fl->get("mlt_service"); 6086 int foundCount = 0; 6087 if (filterService == QLatin1String("avfilter.fieldorder")) { 6088 foundCount++; 6089 if ((ptr->progressive() || foundCount > 1) && fl->get_int("internal_added") == 237) { 6090 // If the profile is progressive, field order is redundant: remove 6091 // Also we only need one field order filter 6092 m_tractor->detach(*fl.get()); 6093 pCore->currentDoc()->setModified(true); 6094 } else { 6095 foFilter = fl; 6096 foFilter->set("internal_added", 237); 6097 QString value = ptr->bottom_field_first() ? "bff" : "tff"; 6098 if (foFilter->get("av.order") != value) { 6099 pCore->currentDoc()->setModified(true); 6100 } 6101 foFilter->set("av.order", value.toUtf8().constData()); 6102 } 6103 } 6104 } 6105 // Build default filter if not found 6106 if (!ptr->progressive() && foFilter == nullptr) { 6107 foFilter.reset(new Mlt::Filter(m_tractor->get_profile(), "avfilter.fieldorder")); 6108 if (foFilter->is_valid()) { 6109 foFilter->set("internal_added", 237); 6110 foFilter->set("av.order", ptr->bottom_field_first() ? "bff" : "tff"); 6111 m_tractor->attach(*foFilter.get()); 6112 pCore->currentDoc()->setModified(true); 6113 } 6114 } 6115 } 6116 6117 int TimelineModel::getBlankSizeNearClip(int clipId, bool after) const 6118 { 6119 READ_LOCK(); 6120 Q_ASSERT(m_allClips.count(clipId) > 0); 6121 int trackId = getClipTrackId(clipId); 6122 if (trackId != -1) { 6123 return getTrackById_const(trackId)->getBlankSizeNearClip(clipId, after); 6124 } 6125 return 0; 6126 } 6127 6128 int TimelineModel::getPreviousTrackId(int trackId) 6129 { 6130 READ_LOCK(); 6131 Q_ASSERT(isTrack(trackId)); 6132 auto it = m_iteratorTable.at(trackId); 6133 bool audioWanted = (*it)->isAudioTrack(); 6134 while (it != m_allTracks.cbegin()) { 6135 --it; 6136 if ((*it)->isAudioTrack() == audioWanted) { 6137 return (*it)->getId(); 6138 } 6139 } 6140 return trackId; 6141 } 6142 6143 int TimelineModel::getNextTrackId(int trackId) 6144 { 6145 READ_LOCK(); 6146 Q_ASSERT(isTrack(trackId)); 6147 auto it = m_iteratorTable.at(trackId); 6148 bool audioWanted = (*it)->isAudioTrack(); 6149 while (it != m_allTracks.cend()) { 6150 ++it; 6151 if (it != m_allTracks.cend() && (*it)->isAudioTrack() == audioWanted) { 6152 break; 6153 } 6154 } 6155 return it == m_allTracks.cend() ? trackId : (*it)->getId(); 6156 } 6157 6158 bool TimelineModel::requestClearSelection(bool onDeletion) 6159 { 6160 QWriteLocker locker(&m_lock); 6161 qDebug() << "::: REQUESTING SELECTION CLEAR!!!!!!"; 6162 TRACE(); 6163 if (m_singleSelectionMode) { 6164 m_singleSelectionMode = false; 6165 Q_EMIT selectionModeChanged(); 6166 } 6167 if (m_selectedMix > -1) { 6168 m_selectedMix = -1; 6169 Q_EMIT selectedMixChanged(-1, nullptr); 6170 } 6171 if (m_currentSelection.size() == 0) { 6172 TRACE_RES(true); 6173 return true; 6174 } 6175 if (isGroup(*m_currentSelection.begin())) { 6176 // Reset offset display on clips 6177 std::unordered_set<int> items = m_groups->getLeaves(*m_currentSelection.begin()); 6178 for (auto &id : items) { 6179 if (isGroup(id)) { 6180 std::unordered_set<int> children = m_groups->getLeaves(id); 6181 items.insert(children.begin(), children.end()); 6182 } else if (isClip(id)) { 6183 m_allClips[id]->clearOffset(); 6184 m_allClips[id]->setGrab(false); 6185 m_allClips[id]->setSelected(false); 6186 } else if (isComposition(id)) { 6187 m_allCompositions[id]->setGrab(false); 6188 m_allCompositions[id]->setSelected(false); 6189 } else if (isSubTitle(id)) { 6190 m_subtitleModel->setSelected(id, false); 6191 } 6192 if (m_groups->getType(*m_currentSelection.begin()) == GroupType::Selection) { 6193 m_groups->destructGroupItem(*m_currentSelection.begin()); 6194 } 6195 } 6196 } else { 6197 for (auto s : m_currentSelection) { 6198 if (isClip(s)) { 6199 m_allClips[s]->setGrab(false); 6200 m_allClips[s]->setSelected(false); 6201 } else if (isComposition(s)) { 6202 m_allCompositions[s]->setGrab(false); 6203 m_allCompositions[s]->setSelected(false); 6204 } else if (isSubTitle(s)) { 6205 m_subtitleModel->setSelected(s, false); 6206 } 6207 Q_ASSERT(onDeletion || isClip(s) || isComposition(s) || isSubTitle(s)); 6208 } 6209 } 6210 m_currentSelection.clear(); 6211 if (m_subtitleModel) { 6212 m_subtitleModel->clearGrab(); 6213 } 6214 Q_EMIT selectionChanged(); 6215 TRACE_RES(true); 6216 return true; 6217 } 6218 6219 bool TimelineModel::hasMultipleSelection() const 6220 { 6221 READ_LOCK(); 6222 if (m_currentSelection.size() == 0) { 6223 return false; 6224 } 6225 if (isGroup(*m_currentSelection.begin())) { 6226 // Reset offset display on clips 6227 std::unordered_set<int> items = m_groups->getLeaves(*m_currentSelection.begin()); 6228 return items.size() > 1; 6229 } 6230 return m_currentSelection.size() > 1; 6231 } 6232 6233 void TimelineModel::requestMixSelection(int cid) 6234 { 6235 requestClearSelection(); 6236 int tid = getItemTrackId(cid); 6237 if (tid > -1) { 6238 m_selectedMix = cid; 6239 Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid)); 6240 } 6241 } 6242 6243 void TimelineModel::requestClearSelection(bool onDeletion, Fun &undo, Fun &redo) 6244 { 6245 Fun operation = [this, onDeletion]() { 6246 requestClearSelection(onDeletion); 6247 return true; 6248 }; 6249 Fun reverse = [this, clips = getCurrentSelection()]() { return requestSetSelection(clips); }; 6250 if (operation()) { 6251 UPDATE_UNDO_REDO(operation, reverse, undo, redo); 6252 } 6253 } 6254 6255 void TimelineModel::clearGroupSelectionOnDelete(std::vector<int> groups) 6256 { 6257 READ_LOCK(); 6258 if (m_currentSelection.size() == 0) { 6259 return; 6260 } 6261 if (std::find(groups.begin(), groups.end(), *m_currentSelection.begin()) != groups.end()) { 6262 requestClearSelection(true); 6263 } 6264 } 6265 6266 std::unordered_set<int> TimelineModel::getCurrentSelection() const 6267 { 6268 READ_LOCK(); 6269 if (m_currentSelection.size() == 0) { 6270 return {}; 6271 } 6272 if (isGroup(*m_currentSelection.begin())) { 6273 return m_groups->getLeaves(*m_currentSelection.begin()); 6274 } else { 6275 for (auto &s : m_currentSelection) { 6276 Q_ASSERT(isClip(s) || isComposition(s) || isSubTitle(s)); 6277 } 6278 return m_currentSelection; 6279 } 6280 } 6281 6282 void TimelineModel::requestAddToSelection(int itemId, bool clear, bool singleSelect) 6283 { 6284 QWriteLocker locker(&m_lock); 6285 TRACE(itemId, clear); 6286 std::unordered_set<int> selection; 6287 if (clear) { 6288 requestClearSelection(); 6289 } else { 6290 selection = getCurrentSelection(); 6291 } 6292 if (singleSelect) { 6293 QWriteLocker locker(&m_lock); 6294 selection.insert(itemId); 6295 m_currentSelection = selection; 6296 setSelected(itemId, true); 6297 Q_EMIT selectionChanged(); 6298 if (!m_singleSelectionMode) { 6299 m_singleSelectionMode = true; 6300 Q_EMIT selectionModeChanged(); 6301 } 6302 return; 6303 } 6304 if (m_singleSelectionMode) { 6305 m_singleSelectionMode = false; 6306 Q_EMIT selectionModeChanged(); 6307 } 6308 if (selection.insert(itemId).second) { 6309 requestSetSelection(selection); 6310 } 6311 } 6312 6313 void TimelineModel::requestRemoveFromSelection(int itemId) 6314 { 6315 QWriteLocker locker(&m_lock); 6316 TRACE(itemId); 6317 std::unordered_set<int> all_items = {itemId}; 6318 int parentGroup = m_groups->getDirectAncestor(itemId); 6319 if (parentGroup > -1 && m_groups->getType(parentGroup) != GroupType::Selection) { 6320 all_items = m_groups->getLeaves(parentGroup); 6321 } 6322 std::unordered_set<int> selection = getCurrentSelection(); 6323 for (int current_itemId : all_items) { 6324 if (selection.count(current_itemId) > 0) { 6325 selection.erase(current_itemId); 6326 } 6327 } 6328 requestSetSelection(selection); 6329 } 6330 6331 bool TimelineModel::requestSetSelection(const std::unordered_set<int> &ids) 6332 { 6333 QWriteLocker locker(&m_lock); 6334 TRACE(ids); 6335 requestClearSelection(); 6336 // if the items are in groups, we must retrieve their topmost containing groups 6337 std::unordered_set<int> roots; 6338 std::transform(ids.begin(), ids.end(), std::inserter(roots, roots.begin()), [&](int id) { return m_groups->getRootId(id); }); 6339 6340 bool result = true; 6341 if (roots.size() == 0) { 6342 m_currentSelection.clear(); 6343 } else if (roots.size() == 1) { 6344 m_currentSelection = {*(roots.begin())}; 6345 setSelected(*m_currentSelection.begin(), true); 6346 } else { 6347 Fun undo = []() { return true; }; 6348 Fun redo = []() { return true; }; 6349 if (ids.size() == 2) { 6350 // Check if we selected 2 clips from the same master 6351 QList<int> pairIds; 6352 for (auto &id : roots) { 6353 if (isClip(id)) { 6354 pairIds << id; 6355 } 6356 } 6357 if (pairIds.size() == 2 && getClipBinId(pairIds.at(0)) == getClipBinId(pairIds.at(1))) { 6358 // Check if they have same bin id 6359 ClipType::ProducerType type = m_allClips[pairIds.at(0)]->clipType(); 6360 if (type == ClipType::AV || type == ClipType::Audio || type == ClipType::Video) { 6361 // Both clips have same bin ID, display offset 6362 int pos1 = getClipPosition(pairIds.at(0)); 6363 int pos2 = getClipPosition(pairIds.at(1)); 6364 if (pos2 > pos1) { 6365 int offset = pos2 - getClipIn(pairIds.at(1)) - (pos1 - getClipIn(pairIds.at(0))); 6366 if (offset != 0) { 6367 m_allClips[pairIds.at(1)]->setOffset(offset); 6368 m_allClips[pairIds.at(0)]->setOffset(-offset); 6369 } 6370 } else { 6371 int offset = pos1 - getClipIn(pairIds.at(0)) - (pos2 - getClipIn(pairIds.at(1))); 6372 if (offset != 0) { 6373 m_allClips[pairIds.at(0)]->setOffset(offset); 6374 m_allClips[pairIds.at(1)]->setOffset(-offset); 6375 } 6376 } 6377 } 6378 } 6379 } 6380 int groupId = m_groups->groupItems(ids, undo, redo, GroupType::Selection); 6381 if (groupId > -1) { 6382 m_currentSelection = {groupId}; 6383 result = true; 6384 } else { 6385 result = false; 6386 } 6387 Q_ASSERT(m_currentSelection.size() > 0); 6388 } 6389 if (m_subtitleModel) { 6390 m_subtitleModel->clearGrab(); 6391 } 6392 Q_EMIT selectionChanged(); 6393 return result; 6394 } 6395 6396 void TimelineModel::setSelected(int itemId, bool sel) 6397 { 6398 if (isClip(itemId)) { 6399 m_allClips[itemId]->setSelected(sel); 6400 } else if (isComposition(itemId)) { 6401 m_allCompositions[itemId]->setSelected(sel); 6402 } else if (isSubTitle(itemId)) { 6403 m_subtitleModel->setSelected(itemId, sel); 6404 } else if (isGroup(itemId)) { 6405 auto leaves = m_groups->getLeaves(itemId); 6406 for (auto &id : leaves) { 6407 setSelected(id, true); 6408 } 6409 } 6410 } 6411 6412 bool TimelineModel::requestSetSelection(const std::unordered_set<int> &ids, Fun &undo, Fun &redo) 6413 { 6414 QWriteLocker locker(&m_lock); 6415 Fun reverse = [this]() { 6416 requestClearSelection(false); 6417 return true; 6418 }; 6419 Fun operation = [this, ids]() { return requestSetSelection(ids); }; 6420 if (operation()) { 6421 UPDATE_UNDO_REDO(operation, reverse, undo, redo); 6422 return true; 6423 } 6424 return false; 6425 } 6426 6427 void TimelineModel::lockTrack(int trackId, bool lock) 6428 { 6429 if (lock) { 6430 getTrackById(trackId)->lock(); 6431 } else { 6432 getTrackById(trackId)->unlock(); 6433 } 6434 } 6435 6436 void TimelineModel::setTrackLockedState(int trackId, bool lock) 6437 { 6438 QWriteLocker locker(&m_lock); 6439 TRACE(trackId, lock); 6440 Fun undo = []() { return true; }; 6441 Fun redo = []() { return true; }; 6442 6443 Fun lock_lambda = [this, trackId]() { 6444 lockTrack(trackId, true); 6445 return true; 6446 }; 6447 Fun unlock_lambda = [this, trackId]() { 6448 lockTrack(trackId, false); 6449 return true; 6450 }; 6451 if (lock) { 6452 if (lock_lambda()) { 6453 UPDATE_UNDO_REDO(lock_lambda, unlock_lambda, undo, redo); 6454 PUSH_UNDO(undo, redo, i18n("Lock track")); 6455 } 6456 } else { 6457 if (unlock_lambda()) { 6458 UPDATE_UNDO_REDO(unlock_lambda, lock_lambda, undo, redo); 6459 PUSH_UNDO(undo, redo, i18n("Unlock track")); 6460 } 6461 } 6462 } 6463 6464 std::unordered_set<int> TimelineModel::getAllTracksIds() const 6465 { 6466 READ_LOCK(); 6467 std::unordered_set<int> result; 6468 std::transform(m_iteratorTable.begin(), m_iteratorTable.end(), std::inserter(result, result.begin()), [&](const auto &track) { return track.first; }); 6469 return result; 6470 } 6471 6472 void TimelineModel::switchComposition(int cid, const QString &compoId) 6473 { 6474 Fun undo = []() { return true; }; 6475 Fun redo = []() { return true; }; 6476 if (isClip(cid)) { 6477 // We are working on a mix 6478 requestClearSelection(true); 6479 int tid = getClipTrackId(cid); 6480 MixInfo mixData = getTrackById_const(tid)->getMixInfo(cid).first; 6481 getTrackById(tid)->switchMix(cid, compoId, undo, redo); 6482 Fun local_update = [cid, mixData, this]() { 6483 requestMixSelection(cid); 6484 int in = mixData.secondClipInOut.first; 6485 int out = mixData.firstClipInOut.second; 6486 Q_EMIT invalidateZone(in, out); 6487 checkRefresh(in, out); 6488 return true; 6489 }; 6490 PUSH_LAMBDA(local_update, redo); 6491 PUSH_FRONT_LAMBDA(local_update, undo); 6492 if (redo()) { 6493 pCore->pushUndo(undo, redo, i18n("Change composition")); 6494 } 6495 return; 6496 } 6497 Q_ASSERT(isComposition(cid)); 6498 std::shared_ptr<CompositionModel> compo = m_allCompositions.at(cid); 6499 int currentPos = compo->getPosition(); 6500 int duration = compo->getPlaytime(); 6501 int currentTrack = compo->getCurrentTrackId(); 6502 int a_track = compo->getATrack(); 6503 int forcedTrack = compo->getForcedTrack(); 6504 // Clear selection 6505 requestClearSelection(true); 6506 if (m_groups->isInGroup(cid)) { 6507 pCore->displayMessage(i18n("Cannot operate on grouped composition, please ungroup"), ErrorMessage); 6508 return; 6509 } 6510 6511 bool res = requestCompositionDeletion(cid, undo, redo); 6512 int newId = -1; 6513 // Check if composition should be reversed (top clip at beginning, bottom at end) 6514 int topClip = getTrackById_const(currentTrack)->getClipByPosition(currentPos); 6515 int bottomTid = a_track < 1 ? -1 : getTrackIndexFromPosition(a_track - 1); 6516 int bottomClip = -1; 6517 if (bottomTid > -1) { 6518 bottomClip = getTrackById_const(bottomTid)->getClipByPosition(currentPos); 6519 } 6520 bool reverse = false; 6521 if (topClip > -1 && bottomClip > -1) { 6522 if (getClipPosition(topClip) + getClipPlaytime(topClip) < getClipPosition(bottomClip) + getClipPlaytime(bottomClip)) { 6523 reverse = true; 6524 } 6525 } 6526 std::unique_ptr<Mlt::Properties> props(nullptr); 6527 if (reverse) { 6528 props = std::make_unique<Mlt::Properties>(); 6529 if (compoId == QLatin1String("dissolve")) { 6530 props->set("reverse", 1); 6531 } else if (compoId == QLatin1String("composite")) { 6532 props->set("invert", 1); 6533 } else if (compoId == QLatin1String("wipe")) { 6534 props->set("geometry", "0=0% 0% 100% 100% 100%;-1=0% 0% 100% 100% 0%"); 6535 } else if (compoId == QLatin1String("slide")) { 6536 props->set("rect", "0=0% 0% 100% 100% 100%;-1=100% 0% 100% 100% 100%"); 6537 } 6538 } 6539 res = res && requestCompositionInsertion(compoId, currentTrack, a_track, currentPos, duration, std::move(props), newId, undo, redo); 6540 if (res) { 6541 if (forcedTrack > -1 && isComposition(newId)) { 6542 m_allCompositions[newId]->setForceTrack(true); 6543 } 6544 Fun local_redo = [newId, this]() { 6545 requestSetSelection({newId}); 6546 return true; 6547 }; 6548 Fun local_undo = [cid, this]() { 6549 requestSetSelection({cid}); 6550 return true; 6551 }; 6552 local_redo(); 6553 PUSH_LAMBDA(local_redo, redo); 6554 PUSH_LAMBDA(local_undo, undo); 6555 PUSH_UNDO(undo, redo, i18n("Change composition")); 6556 } else { 6557 undo(); 6558 } 6559 } 6560 6561 bool TimelineModel::plantMix(int tid, Mlt::Transition *t) 6562 { 6563 if (getTrackById_const(tid)->hasClipStart(t->get_in())) { 6564 int a_track = t->get_a_track(); 6565 int b_track = t->get_b_track(); 6566 getTrackById_const(tid)->getTrackService()->plant_transition(*t, a_track, b_track); 6567 return getTrackById_const(tid)->loadMix(t); 6568 } else { 6569 qDebug() << "=== INVALID MIX FOUND AT: " << t->get_in() << " - " << t->get("mlt_service"); 6570 return false; 6571 } 6572 } 6573 6574 bool TimelineModel::resizeStartMix(int cid, int duration, bool singleResize) 6575 { 6576 Q_ASSERT(isClip(cid)); 6577 int tid = m_allClips.at(cid)->getCurrentTrackId(); 6578 if (tid > -1) { 6579 std::pair<MixInfo, MixInfo> mixData = getTrackById_const(tid)->getMixInfo(cid); 6580 if (mixData.first.firstClipId > -1) { 6581 int clipToResize = mixData.first.firstClipId; 6582 Q_ASSERT(isClip(clipToResize)); 6583 duration = qMin(duration, m_allClips.at(cid)->getPlaytime()); 6584 int updatedDuration = m_allClips.at(cid)->getPosition() + duration - m_allClips[clipToResize]->getPosition(); 6585 int result = requestItemResize(clipToResize, updatedDuration, true, true, 0, singleResize); 6586 return result > -1; 6587 } 6588 } 6589 return false; 6590 } 6591 6592 int TimelineModel::getMixDuration(int cid) const 6593 { 6594 Q_ASSERT(isClip(cid)); 6595 int tid = m_allClips.at(cid)->getCurrentTrackId(); 6596 if (tid > -1) { 6597 if (getTrackById_const(tid)->hasStartMix(cid)) { 6598 return getTrackById_const(tid)->getMixDuration(cid); 6599 } else { 6600 // Mix is not yet inserted in timeline 6601 std::pair<int, int> mixInOut = getMixInOut(cid); 6602 return mixInOut.second - mixInOut.first; 6603 } 6604 } 6605 return 0; 6606 } 6607 6608 std::pair<int, int> TimelineModel::getMixInOut(int cid) const 6609 { 6610 Q_ASSERT(isClip(cid)); 6611 int tid = m_allClips.at(cid)->getCurrentTrackId(); 6612 if (tid > -1) { 6613 MixInfo mixData = getTrackById_const(tid)->getMixInfo(cid).first; 6614 if (mixData.firstClipId > -1) { 6615 return {mixData.secondClipInOut.first, mixData.firstClipInOut.second}; 6616 } 6617 } 6618 return {-1, -1}; 6619 } 6620 6621 int TimelineModel::getMixCutPos(int cid) const 6622 { 6623 Q_ASSERT(isClip(cid)); 6624 return m_allClips.at(cid)->getMixCutPosition(); 6625 } 6626 6627 MixAlignment TimelineModel::getMixAlign(int cid) const 6628 { 6629 Q_ASSERT(isClip(cid)); 6630 int tid = m_allClips.at(cid)->getCurrentTrackId(); 6631 if (tid > -1) { 6632 int mixDuration = m_allClips.at(cid)->getMixDuration(); 6633 int mixCutPos = m_allClips.at(cid)->getMixCutPosition(); 6634 if (mixCutPos == 0) { 6635 return MixAlignment::AlignRight; 6636 } else if (mixCutPos == mixDuration) { 6637 return MixAlignment::AlignLeft; 6638 } else if (mixCutPos == mixDuration - mixDuration / 2) { 6639 return MixAlignment::AlignCenter; 6640 } 6641 } 6642 return MixAlignment::AlignNone; 6643 } 6644 6645 void TimelineModel::requestResizeMix(int cid, int duration, MixAlignment align, int leftFrames) 6646 { 6647 Q_ASSERT(isClip(cid)); 6648 int tid = m_allClips.at(cid)->getCurrentTrackId(); 6649 if (tid > -1) { 6650 MixInfo mixData = getTrackById_const(tid)->getMixInfo(cid).first; 6651 int clipToResize = mixData.firstClipId; 6652 if (clipToResize > -1) { 6653 Fun undo = []() { return true; }; 6654 Fun redo = []() { return true; }; 6655 // The mix cut position shoud never change through a resize operation 6656 int cutPos = m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime() - m_allClips.at(cid)->getMixCutPosition(); 6657 int maxLengthLeft = m_allClips.at(clipToResize)->getMaxDuration(); 6658 // Maximum space for expanding the right clip part 6659 int leftMax = maxLengthLeft > -1 ? (maxLengthLeft - 1 - m_allClips.at(clipToResize)->getOut()) : -1; 6660 // Maximum space available on the right clip 6661 int availableLeft = m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - 6662 (m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime()); 6663 if (leftMax == -1) { 6664 leftMax = availableLeft; 6665 } else { 6666 leftMax = qMin(leftMax, availableLeft); 6667 } 6668 6669 int maxLengthRight = m_allClips.at(cid)->getMaxDuration(); 6670 // maximum space to resize clip on the left 6671 int availableRight = m_allClips.at(cid)->getPosition() - m_allClips.at(clipToResize)->getPosition(); 6672 int rightMax = maxLengthRight > -1 ? (m_allClips.at(cid)->getIn()) : -1; 6673 if (rightMax == -1) { 6674 rightMax = availableRight; 6675 } else { 6676 rightMax = qMin(rightMax, availableRight); 6677 } 6678 Fun adjust_mix_undo = [this, tid, cid, prevCut = m_allClips.at(cid)->getMixCutPosition(), prevDuration = m_allClips.at(cid)->getMixDuration()]() { 6679 getTrackById_const(tid)->setMixDuration(cid, prevDuration, prevCut); 6680 QModelIndex ix = makeClipIndexFromID(cid); 6681 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 6682 return true; 6683 }; 6684 if (align == MixAlignment::AlignLeft) { 6685 // Adjust left clip 6686 int updatedDurationLeft = cutPos + duration - m_allClips.at(clipToResize)->getPosition(); 6687 if (leftMax > -1) { 6688 updatedDurationLeft = qMin(updatedDurationLeft, m_allClips.at(clipToResize)->getPlaytime() + leftMax); 6689 } 6690 // Adjust right clip 6691 int updatedDurationRight = m_allClips.at(cid)->getPlaytime(); 6692 if (cutPos != m_allClips.at(cid)->getPosition()) { 6693 updatedDurationRight = m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - cutPos; 6694 if (rightMax > -1) { 6695 updatedDurationRight = qMin(updatedDurationRight, m_allClips.at(cid)->getPlaytime() + rightMax); 6696 } 6697 } 6698 int updatedDuration = m_allClips.at(clipToResize)->getPosition() + updatedDurationLeft - 6699 (m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - updatedDurationRight); 6700 if (updatedDuration < 1) { 6701 // 6702 pCore->displayMessage(i18n("Cannot resize mix to less than 1 frame"), ErrorMessage, 500); 6703 // update mix widget 6704 Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true); 6705 return; 6706 } 6707 requestItemResize(clipToResize, updatedDurationLeft, true, true, undo, redo); 6708 if (m_allClips.at(cid)->getPlaytime() != updatedDurationRight) { 6709 requestItemResize(cid, updatedDurationRight, false, true, undo, redo); 6710 } 6711 int updatedCutPosition = m_allClips.at(cid)->getPosition(); 6712 if (updatedCutPosition != cutPos) { 6713 pCore->displayMessage(i18n("Cannot resize mix"), ErrorMessage, 500); 6714 undo(); 6715 Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true); 6716 return; 6717 } 6718 Fun adjust_mix = [this, tid, cid, updatedDuration]() { 6719 getTrackById_const(tid)->setMixDuration(cid, updatedDuration, updatedDuration); 6720 QModelIndex ix = makeClipIndexFromID(cid); 6721 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 6722 return true; 6723 }; 6724 adjust_mix(); 6725 UPDATE_UNDO_REDO(adjust_mix, adjust_mix_undo, undo, redo); 6726 } else if (align == MixAlignment::AlignRight) { 6727 int updatedDurationRight = m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - cutPos + duration; 6728 if (rightMax > -1) { 6729 updatedDurationRight = qMin(updatedDurationRight, m_allClips.at(cid)->getPlaytime() + rightMax); 6730 } 6731 int updatedDurationLeft = cutPos - m_allClips.at(clipToResize)->getPosition(); 6732 if (leftMax > -1) { 6733 updatedDurationLeft = qMin(updatedDurationLeft, m_allClips.at(clipToResize)->getPlaytime() + leftMax); 6734 } 6735 int updatedDuration = m_allClips.at(clipToResize)->getPosition() + updatedDurationLeft - 6736 (m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - updatedDurationRight); 6737 if (updatedDuration < 1) { 6738 // 6739 pCore->displayMessage(i18n("Cannot resize mix to less than 1 frame"), ErrorMessage, 500); 6740 Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true); 6741 return; 6742 } 6743 requestItemResize(cid, updatedDurationRight, false, true, undo, redo); 6744 requestItemResize(clipToResize, updatedDurationLeft, true, true, undo, redo); 6745 int updatedCutPosition = m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime(); 6746 if (updatedCutPosition != cutPos) { 6747 pCore->displayMessage(i18n("Cannot resize mix"), ErrorMessage, 500); 6748 undo(); 6749 Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true); 6750 return; 6751 } 6752 Fun adjust_mix = [this, tid, cid, updatedDuration]() { 6753 getTrackById_const(tid)->setMixDuration(cid, updatedDuration, 0); 6754 QModelIndex ix = makeClipIndexFromID(cid); 6755 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 6756 return true; 6757 }; 6758 adjust_mix(); 6759 UPDATE_UNDO_REDO(adjust_mix, adjust_mix_undo, undo, redo); 6760 } else if (align == MixAlignment::AlignCenter) { 6761 int updatedDurationRight = m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - cutPos + duration / 2; 6762 if (rightMax > -1) { 6763 updatedDurationRight = qMin(updatedDurationRight, m_allClips.at(cid)->getPlaytime() + rightMax); 6764 } 6765 int updatedDurationLeft = cutPos + (duration - duration / 2) - m_allClips.at(clipToResize)->getPosition(); 6766 if (leftMax > -1) { 6767 updatedDurationLeft = qMin(updatedDurationLeft, m_allClips.at(clipToResize)->getPlaytime() + leftMax); 6768 } 6769 int updatedDuration = m_allClips.at(clipToResize)->getPosition() + updatedDurationLeft - 6770 (m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - updatedDurationRight); 6771 if (updatedDuration < 1) { 6772 pCore->displayMessage(i18n("Cannot resize mix to less than 1 frame"), ErrorMessage, 500); 6773 Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true); 6774 return; 6775 } 6776 int deltaLeft = m_allClips.at(clipToResize)->getPosition() + updatedDurationLeft - cutPos; 6777 int deltaRight = cutPos - (m_allClips.at(cid)->getPosition() + m_allClips.at(cid)->getPlaytime() - updatedDurationRight); 6778 6779 if (deltaRight) { 6780 if (!requestItemResize(cid, updatedDurationRight, false, true, undo, redo)) { 6781 qDebug() << ":::: ERROR RESIZING CID1\n\nAAAAAAAAAAAAAAAAAAAA"; 6782 } 6783 } 6784 if (deltaLeft > 0) { 6785 if (!requestItemResize(clipToResize, updatedDurationLeft, true, true, undo, redo)) { 6786 qDebug() << ":::: ERROR RESIZING clipToResize\n\nAAAAAAAAAAAAAAAAAAAA"; 6787 } 6788 } 6789 int mixCutPos = m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime() - cutPos; 6790 if (mixCutPos > updatedDuration) { 6791 pCore->displayMessage(i18n("Cannot resize mix"), ErrorMessage, 500); 6792 undo(); 6793 Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true); 6794 return; 6795 } 6796 if (qAbs(deltaLeft - deltaRight) > 2) { 6797 // Mix not exactly centered 6798 Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true); 6799 return; 6800 } 6801 Fun adjust_mix = [this, tid, cid, updatedDuration, mixCutPos]() { 6802 getTrackById_const(tid)->setMixDuration(cid, updatedDuration, mixCutPos); 6803 QModelIndex ix = makeClipIndexFromID(cid); 6804 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 6805 return true; 6806 }; 6807 adjust_mix(); 6808 UPDATE_UNDO_REDO(adjust_mix, adjust_mix_undo, undo, redo); 6809 } else { 6810 // No alignment specified 6811 int updatedDurationRight; 6812 int updatedDurationLeft; 6813 if (leftFrames > -1) { 6814 // A right frame offset was specified 6815 updatedDurationLeft = qBound(0, leftFrames, duration); 6816 updatedDurationRight = duration - updatedDurationLeft; 6817 } else { 6818 updatedDurationRight = m_allClips.at(cid)->getMixCutPosition(); 6819 updatedDurationLeft = m_allClips.at(cid)->getMixDuration() - updatedDurationRight; 6820 int currentDuration = m_allClips.at(cid)->getMixDuration(); 6821 if (qAbs(duration - currentDuration) == 1) { 6822 if (duration < currentDuration) { 6823 // We are reducing the duration 6824 if (currentDuration % 2 == 0) { 6825 updatedDurationRight--; 6826 if (updatedDurationRight < 0) { 6827 updatedDurationRight = 0; 6828 updatedDurationLeft--; 6829 } 6830 } else { 6831 updatedDurationLeft--; 6832 if (updatedDurationLeft < 0) { 6833 updatedDurationLeft = 0; 6834 updatedDurationRight--; 6835 } 6836 } 6837 } else { 6838 // Increasing duration 6839 if (currentDuration % 2 == 0) { 6840 updatedDurationRight++; 6841 } else { 6842 updatedDurationLeft++; 6843 } 6844 } 6845 } else { 6846 double ratio = double(duration) / currentDuration; 6847 updatedDurationRight *= ratio; 6848 updatedDurationLeft = duration - updatedDurationRight; 6849 } 6850 } 6851 if (updatedDurationLeft + updatedDurationRight < 1) { 6852 // 6853 pCore->displayMessage(i18n("Cannot resize mix to less than 1 frame"), ErrorMessage, 500); 6854 Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true); 6855 return; 6856 } 6857 updatedDurationLeft -= (m_allClips.at(cid)->getMixDuration() - m_allClips.at(cid)->getMixCutPosition()); 6858 updatedDurationRight -= m_allClips.at(cid)->getMixCutPosition(); 6859 if (leftMax > -1) { 6860 updatedDurationLeft = qMin(updatedDurationLeft, m_allClips.at(clipToResize)->getPlaytime() + leftMax); 6861 } 6862 if (rightMax > -1) { 6863 updatedDurationRight = qMin(updatedDurationRight, m_allClips.at(cid)->getPlaytime() + rightMax); 6864 } 6865 if (updatedDurationLeft != 0) { 6866 int updatedDurL = m_allClips.at(cid)->getPlaytime() + updatedDurationLeft; 6867 requestItemResize(cid, updatedDurL, false, true, undo, redo); 6868 } 6869 if (updatedDurationRight != 0) { 6870 int updatedDurR = m_allClips.at(clipToResize)->getPlaytime() + updatedDurationRight; 6871 requestItemResize(clipToResize, updatedDurR, true, true, undo, redo); 6872 } 6873 int mixCutPos = m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime() - cutPos; 6874 int updatedDuration = 6875 m_allClips.at(clipToResize)->getPosition() + m_allClips.at(clipToResize)->getPlaytime() - m_allClips.at(cid)->getPosition(); 6876 if (mixCutPos > updatedDuration) { 6877 pCore->displayMessage(i18n("Cannot resize mix"), ErrorMessage, 500); 6878 undo(); 6879 Q_EMIT selectedMixChanged(cid, getTrackById_const(tid)->mixModel(cid), true); 6880 return; 6881 } 6882 Fun adjust_mix = [this, tid, cid, updatedDuration, mixCutPos]() { 6883 getTrackById_const(tid)->setMixDuration(cid, updatedDuration, mixCutPos); 6884 QModelIndex ix = makeClipIndexFromID(cid); 6885 Q_EMIT dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 6886 return true; 6887 }; 6888 adjust_mix(); 6889 UPDATE_UNDO_REDO(adjust_mix, adjust_mix_undo, undo, redo); 6890 } 6891 pCore->pushUndo(undo, redo, i18n("Resize mix")); 6892 } 6893 } 6894 } 6895 6896 int TimelineModel::getSubtitleIndex(int subId) const 6897 { 6898 if (m_allSubtitles.count(subId) == 0) { 6899 return -1; 6900 } 6901 auto it = m_allSubtitles.find(subId); 6902 return int(std::distance(m_allSubtitles.begin(), it)); 6903 } 6904 6905 std::pair<int, GenTime> TimelineModel::getSubtitleIdFromIndex(int index) const 6906 { 6907 if (index >= static_cast<int>(m_allSubtitles.size())) { 6908 return {-1, GenTime()}; 6909 } 6910 auto it = m_allSubtitles.begin(); 6911 std::advance(it, index); 6912 return {it->first, it->second}; 6913 } 6914 6915 QVariantList TimelineModel::getMasterEffectZones() const 6916 { 6917 if (m_masterStack) { 6918 return m_masterStack->getEffectZones(); 6919 } 6920 return {}; 6921 } 6922 6923 const QSize TimelineModel::getCompositionSizeOnTrack(const ObjectId &id) 6924 { 6925 int pos = getCompositionPosition(id.itemId); 6926 int tid = getCompositionTrackId(id.itemId); 6927 int cid = getTrackById_const(tid)->getClipByPosition(pos); 6928 if (cid > -1) { 6929 return getClipFrameSize(cid); 6930 } 6931 return QSize(); 6932 } 6933 6934 QStringList TimelineModel::getProxiesAt(int position) 6935 { 6936 QStringList done; 6937 QStringList proxied; 6938 auto it = m_allTracks.begin(); 6939 while (it != m_allTracks.end()) { 6940 if ((*it)->isAudioTrack()) { 6941 ++it; 6942 continue; 6943 } 6944 int clip1 = (*it)->getClipByPosition(position, 0); 6945 int clip2 = (*it)->getClipByPosition(position, 1); 6946 if (clip1 > -1) { 6947 // Check if proxied 6948 const QString binId = m_allClips[clip1]->binId(); 6949 if (!done.contains(binId)) { 6950 done << binId; 6951 std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(binId); 6952 if (binClip->hasProxy()) { 6953 proxied << binId; 6954 } 6955 } 6956 } 6957 if (clip2 > -1) { 6958 // Check if proxied 6959 const QString binId = m_allClips[clip2]->binId(); 6960 if (!done.contains(binId)) { 6961 done << binId; 6962 std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(binId); 6963 if (binClip->hasProxy()) { 6964 proxied << binId; 6965 } 6966 } 6967 } 6968 ++it; 6969 } 6970 return proxied; 6971 } 6972 6973 QByteArray TimelineModel::timelineHash() 6974 { 6975 QByteArray fileData; 6976 // Get track hashes 6977 auto it = m_allTracks.begin(); 6978 while (it != m_allTracks.end()) { 6979 fileData.append((*it)->trackHash()); 6980 ++it; 6981 } 6982 // Compositions hash 6983 for (auto &compo : m_allCompositions) { 6984 int track = getTrackPosition(compo.second->getCurrentTrackId()); 6985 QString compoData = QString("%1 %2 %3 %4") 6986 .arg(QString::number(compo.second->getATrack()), QString::number(track), QString::number(compo.second->getPosition()), 6987 QString::number(compo.second->getPlaytime())); 6988 compoData.append(compo.second->getAssetId()); 6989 fileData.append(compoData.toLatin1()); 6990 } 6991 // Guides 6992 if (m_guidesModel) { 6993 QString guidesData = m_guidesModel->toJson(); 6994 fileData.append(guidesData.toUtf8().constData()); 6995 } 6996 QByteArray fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5); 6997 return fileHash; 6998 } 6999 7000 std::shared_ptr<MarkerSortModel> TimelineModel::getFilteredGuideModel() 7001 { 7002 return m_guidesFilterModel; 7003 } 7004 7005 std::shared_ptr<MarkerListModel> TimelineModel::getGuideModel() 7006 { 7007 return m_guidesModel; 7008 } 7009 7010 bool TimelineModel::trackIsLocked(int trackId) const 7011 { 7012 if (isSubtitleTrack(trackId)) { 7013 return m_subtitleModel->isLocked(); 7014 } 7015 Q_ASSERT(isTrack(trackId)); 7016 return getTrackById_const(trackId)->isLocked(); 7017 } 7018 7019 const QUuid TimelineModel::uuid() const 7020 { 7021 return m_uuid; 7022 } 7023 7024 void TimelineModel::initializePreviewManager() 7025 { 7026 if (m_timelinePreview == nullptr) { 7027 m_timelinePreview = std::shared_ptr<PreviewManager>(new PreviewManager(m_tractor.get(), m_uuid, this)); 7028 bool initialized = m_timelinePreview->initialize(); 7029 if (!initialized) { 7030 pCore->displayMessage(i18n("Error initializing timeline preview"), ErrorMessage); 7031 m_timelinePreview.reset(); 7032 return; 7033 } 7034 Q_EMIT connectPreviewManager(); 7035 connect(this, &TimelineModel::invalidateZone, m_timelinePreview.get(), &PreviewManager::invalidatePreview, Qt::DirectConnection); 7036 } 7037 } 7038 7039 std::shared_ptr<PreviewManager> TimelineModel::previewManager() 7040 { 7041 return m_timelinePreview; 7042 } 7043 7044 void TimelineModel::resetPreviewManager() 7045 { 7046 if (m_timelinePreview) { 7047 disconnect(this, &TimelineModel::invalidateZone, m_timelinePreview.get(), &PreviewManager::invalidatePreview); 7048 m_timelinePreview.reset(); 7049 } 7050 } 7051 7052 bool TimelineModel::hasTimelinePreview() const 7053 { 7054 return m_timelinePreview != nullptr; 7055 } 7056 7057 void TimelineModel::updatePreviewConnection(bool enable) 7058 { 7059 if (hasTimelinePreview()) { 7060 if (enable) { 7061 m_timelinePreview->enable(); 7062 } else { 7063 m_timelinePreview->disable(); 7064 } 7065 } 7066 } 7067 7068 bool TimelineModel::buildPreviewTrack() 7069 { 7070 bool res = false; 7071 if (m_timelinePreview) { 7072 res = m_timelinePreview->buildPreviewTrack(); 7073 m_overlayTrackCount = m_timelinePreview->addedTracks(); 7074 } 7075 return res; 7076 } 7077 7078 void TimelineModel::setOverlayTrack(Mlt::Playlist *overlay) 7079 { 7080 if (m_timelinePreview) { 7081 m_timelinePreview->setOverlayTrack(overlay); 7082 m_overlayTrackCount = m_timelinePreview->addedTracks(); 7083 } 7084 } 7085 7086 void TimelineModel::removeOverlayTrack() 7087 { 7088 if (m_timelinePreview) { 7089 m_timelinePreview->removeOverlayTrack(); 7090 m_overlayTrackCount = m_timelinePreview->addedTracks(); 7091 } 7092 } 7093 7094 void TimelineModel::deletePreviewTrack() 7095 { 7096 if (m_timelinePreview) { 7097 m_timelinePreview->deletePreviewTrack(); 7098 m_overlayTrackCount = m_timelinePreview->addedTracks(); 7099 } 7100 } 7101 7102 bool TimelineModel::hasSubtitleModel() 7103 { 7104 return m_subtitleModel != nullptr; 7105 } 7106 7107 void TimelineModel::makeTransparentBg(bool transparent) 7108 { 7109 m_blackClip->lock(); 7110 if (transparent) { 7111 m_blackClip->set("resource", 0); 7112 } else { 7113 m_blackClip->set("resource", "black"); 7114 } 7115 m_blackClip->unlock(); 7116 } 7117 7118 void TimelineModel::prepareShutDown() 7119 { 7120 m_softDelete = true; 7121 } 7122 7123 void TimelineModel::updateVisibleSequenceName(const QString displayName) 7124 { 7125 m_visibleSequenceName = displayName; 7126 Q_EMIT visibleSequenceNameChanged(); 7127 } 7128 7129 void TimelineModel::registerTimeline() 7130 { 7131 qDebug() << "::: CLIPS IN THIS MODDEL: " << m_allClips.size(); 7132 for (auto clip : m_allClips) { 7133 clip.second->registerClipToBin(clip.second->getProducer(), false); 7134 } 7135 } 7136 7137 void TimelineModel::loadPreview(const QString &chunks, const QString &dirty, bool enable, Mlt::Playlist &playlist) 7138 { 7139 if (chunks.isEmpty() && dirty.isEmpty()) { 7140 return; 7141 } 7142 if (!hasTimelinePreview()) { 7143 initializePreviewManager(); 7144 } 7145 QVariantList renderedChunks; 7146 QVariantList dirtyChunks; 7147 QStringList chunksList = chunks.split(QLatin1Char(','), Qt::SkipEmptyParts); 7148 QStringList dirtyList = dirty.split(QLatin1Char(','), Qt::SkipEmptyParts); 7149 for (const QString &frame : qAsConst(chunksList)) { 7150 if (frame.contains(QLatin1Char('-'))) { 7151 // Range, process 7152 int start = frame.section(QLatin1Char('-'), 0, 0).toInt(); 7153 int end = frame.section(QLatin1Char('-'), 1, 1).toInt(); 7154 for (int i = start; i <= end; i += 25) { 7155 renderedChunks << i; 7156 } 7157 } else { 7158 renderedChunks << frame.toInt(); 7159 } 7160 } 7161 for (const QString &frame : qAsConst(dirtyList)) { 7162 if (frame.contains(QLatin1Char('-'))) { 7163 // Range, process 7164 int start = frame.section(QLatin1Char('-'), 0, 0).toInt(); 7165 int end = frame.section(QLatin1Char('-'), 1, 1).toInt(); 7166 for (int i = start; i <= end; i += 25) { 7167 dirtyChunks << i; 7168 } 7169 } else { 7170 dirtyChunks << frame.toInt(); 7171 } 7172 } 7173 7174 if (hasTimelinePreview()) { 7175 if (!enable) { 7176 buildPreviewTrack(); 7177 } 7178 previewManager()->loadChunks(renderedChunks, dirtyChunks, playlist); 7179 } 7180 } 7181 7182 bool TimelineModel::clipIsAudio(int cid) const 7183 { 7184 if (isClip(cid)) { 7185 int tid = getClipTrackId(cid); 7186 if (tid > -1) { 7187 return getTrackById_const(tid)->isAudioTrack(); 7188 } 7189 } 7190 return false; 7191 } 7192 7193 bool TimelineModel::singleSelectionMode() const 7194 { 7195 return m_singleSelectionMode; 7196 }