File indexing completed on 2024-04-28 04:52:36
0001 /* 0002 SPDX-FileCopyrightText: 2017 Nicolas Carion 0003 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0004 */ 0005 0006 #include "trackmodel.hpp" 0007 #include "clipmodel.hpp" 0008 #include "compositionmodel.hpp" 0009 #include "core.h" 0010 #include "effects/effectstack/model/effectstackmodel.hpp" 0011 #include "kdenlivesettings.h" 0012 #include "transitions/transitionsrepository.hpp" 0013 #ifdef CRASH_AUTO_TEST 0014 #include "logger.hpp" 0015 #else 0016 #define TRACE_CONSTR(...) 0017 #endif 0018 #include "snapmodel.hpp" 0019 #include "timelinemodel.hpp" 0020 #include <QDebug> 0021 #include <QModelIndex> 0022 #include <memory> 0023 #include <mlt++/MltTransition.h> 0024 0025 TrackModel::TrackModel(const std::weak_ptr<TimelineModel> &parent, int id, const QString &trackName, bool audioTrack) 0026 : m_parent(parent) 0027 , m_id(id == -1 ? TimelineModel::getNextId() : id) 0028 , m_lock(QReadWriteLock::Recursive) 0029 , m_softDelete(false) 0030 { 0031 if (auto ptr = parent.lock()) { 0032 m_track = std::make_shared<Mlt::Tractor>(pCore->getProjectProfile()); 0033 m_playlists[0].set_profile(pCore->getProjectProfile().get_profile()); 0034 m_playlists[1].set_profile(pCore->getProjectProfile().get_profile()); 0035 m_track->insert_track(m_playlists[0], 0); 0036 m_track->insert_track(m_playlists[1], 1); 0037 if (!trackName.isEmpty()) { 0038 m_track->set("kdenlive:track_name", trackName.toUtf8().constData()); 0039 } 0040 if (audioTrack) { 0041 m_track->set("kdenlive:audio_track", 1); 0042 for (auto &m_playlist : m_playlists) { 0043 m_playlist.set("hide", 1); 0044 } 0045 } else { 0046 // Video track 0047 for (auto &m_playlist : m_playlists) { 0048 m_playlist.set("hide", 2); 0049 } 0050 } 0051 // For now we never use the second playlist, only planned for same track transitions 0052 m_track->set("kdenlive:trackheight", KdenliveSettings::trackheight()); 0053 m_track->set("kdenlive:timeline_active", 1); 0054 m_effectStack = EffectStackModel::construct(m_track, ObjectId(KdenliveObjectType::TimelineTrack, m_id, ptr->uuid()), ptr->m_undoStack); 0055 // TODO 0056 // When we use the second playlist, register it's stask as child of main playlist effectstack 0057 // m_subPlaylist = std::make_shared<Mlt::Producer>(&m_playlists[1]); 0058 // m_effectStack->addService(m_subPlaylist); 0059 QObject::connect(m_effectStack.get(), &EffectStackModel::dataChanged, [&](const QModelIndex &, const QModelIndex &, const QVector<int> &roles) { 0060 if (auto ptr2 = m_parent.lock()) { 0061 QModelIndex ix = ptr2->makeTrackIndexFromID(m_id); 0062 qDebug() << "==== TRACK ZONES CHANGED"; 0063 Q_EMIT ptr2->dataChanged(ix, ix, roles); 0064 } 0065 }); 0066 } else { 0067 qDebug() << "Error : construction of track failed because parent timeline is not available anymore"; 0068 Q_ASSERT(false); 0069 } 0070 } 0071 0072 TrackModel::TrackModel(const std::weak_ptr<TimelineModel> &parent, Mlt::Tractor mltTrack, int id) 0073 : m_parent(parent) 0074 , m_id(id == -1 ? TimelineModel::getNextId() : id) 0075 , m_softDelete(false) 0076 { 0077 if (auto ptr = parent.lock()) { 0078 m_track = std::make_shared<Mlt::Tractor>(mltTrack); 0079 m_playlists[0] = *m_track->track(0); 0080 m_playlists[1] = *m_track->track(1); 0081 m_effectStack = EffectStackModel::construct(m_track, ObjectId(KdenliveObjectType::TimelineTrack, m_id, ptr->uuid()), ptr->m_undoStack); 0082 } else { 0083 qDebug() << "Error : construction of track failed because parent timeline is not available anymore"; 0084 Q_ASSERT(false); 0085 } 0086 } 0087 0088 TrackModel::~TrackModel() 0089 { 0090 if (!m_softDelete) { 0091 QScopedPointer<Mlt::Service> service(m_track->field()); 0092 QScopedPointer<Mlt::Field> field(m_track->field()); 0093 field->lock(); 0094 while (service != nullptr && service->is_valid()) { 0095 if (service->type() == mlt_service_transition_type) { 0096 Mlt::Transition t(mlt_transition(service->get_service())); 0097 service.reset(service->producer()); 0098 // remove all compositing 0099 field->disconnect_service(t); 0100 t.disconnect_all_producers(); 0101 } else { 0102 service.reset(service->producer()); 0103 } 0104 } 0105 field->unlock(); 0106 m_sameCompositions.clear(); 0107 m_allClips.clear(); 0108 m_allCompositions.clear(); 0109 m_track->remove_track(1); 0110 m_track->remove_track(0); 0111 } 0112 } 0113 0114 int TrackModel::construct(const std::weak_ptr<TimelineModel> &parent, int id, int pos, const QString &trackName, bool audioTrack, bool singleOperation) 0115 { 0116 std::shared_ptr<TrackModel> track(new TrackModel(parent, id, trackName, audioTrack)); 0117 TRACE_CONSTR(track.get(), parent, id, pos, trackName, audioTrack); 0118 id = track->m_id; 0119 if (auto ptr = parent.lock()) { 0120 ptr->registerTrack(std::move(track), pos, true, singleOperation); 0121 } else { 0122 qDebug() << "Error : construction of track failed because parent timeline is not available anymore"; 0123 Q_ASSERT(false); 0124 } 0125 return id; 0126 } 0127 0128 int TrackModel::getClipsCount() 0129 { 0130 READ_LOCK(); 0131 #ifdef QT_DEBUG 0132 int count = 0; 0133 for (auto &m_playlist : m_playlists) { 0134 for (int i = 0; i < m_playlist.count(); i++) { 0135 if (!m_playlist.is_blank(i)) { 0136 count++; 0137 } 0138 } 0139 } 0140 Q_ASSERT(count == static_cast<int>(m_allClips.size())); 0141 #else 0142 int count = int(m_allClips.size()); 0143 #endif 0144 return count; 0145 } 0146 0147 bool TrackModel::switchPlaylist(int clipId, int position, int sourcePlaylist, int destPlaylist) 0148 { 0149 QWriteLocker locker(&m_lock); 0150 if (sourcePlaylist == destPlaylist) { 0151 return true; 0152 } 0153 Q_ASSERT(!m_playlists[sourcePlaylist].is_blank_at(position) && m_playlists[destPlaylist].is_blank_at(position)); 0154 int target_clip = m_playlists[sourcePlaylist].get_clip_index_at(position); 0155 std::unique_ptr<Mlt::Producer> prod(m_playlists[sourcePlaylist].replace_with_blank(target_clip)); 0156 m_playlists[sourcePlaylist].consolidate_blanks(); 0157 if (auto ptr = m_parent.lock()) { 0158 std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId); 0159 clip->setSubPlaylistIndex(destPlaylist, m_id); 0160 int index = m_playlists[destPlaylist].insert_at(position, *clip, 1); 0161 m_playlists[destPlaylist].consolidate_blanks(); 0162 return index != -1; 0163 } 0164 return false; 0165 } 0166 0167 Fun TrackModel::requestClipInsertion_lambda(int clipId, int position, bool updateView, bool finalMove, bool groupMove, const QList<int> &allowedClipMixes) 0168 { 0169 QWriteLocker locker(&m_lock); 0170 // By default, insertion occurs in topmost track 0171 int target_playlist = 0; 0172 int length = -1; 0173 if (auto ptr = m_parent.lock()) { 0174 Q_ASSERT(ptr->getClipPtr(clipId)->getCurrentTrackId() == -1); 0175 target_playlist = ptr->getClipPtr(clipId)->getSubPlaylistIndex(); 0176 length = ptr->getClipPtr(clipId)->getPlaytime() - 1; 0177 /*if (target_playlist == 1 && ptr->getClipPtr(clipId)->getMixDuration() == 0) { 0178 target_playlist = 0; 0179 }*/ 0180 // qDebug()<<"==== GOT TRARGET PLAYLIST: "<<target_playlist; 0181 } else { 0182 qDebug() << "impossible to get parent timeline"; 0183 Q_ASSERT(false); 0184 } 0185 // Find out the clip id at position 0186 int target_clip = m_playlists[target_playlist].get_clip_index_at(position); 0187 int count = m_playlists[target_playlist].count(); 0188 0189 // we create the function that has to be executed after the melt order. This is essentially book-keeping 0190 auto end_function = [clipId, this, position, updateView, finalMove](int subPlaylist) { 0191 if (auto ptr = m_parent.lock()) { 0192 std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId); 0193 m_allClips[clip->getId()] = clip; // store clip 0194 // update clip position and track 0195 clip->setPosition(position); 0196 if (finalMove) { 0197 clip->setSubPlaylistIndex(subPlaylist, m_id); 0198 } 0199 int new_in = clip->getPosition(); 0200 int new_out = new_in + clip->getPlaytime(); 0201 ptr->m_snaps->addPoint(new_in); 0202 ptr->m_snaps->addPoint(new_out); 0203 if (updateView) { 0204 int clip_index = getRowfromClip(clipId); 0205 ptr->_beginInsertRows(ptr->makeTrackIndexFromID(m_id), clip_index, clip_index); 0206 ptr->_endInsertRows(); 0207 bool audioOnly = clip->isAudioOnly(); 0208 if (!audioOnly && !isHidden() && !isAudioTrack()) { 0209 // only refresh monitor if not an audio track and not hidden 0210 ptr->checkRefresh(new_in, new_out); 0211 } 0212 if (!audioOnly && finalMove && !isAudioTrack()) { 0213 Q_EMIT ptr->invalidateZone(new_in, new_out); 0214 } 0215 } 0216 return true; 0217 } 0218 qDebug() << "Error : Clip Insertion failed because timeline is not available anymore"; 0219 return false; 0220 }; 0221 if (!finalMove && !hasMix(clipId)) { 0222 if (allowedClipMixes.isEmpty()) { 0223 if (!m_playlists[0].is_blank_at(position) || !m_playlists[1].is_blank_at(position)) { 0224 // Track is not empty 0225 qWarning() << "clip insert failed - non blank 1"; 0226 return []() { return false; }; 0227 } 0228 } else { 0229 // This is a group move with a mix, some clips are allowed 0230 if (!m_playlists[target_playlist].is_blank_at(position)) { 0231 // Track is not empty 0232 qWarning() << "clip insert failed - non blank 2"; 0233 return []() { return false; }; 0234 } 0235 // Check if there are clips on the other playlist, and if they are in the allowed list 0236 std::unordered_set<int> collisions = getClipsInRange(position, position + length); 0237 qDebug() << "==== DETECTING COLLISIONS AT: " << position << " to " << (position + length) << " COUNT: " << collisions.size(); 0238 for (int c : collisions) { 0239 if (!allowedClipMixes.contains(c)) { 0240 // Track is not empty 0241 qWarning() << "clip insert failed - non blank 3"; 0242 return []() { return false; }; 0243 } 0244 } 0245 } 0246 } 0247 if (target_clip >= count && m_playlists[target_playlist].is_blank_at(position)) { 0248 // In that case, we append after, in the first playlist 0249 return [this, position, clipId, end_function, finalMove, groupMove, target_playlist]() { 0250 if (isLocked()) { 0251 qWarning() << "clip insert failed - locked track"; 0252 return false; 0253 } 0254 if (auto ptr = m_parent.lock()) { 0255 // Lock MLT playlist so that we don't end up with an invalid frame being displayed 0256 std::unique_ptr<Mlt::Field> field(m_track->field()); 0257 field->block(); 0258 m_playlists[target_playlist].lock(); 0259 std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId); 0260 clip->setCurrentTrackId(m_id, finalMove); 0261 int index = m_playlists[target_playlist].insert_at(position, *clip, 1); 0262 m_playlists[target_playlist].consolidate_blanks(); 0263 m_playlists[target_playlist].unlock(); 0264 field->unblock(); 0265 if (finalMove && !groupMove) { 0266 ptr->updateDuration(); 0267 } 0268 return index != -1 && end_function(target_playlist); 0269 } 0270 qDebug() << "Error : Clip Insertion failed because timeline is not available anymore"; 0271 return false; 0272 }; 0273 } 0274 if (m_playlists[target_playlist].is_blank_at(position)) { 0275 int blank_end = getBlankEnd(position, target_playlist); 0276 if (blank_end >= position + length) { 0277 return [this, position, clipId, end_function, target_playlist]() { 0278 if (isLocked()) return false; 0279 if (auto ptr = m_parent.lock()) { 0280 // Lock MLT playlist so that we don't end up with an invalid frame being displayed 0281 std::unique_ptr<Mlt::Field> field(m_track->field()); 0282 field->block(); 0283 m_playlists[target_playlist].lock(); 0284 std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId); 0285 clip->setCurrentTrackId(m_id); 0286 int index = m_playlists[target_playlist].insert_at(position, *clip, 1); 0287 m_playlists[target_playlist].consolidate_blanks(); 0288 m_playlists[target_playlist].unlock(); 0289 field->unblock(); 0290 return index != -1 && end_function(target_playlist); 0291 } 0292 qDebug() << "Error : Clip Insertion failed because timeline is not available anymore"; 0293 return false; 0294 }; 0295 } 0296 } 0297 return []() { return false; }; 0298 } 0299 0300 bool TrackModel::requestClipInsertion(int clipId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove, bool newInsertion, 0301 const QList<int> &allowedClipMixes) 0302 { 0303 QWriteLocker locker(&m_lock); 0304 if (isLocked()) { 0305 qDebug() << "==== ERROR INSERT OK LOCKED TK"; 0306 return false; 0307 } 0308 if (position < 0) { 0309 qDebug() << "==== ERROR INSERT ON NEGATIVE POS: " << position; 0310 return false; 0311 } 0312 if (auto ptr = m_parent.lock()) { 0313 std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId); 0314 if (isAudioTrack() && !clip->canBeAudio()) { 0315 qDebug() << "// ATTEMPTING TO INSERT NON AUDIO CLIP ON AUDIO TRACK"; 0316 return false; 0317 } 0318 if (!isAudioTrack() && !clip->canBeVideo()) { 0319 qDebug() << "// ATTEMPTING TO INSERT NON VIDEO CLIP ON VIDEO TRACK"; 0320 return false; 0321 } 0322 Fun local_undo = []() { return true; }; 0323 Fun local_redo = []() { return true; }; 0324 bool res = true; 0325 if (clip->clipState() != PlaylistState::Disabled) { 0326 res = clip->setClipState(isAudioTrack() ? PlaylistState::AudioOnly : PlaylistState::VideoOnly, local_undo, local_redo); 0327 } 0328 int duration = trackDuration(); 0329 auto operation = requestClipInsertion_lambda(clipId, position, updateView, finalMove, groupMove, allowedClipMixes); 0330 res = res && operation(); 0331 if (res) { 0332 if (finalMove && duration != trackDuration()) { 0333 // A clip move changed the track duration, update track effects 0334 m_effectStack->adjustStackLength(true, 0, duration, 0, trackDuration(), 0, undo, redo, true); 0335 } 0336 auto reverse = requestClipDeletion_lambda(clipId, updateView, finalMove, groupMove, newInsertion && finalMove); 0337 UPDATE_UNDO_REDO(operation, reverse, local_undo, local_redo); 0338 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 0339 return true; 0340 } 0341 bool undone = local_undo(); 0342 Q_ASSERT(undone); 0343 return false; 0344 } 0345 return false; 0346 } 0347 0348 void TrackModel::adjustStackLength(int duration, int newDuration, Fun &undo, Fun &redo) 0349 { 0350 m_effectStack->adjustStackLength(true, 0, duration, 0, newDuration, 0, undo, redo, true); 0351 } 0352 0353 void TrackModel::temporaryUnplugClip(int clipId) 0354 { 0355 QWriteLocker locker(&m_lock); 0356 int clip_position = m_allClips[clipId]->getPosition(); 0357 int target_track = m_allClips[clipId]->getSubPlaylistIndex(); 0358 auto clip_loc = getClipIndexAt(clip_position, target_track); 0359 int target_clip = clip_loc.second; 0360 // lock MLT playlist so that we don't end up with invalid frames in monitor 0361 m_playlists[target_track].lock(); 0362 Q_ASSERT(target_clip < m_playlists[target_track].count()); 0363 Q_ASSERT(!m_playlists[target_track].is_blank(target_clip)); 0364 std::unique_ptr<Mlt::Producer> prod(m_playlists[target_track].replace_with_blank(target_clip)); 0365 m_playlists[target_track].unlock(); 0366 } 0367 0368 void TrackModel::temporaryReplugClip(int cid) 0369 { 0370 QWriteLocker locker(&m_lock); 0371 int clip_position = m_allClips[cid]->getPosition(); 0372 int target_track = m_allClips[cid]->getSubPlaylistIndex(); 0373 m_playlists[target_track].lock(); 0374 if (auto ptr = m_parent.lock()) { 0375 std::shared_ptr<ClipModel> clip = ptr->getClipPtr(cid); 0376 m_playlists[target_track].insert_at(clip_position, *clip, 1); 0377 } 0378 m_playlists[target_track].unlock(); 0379 } 0380 0381 void TrackModel::replugClip(int clipId) 0382 { 0383 QWriteLocker locker(&m_lock); 0384 int clip_position = m_allClips[clipId]->getPosition(); 0385 auto clip_loc = getClipIndexAt(clip_position, m_allClips[clipId]->getSubPlaylistIndex()); 0386 int target_track = clip_loc.first; 0387 int target_clip = clip_loc.second; 0388 // lock MLT playlist so that we don't end up with invalid frames in monitor 0389 m_playlists[target_track].lock(); 0390 Q_ASSERT(target_clip < m_playlists[target_track].count()); 0391 Q_ASSERT(!m_playlists[target_track].is_blank(target_clip)); 0392 std::unique_ptr<Mlt::Producer> prod(m_playlists[target_track].replace_with_blank(target_clip)); 0393 if (auto ptr = m_parent.lock()) { 0394 std::shared_ptr<ClipModel> clip = ptr->getClipPtr(clipId); 0395 m_playlists[target_track].insert_at(clip_position, *clip, 1); 0396 if (!clip->isAudioOnly() && !isAudioTrack()) { 0397 Q_EMIT ptr->invalidateZone(clip->getIn(), clip->getOut()); 0398 } 0399 if (!clip->isAudioOnly() && !isHidden() && !isAudioTrack()) { 0400 // only refresh monitor if not an audio track and not hidden 0401 ptr->checkRefresh(clip->getIn(), clip->getOut()); 0402 } 0403 } 0404 m_playlists[target_track].consolidate_blanks(); 0405 m_playlists[target_track].unlock(); 0406 } 0407 0408 Fun TrackModel::requestClipDeletion_lambda(int clipId, bool updateView, bool finalMove, bool groupMove, bool finalDeletion) 0409 { 0410 QWriteLocker locker(&m_lock); 0411 // Find index of clip 0412 int clip_position = m_allClips[clipId]->getPosition(); 0413 bool audioOnly = m_allClips[clipId]->isAudioOnly(); 0414 int old_in = clip_position; 0415 int old_out = old_in + m_allClips[clipId]->getPlaytime(); 0416 return [clip_position, clipId, old_in, old_out, updateView, audioOnly, finalMove, groupMove, finalDeletion, this]() { 0417 if (isLocked()) return false; 0418 if (finalDeletion && m_allClips[clipId]->selected) { 0419 m_allClips[clipId]->selected = false; 0420 if (auto ptr = m_parent.lock()) { 0421 // item was selected, unselect 0422 ptr->requestClearSelection(true); 0423 } 0424 } 0425 int target_track = m_allClips[clipId]->getSubPlaylistIndex(); 0426 auto clip_loc = getClipIndexAt(clip_position, target_track); 0427 if (updateView) { 0428 int old_clip_index = getRowfromClip(clipId); 0429 auto ptr = m_parent.lock(); 0430 ptr->_beginRemoveRows(ptr->makeTrackIndexFromID(getId()), old_clip_index, old_clip_index); 0431 ptr->_endRemoveRows(); 0432 } 0433 int target_clip = clip_loc.second; 0434 // lock MLT playlist so that we don't end up with invalid frames in monitor 0435 m_playlists[target_track].lock(); 0436 std::unique_ptr<Mlt::Field> field(m_track->field()); 0437 field->block(); 0438 Q_ASSERT(target_clip < m_playlists[target_track].count()); 0439 Q_ASSERT(!m_playlists[target_track].is_blank(target_clip)); 0440 auto prod = m_playlists[target_track].replace_with_blank(target_clip); 0441 if (prod != nullptr) { 0442 m_playlists[target_track].consolidate_blanks(); 0443 m_allClips[clipId]->setCurrentTrackId(-1); 0444 // m_allClips[clipId]->setSubPlaylistIndex(-1); 0445 m_allClips.erase(clipId); 0446 delete prod; 0447 field->unblock(); 0448 m_playlists[target_track].unlock(); 0449 if (auto ptr = m_parent.lock()) { 0450 ptr->m_snaps->removePoint(old_in); 0451 ptr->m_snaps->removePoint(old_out); 0452 if (finalMove && !ptr->m_closing) { 0453 if (!audioOnly && !isAudioTrack()) { 0454 Q_EMIT ptr->invalidateZone(old_in, old_out); 0455 } 0456 if (!groupMove && target_clip >= m_playlists[target_track].count()) { 0457 // deleted last clip in playlist 0458 ptr->updateDuration(); 0459 } 0460 } 0461 if (!audioOnly && !isHidden() && !isAudioTrack()) { 0462 // only refresh monitor if not an audio track and not hidden 0463 ptr->checkRefresh(old_in, old_out); 0464 } 0465 } 0466 return true; 0467 } 0468 field->unblock(); 0469 m_playlists[target_track].unlock(); 0470 return false; 0471 }; 0472 } 0473 0474 bool TrackModel::requestClipDeletion(int clipId, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool groupMove, bool finalDeletion, 0475 const QList<int> &allowedClipMixes) 0476 { 0477 QWriteLocker locker(&m_lock); 0478 Q_ASSERT(m_allClips.count(clipId) > 0); 0479 if (isLocked()) { 0480 return false; 0481 } 0482 auto old_clip = m_allClips[clipId]; 0483 bool closing = false; 0484 QUuid timelineUuid; 0485 if (auto ptr = m_parent.lock()) { 0486 closing = ptr->m_closing; 0487 timelineUuid = ptr->uuid(); 0488 } 0489 int old_position = old_clip->getPosition(); 0490 // qDebug() << "/// REQUESTOING CLIP DELETION_: " << updateView; 0491 int duration = trackDuration(); 0492 if (finalDeletion) { 0493 pCore->taskManager.discardJobs(ObjectId(KdenliveObjectType::TimelineClip, clipId, timelineUuid)); 0494 } 0495 auto operation = requestClipDeletion_lambda(clipId, updateView, finalMove, groupMove, finalDeletion); 0496 if (operation()) { 0497 if (!closing && finalMove && duration != trackDuration()) { 0498 // A clip move changed the track duration, update track effects 0499 m_effectStack->adjustStackLength(true, 0, duration, 0, trackDuration(), 0, undo, redo, true); 0500 } 0501 auto reverse = requestClipInsertion_lambda(clipId, old_position, updateView, finalMove, groupMove, allowedClipMixes); 0502 UPDATE_UNDO_REDO(operation, reverse, undo, redo); 0503 return true; 0504 } 0505 return false; 0506 } 0507 0508 int TrackModel::getBlankSizeAtPos(int frame) 0509 { 0510 READ_LOCK(); 0511 int min_length = 0; 0512 int blank_length = 0; 0513 for (auto &m_playlist : m_playlists) { 0514 int playlistLength = m_playlist.get_length(); 0515 if (frame >= playlistLength) { 0516 blank_length = frame - playlistLength + 1; 0517 } else { 0518 int ix = m_playlist.get_clip_index_at(frame); 0519 if (m_playlist.is_blank(ix)) { 0520 blank_length = m_playlist.clip_length(ix); 0521 } else { 0522 // There is a clip at that position, abort 0523 return 0; 0524 } 0525 } 0526 if (min_length == 0 || blank_length < min_length) { 0527 min_length = blank_length; 0528 } 0529 } 0530 if (blank_length == 0) { 0531 // playlists are shorter than frame 0532 return -1; 0533 } 0534 return min_length; 0535 } 0536 0537 int TrackModel::suggestCompositionLength(int position) 0538 { 0539 READ_LOCK(); 0540 if (m_playlists[0].is_blank_at(position) && m_playlists[1].is_blank_at(position)) { 0541 return -1; 0542 } 0543 auto clip_loc = getClipIndexAt(position); 0544 int track = clip_loc.first; 0545 int index = clip_loc.second; 0546 int other_index; // index in the other track 0547 int other_track = 1 - track; 0548 int end_pos = m_playlists[track].clip_start(index) + m_playlists[track].clip_length(index); 0549 other_index = m_playlists[other_track].get_clip_index_at(end_pos); 0550 if (other_index < m_playlists[other_track].count()) { 0551 end_pos = std::min(end_pos, m_playlists[other_track].clip_start(other_index) + m_playlists[other_track].clip_length(other_index)); 0552 } 0553 return end_pos - position; 0554 } 0555 0556 QPair<int, int> TrackModel::validateCompositionLength(int pos, int offset, int duration, int endPos) 0557 { 0558 int startPos = pos; 0559 bool startingFromOffset = false; 0560 if (duration < offset) { 0561 startPos += offset; 0562 startingFromOffset = true; 0563 if (startPos + duration > endPos) { 0564 startPos = endPos - duration; 0565 } 0566 } 0567 0568 int compsitionEnd = startPos + duration; 0569 std::unordered_set<int> existing; 0570 if (startingFromOffset) { 0571 existing = getCompositionsInRange(startPos, compsitionEnd); 0572 for (int id : existing) { 0573 if (m_allCompositions[id]->getPosition() < startPos) { 0574 int end = m_allCompositions[id]->getPosition() + m_allCompositions[id]->getPlaytime(); 0575 startPos = qMax(startPos, end); 0576 } 0577 } 0578 } else if (offset > 0) { 0579 existing = getCompositionsInRange(startPos, startPos + offset); 0580 for (int id : existing) { 0581 int end = m_allCompositions[id]->getPosition() + m_allCompositions[id]->getPlaytime(); 0582 startPos = qMax(startPos, end); 0583 } 0584 } 0585 existing = getCompositionsInRange(startPos, compsitionEnd); 0586 for (int id : existing) { 0587 int start = m_allCompositions[id]->getPosition(); 0588 compsitionEnd = qMin(compsitionEnd, start); 0589 } 0590 return {startPos, compsitionEnd - startPos}; 0591 } 0592 0593 int TrackModel::getBlankSizeNearClip(int clipId, bool after) 0594 { 0595 READ_LOCK(); 0596 Q_ASSERT(m_allClips.count(clipId) > 0); 0597 int clip_position = m_allClips[clipId]->getPosition(); 0598 auto clip_loc = getClipIndexAt(clip_position); 0599 int track = clip_loc.first; 0600 int index = clip_loc.second; 0601 int other_index; // index in the other track 0602 int other_track = 1 - track; 0603 if (after) { 0604 int first_pos = m_playlists[track].clip_start(index) + m_playlists[track].clip_length(index); 0605 other_index = m_playlists[other_track].get_clip_index_at(first_pos); 0606 index++; 0607 } else { 0608 int last_pos = m_playlists[track].clip_start(index) - 1; 0609 other_index = m_playlists[other_track].get_clip_index_at(last_pos); 0610 index--; 0611 } 0612 if (index < 0) return 0; 0613 int length = INT_MAX; 0614 if (index < m_playlists[track].count()) { 0615 if (!m_playlists[track].is_blank(index)) { 0616 return 0; 0617 } 0618 length = std::min(length, m_playlists[track].clip_length(index)); 0619 } else if (!after) { 0620 length = std::min(length, m_playlists[track].clip_start(clip_loc.second) - m_playlists[track].get_length()); 0621 } 0622 if (other_index < m_playlists[other_track].count()) { 0623 if (!m_playlists[other_track].is_blank(other_index)) { 0624 return 0; 0625 } 0626 length = std::min(length, m_playlists[other_track].clip_length(other_index)); 0627 } else if (!after) { 0628 length = std::min(length, m_playlists[track].clip_start(clip_loc.second) - m_playlists[other_track].get_length()); 0629 } 0630 return length; 0631 } 0632 0633 int TrackModel::getBlankSizeNearComposition(int compoId, bool after) 0634 { 0635 READ_LOCK(); 0636 Q_ASSERT(m_allCompositions.count(compoId) > 0); 0637 int clip_position = m_allCompositions[compoId]->getPosition(); 0638 Q_ASSERT(m_compoPos.count(clip_position) > 0); 0639 Q_ASSERT(m_compoPos[clip_position] == compoId); 0640 auto it = m_compoPos.find(clip_position); 0641 int clip_length = m_allCompositions[compoId]->getPlaytime(); 0642 int length = INT_MAX; 0643 if (after) { 0644 ++it; 0645 if (it != m_compoPos.end()) { 0646 return it->first - clip_position - clip_length; 0647 } 0648 } else { 0649 if (it != m_compoPos.begin()) { 0650 --it; 0651 return clip_position - it->first - m_allCompositions[it->second]->getPlaytime(); 0652 } 0653 return clip_position; 0654 } 0655 return length; 0656 } 0657 0658 Fun TrackModel::requestClipResize_lambda(int clipId, int in, int out, bool right, bool hasMix, bool finalMove) 0659 { 0660 QWriteLocker locker(&m_lock); 0661 int clip_position = m_allClips[clipId]->getPosition(); 0662 int old_in = clip_position; 0663 int old_out = old_in + m_allClips[clipId]->getPlaytime(); 0664 auto clip_loc = getClipIndexAt(clip_position, m_allClips[clipId]->getSubPlaylistIndex()); 0665 int target_track = clip_loc.first; 0666 int target_clip = clip_loc.second; 0667 Q_ASSERT(target_clip < m_playlists[target_track].count()); 0668 int size = out - in + 1; 0669 bool checkRefresh = false; 0670 if (!isHidden() && !isAudioTrack()) { 0671 checkRefresh = true; 0672 } 0673 auto update_snaps = [old_in, old_out, checkRefresh, right, this](int new_in, int new_out) { 0674 if (auto ptr = m_parent.lock()) { 0675 if (right) { 0676 ptr->m_snaps->removePoint(old_out); 0677 ptr->m_snaps->addPoint(new_out); 0678 } else { 0679 ptr->m_snaps->removePoint(old_in); 0680 ptr->m_snaps->addPoint(new_in); 0681 } 0682 if (checkRefresh) { 0683 if (right) { 0684 if (old_out < new_out) { 0685 ptr->checkRefresh(old_out, new_out); 0686 } else { 0687 ptr->checkRefresh(new_out, old_out); 0688 } 0689 } else if (old_in < new_in) { 0690 ptr->checkRefresh(old_in, new_in); 0691 } else { 0692 ptr->checkRefresh(new_in, old_in); 0693 } 0694 } 0695 } else { 0696 qDebug() << "Error : clip resize failed because parent timeline is not available anymore"; 0697 Q_ASSERT(false); 0698 } 0699 }; 0700 0701 int delta = m_allClips[clipId]->getPlaytime() - size; 0702 if (delta == 0) { 0703 return []() { return true; }; 0704 } 0705 if (delta > 0) { // we shrink clip 0706 return [right, target_clip, target_track, clip_position, delta, in, out, clipId, update_snaps, finalMove, this]() { 0707 if (isLocked()) return false; 0708 int target_clip_mutable = target_clip; 0709 int blank_index = right ? (target_clip_mutable + 1) : target_clip_mutable; 0710 // insert blank to space that is going to be empty 0711 m_playlists[target_track].lock(); 0712 // The second is parameter is delta - 1 because this function expects an out time, which is basically size - 1 0713 m_playlists[target_track].insert_blank(blank_index, delta - 1); 0714 if (!right) { 0715 m_allClips[clipId]->setPosition(clip_position + delta); 0716 // Because we inserted blank before, the index of our clip has increased 0717 target_clip_mutable++; 0718 } 0719 int err = m_playlists[target_track].resize_clip(target_clip_mutable, in, out); 0720 // make sure to do this after, to avoid messing the indexes 0721 m_playlists[target_track].consolidate_blanks(); 0722 m_playlists[target_track].unlock(); 0723 if (err == 0) { 0724 update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1); 0725 if (right && finalMove && m_playlists[target_track].count() - 1 == target_clip_mutable) { 0726 // deleted last clip in playlist 0727 if (auto ptr = m_parent.lock()) { 0728 ptr->updateDuration(); 0729 } 0730 } 0731 } 0732 return err == 0; 0733 }; 0734 } 0735 int blank = -1; 0736 int startPos = clip_position; 0737 if (hasMix) { 0738 startPos += m_allClips[clipId]->getMixDuration(); 0739 } 0740 int endPos = m_allClips[clipId]->getPosition() + (out - in); 0741 int other_blank_end = getBlankEnd(startPos, 1 - target_track); 0742 if (right) { 0743 if (target_clip == m_playlists[target_track].count() - 1 && (hasMix || other_blank_end >= endPos)) { 0744 // clip is last, it can always be extended 0745 if (hasMix && other_blank_end < endPos && !hasEndMix(clipId)) { 0746 // If clip has a start mix only, limit to next clip on other track 0747 return []() { return false; }; 0748 } 0749 return [this, target_clip, target_track, in, out, update_snaps, clipId, finalMove]() { 0750 if (isLocked()) return false; 0751 // color, image and title clips can have unlimited resize 0752 QScopedPointer<Mlt::Producer> clip(m_playlists[target_track].get_clip(target_clip)); 0753 if (out >= clip->get_length()) { 0754 clip->parent().set("length", out + 1); 0755 clip->parent().set("out", out); 0756 clip->set("length", out + 1); 0757 } 0758 int err = m_playlists[target_track].resize_clip(target_clip, in, out); 0759 if (err == 0) { 0760 update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1); 0761 } 0762 m_playlists[target_track].consolidate_blanks(); 0763 if (finalMove && m_playlists[target_track].count() - 1 == target_clip) { 0764 // Resized last clip in playlist 0765 if (auto ptr = m_parent.lock()) { 0766 ptr->updateDuration(); 0767 } 0768 } 0769 return err == 0; 0770 }; 0771 } else { 0772 } 0773 blank = target_clip + 1; 0774 } else { 0775 if (target_clip == 0) { 0776 // clip is first, it can never be extended on the left 0777 return []() { return false; }; 0778 } 0779 blank = target_clip - 1; 0780 } 0781 if (m_playlists[target_track].is_blank(blank)) { 0782 int blank_length = m_playlists[target_track].clip_length(blank); 0783 if (blank_length + delta >= 0 && (hasMix || other_blank_end >= out - in)) { 0784 return [blank_length, blank, right, clipId, delta, update_snaps, this, in, out, target_clip, target_track]() { 0785 if (isLocked()) return false; 0786 int target_clip_mutable = target_clip; 0787 int err = 0; 0788 m_playlists[target_track].lock(); 0789 if (blank_length + delta == 0) { 0790 err = m_playlists[target_track].remove(blank); 0791 if (!right) { 0792 target_clip_mutable--; 0793 } 0794 } else { 0795 err = m_playlists[target_track].resize_clip(blank, 0, blank_length + delta - 1); 0796 } 0797 if (err == 0) { 0798 // m_track->block(); 0799 QScopedPointer<Mlt::Producer> clip(m_playlists[target_track].get_clip(target_clip_mutable)); 0800 if (out >= clip->get_length()) { 0801 clip->parent().set("length", out + 1); 0802 clip->parent().set("out", out); 0803 clip->set("length", out + 1); 0804 clip->set("out", out); 0805 } 0806 err = m_playlists[target_track].resize_clip(target_clip_mutable, in, out); 0807 // m_track->unblock(); 0808 } 0809 if (!right && err == 0) { 0810 m_allClips[clipId]->setPosition(m_playlists[target_track].clip_start(target_clip_mutable)); 0811 } 0812 if (err == 0) { 0813 update_snaps(m_allClips[clipId]->getPosition(), m_allClips[clipId]->getPosition() + out - in + 1); 0814 } 0815 m_playlists[target_track].consolidate_blanks(); 0816 m_playlists[target_track].unlock(); 0817 return err == 0; 0818 }; 0819 } else { 0820 } 0821 } 0822 return []() { 0823 qDebug() << "=====FULL FAILURE "; 0824 return false; 0825 }; 0826 } 0827 0828 int TrackModel::getId() const 0829 { 0830 return m_id; 0831 } 0832 0833 int TrackModel::getClipByStartPosition(int position) const 0834 { 0835 READ_LOCK(); 0836 for (auto &clip : m_allClips) { 0837 if (clip.second->getPosition() == position) { 0838 return clip.second->getId(); 0839 } 0840 } 0841 return -1; 0842 } 0843 0844 int TrackModel::getClipByPosition(int position, int playlist) 0845 { 0846 READ_LOCK(); 0847 QSharedPointer<Mlt::Producer> prod(nullptr); 0848 if ((playlist == 0 || playlist == -1) && m_playlists[0].count() > 0) { 0849 prod = QSharedPointer<Mlt::Producer>(m_playlists[0].get_clip_at(position)); 0850 } 0851 if (playlist != 0 && (!prod || prod->is_blank()) && m_playlists[1].count() > 0) { 0852 prod = QSharedPointer<Mlt::Producer>(m_playlists[1].get_clip_at(position)); 0853 } 0854 if (!prod || prod->is_blank()) { 0855 return -1; 0856 } 0857 int cid = prod->get_int("_kdenlive_cid"); 0858 if (playlist == -1) { 0859 if (hasStartMix(cid)) { 0860 if (position < m_allClips[cid]->getPosition() + m_allClips[cid]->getMixCutPosition()) { 0861 return m_mixList.key(cid, -1); 0862 } 0863 } 0864 if (m_mixList.contains(cid)) { 0865 // Clip has end mix 0866 int otherId = m_mixList.value(cid); 0867 if (position >= m_allClips[cid]->getPosition() + m_allClips[cid]->getPlaytime() - m_allClips[otherId]->getMixCutPosition()) { 0868 return otherId; 0869 } 0870 } 0871 } 0872 return cid; 0873 } 0874 0875 QSharedPointer<Mlt::Producer> TrackModel::getClipProducer(int clipId) 0876 { 0877 READ_LOCK(); 0878 QSharedPointer<Mlt::Producer> prod(nullptr); 0879 if (m_playlists[0].count() > 0) { 0880 prod = QSharedPointer<Mlt::Producer>(m_playlists[0].get_clip(clipId)); 0881 } 0882 if ((!prod || prod->is_blank()) && m_playlists[1].count() > 0) { 0883 prod = QSharedPointer<Mlt::Producer>(m_playlists[1].get_clip(clipId)); 0884 } 0885 return prod; 0886 } 0887 0888 int TrackModel::getCompositionByPosition(int position) 0889 { 0890 READ_LOCK(); 0891 for (const auto &comp : m_compoPos) { 0892 if (comp.first == position) { 0893 return comp.second; 0894 } else if (comp.first < position) { 0895 if (comp.first + m_allCompositions[comp.second]->getPlaytime() >= position) { 0896 return comp.second; 0897 } 0898 } 0899 } 0900 return -1; 0901 } 0902 0903 int TrackModel::getClipByRow(int row) const 0904 { 0905 READ_LOCK(); 0906 if (row >= static_cast<int>(m_allClips.size())) { 0907 return -1; 0908 } 0909 auto it = m_allClips.cbegin(); 0910 std::advance(it, row); 0911 return (*it).first; 0912 } 0913 0914 std::unordered_set<int> TrackModel::getClipsInRange(int position, int end) 0915 { 0916 READ_LOCK(); 0917 std::unordered_set<int> ids; 0918 for (const auto &clp : m_allClips) { 0919 int pos = clp.second->getPosition(); 0920 int length = clp.second->getPlaytime(); 0921 if (end > -1 && pos >= end) { 0922 continue; 0923 } 0924 if (pos >= position || pos + length - 1 >= position) { 0925 ids.insert(clp.first); 0926 } 0927 } 0928 return ids; 0929 } 0930 0931 int TrackModel::getRowfromClip(int clipId) const 0932 { 0933 READ_LOCK(); 0934 Q_ASSERT(m_allClips.count(clipId) > 0); 0935 return int(std::distance(m_allClips.begin(), m_allClips.find(clipId))); 0936 } 0937 0938 std::unordered_set<int> TrackModel::getCompositionsInRange(int position, int end) 0939 { 0940 READ_LOCK(); 0941 // TODO: this function doesn't take into accounts the fact that there are two tracks 0942 std::unordered_set<int> ids; 0943 for (const auto &compo : m_allCompositions) { 0944 int pos = compo.second->getPosition(); 0945 int length = compo.second->getPlaytime(); 0946 if (end > -1 && pos >= end) { 0947 continue; 0948 } 0949 if (pos >= position || pos + length - 1 >= position) { 0950 0951 ids.insert(compo.first); 0952 } 0953 } 0954 return ids; 0955 } 0956 0957 int TrackModel::getRowfromComposition(int tid) const 0958 { 0959 READ_LOCK(); 0960 Q_ASSERT(m_allCompositions.count(tid) > 0); 0961 return int(m_allClips.size()) + int(std::distance(m_allCompositions.begin(), m_allCompositions.find(tid))); 0962 } 0963 0964 QVariant TrackModel::getProperty(const QString &name) const 0965 { 0966 READ_LOCK(); 0967 return QString(m_track->get(name.toUtf8().constData())); 0968 } 0969 0970 void TrackModel::setProperty(const QString &name, const QString &value) 0971 { 0972 QWriteLocker locker(&m_lock); 0973 m_track->set(name.toUtf8().constData(), value.toUtf8().constData()); 0974 // Hide property must be defined at playlist level or it won't be saved 0975 if (name == QLatin1String("kdenlive:audio_track") || name == QLatin1String("hide")) { 0976 for (auto &m_playlist : m_playlists) { 0977 m_playlist.set(name.toUtf8().constData(), value.toInt()); 0978 } 0979 } 0980 } 0981 0982 bool TrackModel::checkConsistency() 0983 { 0984 auto ptr = m_parent.lock(); 0985 if (!ptr) { 0986 return false; 0987 } 0988 auto check_blank_zone = [&](int playlist, int in, int out) { 0989 if (in >= m_playlists[playlist].get_playtime()) { 0990 return true; 0991 } 0992 int index = m_playlists[playlist].get_clip_index_at(in); 0993 if (!m_playlists[playlist].is_blank(index)) { 0994 return false; 0995 } 0996 int cin = m_playlists[playlist].clip_start(index); 0997 if (cin > in) { 0998 return false; 0999 } 1000 if (cin + m_playlists[playlist].clip_length(index) - 1 < out) { 1001 return false; 1002 } 1003 return true; 1004 }; 1005 std::vector<std::pair<int, int>> clips; // clips stored by (position, id) 1006 for (const auto &c : m_allClips) { 1007 Q_ASSERT(c.second); 1008 Q_ASSERT(c.second.get() == ptr->getClipPtr(c.first).get()); 1009 clips.emplace_back(c.second->getPosition(), c.first); 1010 } 1011 std::sort(clips.begin(), clips.end()); 1012 int last_out = 0; 1013 for (size_t i = 0; i < clips.size(); ++i) { 1014 auto cur_clip = m_allClips[clips[i].second]; 1015 if (last_out < clips[i].first) { 1016 // we have some blank space before this clip, check it 1017 for (int pl = 0; pl <= 1; ++pl) { 1018 if (!check_blank_zone(pl, last_out, clips[i].first - 1)) { 1019 qDebug() << "ERROR: Some blank was required on playlist " << pl << " between " << last_out << " and " << clips[i].first - 1; 1020 return false; 1021 } 1022 } 1023 } 1024 int cur_playlist = cur_clip->getSubPlaylistIndex(); 1025 int clip_index = m_playlists[cur_playlist].get_clip_index_at(clips[i].first); 1026 if (m_playlists[cur_playlist].is_blank(clip_index)) { 1027 qDebug() << "ERROR: Found blank when clip was required at position " << clips[i].first; 1028 return false; 1029 } 1030 if (m_playlists[cur_playlist].clip_start(clip_index) != clips[i].first) { 1031 qDebug() << "ERROR: Inconsistent start position for clip at position " << clips[i].first; 1032 return false; 1033 } 1034 if (m_playlists[cur_playlist].clip_start(clip_index) != clips[i].first) { 1035 qDebug() << "ERROR: Inconsistent start position for clip at position " << clips[i].first; 1036 return false; 1037 } 1038 if (m_playlists[cur_playlist].clip_length(clip_index) != cur_clip->getPlaytime()) { 1039 qDebug() << "ERROR: Inconsistent length for clip at position " << clips[i].first; 1040 return false; 1041 } 1042 auto pr = m_playlists[cur_playlist].get_clip(clip_index); 1043 Mlt::Producer prod(pr); 1044 if (!prod.same_clip(*cur_clip)) { 1045 qDebug() << "ERROR: Wrong clip at position " << clips[i].first; 1046 delete pr; 1047 return false; 1048 } 1049 delete pr; 1050 1051 // the current playlist is valid, we check that the other is essentially blank 1052 int other_playlist = (cur_playlist + 1) % 2; 1053 int in_blank = clips[i].first; 1054 int out_blank = clips[i].first + cur_clip->getPlaytime() - 1; 1055 1056 // the previous clip on the same playlist must not intersect 1057 int prev_clip_id_same_playlist = -1; 1058 for (int j = int(i) - 1; j >= 0; --j) { 1059 if (cur_playlist == m_allClips[clips[size_t(j)].second]->getSubPlaylistIndex()) { 1060 prev_clip_id_same_playlist = j; 1061 break; 1062 } 1063 } 1064 if (prev_clip_id_same_playlist >= 0 && 1065 clips[size_t(prev_clip_id_same_playlist)].first + m_allClips[clips[size_t(prev_clip_id_same_playlist)].second]->getPlaytime() > clips[i].first) { 1066 qDebug() << "ERROR: found overlapping clips at position " << clips[i].first; 1067 return false; 1068 } 1069 1070 // the previous clip on the other playlist might restrict the blank in/out 1071 int prev_clip_id_other_playlist = -1; 1072 for (int j = int(i) - 1; j >= 0; --j) { 1073 if (other_playlist == m_allClips[clips[size_t(j)].second]->getSubPlaylistIndex()) { 1074 prev_clip_id_other_playlist = j; 1075 break; 1076 } 1077 } 1078 if (prev_clip_id_other_playlist >= 0) { 1079 in_blank = std::max(in_blank, clips[size_t(prev_clip_id_other_playlist)].first + 1080 m_allClips[clips[size_t(prev_clip_id_other_playlist)].second]->getPlaytime()); 1081 } 1082 1083 // the next clip on the other playlist might restrict the blank in/out 1084 int next_clip_id_other_playlist = -1; 1085 for (size_t j = i + 1; j < clips.size(); ++j) { 1086 if (other_playlist == m_allClips[clips[j].second]->getSubPlaylistIndex()) { 1087 next_clip_id_other_playlist = int(j); 1088 break; 1089 } 1090 } 1091 if (next_clip_id_other_playlist >= 0) { 1092 out_blank = std::min(out_blank, clips[size_t(next_clip_id_other_playlist)].first - 1); 1093 } 1094 if (in_blank <= out_blank && !check_blank_zone(other_playlist, in_blank, out_blank)) { 1095 qDebug() << "ERROR: we expected blank on playlist " << other_playlist << " between " << in_blank << " and " << out_blank; 1096 return false; 1097 } 1098 1099 last_out = clips[i].first + cur_clip->getPlaytime(); 1100 } 1101 int playtime = std::max(m_playlists[0].get_playtime(), m_playlists[1].get_playtime()); 1102 if (!clips.empty() && playtime != clips.back().first + m_allClips[clips.back().second]->getPlaytime()) { 1103 qDebug() << "Error: playtime is " << playtime << " but was expected to be" << clips.back().first + m_allClips[clips.back().second]->getPlaytime(); 1104 return false; 1105 } 1106 1107 // We now check compositions positions 1108 if (m_allCompositions.size() != m_compoPos.size()) { 1109 qDebug() << "Error: the number of compositions position doesn't match number of compositions"; 1110 return false; 1111 } 1112 for (const auto &compo : m_allCompositions) { 1113 int pos = compo.second->getPosition(); 1114 if (m_compoPos.count(pos) == 0) { 1115 qDebug() << "Error: the position of composition " << compo.first << " is not properly stored"; 1116 return false; 1117 } 1118 if (m_compoPos[pos] != compo.first) { 1119 qDebug() << "Error: found composition" << m_compoPos[pos] << "instead of " << compo.first << "at position" << pos; 1120 return false; 1121 } 1122 } 1123 for (auto it = m_compoPos.begin(); it != m_compoPos.end(); ++it) { 1124 int compoId = it->second; 1125 int cur_in = m_allCompositions[compoId]->getPosition(); 1126 Q_ASSERT(cur_in == it->first); 1127 int cur_out = cur_in + m_allCompositions[compoId]->getPlaytime() - 1; 1128 ++it; 1129 if (it != m_compoPos.end()) { 1130 int next_compoId = it->second; 1131 int next_in = m_allCompositions[next_compoId]->getPosition(); 1132 int next_out = next_in + m_allCompositions[next_compoId]->getPlaytime() - 1; 1133 if (next_in <= cur_out) { 1134 qDebug() << "Error: found collision between composition " << compoId << "[ " << cur_in << ", " << cur_out << "] and " << next_compoId << "[ " 1135 << next_in << ", " << next_out << "]"; 1136 return false; 1137 } 1138 } 1139 --it; 1140 } 1141 // Check Mixes 1142 QScopedPointer<Mlt::Service> service(m_track->field()); 1143 int mixCount = 0; 1144 while (service != nullptr && service->is_valid()) { 1145 if (service->type() == mlt_service_transition_type) { 1146 Mlt::Transition t(mlt_transition(service->get_service())); 1147 service.reset(service->producer()); 1148 // Check that the mix has correct in/out 1149 int mainId = -1; 1150 int mixIn = t.get_in(); 1151 for (auto &sameComposition : m_sameCompositions) { 1152 if (static_cast<Mlt::Transition *>(sameComposition.second->getAsset())->get_in() == mixIn) { 1153 // Found mix in list 1154 mainId = sameComposition.first; 1155 break; 1156 } 1157 } 1158 if (mainId == -1) { 1159 qDebug() << "=== Incoherent mix found at: " << mixIn; 1160 return false; 1161 } 1162 // Check in/out) 1163 if (mixIn != m_allClips[mainId]->getPosition()) { 1164 qDebug() << "=== Mix not aligned with its master clip: " << mainId << ", at: " << m_allClips[mainId]->getPosition() << ", MIX at: " << mixIn; 1165 return false; 1166 } 1167 int secondClipId = m_mixList.key(mainId); 1168 if (t.get_out() != m_allClips[secondClipId]->getPosition() + m_allClips[secondClipId]->getPlaytime()) { 1169 qDebug() << "=== Mix not aligned with its second clip: " << secondClipId 1170 << ", end at: " << m_allClips[secondClipId]->getPosition() + m_allClips[secondClipId]->getPlaytime() << ", MIX at: " << t.get_out(); 1171 return false; 1172 } 1173 mixCount++; 1174 } else { 1175 service.reset(service->producer()); 1176 } 1177 } 1178 if (mixCount != static_cast<int>(m_sameCompositions.size()) || static_cast<int>(m_sameCompositions.size()) != m_mixList.count()) { 1179 // incoherent mix count 1180 qDebug() << "=== INCORRECT mix count. Existing: " << mixCount << "; REGISTERED: " << m_mixList.count(); 1181 return false; 1182 } 1183 return true; 1184 } 1185 1186 std::pair<int, int> TrackModel::getClipIndexAt(int position, int playlist) 1187 { 1188 READ_LOCK(); 1189 if (playlist == -1) { 1190 for (int j = 0; j < 2; j++) { 1191 if (!m_playlists[j].is_blank_at(position)) { 1192 return {j, m_playlists[j].get_clip_index_at(position)}; 1193 } 1194 } 1195 return {-1, -1}; 1196 } 1197 if (!m_playlists[playlist].is_blank_at(position)) { 1198 return {playlist, m_playlists[playlist].get_clip_index_at(position)}; 1199 } 1200 qDebug() << "=== CANNOT FIND CLIP ON PLAYLIST: " << playlist << " AT POSITION: " << position << ", TID: " << m_id; 1201 Q_ASSERT(false); 1202 return {-1, -1}; 1203 } 1204 1205 bool TrackModel::isLastClip(int position) 1206 { 1207 READ_LOCK(); 1208 for (auto &m_playlist : m_playlists) { 1209 if (!m_playlist.is_blank_at(position)) { 1210 return m_playlist.get_clip_index_at(position) == m_playlist.count() - 1; 1211 } 1212 } 1213 return false; 1214 } 1215 1216 bool TrackModel::isBlankAt(int position, int playlist) 1217 { 1218 READ_LOCK(); 1219 if (playlist == -1) { 1220 return m_playlists[0].is_blank_at(position) && m_playlists[1].is_blank_at(position); 1221 } 1222 return m_playlists[playlist].is_blank_at(position); 1223 } 1224 1225 int TrackModel::getNextBlankStart(int position) 1226 { 1227 while (!isBlankAt(position)) { 1228 int end1 = getClipEnd(position, 0); 1229 int end2 = getClipEnd(position, 1); 1230 if (end1 > position) { 1231 position = end1; 1232 } else if (end2 > position) { 1233 position = end2; 1234 } else { 1235 // We reached playlist end 1236 return -1; 1237 } 1238 } 1239 return getBlankStart(position); 1240 } 1241 1242 int TrackModel::getBlankStart(int position) 1243 { 1244 READ_LOCK(); 1245 int result = 0; 1246 for (auto &playlist : m_playlists) { 1247 if (playlist.count() == 0) { 1248 break; 1249 } 1250 if (!playlist.is_blank_at(position)) { 1251 result = position; 1252 break; 1253 } 1254 int clip_index = playlist.get_clip_index_at(position); 1255 int start = playlist.clip_start(clip_index); 1256 if (start > result) { 1257 result = start; 1258 } 1259 } 1260 return result; 1261 } 1262 1263 int TrackModel::getClipStart(int position, int track) 1264 { 1265 if (track == -1) { 1266 return getBlankStart(position); 1267 } 1268 READ_LOCK(); 1269 if (m_playlists[track].is_blank_at(position)) { 1270 return position; 1271 } 1272 int clip_index = m_playlists[track].get_clip_index_at(position); 1273 return m_playlists[track].clip_start(clip_index); 1274 } 1275 1276 int TrackModel::getClipEnd(int position, int track) 1277 { 1278 if (track == -1) { 1279 return getBlankStart(position); 1280 } 1281 READ_LOCK(); 1282 if (m_playlists[track].is_blank_at(position)) { 1283 return position; 1284 } 1285 int clip_index = m_playlists[track].get_clip_index_at(position); 1286 clip_index++; 1287 return m_playlists[track].clip_start(clip_index); 1288 } 1289 1290 int TrackModel::getBlankStart(int position, int track) 1291 { 1292 if (track == -1) { 1293 return getBlankStart(position); 1294 } 1295 READ_LOCK(); 1296 int result = 0; 1297 if (!m_playlists[track].is_blank_at(position)) { 1298 return position; 1299 } 1300 int clip_index = m_playlists[track].get_clip_index_at(position); 1301 int start = m_playlists[track].clip_start(clip_index); 1302 if (start > result) { 1303 result = start; 1304 } 1305 return result; 1306 } 1307 1308 int TrackModel::getBlankEnd(int position, int track) 1309 { 1310 if (track == -1) { 1311 return getBlankEnd(position); 1312 } 1313 READ_LOCK(); 1314 // Q_ASSERT(m_playlists[track].is_blank_at(position)); 1315 if (!m_playlists[track].is_blank_at(position)) { 1316 return position; 1317 } 1318 int clip_index = m_playlists[track].get_clip_index_at(position); 1319 int count = m_playlists[track].count(); 1320 if (clip_index < count) { 1321 int blank_start = m_playlists[track].clip_start(clip_index); 1322 int blank_length = m_playlists[track].clip_length(clip_index) - 1; 1323 return blank_start + blank_length; 1324 } 1325 return INT_MAX; 1326 } 1327 1328 int TrackModel::getBlankEnd(int position) 1329 { 1330 READ_LOCK(); 1331 int end = INT_MAX; 1332 for (int j = 0; j < 2; j++) { 1333 end = std::min(getBlankEnd(position, j), end); 1334 } 1335 return end; 1336 } 1337 1338 Fun TrackModel::requestCompositionResize_lambda(int compoId, int in, int out, bool logUndo) 1339 { 1340 QWriteLocker locker(&m_lock); 1341 int compo_position = m_allCompositions[compoId]->getPosition(); 1342 Q_ASSERT(m_compoPos.count(compo_position) > 0); 1343 Q_ASSERT(m_compoPos[compo_position] == compoId); 1344 int old_in = compo_position; 1345 int old_out = old_in + m_allCompositions[compoId]->getPlaytime() - 1; 1346 qDebug() << "compo resize " << compoId << in << "-" << out << " / " << old_in << "-" << old_out; 1347 if (out == -1) { 1348 out = in + old_out - old_in; 1349 } 1350 1351 auto update_snaps = [old_in, old_out, logUndo, this](int new_in, int new_out) { 1352 if (auto ptr = m_parent.lock()) { 1353 ptr->m_snaps->removePoint(old_in); 1354 ptr->m_snaps->removePoint(old_out + 1); 1355 ptr->m_snaps->addPoint(new_in); 1356 ptr->m_snaps->addPoint(new_out); 1357 ptr->checkRefresh(old_in, old_out); 1358 ptr->checkRefresh(new_in, new_out); 1359 if (logUndo) { 1360 Q_EMIT ptr->invalidateZone(old_in, old_out); 1361 Q_EMIT ptr->invalidateZone(new_in, new_out); 1362 } 1363 // ptr->adjustAssetRange(compoId, new_in, new_out); 1364 } else { 1365 qDebug() << "Error : Composition resize failed because parent timeline is not available anymore"; 1366 Q_ASSERT(false); 1367 } 1368 }; 1369 1370 if (in == compo_position && (out == -1 || out == old_out)) { 1371 return []() { 1372 qDebug() << "//// NO MOVE PERFORMED\n!!!!!!!!!!!!!!!!!!!!!!!!!!"; 1373 return true; 1374 }; 1375 } 1376 1377 // temporary remove of current compo to check collisions 1378 qDebug() << "// CURRENT COMPOSITIONS ----\n" << m_compoPos << "\n--------------"; 1379 m_compoPos.erase(compo_position); 1380 bool intersecting = hasIntersectingComposition(in, out); 1381 // put it back 1382 m_compoPos[compo_position] = compoId; 1383 1384 if (intersecting) { 1385 return []() { 1386 qDebug() << "//// FALSE MOVE PERFORMED\n!!!!!!!!!!!!!!!!!!!!!!!!!!"; 1387 return false; 1388 }; 1389 } 1390 1391 return [in, out, compoId, update_snaps, this]() { 1392 if (isLocked()) return false; 1393 m_compoPos.erase(m_allCompositions[compoId]->getPosition()); 1394 m_allCompositions[compoId]->setInOut(in, out); 1395 update_snaps(in, out + 1); 1396 m_compoPos[m_allCompositions[compoId]->getPosition()] = compoId; 1397 return true; 1398 }; 1399 } 1400 1401 bool TrackModel::requestCompositionInsertion(int compoId, int position, bool updateView, bool finalMove, Fun &undo, Fun &redo) 1402 { 1403 QWriteLocker locker(&m_lock); 1404 if (isLocked()) { 1405 return false; 1406 } 1407 auto operation = requestCompositionInsertion_lambda(compoId, position, updateView, finalMove); 1408 if (operation()) { 1409 auto reverse = requestCompositionDeletion_lambda(compoId, updateView, finalMove); 1410 UPDATE_UNDO_REDO(operation, reverse, undo, redo); 1411 return true; 1412 } 1413 return false; 1414 } 1415 1416 bool TrackModel::requestCompositionDeletion(int compoId, bool updateView, bool finalMove, Fun &undo, Fun &redo, bool finalDeletion) 1417 { 1418 QWriteLocker locker(&m_lock); 1419 if (isLocked()) { 1420 return false; 1421 } 1422 Q_ASSERT(m_allCompositions.count(compoId) > 0); 1423 auto old_composition = m_allCompositions[compoId]; 1424 int old_position = old_composition->getPosition(); 1425 Q_ASSERT(m_compoPos.count(old_position) > 0); 1426 Q_ASSERT(m_compoPos[old_position] == compoId); 1427 auto operation = requestCompositionDeletion_lambda(compoId, updateView, finalMove && finalDeletion); 1428 if (operation()) { 1429 auto reverse = requestCompositionInsertion_lambda(compoId, old_position, updateView, finalMove); 1430 UPDATE_UNDO_REDO(operation, reverse, undo, redo); 1431 return true; 1432 } 1433 return false; 1434 } 1435 1436 Fun TrackModel::requestCompositionDeletion_lambda(int compoId, bool updateView, bool finalMove) 1437 { 1438 QWriteLocker locker(&m_lock); 1439 // Find index of clip 1440 int clip_position = m_allCompositions[compoId]->getPosition(); 1441 int old_in = clip_position; 1442 int old_out = old_in + m_allCompositions[compoId]->getPlaytime(); 1443 return [compoId, old_in, old_out, updateView, finalMove, this]() { 1444 if (isLocked()) return false; 1445 int old_clip_index = getRowfromComposition(compoId); 1446 if (finalMove && m_allCompositions[compoId]->selected) { 1447 m_allCompositions[compoId]->selected = false; 1448 if (auto ptr = m_parent.lock()) { 1449 // item was selected, unselect 1450 ptr->requestClearSelection(true); 1451 } 1452 } 1453 auto ptr = m_parent.lock(); 1454 if (updateView) { 1455 ptr->_beginRemoveRows(ptr->makeTrackIndexFromID(getId()), old_clip_index, old_clip_index); 1456 ptr->_endRemoveRows(); 1457 } 1458 m_allCompositions[compoId]->setCurrentTrackId(-1); 1459 m_allCompositions.erase(compoId); 1460 m_compoPos.erase(old_in); 1461 ptr->m_snaps->removePoint(old_in); 1462 ptr->m_snaps->removePoint(old_out); 1463 if (finalMove) { 1464 Q_EMIT ptr->invalidateZone(old_in, old_out); 1465 } 1466 return true; 1467 }; 1468 } 1469 1470 int TrackModel::getCompositionByRow(int row) const 1471 { 1472 READ_LOCK(); 1473 if (row < int(m_allClips.size())) { 1474 return -1; 1475 } 1476 Q_ASSERT(row <= int(m_allClips.size() + m_allCompositions.size())); 1477 auto it = m_allCompositions.cbegin(); 1478 std::advance(it, row - int(m_allClips.size())); 1479 return (*it).first; 1480 } 1481 1482 int TrackModel::getCompositionsCount() const 1483 { 1484 READ_LOCK(); 1485 return int(m_allCompositions.size()); 1486 } 1487 1488 Fun TrackModel::requestCompositionInsertion_lambda(int compoId, int position, bool updateView, bool finalMove) 1489 { 1490 QWriteLocker locker(&m_lock); 1491 bool intersecting = true; 1492 if (auto ptr = m_parent.lock()) { 1493 intersecting = hasIntersectingComposition(position, position + ptr->getCompositionPlaytime(compoId) - 1); 1494 } else { 1495 qDebug() << "Error : Composition Insertion failed because timeline is not available anymore"; 1496 } 1497 if (!intersecting) { 1498 return [compoId, this, position, updateView, finalMove]() { 1499 if (isLocked()) return false; 1500 if (auto ptr = m_parent.lock()) { 1501 std::shared_ptr<CompositionModel> composition = ptr->getCompositionPtr(compoId); 1502 m_allCompositions[composition->getId()] = composition; // store clip 1503 // update clip position and track 1504 composition->setCurrentTrackId(getId()); 1505 int new_in = position; 1506 int new_out = new_in + composition->getPlaytime(); 1507 composition->setInOut(new_in, new_out - 1); 1508 if (updateView) { 1509 int composition_index = getRowfromComposition(composition->getId()); 1510 ptr->_beginInsertRows(ptr->makeTrackIndexFromID(composition->getCurrentTrackId()), composition_index, composition_index); 1511 ptr->_endInsertRows(); 1512 } 1513 ptr->m_snaps->addPoint(new_in); 1514 ptr->m_snaps->addPoint(new_out); 1515 m_compoPos[new_in] = composition->getId(); 1516 if (finalMove) { 1517 Q_EMIT ptr->invalidateZone(new_in, new_out); 1518 } 1519 return true; 1520 } 1521 qDebug() << "Error : Composition Insertion failed because timeline is not available anymore"; 1522 return false; 1523 }; 1524 } 1525 return []() { return false; }; 1526 } 1527 1528 bool TrackModel::hasIntersectingComposition(int in, int out) const 1529 { 1530 READ_LOCK(); 1531 auto it = m_compoPos.lower_bound(in); 1532 if (m_compoPos.empty()) { 1533 return false; 1534 } 1535 if (it != m_compoPos.end() && it->first <= out) { 1536 // compo at it intersects 1537 return true; 1538 } 1539 if (it == m_compoPos.begin()) { 1540 return false; 1541 } 1542 --it; 1543 int end = it->first + m_allCompositions.at(it->second)->getPlaytime() - 1; 1544 return end >= in; 1545 1546 return false; 1547 } 1548 1549 bool TrackModel::addEffect(const QString &effectId) 1550 { 1551 READ_LOCK(); 1552 return m_effectStack->appendEffect(effectId); 1553 } 1554 1555 const QString TrackModel::effectNames() const 1556 { 1557 READ_LOCK(); 1558 return m_effectStack->effectNames(); 1559 } 1560 1561 bool TrackModel::stackEnabled() const 1562 { 1563 READ_LOCK(); 1564 return m_effectStack->isStackEnabled(); 1565 } 1566 1567 void TrackModel::setEffectStackEnabled(bool enable) 1568 { 1569 m_effectStack->setEffectStackEnabled(enable); 1570 } 1571 1572 int TrackModel::trackDuration() const 1573 { 1574 return m_track->get_length(); 1575 } 1576 1577 bool TrackModel::isLocked() const 1578 { 1579 READ_LOCK(); 1580 return m_track->get_int("kdenlive:locked_track"); 1581 } 1582 1583 bool TrackModel::isTimelineActive() const 1584 { 1585 READ_LOCK(); 1586 return m_track->get_int("kdenlive:timeline_active"); 1587 } 1588 1589 bool TrackModel::shouldReceiveTimelineOp() const 1590 { 1591 READ_LOCK(); 1592 return isTimelineActive() && !isLocked(); 1593 } 1594 1595 bool TrackModel::isAudioTrack() const 1596 { 1597 return m_track->get_int("kdenlive:audio_track") == 1; 1598 } 1599 1600 Mlt::Tractor *TrackModel::getTrackService() 1601 { 1602 return m_track.get(); 1603 } 1604 1605 PlaylistState::ClipState TrackModel::trackType() const 1606 { 1607 return (m_track->get_int("kdenlive:audio_track") == 1 ? PlaylistState::AudioOnly : PlaylistState::VideoOnly); 1608 } 1609 1610 bool TrackModel::isHidden() const 1611 { 1612 return m_track->get_int("hide") & 1; 1613 } 1614 1615 bool TrackModel::isMute() const 1616 { 1617 return m_track->get_int("hide") & 2; 1618 } 1619 1620 bool TrackModel::importEffects(std::weak_ptr<Mlt::Service> service) 1621 { 1622 QWriteLocker locker(&m_lock); 1623 m_effectStack->importEffects(std::move(service), trackType()); 1624 return true; 1625 } 1626 1627 bool TrackModel::copyEffect(const std::shared_ptr<EffectStackModel> &stackModel, int rowId) 1628 { 1629 QWriteLocker locker(&m_lock); 1630 return m_effectStack->copyEffect(stackModel->getEffectStackRow(rowId), isAudioTrack() ? PlaylistState::AudioOnly : PlaylistState::VideoOnly); 1631 } 1632 1633 void TrackModel::lock() 1634 { 1635 setProperty(QStringLiteral("kdenlive:locked_track"), QStringLiteral("1")); 1636 if (auto ptr = m_parent.lock()) { 1637 QModelIndex ix = ptr->makeTrackIndexFromID(m_id); 1638 Q_EMIT ptr->dataChanged(ix, ix, {TimelineModel::IsLockedRole}); 1639 } 1640 } 1641 void TrackModel::unlock() 1642 { 1643 setProperty(QStringLiteral("kdenlive:locked_track"), nullptr); 1644 if (auto ptr = m_parent.lock()) { 1645 QModelIndex ix = ptr->makeTrackIndexFromID(m_id); 1646 Q_EMIT ptr->dataChanged(ix, ix, {TimelineModel::IsLockedRole}); 1647 } 1648 } 1649 1650 bool TrackModel::isAvailable(int position, int duration, int playlist) 1651 { 1652 if (playlist == -1) { 1653 // Check on both playlists 1654 for (auto &pl : m_playlists) { 1655 int start_clip = pl.get_clip_index_at(position); 1656 int end_clip = pl.get_clip_index_at(position + duration - 1); 1657 if (start_clip != end_clip || !pl.is_blank(start_clip)) { 1658 return false; 1659 } 1660 } 1661 return true; 1662 } 1663 int start_clip = m_playlists[playlist].get_clip_index_at(position); 1664 int end_clip = m_playlists[playlist].get_clip_index_at(position + duration - 1); 1665 if (start_clip != end_clip) { 1666 return false; 1667 } 1668 return m_playlists[playlist].is_blank(start_clip); 1669 } 1670 1671 bool TrackModel::isAvailableWithExceptions(int position, int duration, const QVector<int> &exceptions) 1672 { 1673 // Check on both playlists 1674 QSharedPointer<Mlt::Producer> prod = nullptr; 1675 for (auto &playlist : m_playlists) { 1676 int start_clip = playlist.get_clip_index_at(position); 1677 int end_clip = playlist.get_clip_index_at(position + duration - 1); 1678 for (int ix = start_clip; ix <= end_clip; ix++) { 1679 if (playlist.is_blank(ix)) { 1680 continue; 1681 } 1682 prod.reset(playlist.get_clip(ix)); 1683 if (prod) { 1684 if (!exceptions.contains(prod->get_int("_kdenlive_cid"))) { 1685 return false; 1686 } 1687 } 1688 } 1689 } 1690 return true; 1691 } 1692 1693 bool TrackModel::requestRemoveMix(std::pair<int, int> clipIds, Fun &undo, Fun &redo) 1694 { 1695 int mixDuration; 1696 int mixCutPos; 1697 int endPos; 1698 int firstInPos; 1699 int secondInPos; 1700 int mixPosition; 1701 int src_track = 0; 1702 int first_src_track = 0; 1703 bool secondClipHasEndMix = false; 1704 bool firstClipHasStartMix = false; 1705 QList<int> allowedMixes = {clipIds.first}; 1706 if (auto ptr = m_parent.lock()) { 1707 // The clip that will be moved to playlist 1 1708 std::shared_ptr<ClipModel> firstClip(ptr->getClipPtr(clipIds.first)); 1709 std::shared_ptr<ClipModel> secondClip(ptr->getClipPtr(clipIds.second)); 1710 mixDuration = secondClip->getMixDuration(); 1711 mixCutPos = secondClip->getMixCutPosition(); 1712 mixPosition = secondClip->getPosition(); 1713 firstInPos = firstClip->getPosition(); 1714 secondInPos = mixPosition + mixDuration - mixCutPos; 1715 endPos = mixPosition + secondClip->getPlaytime(); 1716 secondClipHasEndMix = hasEndMix(clipIds.second); 1717 firstClipHasStartMix = hasStartMix(clipIds.first); 1718 src_track = secondClip->getSubPlaylistIndex(); 1719 first_src_track = firstClip->getSubPlaylistIndex(); 1720 } else { 1721 return false; 1722 } 1723 bool result = false; 1724 bool closing = false; 1725 Fun local_undo = []() { return true; }; 1726 Fun local_redo = []() { return true; }; 1727 if (auto ptr = m_parent.lock()) { 1728 // Resize first part clip 1729 closing = ptr->m_closing; 1730 if (closing) { 1731 result = true; 1732 } else { 1733 result = ptr->getClipPtr(clipIds.first)->requestResize(secondInPos - firstInPos, true, local_undo, local_redo, true, true); 1734 // Resize main clip 1735 result = result && ptr->getClipPtr(clipIds.second)->requestResize(endPos - secondInPos, false, local_undo, local_redo, true, true); 1736 } 1737 } 1738 if (result) { 1739 QString assetId = m_sameCompositions[clipIds.second]->getAssetId(); 1740 QVector<QPair<QString, QVariant>> params = m_sameCompositions[clipIds.second]->getAllParameters(); 1741 std::pair<int, int> tracks = getMixTracks(clipIds.second); 1742 bool switchSecondTrack = false; 1743 bool switchFirstTrack = false; 1744 if (src_track == 1 && !secondClipHasEndMix && !closing) { 1745 switchSecondTrack = true; 1746 } 1747 if (first_src_track == 1 && !firstClipHasStartMix && !closing) { 1748 switchFirstTrack = true; 1749 } 1750 Fun replay = [this, clipIds, firstInPos, secondInPos, switchFirstTrack, switchSecondTrack]() { 1751 if (switchFirstTrack) { 1752 // Revert clip to playlist 0 since it has no mix 1753 switchPlaylist(clipIds.first, firstInPos, 1, 0); 1754 } 1755 if (switchSecondTrack) { 1756 // Revert clip to playlist 0 since it has no mix 1757 switchPlaylist(clipIds.second, secondInPos, 1, 0); 1758 } 1759 // Delete transition 1760 Mlt::Transition &transition = *static_cast<Mlt::Transition *>(m_sameCompositions[clipIds.second]->getAsset()); 1761 QScopedPointer<Mlt::Field> field(m_track->field()); 1762 field->lock(); 1763 field->disconnect_service(transition); 1764 field->unlock(); 1765 m_sameCompositions.erase(clipIds.second); 1766 m_mixList.remove(clipIds.first); 1767 if (auto ptr = m_parent.lock()) { 1768 std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipIds.second)); 1769 movedClip->setMixDuration(0); 1770 /*QModelIndex ix = ptr->makeClipIndexFromID(clipIds.first); 1771 Q_EMIT ptr->dataChanged(ix, ix, {TimelineModel::DurationRole});*/ 1772 QModelIndex ix2 = ptr->makeClipIndexFromID(clipIds.second); 1773 Q_EMIT ptr->dataChanged(ix2, ix2, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 1774 } 1775 return true; 1776 }; 1777 replay(); 1778 Fun reverse = [this, clipIds, assetId, params, tracks, mixDuration, mixPosition, mixCutPos, firstInPos, secondInPos, switchFirstTrack, 1779 switchSecondTrack]() { 1780 // First restore correct playlist 1781 if (switchFirstTrack) { 1782 // Revert clip to playlist 1 1783 switchPlaylist(clipIds.first, firstInPos, 0, 1); 1784 } 1785 if (switchSecondTrack) { 1786 // Revert clip to playlist 1 1787 switchPlaylist(clipIds.second, secondInPos, 0, 1); 1788 } 1789 // Build mix 1790 if (auto ptr = m_parent.lock()) { 1791 std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipIds.second)); 1792 movedClip->setMixDuration(mixDuration, mixCutPos); 1793 // Insert mix transition 1794 std::unique_ptr<Mlt::Transition> t = TransitionsRepository::get()->getTransition(assetId); 1795 t->set_in_and_out(mixPosition, mixPosition + mixDuration); 1796 t->set("kdenlive:mixcut", mixCutPos); 1797 t->set("kdenlive_id", assetId.toUtf8().constData()); 1798 t->set_tracks(tracks.first, tracks.second); 1799 m_track->plant_transition(*t.get(), tracks.first, tracks.second); 1800 QDomElement xml = TransitionsRepository::get()->getXml(assetId); 1801 QDomNodeList xmlParams = xml.elementsByTagName(QStringLiteral("parameter")); 1802 for (int i = 0; i < xmlParams.count(); ++i) { 1803 QDomElement currentParameter = xmlParams.item(i).toElement(); 1804 QString paramName = currentParameter.attribute(QStringLiteral("name")); 1805 for (const auto &p : qAsConst(params)) { 1806 if (p.first == paramName) { 1807 currentParameter.setAttribute(QStringLiteral("value"), p.second.toString()); 1808 break; 1809 } 1810 } 1811 } 1812 std::shared_ptr<AssetParameterModel> asset( 1813 new AssetParameterModel(std::move(t), xml, assetId, ObjectId(KdenliveObjectType::TimelineMix, clipIds.second, ptr->uuid()), QString())); 1814 m_sameCompositions[clipIds.second] = asset; 1815 m_mixList.insert(clipIds.first, clipIds.second); 1816 QModelIndex ix2 = ptr->makeClipIndexFromID(clipIds.second); 1817 Q_EMIT ptr->dataChanged(ix2, ix2, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 1818 } 1819 return true; 1820 }; 1821 PUSH_LAMBDA(replay, local_redo); 1822 PUSH_FRONT_LAMBDA(reverse, local_undo); 1823 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 1824 return true; 1825 } 1826 return false; 1827 } 1828 1829 bool TrackModel::requestClipMix(const QString &mixId, std::pair<int, int> clipIds, std::pair<int, int> mixDurations, bool updateView, bool finalMove, Fun &undo, 1830 Fun &redo, bool groupMove) 1831 { 1832 QWriteLocker locker(&m_lock); 1833 // By default, insertion occurs in topmost track 1834 // Find out the clip id at position 1835 int firstClipPos; 1836 int secondClipPos; 1837 int secondClipDuration; 1838 int firstClipDuration; 1839 int source_track; 1840 int mixPosition; 1841 int secondClipCut = 0; 1842 int dest_track = 1; 1843 bool remixPlaylists = false; 1844 bool clipHasEndMix = false; 1845 if (auto ptr = m_parent.lock()) { 1846 // The clip that will be moved to playlist 1 1847 std::shared_ptr<ClipModel> secondClip(ptr->getClipPtr(clipIds.second)); 1848 secondClipDuration = secondClip->getPlaytime(); 1849 secondClipPos = secondClip->getPosition(); 1850 source_track = secondClip->getSubPlaylistIndex(); 1851 std::shared_ptr<ClipModel> firstClip(ptr->getClipPtr(clipIds.first)); 1852 firstClipDuration = firstClip->getPlaytime(); 1853 // Ensure mix is not longer than clip and doesn't overlap other mixes 1854 firstClipPos = firstClip->getPosition(); 1855 if (firstClip->getSubPlaylistIndex() == 1) { 1856 dest_track = 0; 1857 } 1858 mixPosition = qMax(firstClipPos, secondClipPos - mixDurations.second); 1859 int maxPos = qMin(secondClipPos + secondClipDuration, secondClipPos + mixDurations.first); 1860 if (hasStartMix(clipIds.first)) { 1861 std::pair<MixInfo, MixInfo> mixData = getMixInfo(clipIds.first); 1862 mixPosition = qMax(mixData.first.firstClipInOut.second, mixPosition); 1863 } 1864 if (hasEndMix(clipIds.second)) { 1865 std::pair<MixInfo, MixInfo> mixData = getMixInfo(clipIds.second); 1866 clipHasEndMix = true; 1867 maxPos = qMin(mixData.second.secondClipInOut.first, maxPos); 1868 if (ptr->m_allClips[mixData.second.secondClipId]->getSubPlaylistIndex() == dest_track) { 1869 remixPlaylists = true; 1870 } 1871 } 1872 mixDurations.first = qMin(mixDurations.first, maxPos - mixPosition); 1873 secondClipCut = maxPos - secondClipPos; 1874 } else { 1875 // Error, timeline unavailable 1876 qDebug() << "=== ERROR NO TIMELINE!!!!"; 1877 return false; 1878 } 1879 1880 // Rearrange subsequent mixes 1881 Fun rearrange_playlists = []() { return true; }; 1882 Fun rearrange_playlists_undo = []() { return true; }; 1883 if (remixPlaylists && source_track != dest_track) { 1884 // A list of clip ids x playlists 1885 QMap<int, int> rearrangedPlaylists; 1886 QMap<int, QVector<QPair<QString, QVariant>>> mixParameters; 1887 int moveId = m_mixList.value(clipIds.second, -1); 1888 while (moveId > -1) { 1889 int current = m_allClips[moveId]->getSubPlaylistIndex(); 1890 rearrangedPlaylists.insert(moveId, current); 1891 QVector<QPair<QString, QVariant>> params = m_sameCompositions.at(moveId)->getAllParameters(); 1892 mixParameters.insert(moveId, params); 1893 if (hasEndMix(moveId)) { 1894 moveId = m_mixList.value(moveId, -1); 1895 } else { 1896 break; 1897 } 1898 } 1899 rearrange_playlists = [this, map = rearrangedPlaylists]() { 1900 // First, remove all clips on playlist 0 1901 QMapIterator<int, int> i(map); 1902 while (i.hasNext()) { 1903 i.next(); 1904 if (i.value() == 0) { 1905 int target_clip = m_playlists[0].get_clip_index_at(m_allClips[i.key()]->getPosition()); 1906 std::unique_ptr<Mlt::Producer> prod(m_playlists[0].replace_with_blank(target_clip)); 1907 } 1908 } 1909 m_playlists[0].consolidate_blanks(); 1910 // Then move all clips from playlist 1 to playlist 0 1911 i.toFront(); 1912 if (auto ptr = m_parent.lock()) { 1913 while (i.hasNext()) { 1914 i.next(); 1915 if (i.value() == 1) { 1916 // Remove 1917 int pos = m_allClips[i.key()]->getPosition(); 1918 int target_clip = m_playlists[1].get_clip_index_at(pos); 1919 std::unique_ptr<Mlt::Producer> prod(m_playlists[1].replace_with_blank(target_clip)); 1920 prod.reset(); 1921 // Replug 1922 std::shared_ptr<ClipModel> clip = ptr->getClipPtr(i.key()); 1923 clip->setSubPlaylistIndex(0, m_id); 1924 int index = m_playlists[0].insert_at(pos, *clip, 1); 1925 m_playlists[0].consolidate_blanks(); 1926 if (index == -1) { 1927 // Something went wrong, abort 1928 m_playlists[1].insert_at(pos, *clip, 1); 1929 m_playlists[1].consolidate_blanks(); 1930 return false; 1931 } 1932 } 1933 } 1934 m_playlists[1].consolidate_blanks(); 1935 // Finally replug playlist 0 clips in playlist 1 and fix transition direction 1936 i.toFront(); 1937 while (i.hasNext()) { 1938 i.next(); 1939 if (i.value() == 0) { 1940 int pos = m_allClips[i.key()]->getPosition(); 1941 std::shared_ptr<ClipModel> clip = ptr->getClipPtr(i.key()); 1942 clip->setSubPlaylistIndex(1, m_id); 1943 int index = m_playlists[1].insert_at(pos, *clip, 1); 1944 m_playlists[1].consolidate_blanks(); 1945 if (index == -1) { 1946 // Something went wrong, abort 1947 m_playlists[0].insert_at(pos, *clip, 1); 1948 m_playlists[0].consolidate_blanks(); 1949 return false; 1950 } 1951 } 1952 std::unique_ptr<Mlt::Field> field(m_track->field()); 1953 field->block(); 1954 if (m_sameCompositions.count(i.key()) > 0) { 1955 // There is a mix at clip start, adjust direction 1956 Mlt::Properties *props = m_sameCompositions[i.key()]->getAsset(); 1957 Mlt::Transition &transition = *static_cast<Mlt::Transition *>(m_sameCompositions[i.key()]->getAsset()); 1958 bool reverse = i.value() == 1; 1959 const QString assetName = m_sameCompositions[i.key()]->getAssetId(); 1960 const ObjectId ownerId = m_sameCompositions[i.key()]->getOwnerId(); 1961 field->disconnect_service(transition); 1962 std::unique_ptr<Mlt::Transition> newTrans = TransitionsRepository::get()->getTransition(m_sameCompositions[i.key()]->getAssetId()); 1963 newTrans->inherit(*props); 1964 updateCompositionDirection(*newTrans.get(), reverse); 1965 m_sameCompositions.erase(i.key()); 1966 QDomElement xml = TransitionsRepository::get()->getXml(assetName); 1967 std::shared_ptr<AssetParameterModel> asset(new AssetParameterModel(std::move(newTrans), xml, assetName, ownerId, QString())); 1968 m_sameCompositions[i.key()] = asset; 1969 } 1970 field->unblock(); 1971 } 1972 return true; 1973 } else { 1974 return false; 1975 } 1976 }; 1977 rearrange_playlists_undo = [this, map = rearrangedPlaylists, mixParameters]() { 1978 // First, remove all clips on playlist 1 1979 QMapIterator<int, int> i(map); 1980 while (i.hasNext()) { 1981 i.next(); 1982 if (i.value() == 0) { 1983 int target_clip = m_playlists[1].get_clip_index_at(m_allClips[i.key()]->getPosition()); 1984 std::unique_ptr<Mlt::Producer> prod(m_playlists[1].replace_with_blank(target_clip)); 1985 } 1986 m_playlists[1].consolidate_blanks(); 1987 } 1988 // Then move all clips from playlist 0 to playlist 1 1989 i.toFront(); 1990 if (auto ptr = m_parent.lock()) { 1991 while (i.hasNext()) { 1992 i.next(); 1993 if (i.value() == 1) { 1994 // Remove 1995 int pos = m_allClips[i.key()]->getPosition(); 1996 int target_clip = m_playlists[0].get_clip_index_at(pos); 1997 std::unique_ptr<Mlt::Producer> prod(m_playlists[0].replace_with_blank(target_clip)); 1998 // Replug 1999 std::shared_ptr<ClipModel> clip = ptr->getClipPtr(i.key()); 2000 clip->setSubPlaylistIndex(1, m_id); 2001 int index = m_playlists[1].insert_at(pos, *clip, 1); 2002 m_playlists[1].consolidate_blanks(); 2003 if (index == -1) { 2004 // Something went wrong, abort 2005 m_playlists[0].insert_at(pos, *clip, 1); 2006 m_playlists[0].consolidate_blanks(); 2007 return false; 2008 } 2009 } 2010 m_playlists[0].consolidate_blanks(); 2011 } 2012 // Finally replug playlist 1 clips in playlist 0 and fix transition direction 2013 i.toFront(); 2014 while (i.hasNext()) { 2015 i.next(); 2016 if (i.value() == 0) { 2017 int pos = m_allClips[i.key()]->getPosition(); 2018 std::shared_ptr<ClipModel> clip = ptr->getClipPtr(i.key()); 2019 clip->setSubPlaylistIndex(0, m_id); 2020 int index = m_playlists[0].insert_at(pos, *clip, 1); 2021 m_playlists[0].consolidate_blanks(); 2022 if (index == -1) { 2023 // Something went wrong, abort 2024 m_playlists[1].insert_at(pos, *clip, 1); 2025 m_playlists[1].consolidate_blanks(); 2026 return false; 2027 } 2028 } 2029 std::unique_ptr<Mlt::Field> field(m_track->field()); 2030 field->block(); 2031 if (m_sameCompositions.count(i.key()) > 0) { 2032 // There is a mix at clip start, adjust direction 2033 Mlt::Properties *props = m_sameCompositions[i.key()]->getAsset(); 2034 Mlt::Transition &transition = *static_cast<Mlt::Transition *>(m_sameCompositions[i.key()]->getAsset()); 2035 bool reverse = i.value() == 0; 2036 const QString assetName = m_sameCompositions[i.key()]->getAssetId(); 2037 const ObjectId ownerId = m_sameCompositions[i.key()]->getOwnerId(); 2038 field->disconnect_service(transition); 2039 std::unique_ptr<Mlt::Transition> newTrans = TransitionsRepository::get()->getTransition(m_sameCompositions[i.key()]->getAssetId()); 2040 newTrans->inherit(*props); 2041 updateCompositionDirection(*newTrans.get(), reverse); 2042 m_sameCompositions.erase(i.key()); 2043 QDomElement xml = TransitionsRepository::get()->getXml(assetName); 2044 std::shared_ptr<AssetParameterModel> asset(new AssetParameterModel(std::move(newTrans), xml, assetName, ownerId, QString())); 2045 m_sameCompositions[i.key()] = asset; 2046 } 2047 field->unblock(); 2048 } 2049 return true; 2050 } else { 2051 return false; 2052 } 2053 }; 2054 } 2055 // Create mix compositing 2056 Fun build_mix = [clipIds, mixPosition, mixDurations, dest_track, secondClipCut, mixId, this]() { 2057 if (auto ptr = m_parent.lock()) { 2058 std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipIds.second)); 2059 movedClip->setMixDuration(mixDurations.first + mixDurations.second, secondClipCut); 2060 // Insert mix transition 2061 QString assetName; 2062 std::unique_ptr<Mlt::Transition> t; 2063 QDomElement xml; 2064 if (isAudioTrack()) { 2065 // Mix is the only audio transition 2066 t = TransitionsRepository::get()->getTransition(QStringLiteral("mix")); 2067 t->set_in_and_out(mixPosition, mixPosition + mixDurations.first + mixDurations.second); 2068 t->set("kdenlive:mixcut", secondClipCut); 2069 t->set("start", -1); 2070 t->set("accepts_blanks", 1); 2071 assetName = QStringLiteral("mix"); 2072 xml = TransitionsRepository::get()->getXml(assetName); 2073 if (dest_track == 0) { 2074 t->set("reverse", 1); 2075 Xml::setXmlParameter(xml, QStringLiteral("reverse"), QStringLiteral("1")); 2076 } 2077 m_track->plant_transition(*t.get(), 0, 1); 2078 } else { 2079 assetName = mixId.isEmpty() || mixId == QLatin1String("mix") ? QStringLiteral("luma") : mixId; 2080 t = TransitionsRepository::get()->getTransition(assetName); 2081 t->set_in_and_out(mixPosition, mixPosition + mixDurations.first + mixDurations.second); 2082 xml = TransitionsRepository::get()->getXml(assetName); 2083 t->set("kdenlive:mixcut", secondClipCut); 2084 t->set("kdenlive_id", assetName.toUtf8().constData()); 2085 if (dest_track == 0) { 2086 t->set_tracks(1, 0); 2087 m_track->plant_transition(*t.get(), 1, 0); 2088 } else { 2089 t->set_tracks(0, 1); 2090 m_track->plant_transition(*t.get(), 0, 1); 2091 } 2092 } 2093 std::shared_ptr<AssetParameterModel> asset( 2094 new AssetParameterModel(std::move(t), xml, assetName, ObjectId(KdenliveObjectType::TimelineMix, clipIds.second, ptr->uuid()), QString())); 2095 m_sameCompositions[clipIds.second] = asset; 2096 m_mixList.insert(clipIds.first, clipIds.second); 2097 } 2098 return true; 2099 }; 2100 2101 Fun destroy_mix = [clipIds, this]() { 2102 if (auto ptr = m_parent.lock()) { 2103 if (m_sameCompositions.count(clipIds.second) == 0) { 2104 // Mix was already deleted 2105 if (m_mixList.contains(clipIds.first)) { 2106 m_mixList.remove(clipIds.first); 2107 } 2108 return true; 2109 } 2110 // Clear asset panel if mix was selected 2111 if (ptr->m_selectedMix == clipIds.second) { 2112 ptr->requestClearAssetView(clipIds.second); 2113 } 2114 Mlt::Transition &transition = *static_cast<Mlt::Transition *>(m_sameCompositions[clipIds.second]->getAsset()); 2115 std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipIds.second)); 2116 movedClip->setMixDuration(0); 2117 QModelIndex ix = ptr->makeClipIndexFromID(clipIds.second); 2118 Q_EMIT ptr->dataChanged(ix, ix, {TimelineModel::StartRole, TimelineModel::MixRole, TimelineModel::MixCutRole}); 2119 QScopedPointer<Mlt::Field> field(m_track->field()); 2120 field->lock(); 2121 field->disconnect_service(transition); 2122 field->unlock(); 2123 m_sameCompositions.erase(clipIds.second); 2124 m_mixList.remove(clipIds.first); 2125 } 2126 return true; 2127 }; 2128 // lock MLT playlist so that we don't end up with invalid frames in monitor 2129 auto operation = requestClipDeletion_lambda(clipIds.second, updateView, finalMove, groupMove, false); 2130 bool res = operation(); 2131 if (res) { 2132 Fun replay = [this, clipIds, dest_track, firstClipPos, secondClipDuration, mixPosition, mixDurations, build_mix, secondClipPos, clipHasEndMix, 2133 updateView, finalMove, groupMove, rearrange_playlists]() { 2134 if (auto ptr = m_parent.lock()) { 2135 ptr->getClipPtr(clipIds.second)->setSubPlaylistIndex(dest_track, m_id); 2136 } 2137 bool result = rearrange_playlists(); 2138 auto op = requestClipInsertion_lambda(clipIds.second, secondClipPos, updateView, finalMove, groupMove); 2139 result = result && op(); 2140 if (result) { 2141 build_mix(); 2142 std::function<bool(void)> local_undo = []() { return true; }; 2143 std::function<bool(void)> local_redo = []() { return true; }; 2144 if (auto ptr = m_parent.lock()) { 2145 result = ptr->getClipPtr(clipIds.second) 2146 ->requestResize(secondClipPos + secondClipDuration - mixPosition, false, local_undo, local_redo, true, clipHasEndMix); 2147 result = result && ptr->getClipPtr(clipIds.first) 2148 ->requestResize(mixPosition + mixDurations.first + mixDurations.second - firstClipPos, true, local_undo, local_redo, 2149 true, true); 2150 QModelIndex ix = ptr->makeClipIndexFromID(clipIds.second); 2151 Q_EMIT ptr->dataChanged(ix, ix, {TimelineModel::StartRole, TimelineModel::MixRole, TimelineModel::MixCutRole}); 2152 } 2153 } 2154 return result; 2155 }; 2156 2157 Fun reverse = [this, clipIds, source_track, secondClipDuration, firstClipDuration, destroy_mix, secondClipPos, updateView, finalMove, groupMove, 2158 operation, rearrange_playlists_undo]() { 2159 destroy_mix(); 2160 std::function<bool(void)> local_undo = []() { return true; }; 2161 std::function<bool(void)> local_redo = []() { return true; }; 2162 if (auto ptr = m_parent.lock()) { 2163 ptr->getClipPtr(clipIds.second)->requestResize(secondClipDuration, false, local_undo, local_redo, false); 2164 ptr->getClipPtr(clipIds.first)->requestResize(firstClipDuration, true, local_undo, local_redo, false); 2165 } 2166 bool result = operation(); 2167 if (auto ptr = m_parent.lock()) { 2168 ptr->getClipPtr(clipIds.second)->setSubPlaylistIndex(source_track, m_id); 2169 } 2170 result = result && rearrange_playlists_undo(); 2171 auto op = requestClipInsertion_lambda(clipIds.second, secondClipPos, updateView, finalMove, groupMove); 2172 result = result && op(); 2173 return result; 2174 }; 2175 res = replay(); 2176 if (res) { 2177 PUSH_LAMBDA(replay, operation); 2178 UPDATE_UNDO_REDO(operation, reverse, undo, redo); 2179 } else { 2180 qDebug() << "=============\nSECOND INSERT FAILED\n\n================="; 2181 reverse(); 2182 } 2183 } else { 2184 qDebug() << "=== CLIP DELETION FAILED"; 2185 } 2186 return res; 2187 } 2188 2189 std::pair<MixInfo, MixInfo> TrackModel::getMixInfo(int clipId) const 2190 { 2191 std::pair<MixInfo, MixInfo> result; 2192 MixInfo startMix; 2193 MixInfo endMix; 2194 if (m_sameCompositions.count(clipId) > 0) { 2195 // There is a mix at clip start 2196 startMix.firstClipId = m_mixList.key(clipId, -1); 2197 startMix.secondClipId = clipId; 2198 if (auto ptr = m_parent.lock()) { 2199 if (ptr->isClip(startMix.firstClipId)) { 2200 std::shared_ptr<ClipModel> clip1 = ptr->getClipPtr(startMix.firstClipId); 2201 std::shared_ptr<ClipModel> clip2 = ptr->getClipPtr(startMix.secondClipId); 2202 startMix.firstClipInOut.first = clip1->getPosition(); 2203 startMix.firstClipInOut.second = startMix.firstClipInOut.first + clip1->getPlaytime(); 2204 startMix.secondClipInOut.first = clip2->getPosition(); 2205 startMix.secondClipInOut.second = startMix.secondClipInOut.first + clip2->getPlaytime(); 2206 startMix.mixOffset = clip2->getMixCutPosition(); 2207 } else { 2208 // Clip was deleted 2209 startMix.firstClipId = -1; 2210 } 2211 } 2212 } else { 2213 startMix.firstClipId = -1; 2214 startMix.secondClipId = -1; 2215 } 2216 int secondClip = m_mixList.value(clipId, -1); 2217 if (secondClip > -1) { 2218 // There is a mix at clip end 2219 endMix.firstClipId = clipId; 2220 endMix.secondClipId = secondClip; 2221 if (auto ptr = m_parent.lock()) { 2222 if (ptr->isClip(secondClip)) { 2223 std::shared_ptr<ClipModel> clip1 = ptr->getClipPtr(endMix.firstClipId); 2224 std::shared_ptr<ClipModel> clip2 = ptr->getClipPtr(endMix.secondClipId); 2225 endMix.firstClipInOut.first = clip1->getPosition(); 2226 endMix.firstClipInOut.second = endMix.firstClipInOut.first + clip1->getPlaytime(); 2227 endMix.secondClipInOut.first = clip2->getPosition(); 2228 endMix.secondClipInOut.second = endMix.secondClipInOut.first + clip2->getPlaytime(); 2229 } else { 2230 // Clip was deleted 2231 endMix.firstClipId = -1; 2232 } 2233 } 2234 } else { 2235 endMix.firstClipId = -1; 2236 endMix.secondClipId = -1; 2237 } 2238 result = {startMix, endMix}; 2239 return result; 2240 } 2241 2242 bool TrackModel::deleteMix(int clipId, bool final, bool notify) 2243 { 2244 Q_ASSERT(m_sameCompositions.count(clipId) > 0); 2245 if (auto ptr = m_parent.lock()) { 2246 if (notify) { 2247 std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipId)); 2248 movedClip->setMixDuration(final ? 0 : 1); 2249 QModelIndex ix = ptr->makeClipIndexFromID(clipId); 2250 Q_EMIT ptr->dataChanged(ix, ix, {TimelineModel::StartRole, TimelineModel::MixRole, TimelineModel::MixCutRole}); 2251 } 2252 if (final) { 2253 Mlt::Transition &transition = *static_cast<Mlt::Transition *>(m_sameCompositions[clipId]->getAsset()); 2254 QScopedPointer<Mlt::Field> field(m_track->field()); 2255 field->lock(); 2256 field->disconnect_service(transition); 2257 field->unlock(); 2258 m_sameCompositions.erase(clipId); 2259 int firstClip = m_mixList.key(clipId, -1); 2260 if (firstClip > -1) { 2261 m_mixList.remove(firstClip); 2262 } 2263 } 2264 return true; 2265 } 2266 return false; 2267 } 2268 2269 bool TrackModel::createMix(MixInfo info, std::pair<QString, QVector<QPair<QString, QVariant>>> params, std::pair<int, int> tracks, bool finalMove) 2270 { 2271 if (m_sameCompositions.count(info.secondClipId) > 0) { 2272 // Clip already has a mix 2273 Q_ASSERT(false); 2274 return false; 2275 } 2276 if (auto ptr = m_parent.lock()) { 2277 // Insert mix transition 2278 std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(info.secondClipId)); 2279 int in = movedClip->getPosition(); 2280 // int out = in + info.firstClipInOut.second - info.secondClipInOut.first; 2281 int duration = info.firstClipInOut.second - info.secondClipInOut.first; 2282 int out = in + duration; 2283 movedClip->setMixDuration(duration, info.mixOffset); 2284 std::unique_ptr<Mlt::Transition> t; 2285 const QString assetId = params.first; 2286 t = std::make_unique<Mlt::Transition>(pCore->getProjectProfile().get_profile(), assetId.toUtf8().constData()); 2287 t->set_in_and_out(in, out); 2288 t->set("kdenlive:mixcut", info.mixOffset); 2289 t->set("kdenlive_id", assetId.toUtf8().constData()); 2290 t->set_tracks(tracks.first, tracks.second); 2291 m_track->plant_transition(*t.get(), tracks.first, tracks.second); 2292 QDomElement xml = TransitionsRepository::get()->getXml(assetId); 2293 QDomNodeList xmlParams = xml.elementsByTagName(QStringLiteral("parameter")); 2294 for (int i = 0; i < xmlParams.count(); ++i) { 2295 QDomElement currentParameter = xmlParams.item(i).toElement(); 2296 QString paramName = currentParameter.attribute(QStringLiteral("name")); 2297 for (const auto &p : qAsConst(params.second)) { 2298 if (p.first == paramName) { 2299 currentParameter.setAttribute(QStringLiteral("value"), p.second.toString()); 2300 break; 2301 } 2302 } 2303 } 2304 std::shared_ptr<AssetParameterModel> asset( 2305 new AssetParameterModel(std::move(t), xml, assetId, ObjectId(KdenliveObjectType::TimelineMix, info.secondClipId, ptr->uuid()), QString())); 2306 m_sameCompositions[info.secondClipId] = asset; 2307 m_mixList.insert(info.firstClipId, info.secondClipId); 2308 if (finalMove) { 2309 QModelIndex ix2 = ptr->makeClipIndexFromID(info.secondClipId); 2310 Q_EMIT ptr->dataChanged(ix2, ix2, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 2311 } 2312 return true; 2313 } 2314 qDebug() << "== COULD NOT PLANT MIX; TRACK UNAVAILABLE"; 2315 return false; 2316 } 2317 2318 bool TrackModel::createMix(MixInfo info, bool isAudio) 2319 { 2320 if (m_sameCompositions.count(info.secondClipId) > 0) { 2321 return false; 2322 } 2323 if (auto ptr = m_parent.lock()) { 2324 // Insert mix transition 2325 std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(info.secondClipId)); 2326 int in = movedClip->getPosition(); 2327 // int out = in + info.firstClipInOut.second - info.secondClipInOut.first; 2328 int out = in + movedClip->getMixDuration(); 2329 movedClip->setMixDuration(out - in); 2330 bool reverse = movedClip->getSubPlaylistIndex() == 0; 2331 QString assetName; 2332 std::unique_ptr<Mlt::Transition> t; 2333 if (isAudio) { 2334 t = std::make_unique<Mlt::Transition>(pCore->getProjectProfile().get_profile(), "mix"); 2335 t->set_in_and_out(in, out); 2336 t->set("start", -1); 2337 t->set("accepts_blanks", 1); 2338 if (reverse) { 2339 t->set("reverse", 1); 2340 } 2341 m_track->plant_transition(*t.get(), 0, 1); 2342 assetName = QStringLiteral("mix"); 2343 } else { 2344 t = std::make_unique<Mlt::Transition>(pCore->getProjectProfile().get_profile(), "luma"); 2345 t->set_in_and_out(in, out); 2346 t->set("kdenlive_id", "luma"); 2347 if (reverse) { 2348 t->set_tracks(1, 0); 2349 m_track->plant_transition(*t.get(), 1, 0); 2350 } else { 2351 t->set_tracks(0, 1); 2352 m_track->plant_transition(*t.get(), 0, 1); 2353 } 2354 /*if (reverse) { 2355 t->set("reverse", 1); 2356 } 2357 m_track->plant_transition(*t.get(), 0, 1);*/ 2358 assetName = QStringLiteral("luma"); 2359 } 2360 QDomElement xml = TransitionsRepository::get()->getXml(assetName); 2361 std::shared_ptr<AssetParameterModel> asset( 2362 new AssetParameterModel(std::move(t), xml, assetName, ObjectId(KdenliveObjectType::TimelineMix, info.secondClipId, ptr->uuid()), QString())); 2363 m_sameCompositions[info.secondClipId] = asset; 2364 m_mixList.insert(info.firstClipId, info.secondClipId); 2365 return true; 2366 } 2367 return false; 2368 } 2369 2370 bool TrackModel::createMix(std::pair<int, int> clipIds, std::pair<int, int> mixData) 2371 { 2372 if (m_sameCompositions.count(clipIds.second) > 0) { 2373 return false; 2374 } 2375 if (auto ptr = m_parent.lock()) { 2376 std::shared_ptr<ClipModel> movedClip(ptr->getClipPtr(clipIds.second)); 2377 movedClip->setMixDuration(mixData.second); 2378 QModelIndex ix = ptr->makeClipIndexFromID(clipIds.second); 2379 Q_EMIT ptr->dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 2380 bool reverse = movedClip->getSubPlaylistIndex() == 0; 2381 // Insert mix transition 2382 QString assetName; 2383 std::unique_ptr<Mlt::Transition> t; 2384 if (isAudioTrack()) { 2385 t = std::make_unique<Mlt::Transition>(pCore->getProjectProfile().get_profile(), "mix"); 2386 t->set_in_and_out(mixData.first, mixData.first + mixData.second); 2387 t->set("start", -1); 2388 t->set("accepts_blanks", 1); 2389 if (reverse) { 2390 t->set("reverse", 1); 2391 } 2392 m_track->plant_transition(*t.get(), 0, 1); 2393 assetName = QStringLiteral("mix"); 2394 } else { 2395 t = std::make_unique<Mlt::Transition>(pCore->getProjectProfile().get_profile(), "luma"); 2396 t->set("kdenlive_id", "luma"); 2397 t->set_in_and_out(mixData.first, mixData.first + mixData.second); 2398 if (reverse) { 2399 t->set_tracks(1, 0); 2400 m_track->plant_transition(*t.get(), 1, 0); 2401 } else { 2402 t->set_tracks(0, 1); 2403 m_track->plant_transition(*t.get(), 0, 1); 2404 } 2405 /*if (reverse) { 2406 t->set("reverse", 1); 2407 } 2408 m_track->plant_transition(*t.get(), 0, 1);*/ 2409 assetName = QStringLiteral("luma"); 2410 } 2411 QDomElement xml = TransitionsRepository::get()->getXml(assetName); 2412 std::shared_ptr<AssetParameterModel> asset( 2413 new AssetParameterModel(std::move(t), xml, assetName, ObjectId(KdenliveObjectType::TimelineMix, clipIds.second, ptr->uuid()), QString())); 2414 m_sameCompositions[clipIds.second] = asset; 2415 m_mixList.insert(clipIds.first, clipIds.second); 2416 return true; 2417 } 2418 return false; 2419 } 2420 2421 void TrackModel::setMixDuration(int cid, int mixDuration, int mixCut) 2422 { 2423 m_allClips[cid]->setMixDuration(mixDuration, mixCut); 2424 m_sameCompositions[cid]->getAsset()->set("kdenlive:mixcut", mixCut); 2425 int in = m_allClips[cid]->getPosition(); 2426 int out = in + mixDuration; 2427 Mlt::Transition &transition = *static_cast<Mlt::Transition *>(m_sameCompositions[cid]->getAsset()); 2428 transition.set_in_and_out(in, out); 2429 Q_EMIT m_sameCompositions[cid]->dataChanged(QModelIndex(), QModelIndex(), {AssetParameterModel::ParentDurationRole}); 2430 } 2431 2432 int TrackModel::getMixDuration(int cid) const 2433 { 2434 Q_ASSERT(m_sameCompositions.count(cid) > 0); 2435 Mlt::Transition &transition = *static_cast<Mlt::Transition *>(m_sameCompositions.at(cid)->getAsset()); 2436 return transition.get_length() - 1; 2437 } 2438 2439 void TrackModel::removeMix(const MixInfo &info) 2440 { 2441 Q_ASSERT(m_sameCompositions.count(info.secondClipId) > 0); 2442 Mlt::Transition &transition = *static_cast<Mlt::Transition *>(m_sameCompositions[info.secondClipId]->getAsset()); 2443 QScopedPointer<Mlt::Field> field(m_track->field()); 2444 field->lock(); 2445 field->disconnect_service(transition); 2446 field->unlock(); 2447 m_sameCompositions.erase(info.secondClipId); 2448 m_mixList.remove(info.firstClipId); 2449 } 2450 2451 void TrackModel::syncronizeMixes(bool finalMove) 2452 { 2453 QList<int> toDelete; 2454 for (const auto &n : m_sameCompositions) { 2455 // qDebug() << "Key:[" << n.first << "] Value:[" << n.second << "]\n"; 2456 int secondClipId = n.first; 2457 int firstClip = m_mixList.key(secondClipId, -1); 2458 Q_ASSERT(firstClip > -1); 2459 if (m_allClips.find(firstClip) == m_allClips.end() || m_allClips.find(secondClipId) == m_allClips.end()) { 2460 // One of the clip was removed, delete the mix 2461 qDebug() << "=== CLIPS: " << firstClip << " / " << secondClipId << " ARE MISSING!!!!"; 2462 Mlt::Transition &transition = *static_cast<Mlt::Transition *>(m_sameCompositions[secondClipId]->getAsset()); 2463 QScopedPointer<Mlt::Field> field(m_track->field()); 2464 field->lock(); 2465 field->disconnect_service(transition); 2466 field->unlock(); 2467 toDelete << secondClipId; 2468 m_mixList.remove(firstClip); 2469 continue; 2470 } 2471 // Asjust mix in/out 2472 int mixIn = m_allClips[secondClipId]->getPosition(); 2473 int mixOut = m_allClips[firstClip]->getPosition() + m_allClips[firstClip]->getPlaytime(); 2474 if (mixOut <= mixIn) { 2475 if (finalMove) { 2476 // Delete mix 2477 mixOut = mixIn; 2478 } else { 2479 mixOut = mixIn + 1; 2480 } 2481 } 2482 Mlt::Transition &transition = *static_cast<Mlt::Transition *>(m_sameCompositions[secondClipId]->getAsset()); 2483 if (mixIn == mixOut) { 2484 QScopedPointer<Mlt::Field> field(m_track->field()); 2485 field->lock(); 2486 field->disconnect_service(transition); 2487 field->unlock(); 2488 toDelete << secondClipId; 2489 m_mixList.remove(firstClip); 2490 } else { 2491 transition.set_in_and_out(mixIn, mixOut); 2492 } 2493 if (auto ptr = m_parent.lock()) { 2494 ptr->getClipPtr(secondClipId)->setMixDuration(mixOut - mixIn); 2495 QModelIndex ix = ptr->makeClipIndexFromID(secondClipId); 2496 Q_EMIT ptr->dataChanged(ix, ix, {TimelineModel::MixRole, TimelineModel::MixCutRole}); 2497 } 2498 } 2499 for (int i : qAsConst(toDelete)) { 2500 m_sameCompositions.erase(i); 2501 } 2502 } 2503 2504 int TrackModel::mixCount() const 2505 { 2506 Q_ASSERT(m_mixList.size() == int(m_sameCompositions.size())); 2507 return m_mixList.size(); 2508 } 2509 2510 bool TrackModel::hasMix(int cid) const 2511 { 2512 if (m_mixList.contains(cid)) { 2513 return true; 2514 } 2515 if (m_sameCompositions.count(cid) > 0) { 2516 return true; 2517 } 2518 return false; 2519 } 2520 2521 bool TrackModel::hasStartMix(int cid) const 2522 { 2523 return m_sameCompositions.count(cid) > 0; 2524 } 2525 2526 int TrackModel::isOnCut(int cid) 2527 { 2528 if (auto ptr = m_parent.lock()) { 2529 std::shared_ptr<CompositionModel> composition = ptr->getCompositionPtr(cid); 2530 // Start and end pos are incremented by 1 to account snapping 2531 int startPos = composition->getPosition() - 1; 2532 int endPos = startPos + composition->getPlaytime() + 1; 2533 int cid1 = getClipByPosition(startPos); 2534 int cid2 = getClipByPosition(endPos); 2535 if (cid1 == -1 || cid2 == -1 || cid1 == cid2) { 2536 return -1; 2537 } 2538 if (m_mixList.contains(cid1) || m_sameCompositions.count(cid2) > 0) { 2539 // Clips are already mixed, abort 2540 return -1; 2541 } 2542 std::shared_ptr<ClipModel> clip = ptr->getClipPtr(cid2); 2543 int mixRight = clip->getPosition(); 2544 std::shared_ptr<ClipModel> clip2 = ptr->getClipPtr(cid1); 2545 int mixLeft = clip2->getPosition() + clip2->getPlaytime(); 2546 if (mixLeft == mixRight) { 2547 return mixLeft; 2548 } 2549 } 2550 return -1; 2551 } 2552 2553 bool TrackModel::hasEndMix(int cid) const 2554 { 2555 return m_mixList.contains(cid); 2556 } 2557 2558 int TrackModel::getSecondMixPartner(int cid) const 2559 { 2560 return hasEndMix(cid) ? m_mixList.find(cid).value() : -1; 2561 } 2562 2563 QDomElement TrackModel::mixXml(QDomDocument &document, int cid) const 2564 { 2565 QDomElement container = document.createElement(QStringLiteral("mix")); 2566 int firstClipId = m_mixList.key(cid); 2567 container.setAttribute(QStringLiteral("firstClip"), firstClipId); 2568 container.setAttribute(QStringLiteral("secondClip"), cid); 2569 if (auto ptr = m_parent.lock()) { 2570 std::shared_ptr<ClipModel> clip = ptr->getClipPtr(firstClipId); 2571 container.setAttribute(QStringLiteral("firstClipPosition"), clip->getPosition()); 2572 } 2573 const QString assetId = m_sameCompositions.at(cid)->getAssetId(); 2574 QVector<QPair<QString, QVariant>> params = m_sameCompositions.at(cid)->getAllParameters(); 2575 container.setAttribute(QStringLiteral("asset"), assetId); 2576 for (const auto &p : qAsConst(params)) { 2577 QDomElement para = document.createElement(QStringLiteral("param")); 2578 para.setAttribute(QStringLiteral("name"), p.first); 2579 QDomText val = document.createTextNode(p.second.toString()); 2580 para.appendChild(val); 2581 container.appendChild(para); 2582 } 2583 std::pair<int, int> tracks = getMixTracks(cid); 2584 container.setAttribute(QStringLiteral("a_track"), tracks.first); 2585 container.setAttribute(QStringLiteral("b_track"), tracks.second); 2586 2587 std::pair<MixInfo, MixInfo> mixData = getMixInfo(cid); 2588 container.setAttribute(QStringLiteral("mixStart"), mixData.first.secondClipInOut.first); 2589 container.setAttribute(QStringLiteral("mixEnd"), mixData.first.firstClipInOut.second); 2590 container.setAttribute(QStringLiteral("mixOffset"), mixData.first.mixOffset); 2591 return container; 2592 } 2593 2594 bool TrackModel::loadMix(Mlt::Transition *t) 2595 { 2596 int in = t->get_in(); 2597 int out = t->get_out() - 1; 2598 bool reverse = t->get_int("reverse") == 1; 2599 int cid1 = getClipByPosition(in, reverse ? 1 : 0); 2600 int cid2 = getClipByPosition(out, reverse ? 0 : 1); 2601 if (cid1 < 0 || cid2 < 0) { 2602 qDebug() << "INVALID CLIP MIX: " << cid1 << " - " << cid2; 2603 // Check if reverse setting was not correctly set 2604 cid1 = getClipByPosition(in, reverse ? 0 : 1); 2605 cid2 = getClipByPosition(out, reverse ? 1 : 0); 2606 if (cid1 < 0 || cid2 < 0) { 2607 QScopedPointer<Mlt::Field> field(m_track->field()); 2608 field->lock(); 2609 field->disconnect_service(*t); 2610 field->unlock(); 2611 return false; 2612 } 2613 } else { 2614 int firstClipIn = m_allClips[cid1]->getPosition(); 2615 if (in == firstClipIn && in != m_allClips[cid2]->getPosition()) { 2616 qDebug() << "/// SWAPPING CLIPS"; 2617 if (m_allClips[cid1]->getPosition() > m_allClips[cid2]->getPosition()) { 2618 // Incorrecty detected revert mix 2619 std::swap(cid1, cid2); 2620 } 2621 } 2622 if (m_allClips[cid1]->getPosition() > m_allClips[cid2]->getPosition() || 2623 (m_allClips[cid1]->getPosition() + m_allClips[cid1]->getPlaytime()) > (m_allClips[cid2]->getPosition() + m_allClips[cid2]->getPlaytime())) { 2624 // Impossible mix, remove 2625 QScopedPointer<Mlt::Field> field(m_track->field()); 2626 field->lock(); 2627 field->disconnect_service(*t); 2628 field->unlock(); 2629 return false; 2630 } 2631 } 2632 // Ensure in/out points are in sync with the clips 2633 int clipIn = m_allClips[cid2]->getPosition(); 2634 int clipOut = m_allClips[cid1]->getPosition() + m_allClips[cid1]->getPlaytime(); 2635 if (in != clipIn || out != clipOut) { 2636 t->set_in_and_out(clipIn, clipOut); 2637 } 2638 QString assetId(t->get("kdenlive_id")); 2639 if (assetId.isEmpty()) { 2640 assetId = QString(t->get("mlt_service")); 2641 } 2642 std::unique_ptr<Mlt::Transition> tr(t); 2643 QDomElement xml = TransitionsRepository::get()->getXml(assetId); 2644 // Paste parameters from existing mix 2645 // std::unique_ptr<Mlt::Properties> sourceProperties(t); 2646 QStringList sourceProps; 2647 for (int i = 0; i < tr->count(); i++) { 2648 sourceProps << tr->get_name(i); 2649 } 2650 QDomNodeList params = xml.elementsByTagName(QStringLiteral("parameter")); 2651 for (int i = 0; i < params.count(); ++i) { 2652 QDomElement currentParameter = params.item(i).toElement(); 2653 QString paramName = currentParameter.attribute(QStringLiteral("name")); 2654 if (!sourceProps.contains(paramName)) { 2655 continue; 2656 } 2657 QString paramValue = tr->get(paramName.toUtf8().constData()); 2658 currentParameter.setAttribute(QStringLiteral("value"), paramValue); 2659 } 2660 QUuid timelineUuid; 2661 if (auto ptr = m_parent.lock()) { 2662 timelineUuid = ptr->uuid(); 2663 } 2664 std::shared_ptr<AssetParameterModel> asset( 2665 new AssetParameterModel(std::move(tr), xml, assetId, ObjectId(KdenliveObjectType::TimelineMix, cid2, timelineUuid), QString())); 2666 m_sameCompositions[cid2] = asset; 2667 m_mixList.insert(cid1, cid2); 2668 int mixDuration = t->get_length() - 1; 2669 int mixCutPos = qMin(t->get_int("kdenlive:mixcut"), mixDuration); 2670 setMixDuration(cid2, mixDuration, mixCutPos); 2671 return true; 2672 } 2673 2674 const std::shared_ptr<AssetParameterModel> TrackModel::mixModel(int cid) 2675 { 2676 if (m_sameCompositions.count(cid) > 0) { 2677 return m_sameCompositions[cid]; 2678 } 2679 return nullptr; 2680 } 2681 2682 bool TrackModel::reAssignEndMix(int currentId, int newId) 2683 { 2684 Q_ASSERT(m_mixList.contains(currentId)); 2685 int mixedClip = m_mixList.value(currentId); 2686 m_mixList.remove(currentId); 2687 m_mixList.insert(newId, mixedClip); 2688 return true; 2689 } 2690 2691 std::pair<int, int> TrackModel::getMixTracks(int cid) const 2692 { 2693 Q_ASSERT(m_sameCompositions.count(cid) > 0); 2694 int a_track = m_sameCompositions.at(cid)->getAsset()->get_int("a_track"); 2695 int b_track = m_sameCompositions.at(cid)->getAsset()->get_int("b_track"); 2696 return {a_track, b_track}; 2697 } 2698 2699 std::pair<QString, QVector<QPair<QString, QVariant>>> TrackModel::getMixParams(int cid) 2700 { 2701 Q_ASSERT(m_sameCompositions.count(cid) > 0); 2702 const QString assetId = m_sameCompositions[cid]->getAssetId(); 2703 QVector<QPair<QString, QVariant>> params = m_sameCompositions[cid]->getAllParameters(); 2704 return {assetId, params}; 2705 } 2706 2707 void TrackModel::switchMix(int cid, const QString &composition, Fun &undo, Fun &redo) 2708 { 2709 // First remove existing mix 2710 // lock MLT playlist so that we don't end up with invalid frames in monitor 2711 const QString currentAsset = m_sameCompositions[cid]->getAssetId(); 2712 QVector<QPair<QString, QVariant>> allParams = m_sameCompositions[cid]->getAllParameters(); 2713 std::pair<int, int> originTracks = getMixTracks(cid); 2714 // Check if mix should be reversed 2715 bool reverse = m_allClips[cid]->getSubPlaylistIndex() == 0; 2716 Fun local_redo = [this, cid, composition, reverse]() { 2717 m_playlists[0].lock(); 2718 m_playlists[1].lock(); 2719 Mlt::Transition &transition = *static_cast<Mlt::Transition *>(m_sameCompositions[cid]->getAsset()); 2720 int in = transition.get_in(); 2721 int out = transition.get_out(); 2722 int mixCutPos = transition.get_int("kdenlive:mixcut"); 2723 QScopedPointer<Mlt::Field> field(m_track->field()); 2724 field->lock(); 2725 field->disconnect_service(transition); 2726 field->unlock(); 2727 m_sameCompositions.erase(cid); 2728 if (auto ptr = m_parent.lock()) { 2729 std::unique_ptr<Mlt::Transition> t = TransitionsRepository::get()->getTransition(composition); 2730 t->set_in_and_out(in, out); 2731 if (reverse) { 2732 t->set_tracks(1, 0); 2733 m_track->plant_transition(*t.get(), 1, 0); 2734 } else { 2735 t->set_tracks(0, 1); 2736 m_track->plant_transition(*t.get(), 0, 1); 2737 } 2738 t->set("kdenlive:mixcut", mixCutPos); 2739 QDomElement xml = TransitionsRepository::get()->getXml(composition); 2740 std::shared_ptr<AssetParameterModel> asset( 2741 new AssetParameterModel(std::move(t), xml, composition, ObjectId(KdenliveObjectType::TimelineMix, cid, ptr->uuid()), QString())); 2742 m_sameCompositions[cid] = asset; 2743 } 2744 m_playlists[0].unlock(); 2745 m_playlists[1].unlock(); 2746 return true; 2747 }; 2748 Fun local_undo = [this, cid, currentAsset, allParams, originTracks]() { 2749 m_playlists[0].lock(); 2750 m_playlists[1].lock(); 2751 Mlt::Transition &transition = *static_cast<Mlt::Transition *>(m_sameCompositions[cid]->getAsset()); 2752 int in = transition.get_in(); 2753 int out = transition.get_out(); 2754 QScopedPointer<Mlt::Field> field(m_track->field()); 2755 field->lock(); 2756 field->disconnect_service(transition); 2757 field->unlock(); 2758 m_sameCompositions.erase(cid); 2759 if (auto ptr = m_parent.lock()) { 2760 std::unique_ptr<Mlt::Transition> t = TransitionsRepository::get()->getTransition(currentAsset); 2761 t->set_in_and_out(in, out); 2762 t->set_tracks(originTracks.first, originTracks.second); 2763 m_track->plant_transition(*t.get(), originTracks.first, originTracks.second); 2764 QDomElement xml = TransitionsRepository::get()->getXml(currentAsset); 2765 QDomNodeList xmlParams = xml.elementsByTagName(QStringLiteral("parameter")); 2766 for (int i = 0; i < xmlParams.count(); ++i) { 2767 QDomElement currentParameter = xmlParams.item(i).toElement(); 2768 QString paramName = currentParameter.attribute(QStringLiteral("name")); 2769 for (const auto &p : qAsConst(allParams)) { 2770 if (p.first == paramName) { 2771 currentParameter.setAttribute(QStringLiteral("value"), p.second.toString()); 2772 break; 2773 } 2774 } 2775 } 2776 std::shared_ptr<AssetParameterModel> asset( 2777 new AssetParameterModel(std::move(t), xml, currentAsset, ObjectId(KdenliveObjectType::TimelineMix, cid, ptr->uuid()), QString())); 2778 m_sameCompositions[cid] = asset; 2779 } 2780 m_playlists[0].unlock(); 2781 m_playlists[1].unlock(); 2782 return true; 2783 }; 2784 UPDATE_UNDO_REDO(local_redo, local_undo, undo, redo); 2785 } 2786 2787 void TrackModel::reverseCompositionXml(const QString &composition, QDomElement xml) 2788 { 2789 if (composition == QLatin1String("luma") || composition == QLatin1String("dissolve") || composition == QLatin1String("mix")) { 2790 Xml::setXmlParameter(xml, QStringLiteral("reverse"), QStringLiteral("1")); 2791 } else if (composition == QLatin1String("composite")) { 2792 Xml::setXmlParameter(xml, QStringLiteral("invert"), QStringLiteral("1")); 2793 } else if (composition == QLatin1String("wipe")) { 2794 Xml::setXmlParameter(xml, QStringLiteral("geometry"), QStringLiteral("0=0% 0% 100% 100% 100%;-1=0% 0% 100% 100% 0%")); 2795 } else if (composition == QLatin1String("slide")) { 2796 Xml::setXmlParameter(xml, QStringLiteral("rect"), QStringLiteral("0=0% 0% 100% 100% 100%;-1=100% 0% 100% 100% 100%")); 2797 } 2798 } 2799 2800 void TrackModel::updateCompositionDirection(Mlt::Transition &transition, bool reverse) 2801 { 2802 if (reverse) { 2803 transition.set_tracks(1, 0); 2804 m_track->plant_transition(transition, 1, 0); 2805 } else { 2806 transition.set_tracks(0, 1); 2807 m_track->plant_transition(transition, 0, 1); 2808 } 2809 } 2810 2811 bool TrackModel::mixIsReversed(int cid) const 2812 { 2813 if (m_sameCompositions.count(cid) > 0) { 2814 // There is a mix at clip start, adjust direction 2815 Mlt::Transition &transition = *static_cast<Mlt::Transition *>(m_sameCompositions.at(cid)->getAsset()); 2816 if (isAudioTrack()) { 2817 qDebug() << "::: CHKING TRANSITION ON AUDIO TRACK: " << transition.get_int("reverse"); 2818 return transition.get_int("reverse") == 1; 2819 } 2820 qDebug() << "::: CHKING TRANSITION ON video TRACK: "; 2821 return (transition.get_a_track() == 1 && transition.get_b_track() == 0); 2822 } 2823 return false; 2824 } 2825 2826 QVariantList TrackModel::stackZones() const 2827 { 2828 return m_effectStack->getEffectZones(); 2829 } 2830 2831 bool TrackModel::hasClipStart(int pos) 2832 { 2833 for (auto &m_playlist : m_playlists) { 2834 if (m_playlist.is_blank_at(pos)) { 2835 continue; 2836 } 2837 if (pos == 0 || m_playlist.get_clip_index_at(pos) != m_playlist.get_clip_index_at(pos - 1)) { 2838 return true; 2839 } 2840 } 2841 return false; 2842 } 2843 2844 QByteArray TrackModel::trackHash() 2845 { 2846 QByteArray fileData; 2847 // Parse clips 2848 for (auto &clip : m_allClips) { 2849 fileData.append(clip.second->clipHash().toUtf8()); 2850 } 2851 // Parse mixes 2852 for (auto &sameComposition : m_sameCompositions) { 2853 Mlt::Transition *tr = static_cast<Mlt::Transition *>(sameComposition.second->getAsset()); 2854 QString mixData = QString("%1 %2 %3").arg(QString::number(tr->get_in()), QString::number(tr->get_out()), sameComposition.second->getAssetId()); 2855 fileData.append(mixData.toLatin1()); 2856 } 2857 return fileData; 2858 }