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