File indexing completed on 2024-05-05 04:54:12
0001 /* 0002 SPDX-FileCopyrightText: 2017 Jean-Baptiste Mardelle 0003 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0004 */ 0005 0006 #include "timelinecontroller.h" 0007 #include "../model/timelinefunctions.hpp" 0008 #include "assets/keyframes/model/keyframemodellist.hpp" 0009 #include "audiomixer/mixermanager.hpp" 0010 #include "bin/bin.h" 0011 #include "bin/clipcreator.hpp" 0012 #include "bin/model/markerlistmodel.hpp" 0013 #include "bin/model/markersortmodel.h" 0014 #include "bin/model/subtitlemodel.hpp" 0015 #include "bin/projectclip.h" 0016 #include "bin/projectfolder.h" 0017 #include "bin/projectitemmodel.h" 0018 #include "core.h" 0019 #include "dialogs/importsubtitle.h" 0020 #include "dialogs/managesubtitles.h" 0021 #include "dialogs/spacerdialog.h" 0022 #include "dialogs/speechdialog.h" 0023 #include "dialogs/speeddialog.h" 0024 #include "dialogs/timeremap.h" 0025 #include "doc/kdenlivedoc.h" 0026 #include "effects/effectsrepository.hpp" 0027 #include "effects/effectstack/model/effectstackmodel.hpp" 0028 #include "glaxnimatelauncher.h" 0029 #include "kdenlivesettings.h" 0030 #include "lib/audio/audioEnvelope.h" 0031 #include "mainwindow.h" 0032 #include "monitor/monitormanager.h" 0033 #include "previewmanager.h" 0034 #include "project/projectmanager.h" 0035 #include "timeline2/model/clipmodel.hpp" 0036 #include "timeline2/model/compositionmodel.hpp" 0037 #include "timeline2/model/groupsmodel.hpp" 0038 #include "timeline2/model/snapmodel.hpp" 0039 #include "timeline2/model/trackmodel.hpp" 0040 #include "timeline2/view/dialogs/clipdurationdialog.h" 0041 #include "timeline2/view/dialogs/trackdialog.h" 0042 #include "timeline2/view/timelinewidget.h" 0043 #include "transitions/transitionsrepository.hpp" 0044 0045 #include <KColorScheme> 0046 #include <KMessageBox> 0047 #include <KRecentDirs> 0048 #include <KUrlRequesterDialog> 0049 #include <QClipboard> 0050 #include <QFontDatabase> 0051 #include <QQuickItem> 0052 #include <kio_version.h> 0053 0054 #include <QtMath> 0055 0056 #include <memory> 0057 #include <unistd.h> 0058 0059 TimelineController::TimelineController(QObject *parent) 0060 : QObject(parent) 0061 , multicamIn(-1) 0062 , m_duration(0) 0063 , m_root(nullptr) 0064 , m_usePreview(false) 0065 , m_audioRef(-1) 0066 , m_zone(-1, -1) 0067 , m_activeTrack(-1) 0068 , m_scale(QFontMetrics(QApplication::font()).maxWidth() / 250) 0069 , m_ready(false) 0070 , m_snapStackIndex(-1) 0071 , m_effectZone({0, 0}) 0072 , m_autotrackHeight(KdenliveSettings::autotrackheight()) 0073 { 0074 m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview")); 0075 connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview); 0076 m_disablePreview->setEnabled(false); 0077 connect(pCore.get(), &Core::autoScrollChanged, this, &TimelineController::autoScrollChanged); 0078 connect(pCore.get(), &Core::refreshActiveGuides, this, [this]() { m_activeSnaps.clear(); }); 0079 connect(pCore.get(), &Core::autoTrackHeight, this, [this](bool enable) { 0080 m_autotrackHeight = enable; 0081 Q_EMIT autotrackHeightChanged(); 0082 }); 0083 } 0084 0085 TimelineController::~TimelineController() {} 0086 0087 void TimelineController::prepareClose() 0088 { 0089 // Clear root so we don't call its methods anymore 0090 QObject::disconnect(m_deleteConnection); 0091 disconnect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions); 0092 disconnect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged); 0093 disconnect(this, &TimelineController::videoTargetChanged, this, &TimelineController::updateVideoTarget); 0094 disconnect(this, &TimelineController::audioTargetChanged, this, &TimelineController::updateAudioTarget); 0095 disconnect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::showMixModel); 0096 disconnect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::selectedMixChanged); 0097 m_ready = false; 0098 m_root = nullptr; 0099 // Delete timeline preview before resetting model so that removing clips from timeline doesn't invalidate 0100 m_model->resetPreviewManager(); 0101 m_model.reset(); 0102 } 0103 0104 void TimelineController::setModel(std::shared_ptr<TimelineItemModel> model) 0105 { 0106 m_zone = QPoint(-1, -1); 0107 m_hasAudioTarget = 0; 0108 m_lastVideoTarget = -1; 0109 m_lastAudioTarget.clear(); 0110 m_usePreview = false; 0111 m_model = model; 0112 m_activeSnaps.clear(); 0113 connect(m_model.get(), &TimelineItemModel::requestClearAssetView, pCore.get(), &Core::clearAssetPanel); 0114 m_deleteConnection = connect(m_model.get(), &TimelineItemModel::checkItemDeletion, this, [this](int id) { 0115 if (m_ready) { 0116 QMetaObject::invokeMethod(m_root, "checkDeletion", Qt::QueuedConnection, Q_ARG(QVariant, id)); 0117 } 0118 }); 0119 connect(m_model.get(), &TimelineItemModel::showTrackEffectStack, this, [&](int tid) { 0120 if (tid > -1) { 0121 showTrackAsset(tid); 0122 } else { 0123 showMasterEffects(); 0124 } 0125 }); 0126 if (m_model->hasTimelinePreview()) { 0127 // this timeline model already contains a timeline preview, connect it 0128 connectPreviewManager(); 0129 } 0130 connect(m_model.get(), &TimelineModel::connectPreviewManager, this, &TimelineController::connectPreviewManager); 0131 connect(m_model.get(), &TimelineModel::selectionModeChanged, this, &TimelineController::colorsChanged); 0132 connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions); 0133 connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateTrimmingMode); 0134 connect(this, &TimelineController::videoTargetChanged, this, &TimelineController::updateVideoTarget); 0135 connect(this, &TimelineController::audioTargetChanged, this, &TimelineController::updateAudioTarget); 0136 connect(m_model.get(), &TimelineItemModel::requestMonitorRefresh, [&]() { pCore->refreshProjectMonitorOnce(); }); 0137 connect(m_model.get(), &TimelineModel::durationUpdated, this, &TimelineController::checkDuration); 0138 connect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged); 0139 connect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::showMixModel); 0140 connect(m_model.get(), &TimelineModel::selectedMixChanged, this, &TimelineController::selectedMixChanged); 0141 connect(m_model.get(), &TimelineModel::dataChanged, this, &TimelineController::checkClipPosition); 0142 connect(m_model.get(), &TimelineModel::checkTrackDeletion, this, &TimelineController::checkTrackDeletion, Qt::DirectConnection); 0143 connect(m_model.get(), &TimelineModel::flashLock, this, &TimelineController::slotFlashLock); 0144 connect(m_model.get(), &TimelineModel::refreshClipActions, this, &TimelineController::updateClipActions); 0145 connect(m_model.get(), &TimelineModel::highlightSub, this, 0146 [this](int index) { QMetaObject::invokeMethod(m_root, "highlightSub", Qt::QueuedConnection, Q_ARG(QVariant, index)); }); 0147 if (m_model->hasSubtitleModel()) { 0148 loadSubtitleIndex(); 0149 } 0150 connect(m_model.get(), &TimelineItemModel::subtitleModelInitialized, this, &TimelineController::loadSubtitleIndex); 0151 } 0152 0153 void TimelineController::loadSubtitleIndex() 0154 { 0155 int currentIx = pCore->currentDoc()->getSequenceProperty(m_model->uuid(), QStringLiteral("kdenlive:activeSubtitleIndex"), QStringLiteral("0")).toInt(); 0156 auto subtitleModel = m_model->getSubtitleModel(); 0157 QMap<std::pair<int, QString>, QString> currentSubs = subtitleModel->getSubtitlesList(); 0158 QMapIterator<std::pair<int, QString>, QString> i(currentSubs); 0159 int counter = 0; 0160 while (i.hasNext()) { 0161 i.next(); 0162 if (i.key().first == currentIx) { 0163 m_activeSubPosition = counter; 0164 break; 0165 } 0166 counter++; 0167 } 0168 Q_EMIT activeSubtitlePositionChanged(); 0169 } 0170 0171 void TimelineController::restoreTargetTracks() 0172 { 0173 setTargetTracks(m_hasVideoTarget, m_model->m_binAudioTargets); 0174 } 0175 0176 void TimelineController::setTargetTracks(bool hasVideo, const QMap<int, QString> &audioTargets) 0177 { 0178 if (m_model->isLoading) { 0179 // Timeline is still being build 0180 return; 0181 } 0182 int videoTrack = -1; 0183 m_model->m_binAudioTargets = audioTargets; 0184 QMap<int, int> audioTracks; 0185 m_hasVideoTarget = hasVideo; 0186 m_hasAudioTarget = audioTargets.size(); 0187 if (m_hasVideoTarget) { 0188 videoTrack = m_model->getFirstVideoTrackIndex(); 0189 } 0190 if (m_hasAudioTarget > 0) { 0191 if (m_lastAudioTarget.count() == audioTargets.count()) { 0192 // Use existing track targets 0193 QList<int> audioStreams = audioTargets.keys(); 0194 QMapIterator<int, int> st(m_lastAudioTarget); 0195 while (st.hasNext()) { 0196 st.next(); 0197 audioTracks.insert(st.key(), audioStreams.takeLast()); 0198 } 0199 } else { 0200 // Use audio tracks from the first 0201 QVector<int> tracks; 0202 auto it = m_model->m_allTracks.cbegin(); 0203 while (it != m_model->m_allTracks.cend()) { 0204 if ((*it)->isAudioTrack()) { 0205 tracks << (*it)->getId(); 0206 } 0207 ++it; 0208 } 0209 if (KdenliveSettings::multistream_checktrack() && audioTargets.count() > tracks.count()) { 0210 pCore->bin()->checkProjectAudioTracks(QString(), audioTargets.count()); 0211 } 0212 QMapIterator<int, QString> st(audioTargets); 0213 while (st.hasNext()) { 0214 st.next(); 0215 if (tracks.isEmpty()) { 0216 break; 0217 } 0218 audioTracks.insert(tracks.takeLast(), st.key()); 0219 } 0220 } 0221 } 0222 Q_EMIT hasAudioTargetChanged(); 0223 Q_EMIT hasVideoTargetChanged(); 0224 setVideoTarget(m_hasVideoTarget && (m_lastVideoTarget > -1) ? m_lastVideoTarget : videoTrack); 0225 setAudioTarget(audioTracks); 0226 } 0227 0228 std::shared_ptr<TimelineItemModel> TimelineController::getModel() const 0229 { 0230 return m_model; 0231 } 0232 0233 void TimelineController::setRoot(QQuickItem *root) 0234 { 0235 m_root = root; 0236 m_ready = true; 0237 } 0238 0239 Mlt::Tractor *TimelineController::tractor() 0240 { 0241 return m_model->tractor(); 0242 } 0243 0244 Mlt::Producer TimelineController::trackProducer(int tid) 0245 { 0246 return *(m_model->getTrackById(tid).get()); 0247 } 0248 0249 double TimelineController::scaleFactor() const 0250 { 0251 return m_scale; 0252 } 0253 0254 const QString TimelineController::getTrackNameFromMltIndex(int trackPos) 0255 { 0256 if (trackPos == -1) { 0257 return i18n("unknown"); 0258 } 0259 if (trackPos == 0) { 0260 return i18n("Black"); 0261 } 0262 return m_model->getTrackTagById(m_model->getTrackIndexFromPosition(trackPos - 1)); 0263 } 0264 0265 const QString TimelineController::getTrackNameFromIndex(int trackIndex) 0266 { 0267 QString trackName = m_model->getTrackFullName(trackIndex); 0268 return trackName.isEmpty() ? m_model->getTrackTagById(trackIndex) : trackName; 0269 } 0270 0271 QMap<int, QString> TimelineController::getTrackNames(bool videoOnly) 0272 { 0273 QMap<int, QString> names; 0274 for (const auto &track : m_model->m_iteratorTable) { 0275 if (videoOnly && m_model->getTrackById_const(track.first)->isAudioTrack()) { 0276 continue; 0277 } 0278 QString trackName = m_model->getTrackFullName(track.first); 0279 names[m_model->getTrackMltIndex(track.first)] = trackName; 0280 } 0281 return names; 0282 } 0283 0284 void TimelineController::setScaleFactorOnMouse(double scale, bool zoomOnMouse) 0285 { 0286 if (m_root) { 0287 m_root->setProperty("zoomOnMouse", zoomOnMouse ? qMax(0, getMousePos()) : -1); 0288 m_scale = scale; 0289 Q_EMIT scaleFactorChanged(); 0290 } else { 0291 qWarning() << "Timeline root not created, impossible to zoom in"; 0292 } 0293 } 0294 0295 void TimelineController::setScaleFactor(double scale) 0296 { 0297 m_scale = scale; 0298 // Update mainwindow's zoom slider 0299 Q_EMIT updateZoom(scale); 0300 // inform qml 0301 Q_EMIT scaleFactorChanged(); 0302 } 0303 0304 int TimelineController::duration() const 0305 { 0306 return m_duration; 0307 } 0308 0309 int TimelineController::fullDuration() const 0310 { 0311 return m_duration + TimelineModel::seekDuration; 0312 } 0313 0314 void TimelineController::checkDuration() 0315 { 0316 int currentLength = m_model->duration(); 0317 if (currentLength != m_duration) { 0318 m_duration = currentLength; 0319 Q_EMIT durationChanged(m_duration); 0320 } 0321 } 0322 0323 void TimelineController::hideTrack(int trackId, bool hide) 0324 { 0325 bool isAudio = m_model->isAudioTrack(trackId); 0326 QString state = hide ? (isAudio ? "1" : "2") : "3"; 0327 QString previousState = m_model->getTrackProperty(trackId, QStringLiteral("hide")).toString(); 0328 Fun undo_lambda = [this, trackId, previousState]() { 0329 m_model->setTrackProperty(trackId, QStringLiteral("hide"), previousState); 0330 m_model->updateDuration(); 0331 return true; 0332 }; 0333 Fun redo_lambda = [this, trackId, state]() { 0334 m_model->setTrackProperty(trackId, QStringLiteral("hide"), state); 0335 m_model->updateDuration(); 0336 return true; 0337 }; 0338 redo_lambda(); 0339 pCore->pushUndo(undo_lambda, redo_lambda, state == QLatin1String("3") ? i18n("Hide Track") : i18n("Enable Track")); 0340 } 0341 0342 int TimelineController::selectedTrack() const 0343 { 0344 std::unordered_set<int> sel = m_model->getCurrentSelection(); 0345 if (sel.empty()) return -1; 0346 std::vector<std::pair<int, int>> selected_tracks; // contains pairs of (track position, track id) for each selected item 0347 for (int s : sel) { 0348 int tid = m_model->getItemTrackId(s); 0349 selected_tracks.emplace_back(m_model->getTrackPosition(tid), tid); 0350 } 0351 // sort by track position 0352 std::sort(selected_tracks.begin(), selected_tracks.begin(), [](const auto &a, const auto &b) { return a.first < b.first; }); 0353 return selected_tracks.front().second; 0354 } 0355 0356 bool TimelineController::selectCurrentItem(KdenliveObjectType type, bool select, bool addToCurrent, bool showErrorMsg) 0357 { 0358 int currentClip = -1; 0359 if (m_activeTrack == -1 || (m_model->isSubtitleTrack(m_activeTrack) && type != KdenliveObjectType::TimelineClip)) { 0360 // Cannot select item 0361 } else if (type == KdenliveObjectType::TimelineClip) { 0362 currentClip = m_model->isSubtitleTrack(m_activeTrack) ? m_model->getSubtitleByPosition(pCore->getMonitorPosition()) 0363 : m_model->getClipByPosition(m_activeTrack, pCore->getMonitorPosition()); 0364 } else if (type == KdenliveObjectType::TimelineComposition) { 0365 currentClip = m_model->getCompositionByPosition(m_activeTrack, pCore->getMonitorPosition()); 0366 } else if (type == KdenliveObjectType::TimelineMix) { 0367 if (m_activeTrack >= 0) { 0368 currentClip = m_model->getClipByPosition(m_activeTrack, pCore->getMonitorPosition()); 0369 } 0370 if (currentClip > -1) { 0371 if (m_model->hasClipEndMix(currentClip)) { 0372 int mixPartner = m_model->getTrackById_const(m_activeTrack)->getSecondMixPartner(currentClip); 0373 int clipEnd = m_model->getClipPosition(currentClip) + m_model->getClipPlaytime(currentClip); 0374 int mixStart = clipEnd - m_model->getMixDuration(mixPartner); 0375 if (mixStart < pCore->getMonitorPosition() && pCore->getMonitorPosition() < clipEnd) { 0376 if (select) { 0377 m_model->requestMixSelection(mixPartner); 0378 return true; 0379 } else if (selectedMix() == mixPartner) { 0380 m_model->requestClearSelection(); 0381 return true; 0382 } 0383 } 0384 } 0385 int delta = pCore->getMonitorPosition() - m_model->getClipPosition(currentClip); 0386 if (m_model->getMixDuration(currentClip) >= delta) { 0387 if (select) { 0388 m_model->requestMixSelection(currentClip); 0389 return true; 0390 } else if (selectedMix() == currentClip) { 0391 m_model->requestClearSelection(); 0392 return true; 0393 } 0394 return true; 0395 } else { 0396 currentClip = -1; 0397 } 0398 } 0399 } 0400 0401 if (currentClip == -1) { 0402 if (showErrorMsg) { 0403 pCore->displayMessage(i18n("No item under timeline cursor in active track"), ErrorMessage, 500); 0404 } 0405 return false; 0406 } 0407 if (!select) { 0408 m_model->requestRemoveFromSelection(currentClip); 0409 } else { 0410 bool grouped = m_model->m_groups->isInGroup(currentClip); 0411 m_model->requestAddToSelection(currentClip, !addToCurrent); 0412 if (grouped) { 0413 // If part of a group, ensure the effect/composition stack displays the selected item's properties 0414 showAsset(currentClip); 0415 } 0416 } 0417 return true; 0418 } 0419 0420 QList<int> TimelineController::selection() const 0421 { 0422 if (!m_root) return QList<int>(); 0423 std::unordered_set<int> sel = m_model->getCurrentSelection(); 0424 QList<int> items; 0425 for (int id : sel) { 0426 items << id; 0427 } 0428 return items; 0429 } 0430 0431 int TimelineController::selectedMix() const 0432 { 0433 return m_model->m_selectedMix; 0434 } 0435 0436 void TimelineController::selectItems(const QList<int> &ids) 0437 { 0438 std::unordered_set<int> ids_s(ids.begin(), ids.end()); 0439 m_model->requestSetSelection(ids_s); 0440 } 0441 0442 void TimelineController::setScrollPos(int pos) 0443 { 0444 if (pos > 0 && m_root) { 0445 QMetaObject::invokeMethod(m_root, "setScrollPos", Qt::QueuedConnection, Q_ARG(QVariant, pos)); 0446 } 0447 } 0448 0449 void TimelineController::resetView() 0450 { 0451 m_model->_resetView(); 0452 if (m_root) { 0453 QMetaObject::invokeMethod(m_root, "updatePalette"); 0454 } 0455 Q_EMIT colorsChanged(); 0456 } 0457 0458 bool TimelineController::snap() 0459 { 0460 return KdenliveSettings::snaptopoints(); 0461 } 0462 0463 bool TimelineController::ripple() 0464 { 0465 return false; 0466 } 0467 0468 bool TimelineController::scrub() 0469 { 0470 return false; 0471 } 0472 0473 int TimelineController::insertClip(int tid, int position, const QString &data_str, bool logUndo, bool refreshView, bool useTargets) 0474 { 0475 int id; 0476 if (tid == -1) { 0477 tid = m_activeTrack; 0478 } 0479 if (position == -1) { 0480 position = pCore->getMonitorPosition(); 0481 } 0482 if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView, useTargets)) { 0483 id = -1; 0484 } 0485 return id; 0486 } 0487 0488 QList<int> TimelineController::insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView) 0489 { 0490 QList<int> clipIds; 0491 if (tid == -1) { 0492 tid = m_activeTrack; 0493 } 0494 if (position == -1) { 0495 position = pCore->getMonitorPosition(); 0496 } 0497 TimelineFunctions::requestMultipleClipsInsertion(m_model, binIds, tid, position, clipIds, logUndo, refreshView); 0498 // we don't need to check the return value of the above function, in case of failure it will return an empty list of ids. 0499 return clipIds; 0500 } 0501 0502 void TimelineController::insertNewMix(int tid, int position, const QString &transitionId) 0503 { 0504 int clipId = m_model->getTrackById_const(tid)->getClipByPosition(position); 0505 if (clipId > 0) { 0506 m_model->mixClip(clipId, transitionId, -1); 0507 } 0508 } 0509 0510 int TimelineController::insertNewCompositionAtPos(int tid, int position, const QString &transitionId) 0511 { 0512 // TODO: adjust position and duration to existing clips ? 0513 return insertComposition(tid, position, transitionId, true); 0514 } 0515 0516 int TimelineController::insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo) 0517 { 0518 int id; 0519 int minimumPos = clipId > -1 ? m_model->getClipPosition(clipId) : offset; 0520 int clip_duration = clipId > -1 ? m_model->getClipPlaytime(clipId) : pCore->getDurationFromString(KdenliveSettings::transition_duration()); 0521 int endPos = minimumPos + clip_duration; 0522 int position = minimumPos; 0523 int duration = qMin(clip_duration, pCore->getDurationFromString(KdenliveSettings::transition_duration())); 0524 int lowerVideoTrackId = m_model->getPreviousVideoTrackIndex(tid); 0525 bool revert = offset > clip_duration / 2; 0526 int bottomId = 0; 0527 if (lowerVideoTrackId > 0) { 0528 bottomId = m_model->getTrackById_const(lowerVideoTrackId)->getClipByPosition(position + offset); 0529 } 0530 if (bottomId <= 0) { 0531 // No video track underneath 0532 if (offset < duration && duration < 2 * clip_duration) { 0533 // Composition dropped close to start, keep default composition duration 0534 } else if (clip_duration - offset < duration * 1.2 && duration < 2 * clip_duration) { 0535 // Composition dropped close to end, keep default composition duration 0536 position = endPos - duration; 0537 } else { 0538 // Use full clip length for duration 0539 duration = m_model->getTrackById_const(tid)->suggestCompositionLength(position); 0540 } 0541 } else { 0542 duration = qMin(duration, m_model->getTrackById_const(tid)->suggestCompositionLength(position)); 0543 QPair<int, int> bottom(m_model->m_allClips[bottomId]->getPosition(), m_model->m_allClips[bottomId]->getPlaytime()); 0544 if (bottom.first > minimumPos) { 0545 // Lower clip is after top clip 0546 if (position + offset > bottom.first) { 0547 int test_duration = m_model->getTrackById_const(tid)->suggestCompositionLength(bottom.first); 0548 if (test_duration > 0) { 0549 offset -= (bottom.first - position); 0550 position = bottom.first; 0551 duration = test_duration; 0552 revert = position > minimumPos; 0553 } 0554 } 0555 } else if (position >= bottom.first) { 0556 // Lower clip is before or at same pos as top clip 0557 int test_duration = m_model->getTrackById_const(lowerVideoTrackId)->suggestCompositionLength(position); 0558 if (test_duration > 0) { 0559 duration = qMin(test_duration, clip_duration); 0560 } 0561 } 0562 } 0563 int defaultLength = pCore->getDurationFromString(KdenliveSettings::transition_duration()); 0564 bool isShortComposition = TransitionsRepository::get()->getType(transitionId) == AssetListType::AssetType::VideoShortComposition; 0565 if (duration < 0 || (isShortComposition && duration > 1.5 * defaultLength)) { 0566 duration = defaultLength; 0567 } else if (duration <= 1) { 0568 // if suggested composition duration is lower than 4 frames, use default 0569 duration = pCore->getDurationFromString(KdenliveSettings::transition_duration()); 0570 if (minimumPos + clip_duration - position < 3) { 0571 position = minimumPos + clip_duration - duration; 0572 } 0573 } 0574 QPair<int, int> finalPos = m_model->getTrackById_const(tid)->validateCompositionLength(position, offset, duration, endPos); 0575 position = finalPos.first; 0576 duration = finalPos.second; 0577 0578 std::unique_ptr<Mlt::Properties> props(nullptr); 0579 if (revert) { 0580 props = std::make_unique<Mlt::Properties>(); 0581 if (transitionId == QLatin1String("dissolve")) { 0582 props->set("reverse", 1); 0583 } else if (transitionId == QLatin1String("composite")) { 0584 props->set("invert", 1); 0585 } else if (transitionId == QLatin1String("wipe")) { 0586 props->set("geometry", "0=0% 0% 100% 100% 100%;-1=0% 0% 100% 100% 0%"); 0587 } else if (transitionId == QLatin1String("slide")) { 0588 props->set("rect", "0=0% 0% 100% 100% 100%;-1=100% 0% 100% 100% 100%"); 0589 } 0590 } 0591 if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) { 0592 id = -1; 0593 pCore->displayMessage(i18n("Could not add composition at selected position"), ErrorMessage, 500); 0594 } 0595 return id; 0596 } 0597 0598 int TimelineController::isOnCut(int cid) const 0599 { 0600 Q_ASSERT(m_model->isComposition(cid)); 0601 int tid = m_model->getItemTrackId(cid); 0602 return m_model->getTrackById_const(tid)->isOnCut(cid); 0603 } 0604 0605 int TimelineController::insertComposition(int tid, int position, const QString &transitionId, bool logUndo) 0606 { 0607 int id; 0608 int duration = pCore->getDurationFromString(KdenliveSettings::transition_duration()); 0609 // Check if composition should be reversed (top clip at beginning, bottom at end) 0610 int a_track = m_model->getPreviousVideoTrackPos(tid); 0611 int topClip = m_model->getTrackById_const(tid)->getClipByPosition(position); 0612 int bottomClip = -1; 0613 if (a_track > 0) { 0614 // There is a video track below, check its clip 0615 int bottomTid = m_model->getTrackIndexFromPosition(a_track - 1); 0616 if (bottomTid > -1) { 0617 bottomClip = m_model->getTrackById_const(bottomTid)->getClipByPosition(position); 0618 } 0619 } 0620 bool reverse = false; 0621 if (topClip > -1 && bottomClip > -1) { 0622 if (m_model->getClipPosition(topClip) + m_model->getClipPlaytime(topClip) < 0623 m_model->getClipPosition(bottomClip) + m_model->getClipPlaytime(bottomClip)) { 0624 reverse = true; 0625 } 0626 } 0627 std::unique_ptr<Mlt::Properties> props(nullptr); 0628 if (reverse) { 0629 props = std::make_unique<Mlt::Properties>(); 0630 if (transitionId == QLatin1String("dissolve")) { 0631 props->set("reverse", 1); 0632 } else if (transitionId == QLatin1String("composite")) { 0633 props->set("invert", 1); 0634 } else if (transitionId == QLatin1String("wipe")) { 0635 props->set("geometry", "0=0% 0% 100% 100% 100%;-1=0% 0% 100% 100% 0%"); 0636 } else if (transitionId == QLatin1String("slide")) { 0637 props->set("rect", "0=0% 0% 100% 100% 100%;-1=100% 0% 100% 100% 100%"); 0638 } 0639 } 0640 if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) { 0641 id = -1; 0642 } 0643 return id; 0644 } 0645 0646 void TimelineController::slotFlashLock(int trackId) 0647 { 0648 QMetaObject::invokeMethod(m_root, "animateLockButton", Qt::QueuedConnection, Q_ARG(QVariant, trackId)); 0649 } 0650 0651 void TimelineController::deleteSelectedClips() 0652 { 0653 if (dragOperationRunning()) { 0654 // Don't allow timeline operation while drag in progress 0655 pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage); 0656 return; 0657 } 0658 auto sel = m_model->getCurrentSelection(); 0659 // Check if we are operating on a locked track 0660 std::unordered_set<int> trackIds; 0661 for (auto &id : sel) { 0662 if (m_model->isItem(id)) { 0663 trackIds.insert(m_model->getItemTrackId(id)); 0664 } 0665 } 0666 for (auto &tid : trackIds) { 0667 if (m_model->trackIsLocked(tid)) { 0668 m_model->flashLock(tid); 0669 return; 0670 } 0671 } 0672 if (sel.empty()) { 0673 // Check if a mix is selected 0674 if (m_model->m_selectedMix > -1 && m_model->isClip(m_model->m_selectedMix)) { 0675 m_model->removeMix(m_model->m_selectedMix); 0676 m_model->requestClearAssetView(m_model->m_selectedMix); 0677 m_model->requestClearSelection(true); 0678 } 0679 return; 0680 } 0681 // only need to delete the first item, the others will be deleted in cascade 0682 if (m_model->m_editMode == TimelineMode::InsertEdit) { 0683 // In insert mode, perform an extract operation (don't leave gaps) 0684 if (m_model->singleSelectionMode()) { 0685 // TODO only create 1 undo operation 0686 m_model->requestClearSelection(); 0687 std::function<bool(void)> undo = []() { return true; }; 0688 std::function<bool(void)> redo = []() { return true; }; 0689 for (auto &s : sel) { 0690 // Remove item from group 0691 int clipToUngroup = s; 0692 std::unordered_set<int> clipsToRegroup = m_model->m_groups->getLeaves(m_model->m_groups->getRootId(s)); 0693 clipsToRegroup.erase(clipToUngroup); 0694 int in = m_model->getClipPosition(s); 0695 int out = in + m_model->getClipPlaytime(s); 0696 int tid = m_model->getClipTrackId(s); 0697 std::pair<MixInfo, MixInfo> mixData = m_model->getTrackById_const(tid)->getMixInfo(s); 0698 if (mixData.first.firstClipId > -1) { 0699 // Clip has a start mix, adjust in point 0700 in += (mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first - mixData.first.mixOffset); 0701 } 0702 if (mixData.second.firstClipId > -1) { 0703 // Clip has end mix, adjust out point 0704 out -= mixData.second.mixOffset; 0705 } 0706 QVector<int> tracks = {tid}; 0707 TimelineFunctions::extractZoneWithUndo(m_model, tracks, QPoint(in, out), false, clipToUngroup, clipsToRegroup, undo, redo); 0708 } 0709 pCore->pushUndo(undo, redo, i18n("Extract zone")); 0710 } else { 0711 extract(*sel.begin()); 0712 } 0713 } else { 0714 m_model->requestItemDeletion(*sel.begin()); 0715 } 0716 } 0717 0718 int TimelineController::getMainSelectedItem(bool restrictToCurrentPos, bool allowComposition) 0719 { 0720 auto sel = m_model->getCurrentSelection(); 0721 if (sel.empty() || sel.size() > 2) { 0722 return -1; 0723 } 0724 int itemId = *(sel.begin()); 0725 if (sel.size() == 2) { 0726 int parentGroup = m_model->m_groups->getRootId(itemId); 0727 if (parentGroup == -1 || m_model->m_groups->getType(parentGroup) != GroupType::AVSplit) { 0728 return -1; 0729 } 0730 } 0731 if (!restrictToCurrentPos) { 0732 if (m_model->isClip(itemId) || (allowComposition && m_model->isComposition(itemId))) { 0733 return itemId; 0734 } 0735 } 0736 if (m_model->isClip(itemId)) { 0737 int position = pCore->getMonitorPosition(); 0738 int start = m_model->getClipPosition(itemId); 0739 int end = start + m_model->getClipPlaytime(itemId); 0740 if (position >= start && position <= end) { 0741 return itemId; 0742 } 0743 } 0744 return -1; 0745 } 0746 0747 std::pair<int, int> TimelineController::selectionPosition(int *aTracks, int *vTracks) 0748 { 0749 std::unordered_set<int> selectedIds = m_model->getCurrentSelection(); 0750 if (selectedIds.empty()) { 0751 return {-1, -1}; 0752 } 0753 int position = -1; 0754 int targetTrackId = -1; 0755 std::pair<int, int> audioTracks = {-1, -1}; 0756 std::pair<int, int> videoTracks = {-1, -1}; 0757 int topVideoWithSplit = -1; 0758 for (auto &id : selectedIds) { 0759 int tid = m_model->getItemTrackId(id); 0760 if (m_model->isSubtitleTrack(tid)) { 0761 // Subtitle track not supported 0762 continue; 0763 } 0764 if (position == -1 || position > m_model->getItemPosition(id)) { 0765 position = m_model->getItemPosition(id); 0766 } 0767 int trackPos = m_model->getTrackPosition(tid); 0768 if (m_model->isAudioTrack(tid)) { 0769 // Find audio track range 0770 if (audioTracks.first < 0 || trackPos < audioTracks.first) { 0771 audioTracks.first = trackPos; 0772 } 0773 if (audioTracks.second < 0 || trackPos > audioTracks.second) { 0774 audioTracks.second = trackPos; 0775 } 0776 } else { 0777 // Find video track range 0778 int splitId = m_model->m_groups->getSplitPartner(id); 0779 if (splitId > -1 && (topVideoWithSplit == -1 || trackPos > topVideoWithSplit)) { 0780 topVideoWithSplit = trackPos; 0781 } 0782 if (videoTracks.first < 0 || trackPos < videoTracks.first) { 0783 videoTracks.first = trackPos; 0784 } 0785 if (videoTracks.second < 0 || trackPos > videoTracks.second) { 0786 videoTracks.second = trackPos; 0787 } 0788 } 0789 } 0790 int minimumMirrorTracks = 0; 0791 if (topVideoWithSplit > -1) { 0792 // Ensure we have enough audio tracks for audio partners 0793 minimumMirrorTracks = topVideoWithSplit - videoTracks.first + 1; 0794 } 0795 0796 if (videoTracks.first > -1) { 0797 *vTracks = videoTracks.second - videoTracks.first + 1; 0798 targetTrackId = m_model->getTrackIndexFromPosition(videoTracks.first); 0799 } else { 0800 *vTracks = 0; 0801 } 0802 if (audioTracks.first > -1) { 0803 *aTracks = qMax(audioTracks.second - audioTracks.first + 1, minimumMirrorTracks); 0804 if (targetTrackId == -1) { 0805 targetTrackId = m_model->getTrackIndexFromPosition(audioTracks.second); 0806 } 0807 } else { 0808 *aTracks = qMax(0, minimumMirrorTracks); 0809 } 0810 return {position, targetTrackId}; 0811 } 0812 0813 int TimelineController::copyItem() 0814 { 0815 std::unordered_set<int> selectedIds = m_model->getCurrentSelection(); 0816 if (selectedIds.empty()) { 0817 return -1; 0818 } 0819 int clipId = *(selectedIds.begin()); 0820 QString copyString = TimelineFunctions::copyClips(m_model, selectedIds); 0821 QClipboard *clipboard = QApplication::clipboard(); 0822 clipboard->setText(copyString); 0823 m_root->setProperty("copiedClip", clipId); 0824 return clipId; 0825 } 0826 0827 std::pair<int, QString> TimelineController::getCopyItemData() 0828 { 0829 std::unordered_set<int> selectedIds = m_model->getCurrentSelection(); 0830 if (selectedIds.empty()) { 0831 return {-1, QString()}; 0832 } 0833 int clipId = *(selectedIds.begin()); 0834 QString copyString = TimelineFunctions::copyClips(m_model, selectedIds); 0835 return {clipId, copyString}; 0836 } 0837 0838 bool TimelineController::pasteItem(int position, int tid) 0839 { 0840 QClipboard *clipboard = QApplication::clipboard(); 0841 QString txt = clipboard->text(); 0842 if (tid == -1) { 0843 tid = m_activeTrack; 0844 } 0845 if (position == -1) { 0846 position = getMenuOrTimelinePos(); 0847 } 0848 return TimelineFunctions::pasteClips(m_model, txt, tid, position); 0849 } 0850 0851 void TimelineController::triggerAction(const QString &name) 0852 { 0853 pCore->triggerAction(name); 0854 } 0855 0856 const QString TimelineController::actionText(const QString &name) 0857 { 0858 return pCore->actionText(name); 0859 } 0860 0861 QString TimelineController::timecode(int frames) const 0862 { 0863 return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df); 0864 } 0865 0866 QString TimelineController::framesToClock(int frames) const 0867 { 0868 return m_model->tractor()->frames_to_time(frames, mlt_time_clock); 0869 } 0870 0871 QString TimelineController::simplifiedTC(int frames) const 0872 { 0873 if (KdenliveSettings::frametimecode()) { 0874 return QString::number(frames); 0875 } 0876 QString s = m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df); 0877 return s.startsWith(QLatin1String("00:")) ? s.remove(0, 3) : s; 0878 } 0879 0880 bool TimelineController::showThumbnails() const 0881 { 0882 return KdenliveSettings::videothumbnails(); 0883 } 0884 0885 bool TimelineController::showAudioThumbnails() const 0886 { 0887 return KdenliveSettings::audiothumbnails(); 0888 } 0889 0890 bool TimelineController::showMarkers() const 0891 { 0892 return KdenliveSettings::showmarkers(); 0893 } 0894 0895 bool TimelineController::audioThumbFormat() const 0896 { 0897 return KdenliveSettings::displayallchannels(); 0898 } 0899 0900 bool TimelineController::audioThumbNormalize() const 0901 { 0902 return KdenliveSettings::normalizechannels(); 0903 } 0904 0905 bool TimelineController::showWaveforms() const 0906 { 0907 return KdenliveSettings::audiothumbnails(); 0908 } 0909 0910 void TimelineController::beginAddTrack(int tid) 0911 { 0912 if (tid == -1) { 0913 tid = m_activeTrack; 0914 } 0915 QScopedPointer<TrackDialog> d(new TrackDialog(m_model, tid, qApp->activeWindow())); 0916 if (d->exec() == QDialog::Accepted) { 0917 auto trackName = d->trackName(); 0918 bool result = 0919 m_model->addTracksAtPosition(d->selectedTrackPosition(), d->tracksCount(), trackName, d->addAudioTrack(), d->addAVTrack(), d->addRecTrack()); 0920 if (!result) { 0921 pCore->displayMessage(i18n("Could not insert track"), ErrorMessage, 500); 0922 } 0923 } 0924 } 0925 0926 void TimelineController::deleteMultipleTracks(int tid) 0927 { 0928 Fun undo = []() { return true; }; 0929 Fun redo = []() { return true; }; 0930 QScopedPointer<TrackDialog> d(new TrackDialog(m_model, tid, qApp->activeWindow(), true, m_activeTrack)); 0931 if (d->exec() == QDialog::Accepted) { 0932 bool result = true; 0933 QList<int> allIds = d->toDeleteTrackIds(); 0934 for (int selectedTrackIx : qAsConst(allIds)) { 0935 result = m_model->requestTrackDeletion(selectedTrackIx, undo, redo); 0936 if (!result) { 0937 break; 0938 } 0939 if (m_activeTrack == -1) { 0940 setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1)); 0941 } 0942 } 0943 if (result) { 0944 pCore->pushUndo(undo, redo, allIds.count() > 1 ? i18n("Delete Tracks") : i18n("Delete Track")); 0945 } else { 0946 undo(); 0947 } 0948 } 0949 } 0950 0951 void TimelineController::switchTrackRecord(int tid, bool monitor) 0952 { 0953 if (tid == -1) { 0954 tid = m_activeTrack; 0955 } 0956 if (!m_model->getTrackById_const(tid)->isAudioTrack()) { 0957 pCore->displayMessage(i18n("Select an audio track to display record controls"), ErrorMessage, 500); 0958 } 0959 int recDisplayed = m_model->getTrackProperty(tid, QStringLiteral("kdenlive:audio_rec")).toInt(); 0960 if (monitor == false) { 0961 // Disable rec controls 0962 if (recDisplayed == 0) { 0963 // Already hidden 0964 return; 0965 } 0966 m_model->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), QStringLiteral("0")); 0967 } else { 0968 // Enable rec controls 0969 if (recDisplayed == 1) { 0970 // Already displayed 0971 return; 0972 } 0973 m_model->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), QStringLiteral("1")); 0974 } 0975 QModelIndex ix = m_model->makeTrackIndexFromID(tid); 0976 if (ix.isValid()) { 0977 Q_EMIT m_model->dataChanged(ix, ix, {TimelineModel::AudioRecordRole}); 0978 } 0979 } 0980 0981 void TimelineController::checkTrackDeletion(int selectedTrackIx) 0982 { 0983 if (m_activeTrack == selectedTrackIx) { 0984 // Make sure we don't keep an index on a deleted track 0985 m_activeTrack = -1; 0986 Q_EMIT activeTrackChanged(); 0987 } 0988 if (m_model->m_audioTarget.contains(selectedTrackIx)) { 0989 QMap<int, int> selection = m_model->m_audioTarget; 0990 selection.remove(selectedTrackIx); 0991 setAudioTarget(selection); 0992 } 0993 if (m_model->m_videoTarget == selectedTrackIx) { 0994 setVideoTarget(-1); 0995 } 0996 if (m_lastAudioTarget.contains(selectedTrackIx)) { 0997 m_lastAudioTarget.remove(selectedTrackIx); 0998 Q_EMIT lastAudioTargetChanged(); 0999 } 1000 if (m_lastVideoTarget == selectedTrackIx) { 1001 m_lastVideoTarget = -1; 1002 Q_EMIT lastVideoTargetChanged(); 1003 } 1004 } 1005 1006 void TimelineController::showConfig(int page, int tab) 1007 { 1008 Q_EMIT pCore->showConfigDialog((Kdenlive::ConfigPage)page, tab); 1009 } 1010 1011 void TimelineController::gotoNextSnap() 1012 { 1013 if (m_activeSnaps.empty() || pCore->undoIndex() != m_snapStackIndex) { 1014 m_snapStackIndex = pCore->undoIndex(); 1015 m_activeSnaps.clear(); 1016 m_activeSnaps = m_model->getGuideModel()->getSnapPoints(); 1017 m_activeSnaps.push_back(m_zone.x()); 1018 m_activeSnaps.push_back(m_zone.y() - 1); 1019 } 1020 std::vector<int> canceled = m_model->getFilteredGuideModel()->getIgnoredSnapPoints(); 1021 int nextSnap = m_model->getNextSnapPos(pCore->getMonitorPosition(), m_activeSnaps, canceled); 1022 if (nextSnap > pCore->getMonitorPosition()) { 1023 setPosition(nextSnap); 1024 } 1025 } 1026 1027 void TimelineController::gotoPreviousSnap() 1028 { 1029 if (pCore->getMonitorPosition() > 0) { 1030 if (m_activeSnaps.empty() || pCore->undoIndex() != m_snapStackIndex) { 1031 m_snapStackIndex = pCore->undoIndex(); 1032 m_activeSnaps.clear(); 1033 m_activeSnaps = m_model->getGuideModel()->getSnapPoints(); 1034 m_activeSnaps.push_back(m_zone.x()); 1035 m_activeSnaps.push_back(m_zone.y() - 1); 1036 } 1037 std::vector<int> canceled = m_model->getFilteredGuideModel()->getIgnoredSnapPoints(); 1038 setPosition(m_model->getPreviousSnapPos(pCore->getMonitorPosition(), m_activeSnaps, canceled)); 1039 } 1040 } 1041 1042 void TimelineController::gotoNextGuide() 1043 { 1044 QList<CommentedTime> guides = m_model->getGuideModel()->getAllMarkers(); 1045 std::vector<int> canceled = m_model->getFilteredGuideModel()->getIgnoredSnapPoints(); 1046 int pos = pCore->getMonitorPosition(); 1047 double fps = pCore->getCurrentFps(); 1048 int guidePos = 0; 1049 for (auto &guide : guides) { 1050 guidePos = guide.time().frames(fps); 1051 if (std::find(canceled.begin(), canceled.end(), guidePos) != canceled.end()) { 1052 continue; 1053 } 1054 if (guidePos > pos) { 1055 setPosition(guidePos); 1056 return; 1057 } 1058 } 1059 setPosition(m_duration - 1); 1060 } 1061 1062 void TimelineController::gotoPreviousGuide() 1063 { 1064 if (pCore->getMonitorPosition() > 0) { 1065 QList<CommentedTime> guides = m_model->getGuideModel()->getAllMarkers(); 1066 std::vector<int> canceled = m_model->getFilteredGuideModel()->getIgnoredSnapPoints(); 1067 int pos = pCore->getMonitorPosition(); 1068 double fps = pCore->getCurrentFps(); 1069 int lastGuidePos = 0; 1070 int guidePos = 0; 1071 for (auto &guide : guides) { 1072 guidePos = guide.time().frames(fps); 1073 if (std::find(canceled.begin(), canceled.end(), guidePos) != canceled.end()) { 1074 continue; 1075 } 1076 if (guidePos >= pos) { 1077 setPosition(lastGuidePos); 1078 return; 1079 } 1080 lastGuidePos = guidePos; 1081 } 1082 setPosition(lastGuidePos); 1083 } 1084 } 1085 1086 void TimelineController::groupSelection() 1087 { 1088 if (dragOperationRunning()) { 1089 // Don't allow timeline operation while drag in progress 1090 pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage); 1091 return; 1092 } 1093 const auto selection = m_model->getCurrentSelection(); 1094 if (selection.size() < 2) { 1095 pCore->displayMessage(i18n("Select at least 2 items to group"), ErrorMessage, 500); 1096 return; 1097 } 1098 m_model->requestClearSelection(); 1099 m_model->requestClipsGroup(selection); 1100 m_model->requestSetSelection(selection); 1101 } 1102 1103 void TimelineController::unGroupSelection(int cid) 1104 { 1105 if (dragOperationRunning()) { 1106 // Don't allow timeline operation while drag in progress 1107 pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage); 1108 return; 1109 } 1110 auto ids = m_model->getCurrentSelection(); 1111 // ask to unselect if needed 1112 m_model->requestClearSelection(); 1113 if (cid > -1) { 1114 ids.insert(cid); 1115 } 1116 if (!ids.empty()) { 1117 m_model->requestClipsUngroup(ids); 1118 } 1119 } 1120 1121 bool TimelineController::dragOperationRunning() 1122 { 1123 QVariant returnedValue; 1124 QMetaObject::invokeMethod(m_root, "isDragging", Qt::DirectConnection, Q_RETURN_ARG(QVariant, returnedValue)); 1125 return returnedValue.toBool(); 1126 } 1127 1128 bool TimelineController::trimmingActive() 1129 { 1130 ToolType::ProjectTool tool = pCore->window()->getCurrentTimeline()->activeTool(); 1131 return tool == ToolType::SlideTool || tool == ToolType::SlipTool || tool == ToolType::RippleTool || tool == ToolType::RollTool; 1132 } 1133 1134 void TimelineController::setInPoint(bool ripple) 1135 { 1136 if (dragOperationRunning()) { 1137 // Don't allow timeline operation while drag in progress 1138 pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage); 1139 qDebug() << "Cannot operate while dragging"; 1140 return; 1141 } 1142 auto requestResize = [this, ripple](int id, int size) { 1143 if (ripple) { 1144 m_model->requestItemRippleResize(m_model, id, size, false, true, !KdenliveSettings::lockedGuides(), 0, false); 1145 setPosition(m_model->getItemPosition(id)); 1146 } else { 1147 m_model->requestItemResize(id, size, false, true, 0, false); 1148 } 1149 }; 1150 int cursorPos = pCore->getMonitorPosition(); 1151 const auto selection = m_model->getCurrentSelection(); 1152 bool selectionFound = false; 1153 if (!selection.empty()) { 1154 for (int id : selection) { 1155 int start = m_model->getItemPosition(id); 1156 if (start == cursorPos) { 1157 continue; 1158 } 1159 int size = start + m_model->getItemPlaytime(id) - cursorPos; 1160 requestResize(id, size); 1161 selectionFound = true; 1162 } 1163 } 1164 if (!selectionFound) { 1165 if (m_activeTrack >= 0) { 1166 int cid = m_model->getClipByPosition(m_activeTrack, cursorPos); 1167 if (cid < 0) { 1168 // Check first item after timeline position 1169 int maximumSpace = m_model->getTrackById_const(m_activeTrack)->getBlankEnd(cursorPos); 1170 if (maximumSpace < INT_MAX) { 1171 cid = m_model->getClipByPosition(m_activeTrack, maximumSpace + 1); 1172 } 1173 } 1174 if (cid >= 0) { 1175 int start = m_model->getItemPosition(cid); 1176 if (start != cursorPos) { 1177 int size = start + m_model->getItemPlaytime(cid) - cursorPos; 1178 requestResize(cid, size); 1179 selectionFound = true; 1180 } 1181 } 1182 } else if (m_model->isSubtitleTrack(m_activeTrack)) { 1183 // Subtitle track 1184 auto subtitleModel = m_model->getSubtitleModel(); 1185 if (subtitleModel) { 1186 int sid = -1; 1187 std::unordered_set<int> sids = subtitleModel->getItemsInRange(cursorPos, cursorPos); 1188 if (sids.empty()) { 1189 sids = subtitleModel->getItemsInRange(cursorPos, -1); 1190 for (int s : sids) { 1191 if (sid == -1 || subtitleModel->getStartPosForId(s) < subtitleModel->getStartPosForId(sid)) { 1192 sid = s; 1193 } 1194 } 1195 } else { 1196 sid = *sids.begin(); 1197 } 1198 if (sid > -1) { 1199 int start = m_model->getItemPosition(sid); 1200 if (start != cursorPos) { 1201 int size = start + m_model->getItemPlaytime(sid) - cursorPos; 1202 requestResize(sid, size); 1203 selectionFound = true; 1204 } 1205 } 1206 } 1207 } 1208 if (!selectionFound) { 1209 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 1210 } 1211 } 1212 } 1213 1214 void TimelineController::setOutPoint(bool ripple) 1215 { 1216 if (dragOperationRunning()) { 1217 // Don't allow timeline operation while drag in progress 1218 pCore->displayMessage(i18n("Cannot perform operation while dragging in timeline"), ErrorMessage); 1219 qDebug() << "Cannot operate while dragging"; 1220 return; 1221 } 1222 auto requestResize = [this, ripple](int id, int size) { 1223 if (ripple) { 1224 m_model->requestItemRippleResize(m_model, id, size, true, true, !KdenliveSettings::lockedGuides(), 0, false); 1225 } else { 1226 m_model->requestItemResize(id, size, true, true, 0, false); 1227 } 1228 }; 1229 int cursorPos = pCore->getMonitorPosition(); 1230 const auto selection = m_model->getCurrentSelection(); 1231 bool selectionFound = false; 1232 if (!selection.empty()) { 1233 for (int id : selection) { 1234 int start = m_model->getItemPosition(id); 1235 if (start + m_model->getItemPlaytime(id) == cursorPos) { 1236 continue; 1237 } 1238 int size = cursorPos - start; 1239 requestResize(id, size); 1240 selectionFound = true; 1241 } 1242 } 1243 if (!selectionFound) { 1244 if (m_activeTrack >= 0) { 1245 int cid = m_model->getClipByPosition(m_activeTrack, cursorPos); 1246 if (cid < 0 || cursorPos == m_model->getItemPosition(cid)) { 1247 // If no clip found at cursor pos or we are at the first frame of a clip, try to find previous clip 1248 // Check first item before timeline position 1249 // If we are at a clip start, check space before this clip 1250 int offset = cid >= 0 ? 1 : 0; 1251 int previousPos = m_model->getTrackById_const(m_activeTrack)->getBlankStart(cursorPos - offset); 1252 cid = m_model->getClipByPosition(m_activeTrack, qMax(0, previousPos - 1)); 1253 } 1254 if (cid >= 0) { 1255 int start = m_model->getItemPosition(cid); 1256 if (start + m_model->getItemPlaytime(cid) != cursorPos) { 1257 int size = cursorPos - start; 1258 requestResize(cid, size); 1259 selectionFound = true; 1260 } 1261 } 1262 } else if (m_model->isSubtitleTrack(m_activeTrack)) { 1263 // Subtitle track 1264 auto subtitleModel = m_model->getSubtitleModel(); 1265 if (subtitleModel) { 1266 int sid = -1; 1267 std::unordered_set<int> sids = subtitleModel->getItemsInRange(cursorPos, cursorPos); 1268 if (sids.empty()) { 1269 sids = subtitleModel->getItemsInRange(0, cursorPos); 1270 for (int s : sids) { 1271 if (sid == -1 || subtitleModel->getSubtitleEnd(s) > subtitleModel->getSubtitleEnd(sid)) { 1272 sid = s; 1273 } 1274 } 1275 } else { 1276 sid = *sids.begin(); 1277 } 1278 if (sid > -1) { 1279 int start = m_model->getItemPosition(sid); 1280 if (start + m_model->getItemPlaytime(sid) != cursorPos) { 1281 int size = cursorPos - start; 1282 requestResize(sid, size); 1283 selectionFound = true; 1284 } 1285 } 1286 } 1287 } 1288 if (!selectionFound) { 1289 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 1290 } 1291 } 1292 } 1293 1294 void TimelineController::editMarker(int cid, int position) 1295 { 1296 if (cid == -1) { 1297 cid = getMainSelectedClip(); 1298 if (cid == -1) { 1299 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 1300 return; 1301 } 1302 } 1303 Q_ASSERT(m_model->isClip(cid)); 1304 double speed = m_model->getClipSpeed(cid); 1305 if (position == -1) { 1306 // Calculate marker position relative to timeline cursor 1307 position = pCore->getMonitorPosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); 1308 position = int(position * speed); 1309 } 1310 if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { 1311 pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500); 1312 return; 1313 } 1314 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid)); 1315 if (clip->getMarkerModel()->hasMarker(position)) { 1316 GenTime pos(position, pCore->getCurrentFps()); 1317 clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), false, clip.get()); 1318 } else { 1319 pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500); 1320 } 1321 } 1322 1323 void TimelineController::addMarker(int cid, int position) 1324 { 1325 if (cid == -1) { 1326 cid = getMainSelectedClip(); 1327 if (cid == -1) { 1328 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 1329 return; 1330 } 1331 } 1332 Q_ASSERT(m_model->isClip(cid)); 1333 double speed = m_model->getClipSpeed(cid); 1334 if (position == -1) { 1335 // Calculate marker position relative to timeline cursor 1336 position = pCore->getMonitorPosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); 1337 position = int(position * speed); 1338 } 1339 if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { 1340 pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500); 1341 return; 1342 } 1343 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid)); 1344 GenTime pos(position, pCore->getCurrentFps()); 1345 clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), true, clip.get()); 1346 } 1347 1348 int TimelineController::getMainSelectedClip() 1349 { 1350 int clipId = m_root->property("mainItemId").toInt(); 1351 if (clipId == -1 || !isInSelection(clipId)) { 1352 std::unordered_set<int> sel = m_model->getCurrentSelection(); 1353 for (int i : sel) { 1354 if (m_model->isClip(i)) { 1355 clipId = i; 1356 break; 1357 } 1358 } 1359 } 1360 return m_model->isClip(clipId) ? clipId : -1; 1361 } 1362 1363 void TimelineController::addQuickMarker(int cid, int position) 1364 { 1365 if (cid == -1) { 1366 cid = getMainSelectedClip(); 1367 if (cid == -1) { 1368 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 1369 return; 1370 } 1371 } 1372 Q_ASSERT(m_model->isClip(cid)); 1373 double speed = m_model->getClipSpeed(cid); 1374 if (position == -1) { 1375 // Calculate marker position relative to timeline cursor 1376 position = pCore->getMonitorPosition() - m_model->getClipPosition(cid); 1377 position = int(position * speed); 1378 } 1379 if (position < (m_model->getClipIn(cid) * speed) || position > ((m_model->getClipIn(cid) + m_model->getClipPlaytime(cid) * speed))) { 1380 pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500); 1381 return; 1382 } 1383 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid)); 1384 GenTime pos(position, pCore->getCurrentFps()); 1385 CommentedTime marker(pos, pCore->currentDoc()->timecode().getDisplayTimecode(pos, false), KdenliveSettings::default_marker_type()); 1386 clip->getMarkerModel()->addMarker(marker.time(), marker.comment(), marker.markerType()); 1387 } 1388 1389 void TimelineController::deleteMarker(int cid, int position) 1390 { 1391 if (cid == -1) { 1392 cid = getMainSelectedClip(); 1393 if (cid == -1) { 1394 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 1395 return; 1396 } 1397 } 1398 Q_ASSERT(m_model->isClip(cid)); 1399 double speed = m_model->getClipSpeed(cid); 1400 if (position == -1) { 1401 // Calculate marker position relative to timeline cursor 1402 position = pCore->getMonitorPosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); 1403 position = int(position * speed); 1404 } 1405 if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { 1406 pCore->displayMessage(i18n("Cannot find clip to edit marker"), ErrorMessage, 500); 1407 return; 1408 } 1409 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid)); 1410 GenTime pos(position, pCore->getCurrentFps()); 1411 clip->getMarkerModel()->removeMarker(pos); 1412 } 1413 1414 void TimelineController::deleteAllMarkers(int cid) 1415 { 1416 if (cid == -1) { 1417 cid = getMainSelectedClip(); 1418 if (cid == -1) { 1419 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 1420 return; 1421 } 1422 } 1423 Q_ASSERT(m_model->isClip(cid)); 1424 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(cid)); 1425 clip->getMarkerModel()->removeAllMarkers(); 1426 } 1427 1428 void TimelineController::editGuide(int frame) 1429 { 1430 if (frame == -1) { 1431 frame = pCore->getMonitorPosition(); 1432 } 1433 auto guideModel = m_model->getGuideModel(); 1434 GenTime pos(frame, pCore->getCurrentFps()); 1435 guideModel->editMarkerGui(pos, qApp->activeWindow(), false); 1436 } 1437 1438 void TimelineController::moveGuideById(int id, int newFrame) 1439 { 1440 if (newFrame < 0) { 1441 return; 1442 } 1443 auto guideModel = m_model->getGuideModel(); 1444 GenTime newPos(newFrame, pCore->getCurrentFps()); 1445 GenTime oldPos = guideModel->markerById(id).time(); 1446 guideModel->editMarker(oldPos, newPos); 1447 } 1448 1449 int TimelineController::moveGuideWithoutUndo(int mid, int newFrame) 1450 { 1451 if (newFrame < 0) { 1452 return -1; 1453 } 1454 auto guideModel = m_model->getGuideModel(); 1455 GenTime newPos(newFrame, pCore->getCurrentFps()); 1456 if (guideModel->moveMarker(mid, newPos)) { 1457 return newFrame; 1458 } 1459 return -1; 1460 } 1461 1462 bool TimelineController::moveGuidesInRange(int start, int end, int offset) 1463 { 1464 std::function<bool(void)> undo = []() { return true; }; 1465 std::function<bool(void)> redo = []() { return true; }; 1466 bool final = false; 1467 final = moveGuidesInRange(start, end, offset, undo, redo); 1468 if (final) { 1469 if (offset > 0) { 1470 pCore->pushUndo(undo, redo, i18n("Insert space")); 1471 } else { 1472 pCore->pushUndo(undo, redo, i18n("Remove space")); 1473 } 1474 return true; 1475 } else { 1476 undo(); 1477 } 1478 return false; 1479 } 1480 1481 bool TimelineController::moveGuidesInRange(int start, int end, int offset, Fun &undo, Fun &redo) 1482 { 1483 GenTime fromPos(start, pCore->getCurrentFps()); 1484 GenTime toPos(start + offset, pCore->getCurrentFps()); 1485 QList<CommentedTime> guides = m_model->getGuideModel()->getMarkersInRange(start, end); 1486 return m_model->getGuideModel()->moveMarkers(guides, fromPos, toPos, undo, redo); 1487 } 1488 1489 void TimelineController::switchGuide(int frame, bool deleteOnly, bool showGui) 1490 { 1491 bool markerFound = false; 1492 if (frame == -1) { 1493 frame = pCore->getMonitorPosition(); 1494 } 1495 qDebug() << "::: ADDING GUIDE TO MODEL: " << m_model->uuid(); 1496 CommentedTime marker = m_model->getGuideModel()->getMarker(frame, &markerFound); 1497 if (!markerFound) { 1498 if (deleteOnly) { 1499 pCore->displayMessage(i18n("No guide found at current position"), ErrorMessage, 500); 1500 return; 1501 } 1502 GenTime pos(frame, pCore->getCurrentFps()); 1503 1504 if (showGui) { 1505 m_model->getGuideModel()->editMarkerGui(pos, qApp->activeWindow(), true); 1506 } else { 1507 m_model->getGuideModel()->addMarker(pos, i18n("guide")); 1508 } 1509 } else { 1510 m_model->getGuideModel()->removeMarker(marker.time()); 1511 } 1512 } 1513 1514 void TimelineController::addAsset(const QVariantMap &data) 1515 { 1516 const auto selection = m_model->getCurrentSelection(); 1517 if (!selection.empty()) { 1518 QString effect = data.value(QStringLiteral("kdenlive/effect")).toString(); 1519 QVariantList effectSelection = m_model->addClipEffect(*selection.begin(), effect, false); 1520 if (effectSelection.isEmpty()) { 1521 QString effectName = EffectsRepository::get()->getName(effect); 1522 pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), ErrorMessage, 500); 1523 } else if (KdenliveSettings::seekonaddeffect() && effectSelection.count() == 1) { 1524 // Move timeline cursor inside clip if it is not 1525 int cid = effectSelection.first().toInt(); 1526 int in = m_model->getClipPosition(cid); 1527 int out = in + m_model->getClipPlaytime(cid); 1528 int position = pCore->getMonitorPosition(); 1529 if (position < in || position > out) { 1530 Q_EMIT seeked(in); 1531 } 1532 } 1533 } else { 1534 pCore->displayMessage(i18n("Select a clip to apply an effect"), ErrorMessage, 500); 1535 } 1536 } 1537 1538 void TimelineController::requestRefresh() 1539 { 1540 pCore->refreshProjectMonitorOnce(); 1541 } 1542 1543 void TimelineController::showAsset(int id) 1544 { 1545 if (m_model->isComposition(id)) { 1546 Q_EMIT showTransitionModel(id, m_model->getCompositionParameterModel(id)); 1547 } else if (m_model->isClip(id)) { 1548 QModelIndex clipIx = m_model->makeClipIndexFromID(id); 1549 QString clipName = m_model->data(clipIx, Qt::DisplayRole).toString(); 1550 bool showKeyframes = m_model->data(clipIx, TimelineModel::ShowKeyframesRole).toInt(); 1551 qDebug() << "-----\n// SHOW KEYFRAMES: " << showKeyframes; 1552 Q_EMIT showItemEffectStack(clipName, m_model->getClipEffectStackModel(id), m_model->getClipFrameSize(id), showKeyframes); 1553 } else if (m_model->isSubTitle(id)) { 1554 qDebug() << "::: SHOWING SUBTITLE: " << id; 1555 Q_EMIT showSubtitle(id); 1556 } 1557 } 1558 1559 void TimelineController::showTrackAsset(int trackId) 1560 { 1561 Q_EMIT showItemEffectStack(getTrackNameFromIndex(trackId), m_model->getTrackEffectStackModel(trackId), pCore->getCurrentFrameSize(), false); 1562 } 1563 1564 void TimelineController::adjustTrackHeight(int trackId, int height) 1565 { 1566 if (trackId > -1) { 1567 m_model->getTrackById(trackId)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(height)); 1568 m_model->setTrackProperty(trackId, "kdenlive:collapsed", QStringLiteral("0")); 1569 QModelIndex modelStart = m_model->makeTrackIndexFromID(trackId); 1570 Q_EMIT m_model->dataChanged(modelStart, modelStart, {TimelineModel::HeightRole}); 1571 return; 1572 } 1573 } 1574 1575 void TimelineController::adjustAllTrackHeight(int trackId, int height) 1576 { 1577 bool isAudio = m_model->getTrackById_const(trackId)->isAudioTrack(); 1578 auto it = m_model->m_allTracks.cbegin(); 1579 while (it != m_model->m_allTracks.cend()) { 1580 int target_track = (*it)->getId(); 1581 if (target_track != trackId && m_model->getTrackById_const(target_track)->isAudioTrack() == isAudio) { 1582 m_model->getTrackById(target_track)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(height)); 1583 } 1584 ++it; 1585 } 1586 int tracksCount = m_model->getTracksCount(); 1587 QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); 1588 QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); 1589 Q_EMIT m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); 1590 } 1591 1592 void TimelineController::collapseAllTrackHeight(int trackId, bool collapse, int collapsedHeight) 1593 { 1594 bool isAudio = m_model->getTrackById_const(trackId)->isAudioTrack(); 1595 auto it = m_model->m_allTracks.cbegin(); 1596 while (it != m_model->m_allTracks.cend()) { 1597 int target_track = (*it)->getId(); 1598 if (m_model->getTrackById_const(target_track)->isAudioTrack() == isAudio) { 1599 if (collapse) { 1600 m_model->setTrackProperty(target_track, "kdenlive:collapsed", QString::number(collapsedHeight)); 1601 } else { 1602 m_model->setTrackProperty(target_track, "kdenlive:collapsed", QStringLiteral("0")); 1603 } 1604 } 1605 ++it; 1606 } 1607 int tracksCount = m_model->getTracksCount(); 1608 QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); 1609 QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); 1610 Q_EMIT m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); 1611 } 1612 1613 void TimelineController::defaultTrackHeight(int trackId) 1614 { 1615 if (trackId > -1) { 1616 m_model->getTrackById(trackId)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight())); 1617 QModelIndex modelStart = m_model->makeTrackIndexFromID(trackId); 1618 Q_EMIT m_model->dataChanged(modelStart, modelStart, {TimelineModel::HeightRole}); 1619 return; 1620 } 1621 auto it = m_model->m_allTracks.cbegin(); 1622 while (it != m_model->m_allTracks.cend()) { 1623 int target_track = (*it)->getId(); 1624 m_model->getTrackById(target_track)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight())); 1625 ++it; 1626 } 1627 int tracksCount = m_model->getTracksCount(); 1628 QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); 1629 QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); 1630 Q_EMIT m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); 1631 } 1632 1633 void TimelineController::setPosition(int position) 1634 { 1635 // Process seek request 1636 Q_EMIT seeked(position); 1637 } 1638 1639 void TimelineController::setAudioTarget(const QMap<int, int> &tracks) 1640 { 1641 // Clear targets before re-adding to trigger qml refresh 1642 m_model->m_audioTarget.clear(); 1643 Q_EMIT audioTargetChanged(); 1644 1645 if ((!tracks.isEmpty() && !m_model->isTrack(tracks.firstKey())) || m_hasAudioTarget == 0) { 1646 return; 1647 } 1648 1649 m_model->m_audioTarget = tracks; 1650 Q_EMIT audioTargetChanged(); 1651 } 1652 1653 void TimelineController::switchAudioTarget(int trackId) 1654 { 1655 if (m_model->m_audioTarget.contains(trackId)) { 1656 m_model->m_audioTarget.remove(trackId); 1657 } else { 1658 // TODO: use track description 1659 if (m_model->m_binAudioTargets.count() == 1) { 1660 // Only one audio stream, remove previous and switch 1661 m_model->m_audioTarget.clear(); 1662 } 1663 int ix = getFirstUnassignedStream(); 1664 if (ix > -1) { 1665 m_model->m_audioTarget.insert(trackId, ix); 1666 } 1667 } 1668 Q_EMIT audioTargetChanged(); 1669 } 1670 1671 void TimelineController::assignCurrentTarget(int index) 1672 { 1673 if (m_activeTrack == -1 || !m_model->isTrack(m_activeTrack)) { 1674 pCore->displayMessage(i18n("No active track"), ErrorMessage, 500); 1675 return; 1676 } 1677 bool isAudio = m_model->isAudioTrack(m_activeTrack); 1678 if (isAudio) { 1679 // Select audio target stream 1680 if (index >= 0 && index < m_model->m_binAudioTargets.size()) { 1681 // activate requested stream 1682 int stream = m_model->m_binAudioTargets.keys().at(index); 1683 assignAudioTarget(m_activeTrack, stream); 1684 } else { 1685 // Remove audio target 1686 m_model->m_audioTarget.remove(m_activeTrack); 1687 Q_EMIT audioTargetChanged(); 1688 } 1689 } else { 1690 // Select video target stream 1691 setVideoTarget(m_activeTrack); 1692 } 1693 } 1694 1695 void TimelineController::assignAudioTarget(int trackId, int stream) 1696 { 1697 QList<int> assignedStreams = m_model->m_audioTarget.values(); 1698 if (assignedStreams.contains(stream)) { 1699 // This stream was assigned to another track, remove 1700 m_model->m_audioTarget.remove(m_model->m_audioTarget.key(stream)); 1701 } 1702 // Remove and re-add target track to trigger a refresh in qml track headers 1703 m_model->m_audioTarget.remove(trackId); 1704 Q_EMIT audioTargetChanged(); 1705 1706 m_model->m_audioTarget.insert(trackId, stream); 1707 Q_EMIT audioTargetChanged(); 1708 } 1709 1710 int TimelineController::getFirstUnassignedStream() const 1711 { 1712 QList<int> keys = m_model->m_binAudioTargets.keys(); 1713 QList<int> assigned = m_model->m_audioTarget.values(); 1714 for (int k : qAsConst(keys)) { 1715 if (!assigned.contains(k)) { 1716 return k; 1717 } 1718 } 1719 return -1; 1720 } 1721 1722 void TimelineController::setVideoTarget(int track) 1723 { 1724 if ((track > -1 && !m_model->isTrack(track)) || !m_hasVideoTarget) { 1725 m_model->m_videoTarget = -1; 1726 return; 1727 } 1728 m_model->m_videoTarget = track; 1729 Q_EMIT videoTargetChanged(); 1730 } 1731 1732 void TimelineController::setActiveTrack(int track) 1733 { 1734 if (track > -1 && !m_model->isTrack(track)) { 1735 return; 1736 } 1737 m_activeTrack = track; 1738 Q_EMIT activeTrackChanged(); 1739 } 1740 1741 void TimelineController::setZoneToSelection() 1742 { 1743 std::unordered_set<int> selection = m_model->getCurrentSelection(); 1744 QPoint zone(-1, -1); 1745 for (int cid : selection) { 1746 int inPos = m_model->getItemPosition(cid); 1747 int outPos = inPos + m_model->getItemPlaytime(cid); 1748 if (zone.x() == -1 || inPos < zone.x()) { 1749 zone.setX(inPos); 1750 } 1751 if (outPos > zone.y()) { 1752 zone.setY(outPos); 1753 } 1754 } 1755 if (zone.x() > -1 && zone.y() > -1) { 1756 updateZone(m_zone, zone, true); 1757 } else { 1758 pCore->displayMessage(i18n("No item selected in timeline"), ErrorMessage, 500); 1759 } 1760 } 1761 1762 void TimelineController::setZone(const QPoint &zone, bool withUndo) 1763 { 1764 if (m_zone.x() > 0) { 1765 m_model->removeSnap(m_zone.x()); 1766 } 1767 if (m_zone.y() > 0) { 1768 m_model->removeSnap(m_zone.y() - 1); 1769 } 1770 if (zone.x() > 0) { 1771 m_model->addSnap(zone.x()); 1772 } 1773 if (zone.y() > 0) { 1774 m_model->addSnap(zone.y() - 1); 1775 } 1776 updateZone(m_zone, zone, withUndo); 1777 } 1778 1779 void TimelineController::updateZone(const QPoint oldZone, const QPoint newZone, bool withUndo) 1780 { 1781 if (!withUndo) { 1782 m_zone = newZone; 1783 Q_EMIT zoneChanged(); 1784 // Update monitor zone 1785 Q_EMIT zoneMoved(m_zone); 1786 return; 1787 } 1788 Fun undo_zone = [this, oldZone]() { 1789 setZone(oldZone, false); 1790 return true; 1791 }; 1792 Fun redo_zone = [this, newZone]() { 1793 setZone(newZone, false); 1794 return true; 1795 }; 1796 redo_zone(); 1797 pCore->pushUndo(undo_zone, redo_zone, i18n("Set Zone")); 1798 } 1799 1800 void TimelineController::updateEffectZone(const QPoint oldZone, const QPoint newZone, bool withUndo) 1801 { 1802 Q_UNUSED(oldZone) 1803 Q_EMIT pCore->updateEffectZone(newZone, withUndo); 1804 } 1805 1806 void TimelineController::setZoneIn(int inPoint) 1807 { 1808 if (m_zone.x() > 0) { 1809 m_model->removeSnap(m_zone.x()); 1810 } 1811 if (inPoint > 0) { 1812 m_model->addSnap(inPoint); 1813 } 1814 m_zone.setX(inPoint); 1815 Q_EMIT zoneChanged(); 1816 // Update monitor zone 1817 Q_EMIT zoneMoved(m_zone); 1818 } 1819 1820 void TimelineController::setZoneOut(int outPoint) 1821 { 1822 if (m_zone.y() > 0) { 1823 m_model->removeSnap(m_zone.y() - 1); 1824 } 1825 if (outPoint > 0) { 1826 m_model->addSnap(outPoint - 1); 1827 } 1828 m_zone.setY(outPoint); 1829 Q_EMIT zoneChanged(); 1830 Q_EMIT zoneMoved(m_zone); 1831 } 1832 1833 void TimelineController::selectItems(const QVariantList &tracks, int startFrame, int endFrame, bool addToSelect, bool selectBottomCompositions, 1834 bool selectSubTitles) 1835 { 1836 std::unordered_set<int> itemsToSelect; 1837 if (addToSelect) { 1838 itemsToSelect = m_model->getCurrentSelection(); 1839 } 1840 for (int i = 0; i < tracks.count(); i++) { 1841 if (m_model->getTrackById_const(tracks.at(i).toInt())->isLocked()) { 1842 continue; 1843 } 1844 auto currentClips = m_model->getItemsInRange(tracks.at(i).toInt(), startFrame, endFrame, i < tracks.count() - 1 ? true : selectBottomCompositions); 1845 itemsToSelect.insert(currentClips.begin(), currentClips.end()); 1846 } 1847 if (selectSubTitles && m_model->hasSubtitleModel()) { 1848 auto currentSubs = m_model->getSubtitleModel()->getItemsInRange(startFrame, endFrame); 1849 itemsToSelect.insert(currentSubs.begin(), currentSubs.end()); 1850 } 1851 m_model->requestSetSelection(itemsToSelect); 1852 } 1853 1854 void TimelineController::requestClipCut(int clipId, int position) 1855 { 1856 if (position == -1) { 1857 position = pCore->getMonitorPosition(); 1858 } 1859 TimelineFunctions::requestClipCut(m_model, clipId, position); 1860 } 1861 1862 void TimelineController::cutClipUnderCursor(int position, int track) 1863 { 1864 if (position == -1) { 1865 position = pCore->getMonitorPosition(); 1866 } 1867 QMutexLocker lk(&m_metaMutex); 1868 bool foundClip = false; 1869 const auto selection = m_model->getCurrentSelection(); 1870 if (track == -1) { 1871 for (int cid : selection) { 1872 if ((m_model->isClip(cid) || m_model->isSubTitle(cid)) && positionIsInItem(cid)) { 1873 if (TimelineFunctions::requestClipCut(m_model, cid, position)) { 1874 foundClip = true; 1875 // Cutting clips in the selection group is handled in TimelineFunctions 1876 } 1877 break; 1878 } 1879 } 1880 } 1881 if (!foundClip) { 1882 if (track == -1) { 1883 track = m_activeTrack; 1884 } 1885 if (track != -1) { 1886 int cid = m_model->getClipByPosition(track, position); 1887 if (cid >= 0 && TimelineFunctions::requestClipCut(m_model, cid, position)) { 1888 foundClip = true; 1889 } 1890 } 1891 } 1892 if (!foundClip) { 1893 pCore->displayMessage(i18n("No clip to cut"), ErrorMessage, 500); 1894 } 1895 } 1896 1897 void TimelineController::cutAllClipsUnderCursor(int position) 1898 { 1899 if (position == -1) { 1900 position = pCore->getMonitorPosition(); 1901 } 1902 QMutexLocker lk(&m_metaMutex); 1903 TimelineFunctions::requestClipCutAll(m_model, position); 1904 } 1905 1906 int TimelineController::requestSpacerStartOperation(int trackId, int position) 1907 { 1908 QMutexLocker lk(&m_metaMutex); 1909 std::pair<int, int> spacerOp = TimelineFunctions::requestSpacerStartOperation(m_model, trackId, position); 1910 int itemId = spacerOp.first; 1911 return itemId; 1912 } 1913 1914 int TimelineController::spacerMinPos() const 1915 { 1916 return TimelineFunctions::spacerMinPos(); 1917 } 1918 1919 void TimelineController::spacerMoveGuides(const QVector<int> &ids, int offset) 1920 { 1921 m_model->getGuideModel()->moveMarkersWithoutUndo(ids, offset); 1922 } 1923 1924 QVector<int> TimelineController::spacerSelection(int startFrame) 1925 { 1926 return m_model->getGuideModel()->getMarkersIdInRange(startFrame, -1); 1927 } 1928 1929 int TimelineController::getGuidePosition(int id) 1930 { 1931 return m_model->getGuideModel()->getMarkerPos(id); 1932 } 1933 1934 bool TimelineController::requestSpacerEndOperation(int clipId, int startPosition, int endPosition, int affectedTrack, const QVector<int> &selectedGuides, 1935 int guideStart) 1936 { 1937 QMutexLocker lk(&m_metaMutex); 1938 // Start undoable command 1939 std::function<bool(void)> undo = []() { return true; }; 1940 std::function<bool(void)> redo = []() { return true; }; 1941 if (guideStart > -1) { 1942 // Move guides back to original position 1943 m_model->getGuideModel()->moveMarkersWithoutUndo(selectedGuides, startPosition - endPosition, false); 1944 } 1945 bool result = TimelineFunctions::requestSpacerEndOperation(m_model, clipId, startPosition, endPosition, affectedTrack, guideStart, undo, redo); 1946 return result; 1947 } 1948 1949 void TimelineController::seekCurrentClip(bool seekToEnd) 1950 { 1951 const auto selection = m_model->getCurrentSelection(); 1952 int cid = -1; 1953 if (!selection.empty()) { 1954 cid = *selection.begin(); 1955 } else { 1956 int cursorPos = pCore->getMonitorPosition(); 1957 cid = m_model->getClipByPosition(m_activeTrack, cursorPos); 1958 if (cid < 0) { 1959 /* If the cursor is at the clip end it is one frame after the clip, 1960 * make it possible to jump to the clip start in that situation too 1961 */ 1962 cid = m_model->getClipByPosition(m_activeTrack, cursorPos - 1); 1963 } 1964 } 1965 if (cid > -1) { 1966 seekToClip(cid, seekToEnd); 1967 } 1968 } 1969 1970 void TimelineController::seekToClip(int cid, bool seekToEnd) 1971 { 1972 int start = m_model->getItemPosition(cid); 1973 if (seekToEnd) { 1974 // -1 because to go to the end of a 10-frame clip, 1975 // need to go from frame 0 to frame 9 (10th frame) 1976 start += m_model->getItemPlaytime(cid) - 1; 1977 } 1978 setPosition(start); 1979 } 1980 1981 void TimelineController::seekToMouse() 1982 { 1983 int mousePos = getMousePos(); 1984 if (mousePos > -1) { 1985 setPosition(mousePos); 1986 } 1987 } 1988 1989 int TimelineController::getMousePos() 1990 { 1991 QVariant returnedValue; 1992 QMetaObject::invokeMethod(m_root, "getMousePos", Qt::DirectConnection, Q_RETURN_ARG(QVariant, returnedValue)); 1993 return returnedValue.toInt(); 1994 } 1995 1996 int TimelineController::getMouseTrack() 1997 { 1998 QVariant returnedValue; 1999 QMetaObject::invokeMethod(m_root, "getMouseTrack", Qt::DirectConnection, Q_RETURN_ARG(QVariant, returnedValue)); 2000 return returnedValue.toInt(); 2001 } 2002 2003 bool TimelineController::positionIsInItem(int id) 2004 { 2005 int in = m_model->getItemPosition(id); 2006 int position = pCore->getMonitorPosition(); 2007 if (in > position) { 2008 return false; 2009 } 2010 if (position <= in + m_model->getItemPlaytime(id)) { 2011 return true; 2012 } 2013 return false; 2014 } 2015 2016 void TimelineController::refreshItem(int id) 2017 { 2018 if (m_model->isClip(id) && m_model->m_allClips[id]->isAudioOnly()) { 2019 return; 2020 } 2021 if (positionIsInItem(id)) { 2022 pCore->refreshProjectMonitorOnce(); 2023 } 2024 } 2025 2026 QPair<int, int> TimelineController::getAvTracksCount() const 2027 { 2028 return m_model->getAVtracksCount(); 2029 } 2030 2031 QStringList TimelineController::extractCompositionLumas() const 2032 { 2033 return m_model->extractCompositionLumas(); 2034 } 2035 2036 QStringList TimelineController::extractExternalEffectFiles() const 2037 { 2038 return m_model->extractExternalEffectFiles(); 2039 } 2040 2041 void TimelineController::addEffectToCurrentClip(const QStringList &effectData) 2042 { 2043 QList<int> activeClips; 2044 for (int track = m_model->getTracksCount() - 1; track >= 0; track--) { 2045 int trackIx = m_model->getTrackIndexFromPosition(track); 2046 int cid = m_model->getClipByPosition(trackIx, pCore->getMonitorPosition()); 2047 if (cid > -1) { 2048 activeClips << cid; 2049 } 2050 } 2051 if (!activeClips.isEmpty()) { 2052 if (effectData.count() == 4) { 2053 QString effectString = effectData.at(1) + QStringLiteral("-") + effectData.at(2) + QStringLiteral("-") + effectData.at(3); 2054 m_model->copyClipEffect(activeClips.first(), effectString); 2055 } else { 2056 m_model->addClipEffect(activeClips.first(), effectData.constFirst()); 2057 } 2058 } 2059 } 2060 2061 void TimelineController::adjustFade(int cid, const QString &effectId, int duration, int initialDuration) 2062 { 2063 if (initialDuration == -2) { 2064 // Add default fade 2065 duration = pCore->getDurationFromString(KdenliveSettings::fade_duration()); 2066 initialDuration = 0; 2067 } 2068 if (duration <= 0) { 2069 // remove fade 2070 if (initialDuration > 0) { 2071 // Restore original fade duration 2072 m_model->adjustEffectLength(cid, effectId, initialDuration, -1); 2073 } 2074 m_model->removeFade(cid, effectId == QLatin1String("fadein")); 2075 } else { 2076 m_model->adjustEffectLength(cid, effectId, duration, initialDuration); 2077 } 2078 } 2079 2080 QPair<int, int> TimelineController::getCompositionATrack(int cid) const 2081 { 2082 QPair<int, int> result; 2083 std::shared_ptr<CompositionModel> compo = m_model->getCompositionPtr(cid); 2084 if (compo) { 2085 result = QPair<int, int>(compo->getATrack(), m_model->getTrackMltIndex(compo->getCurrentTrackId())); 2086 } 2087 return result; 2088 } 2089 2090 void TimelineController::setCompositionATrack(int cid, int aTrack) 2091 { 2092 TimelineFunctions::setCompositionATrack(m_model, cid, aTrack); 2093 } 2094 2095 bool TimelineController::compositionAutoTrack(int cid) const 2096 { 2097 std::shared_ptr<CompositionModel> compo = m_model->getCompositionPtr(cid); 2098 return compo && compo->getForcedTrack() == -1; 2099 } 2100 2101 const QString TimelineController::getClipBinId(int clipId) const 2102 { 2103 return m_model->getClipBinId(clipId); 2104 } 2105 2106 void TimelineController::focusItem(int itemId) 2107 { 2108 int start = m_model->getItemPosition(itemId); 2109 int tid = m_model->getItemTrackId(itemId); 2110 setPosition(start); 2111 setActiveTrack(tid); 2112 Q_EMIT centerView(); 2113 } 2114 2115 int TimelineController::headerWidth() const 2116 { 2117 return qMax(10, KdenliveSettings::headerwidth()); 2118 } 2119 2120 void TimelineController::setHeaderWidth(int width) 2121 { 2122 KdenliveSettings::setHeaderwidth(width); 2123 } 2124 2125 bool TimelineController::createSplitOverlay(int clipId, std::shared_ptr<Mlt::Filter> filter) 2126 { 2127 if (m_model->hasTimelinePreview() && m_model->previewManager()->hasOverlayTrack()) { 2128 return true; 2129 } 2130 if (clipId == -1) { 2131 pCore->displayMessage(i18n("Select a clip to compare effect"), ErrorMessage, 500); 2132 return false; 2133 } 2134 std::shared_ptr<ClipModel> clip = m_model->getClipPtr(clipId); 2135 const QString binId = clip->binId(); 2136 2137 // Get clean bin copy of the clip 2138 std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(binId); 2139 std::shared_ptr<Mlt::Producer> binProd(binClip->masterProducer()->cut(clip->getIn(), clip->getOut())); 2140 2141 // Get copy of timeline producer 2142 std::shared_ptr<Mlt::Producer> clipProducer(new Mlt::Producer(*clip)); 2143 2144 // Built tractor and compositing 2145 Mlt::Tractor trac(pCore->getProjectProfile()); 2146 Mlt::Playlist play(pCore->getProjectProfile()); 2147 Mlt::Playlist play2(pCore->getProjectProfile()); 2148 play.append(*clipProducer.get()); 2149 play2.append(*binProd); 2150 trac.set_track(play, 0); 2151 trac.set_track(play2, 1); 2152 play2.attach(*filter.get()); 2153 QString splitTransition = TransitionsRepository::get()->getCompositingTransition(); 2154 Mlt::Transition t(pCore->getProjectProfile(), splitTransition.toUtf8().constData()); 2155 t.set("always_active", 1); 2156 trac.plant_transition(t, 0, 1); 2157 int startPos = m_model->getClipPosition(clipId); 2158 2159 // plug in overlay playlist 2160 auto *overlay = new Mlt::Playlist(pCore->getProjectProfile()); 2161 overlay->insert_blank(0, startPos); 2162 Mlt::Producer split(trac.get_producer()); 2163 overlay->insert_at(startPos, &split, 1); 2164 2165 // insert in tractor 2166 if (!m_model->hasTimelinePreview()) { 2167 initializePreview(); 2168 } 2169 m_model->setOverlayTrack(overlay); 2170 return true; 2171 } 2172 2173 void TimelineController::removeSplitOverlay() 2174 { 2175 if (!m_model->hasTimelinePreview() || !m_model->previewManager()->hasOverlayTrack()) { 2176 return; 2177 } 2178 // disconnect 2179 m_model->removeOverlayTrack(); 2180 } 2181 2182 int TimelineController::requestItemRippleResize(int itemId, int size, bool right, bool logUndo, int snapDistance, bool allowSingleResize) 2183 { 2184 return m_model->requestItemRippleResize(m_model, itemId, size, right, logUndo, !KdenliveSettings::lockedGuides(), snapDistance, allowSingleResize); 2185 } 2186 2187 void TimelineController::updateTrimmingMode() 2188 { 2189 if (trimmingActive()) { 2190 requestStartTrimmingMode(); 2191 } else { 2192 requestEndTrimmingMode(); 2193 } 2194 } 2195 2196 int TimelineController::trimmingBoundOffset(int offset) 2197 { 2198 std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(m_trimmingMainClip); 2199 return qBound(mainClip->getOut() - mainClip->getMaxDuration() + 1, offset, mainClip->getIn()); 2200 } 2201 2202 void TimelineController::slipPosChanged(int offset) 2203 { 2204 if (!m_model->isClip(m_trimmingMainClip) || !pCore->monitorManager()->isTrimming()) { 2205 return; 2206 } 2207 std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(m_trimmingMainClip); 2208 offset = qBound(mainClip->getOut() - mainClip->getMaxDuration() + 1, offset, mainClip->getIn()); 2209 int outPoint = mainClip->getOut() - offset; 2210 int inPoint = mainClip->getIn() - offset; 2211 2212 pCore->monitorManager()->projectMonitor()->slotTrimmingPos(inPoint, offset, inPoint, outPoint); 2213 showToolTip(i18n("In:%1, Out:%2 (%3%4)", simplifiedTC(inPoint), simplifiedTC(outPoint), (offset < 0 ? "-" : "+"), simplifiedTC(qFabs(offset)))); 2214 } 2215 2216 void TimelineController::ripplePosChanged(int size, bool right) 2217 { 2218 if (!m_model->isClip(m_trimmingMainClip) || !pCore->monitorManager()->isTrimming()) { 2219 return; 2220 } 2221 if (size < 0) { 2222 return; 2223 } 2224 qDebug() << "ripplePosChanged" << size << right; 2225 std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(m_trimmingMainClip); 2226 int delta = size - mainClip->getPlaytime(); 2227 if (!right) { 2228 delta *= -1; 2229 } 2230 int pos = right ? mainClip->getOut() : mainClip->getIn(); 2231 pos += delta; 2232 if (mainClip->getMaxDuration() > -1) { 2233 pos = qBound(0, pos, mainClip->getMaxDuration()); 2234 } else { 2235 pos = qMax(0, pos); 2236 } 2237 pCore->monitorManager()->projectMonitor()->slotTrimmingPos(pos + 1, delta, right ? mainClip->getIn() : pos, right ? pos : mainClip->getOut()); 2238 } 2239 2240 bool TimelineController::slipProcessSelection(int mainClipId, bool addToSelection) 2241 { 2242 std::unordered_set<int> sel = m_model->getCurrentSelection(); 2243 std::unordered_set<int> newSel; 2244 2245 for (int i : sel) { 2246 if (m_model->isClip(i) && m_model->getClipPtr(i)->getMaxDuration() != -1) { 2247 newSel.insert(i); 2248 } 2249 } 2250 2251 if (mainClipId != -1 && !m_model->isClip(mainClipId) && m_model->getClipPtr(mainClipId)->getMaxDuration() == -1) { 2252 mainClipId = -1; 2253 } 2254 2255 if ((newSel.empty() || !isInSelection(mainClipId)) && mainClipId != -1) { 2256 m_trimmingMainClip = mainClipId; 2257 Q_EMIT trimmingMainClipChanged(); 2258 if (!addToSelection) { 2259 newSel.clear(); 2260 } 2261 newSel.insert(mainClipId); 2262 } 2263 2264 if (newSel != sel) { 2265 m_model->requestSetSelection(newSel); 2266 return false; 2267 } 2268 2269 if (sel.empty()) { 2270 return false; 2271 } 2272 2273 Q_ASSERT(!sel.empty()); 2274 2275 if (mainClipId == -1) { 2276 mainClipId = getMainSelectedClip(); 2277 } 2278 2279 if (m_model->getTrackById(m_model->getClipTrackId(mainClipId))->isLocked()) { 2280 int partnerId = m_model->m_groups->getSplitPartner(mainClipId); 2281 if (partnerId == -1 || m_model->getTrackById(m_model->getClipTrackId(partnerId))->isLocked()) { 2282 mainClipId = -1; 2283 for (int i : sel) { 2284 if (i != mainClipId && !m_model->getTrackById(m_model->getClipTrackId(i))->isLocked()) { 2285 mainClipId = i; 2286 break; 2287 } 2288 } 2289 } else { 2290 mainClipId = partnerId; 2291 } 2292 } 2293 2294 if (mainClipId == -1) { 2295 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 2296 return false; 2297 } 2298 2299 std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(mainClipId); 2300 2301 if (mainClip->getMaxDuration() == -1) { 2302 return false; 2303 } 2304 2305 int partnerId = m_model->m_groups->getSplitPartner(mainClipId); 2306 2307 if (mainClip->isAudioOnly() && partnerId != -1 && !m_model->getTrackById(m_model->getClipTrackId(partnerId))->isLocked()) { 2308 mainClip = m_model->getClipPtr(partnerId); 2309 } 2310 2311 m_trimmingMainClip = mainClip->getId(); 2312 Q_EMIT trimmingMainClipChanged(); 2313 return true; 2314 } 2315 2316 bool TimelineController::requestStartTrimmingMode(int mainClipId, bool addToSelection, bool right) 2317 { 2318 2319 if (pCore->monitorManager()->isTrimming() && m_trimmingMainClip == mainClipId) { 2320 return true; 2321 } 2322 2323 if (pCore->activeTool() == ToolType::SlipTool && !slipProcessSelection(mainClipId, addToSelection)) { 2324 return false; 2325 } 2326 2327 if (pCore->activeTool() == ToolType::RippleTool) { 2328 if (m_model.get()->isClip(mainClipId)) { 2329 m_trimmingMainClip = mainClipId; 2330 Q_EMIT trimmingMainClipChanged(); 2331 } else { 2332 return false; 2333 } 2334 } 2335 2336 std::shared_ptr<ClipModel> mainClip = m_model->getClipPtr(m_trimmingMainClip); 2337 2338 const int previousClipId = m_model->getTrackById_const(mainClip->getCurrentTrackId())->getClipByPosition(mainClip->getPosition() - 1); 2339 std::shared_ptr<Mlt::Producer> previousFrame; 2340 if (pCore->activeTool() == ToolType::SlipTool && previousClipId > -1) { 2341 std::shared_ptr<ClipModel> previousClip = m_model->getClipPtr(previousClipId); 2342 previousFrame = std::shared_ptr<Mlt::Producer>(previousClip->getProducer()->cut(0)); 2343 Mlt::Filter filter(pCore->getProjectProfile(), "freeze"); 2344 filter.set("mlt_service", "freeze"); 2345 filter.set("frame", previousClip->getOut()); 2346 previousFrame->attach(filter); 2347 } else { 2348 previousFrame = std::shared_ptr<Mlt::Producer>(new Mlt::Producer(pCore->getProjectProfile(), "color:black")); 2349 } 2350 2351 const int nextClipId = m_model->getTrackById_const(mainClip->getCurrentTrackId())->getClipByPosition(mainClip->getPosition() + mainClip->getPlaytime()); 2352 std::shared_ptr<Mlt::Producer> nextFrame; 2353 if (pCore->activeTool() == ToolType::SlipTool && nextClipId > -1) { 2354 std::shared_ptr<ClipModel> nextClip = m_model->getClipPtr(nextClipId); 2355 nextFrame = std::shared_ptr<Mlt::Producer>(nextClip->getProducer()->cut(0)); 2356 Mlt::Filter filter(pCore->getProjectProfile(), "freeze"); 2357 filter.set("mlt_service", "freeze"); 2358 filter.set("frame", nextClip->getIn()); 2359 nextFrame->attach(filter); 2360 } else { 2361 nextFrame = std::shared_ptr<Mlt::Producer>(new Mlt::Producer(pCore->getProjectProfile(), "color:black")); 2362 } 2363 2364 std::shared_ptr<Mlt::Producer> inOutFrame; 2365 if (pCore->activeTool() == ToolType::RippleTool) { 2366 inOutFrame = std::shared_ptr<Mlt::Producer>(mainClip->getProducer()->cut(0)); 2367 Mlt::Filter filter(pCore->getProjectProfile(), "freeze"); 2368 filter.set("mlt_service", "freeze"); 2369 filter.set("frame", right ? mainClip->getIn() : mainClip->getOut()); 2370 inOutFrame->attach(filter); 2371 } 2372 2373 std::vector<std::shared_ptr<Mlt::Producer>> producers; 2374 int previewLength = 0; 2375 switch (pCore->activeTool()) { 2376 case ToolType::SlipTool: 2377 producers.push_back(std::shared_ptr<Mlt::Producer>(previousFrame)); 2378 producers.push_back(std::shared_ptr<Mlt::Producer>(mainClip->getProducer()->cut(0))); 2379 producers.push_back(std::shared_ptr<Mlt::Producer>(mainClip->getProducer()->cut(mainClip->getOut() - mainClip->getIn()))); 2380 producers.push_back(nextFrame); 2381 previewLength = producers[1]->get_length(); 2382 break; 2383 case ToolType::SlideTool: 2384 break; 2385 case ToolType::RollTool: 2386 break; 2387 case ToolType::RippleTool: 2388 if (right) { 2389 producers.push_back(std::shared_ptr<Mlt::Producer>(inOutFrame)); 2390 } 2391 producers.push_back(std::shared_ptr<Mlt::Producer>(mainClip->getProducer()->cut(0))); 2392 if (!right) { 2393 producers.push_back(std::shared_ptr<Mlt::Producer>(inOutFrame)); 2394 previewLength = producers[0]->get_length(); 2395 } else { 2396 previewLength = producers[1]->get_length(); 2397 } 2398 break; 2399 default: 2400 return false; 2401 } 2402 2403 // Built tractor 2404 Mlt::Tractor trac(pCore->getProjectProfile()); 2405 2406 // Now that we know the length of the preview create and add black background producer 2407 std::shared_ptr<Mlt::Producer> black(new Mlt::Producer(pCore->getProjectProfile(), "color:black")); 2408 black->set("length", previewLength); 2409 black->set_in_and_out(0, previewLength); 2410 black->set("mlt_image_format", "rgba"); 2411 trac.set_track(*black.get(), 0); 2412 // trac.set_track( 1); 2413 2414 if (!mainClip->isAudioOnly()) { 2415 int count = 1; // 0 is background track so we start at 1 2416 for (auto const &producer : producers) { 2417 trac.set_track(*producer.get(), count); 2418 count++; 2419 } 2420 2421 // Add "composite" transitions for multi clip view 2422 for (int i = 0; i < int(producers.size()); i++) { 2423 // Construct transition 2424 Mlt::Transition transition(pCore->getProjectProfile(), "composite"); 2425 transition.set("mlt_service", "composite"); 2426 transition.set("a_track", 0); 2427 transition.set("b_track", i + 1); 2428 transition.set("distort", 0); 2429 transition.set("aligned", 0); 2430 // 200 is an arbitrary number so we can easily remove these transition later 2431 // transition.set("internal_added", 200); 2432 2433 QString geometry; 2434 switch (pCore->activeTool()) { 2435 case ToolType::RollTool: 2436 case ToolType::RippleTool: 2437 switch (i) { 2438 case 0: 2439 geometry = QStringLiteral("0 0 50% 100%"); 2440 break; 2441 case 1: 2442 geometry = QStringLiteral("50% 0 50% 100%"); 2443 break; 2444 } 2445 break; 2446 case ToolType::SlipTool: 2447 switch (i) { 2448 case 0: 2449 geometry = QStringLiteral("0 0 25% 25%"); 2450 break; 2451 case 1: 2452 geometry = QStringLiteral("0 25% 50% 50%"); 2453 break; 2454 case 2: 2455 geometry = QStringLiteral("50% 25% 50% 50%"); 2456 break; 2457 case 3: 2458 geometry = QStringLiteral("75% 75% 25% 25%"); 2459 break; 2460 } 2461 break; 2462 case ToolType::SlideTool: 2463 switch (i) { 2464 case 0: 2465 geometry = QStringLiteral("0 0 25% 25%"); 2466 break; 2467 case 1: 2468 geometry = QStringLiteral("50% 25% 50% 50%"); 2469 break; 2470 case 2: 2471 geometry = QStringLiteral("0 25% 50% 50%"); 2472 break; 2473 case 3: 2474 geometry = QStringLiteral("50% 75% 25% 25%"); 2475 break; 2476 } 2477 break; 2478 default: 2479 break; 2480 } 2481 2482 // Add transition to track: 2483 transition.set("geometry", geometry.toUtf8().constData()); 2484 transition.set("always_active", 1); 2485 trac.plant_transition(transition, 0, i + 1); 2486 } 2487 } 2488 2489 pCore->monitorManager()->projectMonitor()->setProducer(std::make_shared<Mlt::Producer>(trac), -2); 2490 pCore->monitorManager()->projectMonitor()->slotSwitchTrimming(true); 2491 2492 switch (pCore->activeTool()) { 2493 case ToolType::RollTool: 2494 case ToolType::RippleTool: 2495 ripplePosChanged(mainClip->getPlaytime(), right); 2496 break; 2497 case ToolType::SlipTool: 2498 slipPosChanged(0); 2499 break; 2500 case ToolType::SlideTool: 2501 break; 2502 default: 2503 break; 2504 } 2505 2506 return true; 2507 } 2508 2509 void TimelineController::requestEndTrimmingMode() 2510 { 2511 if (pCore->monitorManager()->isTrimming()) { 2512 pCore->monitorManager()->projectMonitor()->setProducer(pCore->window()->getCurrentTimeline()->model()->producer(), 0); 2513 pCore->monitorManager()->projectMonitor()->slotSwitchTrimming(false); 2514 } 2515 } 2516 2517 void TimelineController::addPreviewRange(bool add) 2518 { 2519 if (m_zone.isNull()) { 2520 return; 2521 } 2522 if (!m_model->hasTimelinePreview()) { 2523 initializePreview(); 2524 } 2525 if (m_model->hasTimelinePreview()) { 2526 m_model->previewManager()->addPreviewRange(m_zone, add); 2527 } 2528 } 2529 2530 void TimelineController::clearPreviewRange(bool resetZones) 2531 { 2532 if (m_model->hasTimelinePreview()) { 2533 m_model->previewManager()->clearPreviewRange(resetZones); 2534 } 2535 } 2536 2537 void TimelineController::startPreviewRender() 2538 { 2539 // Timeline preview stuff 2540 if (!m_model->hasTimelinePreview()) { 2541 initializePreview(); 2542 } else if (m_disablePreview->isChecked()) { 2543 m_disablePreview->setChecked(false); 2544 disablePreview(false); 2545 } 2546 if (m_model->hasTimelinePreview()) { 2547 if (!m_usePreview) { 2548 m_model->buildPreviewTrack(); 2549 m_usePreview = true; 2550 } 2551 if (!m_model->previewManager()->hasDefinedRange()) { 2552 addPreviewRange(true); 2553 } 2554 m_model->previewManager()->startPreviewRender(); 2555 } 2556 } 2557 2558 void TimelineController::stopPreviewRender() 2559 { 2560 if (m_model->hasTimelinePreview()) { 2561 m_model->previewManager()->abortRendering(); 2562 } 2563 } 2564 2565 void TimelineController::initializePreview() 2566 { 2567 if (m_model->hasTimelinePreview()) { 2568 // Update parameters 2569 if (!m_model->previewManager()->loadParams()) { 2570 if (m_usePreview) { 2571 // Disconnect preview track 2572 m_model->previewManager()->disconnectTrack(); 2573 m_usePreview = false; 2574 } 2575 m_model->resetPreviewManager(); 2576 } 2577 } else { 2578 m_model->initializePreviewManager(); 2579 } 2580 } 2581 2582 void TimelineController::connectPreviewManager() 2583 { 2584 if (m_model->hasTimelinePreview()) { 2585 connect(m_model->previewManager().get(), &PreviewManager::dirtyChunksChanged, this, &TimelineController::dirtyChunksChanged, 2586 static_cast<Qt::ConnectionType>(Qt::DirectConnection | Qt::UniqueConnection)); 2587 connect(m_model->previewManager().get(), &PreviewManager::renderedChunksChanged, this, &TimelineController::renderedChunksChanged, 2588 static_cast<Qt::ConnectionType>(Qt::DirectConnection | Qt::UniqueConnection)); 2589 connect(m_model->previewManager().get(), &PreviewManager::workingPreviewChanged, this, &TimelineController::workingPreviewChanged, 2590 static_cast<Qt::ConnectionType>(Qt::DirectConnection | Qt::UniqueConnection)); 2591 } 2592 } 2593 2594 bool TimelineController::hasPreviewTrack() const 2595 { 2596 return (m_model->hasTimelinePreview() && (m_model->previewManager()->hasOverlayTrack() || m_model->previewManager()->hasPreviewTrack())); 2597 } 2598 2599 void TimelineController::disablePreview(bool disable) 2600 { 2601 if (disable) { 2602 m_model->deletePreviewTrack(); 2603 m_usePreview = false; 2604 } else { 2605 if (!m_usePreview) { 2606 if (!m_model->buildPreviewTrack()) { 2607 // preview track already exists, reconnect 2608 m_model->m_tractor->lock(); 2609 m_model->previewManager()->reconnectTrack(); 2610 m_model->m_tractor->unlock(); 2611 } 2612 Mlt::Playlist playlist; 2613 m_model->previewManager()->loadChunks(QVariantList(), QVariantList(), playlist); 2614 m_usePreview = true; 2615 } 2616 } 2617 } 2618 2619 QVariantList TimelineController::dirtyChunks() const 2620 { 2621 return m_model->hasTimelinePreview() ? m_model->previewManager()->m_dirtyChunks : QVariantList(); 2622 } 2623 2624 QVariantList TimelineController::renderedChunks() const 2625 { 2626 return m_model->hasTimelinePreview() ? m_model->previewManager()->m_renderedChunks : QVariantList(); 2627 } 2628 2629 int TimelineController::workingPreview() const 2630 { 2631 return m_model->hasTimelinePreview() ? m_model->previewManager()->workingPreview : -1; 2632 } 2633 2634 bool TimelineController::useRuler() const 2635 { 2636 return pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1; 2637 } 2638 2639 bool TimelineController::scrollVertically() const 2640 { 2641 return KdenliveSettings::scrollvertically() == 1; 2642 } 2643 2644 void TimelineController::resetPreview() 2645 { 2646 if (m_model->hasTimelinePreview()) { 2647 m_model->previewManager()->clearPreviewRange(true); 2648 initializePreview(); 2649 } 2650 } 2651 2652 void TimelineController::saveSequenceProperties() 2653 { 2654 int activeTrack = m_activeTrack < 0 ? m_activeTrack : m_model->getTrackPosition(m_activeTrack); 2655 m_model->tractor()->set("kdenlive:sequenceproperties.activeTrack", activeTrack); 2656 QVariant returnedValue; 2657 QMetaObject::invokeMethod(m_root, "getScrollPos", Qt::DirectConnection, Q_RETURN_ARG(QVariant, returnedValue)); 2658 int scrollPos = returnedValue.toInt(); 2659 m_model->tractor()->set("kdenlive:sequenceproperties.scrollPos", scrollPos); 2660 m_model->tractor()->set("kdenlive:sequenceproperties.zonein", m_zone.x()); 2661 m_model->tractor()->set("kdenlive:sequenceproperties.zoneout", m_zone.y()); 2662 tractor()->set("kdenlive:sequenceproperties.disablepreview", m_disablePreview->isChecked()); 2663 if (m_model->hasSubtitleModel()) { 2664 const QString subtitlesData = m_model->getSubtitleModel()->subtitlesFilesToJson(); 2665 m_model->tractor()->set("kdenlive:sequenceproperties.subtitlesList", subtitlesData.toUtf8().constData()); 2666 } 2667 } 2668 2669 QMap<QString, QString> TimelineController::documentProperties() 2670 { 2671 QMap<QString, QString> props = pCore->currentDoc()->documentProperties(); 2672 // Ensure current timeline properties are saved (groups, guides, etc) 2673 saveSequenceProperties(); 2674 return props; 2675 } 2676 2677 int TimelineController::getMenuOrTimelinePos() const 2678 { 2679 int frame = m_root->property("clickFrame").toInt(); 2680 if (frame == -1) { 2681 frame = pCore->getMonitorPosition(); 2682 } 2683 return frame; 2684 } 2685 2686 void TimelineController::insertSpace(int trackId, int frame) 2687 { 2688 if (frame == -1) { 2689 frame = getMenuOrTimelinePos(); 2690 } 2691 if (trackId == -1) { 2692 trackId = m_activeTrack; 2693 } 2694 QPointer<SpacerDialog> d = new SpacerDialog(GenTime(65, pCore->getCurrentFps()), pCore->currentDoc()->timecode(), qApp->activeWindow()); 2695 if (d->exec() != QDialog::Accepted) { 2696 delete d; 2697 return; 2698 } 2699 bool affectAllTracks = d->affectAllTracks(); 2700 int cid = requestSpacerStartOperation(affectAllTracks ? -1 : trackId, frame); 2701 int spaceDuration = d->selectedDuration().frames(pCore->getCurrentFps()); 2702 delete d; 2703 if (cid == -1) { 2704 pCore->displayMessage(i18n("No clips found to insert space"), ErrorMessage, 500); 2705 return; 2706 } 2707 int start = m_model->getItemPosition(cid); 2708 requestSpacerEndOperation(cid, start, start + spaceDuration, affectAllTracks ? -1 : trackId); 2709 } 2710 2711 void TimelineController::removeSpace(int trackId, int frame, bool affectAllTracks) 2712 { 2713 if (frame == -1) { 2714 frame = getMenuOrTimelinePos(); 2715 } 2716 if (trackId == -1) { 2717 trackId = m_activeTrack; 2718 } 2719 bool res = TimelineFunctions::requestDeleteBlankAt(m_model, trackId, frame, affectAllTracks); 2720 if (!res) { 2721 pCore->displayMessage(i18n("Cannot remove space at given position"), ErrorMessage, 500); 2722 } 2723 } 2724 2725 void TimelineController::removeTrackSpaces(int trackId, int frame) 2726 { 2727 if (frame == -1) { 2728 frame = getMenuOrTimelinePos(); 2729 } 2730 if (trackId == -1) { 2731 trackId = m_activeTrack; 2732 } 2733 bool res = TimelineFunctions::requestDeleteAllBlanksFrom(m_model, trackId, frame); 2734 if (!res) { 2735 pCore->displayMessage(i18n("Cannot remove all spaces"), ErrorMessage, 500); 2736 } 2737 } 2738 2739 void TimelineController::removeTrackClips(int trackId, int frame) 2740 { 2741 if (frame == -1) { 2742 frame = getMenuOrTimelinePos(); 2743 } 2744 if (trackId == -1) { 2745 trackId = m_activeTrack; 2746 } 2747 bool res = TimelineFunctions::requestDeleteAllClipsFrom(m_model, trackId, frame); 2748 if (!res) { 2749 pCore->displayMessage(i18n("Cannot remove all clips"), ErrorMessage, 500); 2750 } 2751 } 2752 2753 void TimelineController::invalidateItem(int cid) 2754 { 2755 if (!m_model->hasTimelinePreview() || !m_model->isItem(cid)) { 2756 return; 2757 } 2758 const int tid = m_model->getItemTrackId(cid); 2759 if (tid == -1 || m_model->getTrackById_const(tid)->isAudioTrack()) { 2760 return; 2761 } 2762 int start = m_model->getItemPosition(cid); 2763 int end = start + m_model->getItemPlaytime(cid); 2764 m_model->previewManager()->invalidatePreview(start, end); 2765 } 2766 2767 void TimelineController::invalidateTrack(int tid) 2768 { 2769 if (!m_model->hasTimelinePreview() || !m_model->isTrack(tid) || m_model->getTrackById_const(tid)->isAudioTrack()) { 2770 return; 2771 } 2772 for (const auto &clp : m_model->getTrackById_const(tid)->m_allClips) { 2773 invalidateItem(clp.first); 2774 } 2775 } 2776 2777 void TimelineController::remapItemTime(int clipId) 2778 { 2779 if (clipId == -1) { 2780 clipId = getMainSelectedClip(); 2781 } 2782 // Don't allow remaping a clip with speed effect 2783 if (clipId == -1 || !m_model->isClip(clipId) || !qFuzzyCompare(1., m_model->m_allClips[clipId]->getSpeed())) { 2784 pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500); 2785 return; 2786 } 2787 ClipType::ProducerType type = m_model->m_allClips[clipId]->clipType(); 2788 if (type == ClipType::Color || type == ClipType::Image) { 2789 pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500); 2790 return; 2791 } 2792 if (m_model->m_allClips[clipId]->hasTimeRemap()) { 2793 // Remove remap effect 2794 m_model->requestClipTimeRemap(clipId, false); 2795 Q_EMIT pCore->remapClip(-1); 2796 } else { 2797 // Add remap effect 2798 Q_EMIT pCore->remapClip(clipId); 2799 } 2800 } 2801 2802 void TimelineController::changeItemSpeed(int clipId, double speed) 2803 { 2804 /*if (clipId == -1) { 2805 clipId = getMainSelectedItem(false, true); 2806 }*/ 2807 if (clipId == -1) { 2808 clipId = getMainSelectedClip(); 2809 } 2810 if (clipId == -1) { 2811 pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500); 2812 return; 2813 } 2814 bool pitchCompensate = m_model->m_allClips[clipId]->getIntProperty(QStringLiteral("warp_pitch")); 2815 if (qFuzzyCompare(speed, -1)) { 2816 speed = 100 * m_model->getClipSpeed(clipId); 2817 int duration = m_model->getItemPlaytime(clipId); 2818 // this is the max speed so that the clip is at least one frame long 2819 double maxSpeed = duration * qAbs(speed); 2820 // this is the min speed so that the clip doesn't bump into the next one on track 2821 double minSpeed = duration * qAbs(speed) / (duration + double(m_model->getBlankSizeNearClip(clipId, true))); 2822 2823 // if there is a split partner, we must also take it into account 2824 int partner = m_model->getClipSplitPartner(clipId); 2825 if (partner != -1) { 2826 double duration2 = m_model->getItemPlaytime(partner); 2827 double maxSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)); 2828 double minSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)) / (duration2 + double(m_model->getBlankSizeNearClip(partner, true))); 2829 minSpeed = std::max(minSpeed, minSpeed2); 2830 maxSpeed = std::min(maxSpeed, maxSpeed2); 2831 } 2832 std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(clipId)); 2833 QScopedPointer<SpeedDialog> d( 2834 new SpeedDialog(QApplication::activeWindow(), std::abs(speed), duration, minSpeed, maxSpeed, speed < 0, pitchCompensate, binClip->clipType())); 2835 if (d->exec() != QDialog::Accepted) { 2836 Q_EMIT regainFocus(); 2837 return; 2838 } 2839 Q_EMIT regainFocus(); 2840 speed = d->getValue(); 2841 pitchCompensate = d->getPitchCompensate(); 2842 qDebug() << "requesting speed " << speed; 2843 } 2844 bool res = m_model->requestClipTimeWarp(clipId, speed, pitchCompensate, true); 2845 if (res) { 2846 updateClipActions(); 2847 } 2848 } 2849 2850 void TimelineController::switchCompositing(bool enable) 2851 { 2852 // m_model->m_tractor->lock(); 2853 pCore->currentDoc()->setDocumentProperty(QStringLiteral("compositing"), QString::number(enable)); 2854 QScopedPointer<Mlt::Service> service(m_model->m_tractor->field()); 2855 QScopedPointer<Mlt::Field> field(m_model->m_tractor->field()); 2856 field->lock(); 2857 while ((service != nullptr) && service->is_valid()) { 2858 if (service->type() == mlt_service_transition_type) { 2859 Mlt::Transition t(mlt_transition(service->get_service())); 2860 service.reset(service->producer()); 2861 QString serviceName = t.get("mlt_service"); 2862 if (t.get_int("internal_added") == 237 && serviceName != QLatin1String("mix")) { 2863 // remove all compositing transitions 2864 field->disconnect_service(t); 2865 t.disconnect_all_producers(); 2866 } 2867 } else { 2868 service.reset(service->producer()); 2869 } 2870 } 2871 if (enable) { 2872 // Loop through tracks 2873 for (int track = 0; track < m_model->getTracksCount(); track++) { 2874 if (m_model->getTrackById(m_model->getTrackIndexFromPosition(track))->getProperty("kdenlive:audio_track").toInt() == 0) { 2875 // This is a video track 2876 Mlt::Transition t(pCore->getProjectProfile(), TransitionsRepository::get()->getCompositingTransition().toUtf8().constData()); 2877 t.set("always_active", 1); 2878 t.set_tracks(0, track + 1); 2879 t.set("internal_added", 237); 2880 field->plant_transition(t, 0, track + 1); 2881 } 2882 } 2883 } 2884 field->unlock(); 2885 pCore->refreshProjectMonitorOnce(); 2886 } 2887 2888 void TimelineController::extractZone(QPoint zone, bool liftOnly) 2889 { 2890 QVector<int> tracks; 2891 auto it = m_model->m_allTracks.cbegin(); 2892 while (it != m_model->m_allTracks.cend()) { 2893 int target_track = (*it)->getId(); 2894 if (m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { 2895 tracks << target_track; 2896 } 2897 ++it; 2898 } 2899 if (tracks.isEmpty()) { 2900 pCore->displayMessage(i18n("Please activate a track for this operation by clicking on its label"), ErrorMessage); 2901 } 2902 if (m_zone.isNull()) { 2903 // Use current timeline position and clip zone length 2904 zone.setY(pCore->getMonitorPosition() + zone.y() - zone.x()); 2905 zone.setX(pCore->getMonitorPosition()); 2906 } 2907 TimelineFunctions::extractZone(m_model, tracks, m_zone == QPoint() ? zone : m_zone, liftOnly); 2908 if (!liftOnly && !m_zone.isNull()) { 2909 setPosition(m_zone.x()); 2910 } 2911 } 2912 2913 void TimelineController::extract(int clipId, bool singleSelectionMode) 2914 { 2915 if (clipId == -1) { 2916 std::unordered_set<int> sel = m_model->getCurrentSelection(); 2917 for (int i : sel) { 2918 if (m_model->isClip(i)) { 2919 clipId = i; 2920 break; 2921 } 2922 } 2923 if (clipId == -1) { 2924 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 2925 return; 2926 } 2927 } 2928 int in = m_model->getClipPosition(clipId); 2929 int out = in + m_model->getClipPlaytime(clipId); 2930 int tid = m_model->getClipTrackId(clipId); 2931 std::pair<MixInfo, MixInfo> mixData = m_model->getTrackById_const(tid)->getMixInfo(clipId); 2932 if (mixData.first.firstClipId > -1) { 2933 // Clip has a start mix, adjust in point 2934 in += (mixData.first.firstClipInOut.second - mixData.first.secondClipInOut.first - mixData.first.mixOffset); 2935 } 2936 if (mixData.second.firstClipId > -1) { 2937 // Clip has end mix, adjust out point 2938 out -= mixData.second.mixOffset; 2939 } 2940 QVector<int> tracks = {tid}; 2941 int clipToUngroup = -1; 2942 std::unordered_set<int> clipsToRegroup; 2943 if (m_model->m_groups->isInGroup(clipId)) { 2944 if (singleSelectionMode) { 2945 // Remove item from group 2946 clipsToRegroup = m_model->m_groups->getLeaves(m_model->m_groups->getRootId(clipId)); 2947 clipToUngroup = clipId; 2948 clipsToRegroup.erase(clipToUngroup); 2949 m_model->requestClearSelection(); 2950 } else { 2951 int targetRoot = m_model->m_groups->getRootId(clipId); 2952 if (m_model->isGroup(targetRoot)) { 2953 std::unordered_set<int> sub = m_model->m_groups->getLeaves(targetRoot); 2954 for (int current_id : sub) { 2955 if (current_id == clipId) { 2956 continue; 2957 } 2958 if (m_model->isClip(current_id)) { 2959 int newIn = m_model->getClipPosition(current_id); 2960 int newOut = newIn + m_model->getClipPlaytime(current_id); 2961 int tk = m_model->getClipTrackId(current_id); 2962 std::pair<MixInfo, MixInfo> cMixData = m_model->getTrackById_const(tk)->getMixInfo(current_id); 2963 if (cMixData.first.firstClipId > -1) { 2964 // Clip has a start mix, adjust in point 2965 newIn += (cMixData.first.firstClipInOut.second - cMixData.first.secondClipInOut.first - cMixData.first.mixOffset); 2966 } 2967 if (cMixData.second.firstClipId > -1) { 2968 // Clip has end mix, adjust out point 2969 newOut -= cMixData.second.mixOffset; 2970 } 2971 in = qMin(in, newIn); 2972 out = qMax(out, newOut); 2973 if (!tracks.contains(tk)) { 2974 tracks << tk; 2975 } 2976 } 2977 } 2978 } 2979 } 2980 } 2981 TimelineFunctions::extractZone(m_model, tracks, QPoint(in, out), false, clipToUngroup, clipsToRegroup); 2982 } 2983 2984 void TimelineController::saveZone(int clipId) 2985 { 2986 if (clipId == -1) { 2987 clipId = getMainSelectedClip(); 2988 if (clipId == -1) { 2989 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 2990 return; 2991 } 2992 } 2993 int in = m_model->getClipIn(clipId); 2994 int out = in + m_model->getClipPlaytime(clipId) - 1; 2995 QString id; 2996 pCore->projectItemModel()->requestAddBinSubClip(id, in, out, {}, m_model->m_allClips[clipId]->binId()); 2997 } 2998 2999 bool TimelineController::insertClipZone(const QString &binId, int tid, int position) 3000 { 3001 QStringList binIdData = binId.split(QLatin1Char('/')); 3002 int in = 0; 3003 int out = -1; 3004 if (binIdData.size() >= 3) { 3005 in = binIdData.at(1).toInt(); 3006 out = binIdData.at(2).toInt(); 3007 } 3008 3009 QString bid = binIdData.first(); 3010 // dropType indicates if we want a normal drop (disabled), audio only or video only drop 3011 PlaylistState::ClipState dropType = PlaylistState::Disabled; 3012 if (bid.startsWith(QLatin1Char('A'))) { 3013 dropType = PlaylistState::AudioOnly; 3014 bid = bid.remove(0, 1); 3015 } else if (bid.startsWith(QLatin1Char('V'))) { 3016 dropType = PlaylistState::VideoOnly; 3017 bid = bid.remove(0, 1); 3018 } 3019 QList<int> audioTracks; 3020 int vTrack = -1; 3021 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(bid); 3022 if (out <= in) { 3023 out = int(clip->frameDuration() - 1); 3024 } 3025 QList<int> audioStreams = m_model->m_binAudioTargets.keys(); 3026 if (dropType == PlaylistState::VideoOnly) { 3027 vTrack = tid; 3028 } else if (dropType == PlaylistState::AudioOnly) { 3029 audioTracks << tid; 3030 if (audioStreams.size() > 1) { 3031 // insert the other audio streams 3032 QList<int> lower = m_model->getLowerTracksId(tid, TrackType::AudioTrack); 3033 while (audioStreams.size() > 1 && !lower.isEmpty()) { 3034 audioTracks << lower.takeFirst(); 3035 audioStreams.takeFirst(); 3036 } 3037 } 3038 } else { 3039 if (m_model->getTrackById_const(tid)->isAudioTrack()) { 3040 audioTracks << tid; 3041 if (audioStreams.size() > 1) { 3042 // insert the other audio streams 3043 QList<int> lower = m_model->getLowerTracksId(tid, TrackType::AudioTrack); 3044 while (audioStreams.size() > 1 && !lower.isEmpty()) { 3045 audioTracks << lower.takeFirst(); 3046 audioStreams.takeFirst(); 3047 } 3048 } 3049 vTrack = clip->hasAudioAndVideo() ? m_model->getMirrorVideoTrackId(tid) : -1; 3050 } else { 3051 vTrack = tid; 3052 if (clip->hasAudioAndVideo()) { 3053 int firstAudio = m_model->getMirrorAudioTrackId(vTrack); 3054 audioTracks << firstAudio; 3055 if (audioStreams.size() > 1) { 3056 // insert the other audio streams 3057 QList<int> lower = m_model->getLowerTracksId(firstAudio, TrackType::AudioTrack); 3058 while (audioStreams.size() > 1 && !lower.isEmpty()) { 3059 audioTracks << lower.takeFirst(); 3060 audioStreams.takeFirst(); 3061 } 3062 } 3063 } 3064 } 3065 } 3066 QList<int> target_tracks; 3067 if (vTrack > -1) { 3068 target_tracks << vTrack; 3069 } 3070 if (!audioTracks.isEmpty()) { 3071 target_tracks << audioTracks; 3072 } 3073 qDebug() << "=====================\n\nREADY TO INSERT IN TRACKS: " << audioTracks << " / VIDEO: " << vTrack << "\n\n========="; 3074 std::function<bool(void)> undo = []() { return true; }; 3075 std::function<bool(void)> redo = []() { return true; }; 3076 bool overwrite = m_model->m_editMode == TimelineMode::OverwriteEdit; 3077 QPoint zone(in, out + 1); 3078 bool res = TimelineFunctions::insertZone(m_model, target_tracks, binId, position, zone, overwrite, false, undo, redo); 3079 if (res) { 3080 int newPos = position + (zone.y() - zone.x()); 3081 int currentPos = pCore->getMonitorPosition(); 3082 Fun redoPos = [this, newPos]() { 3083 Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id(); 3084 pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); 3085 pCore->monitorManager()->refreshProjectMonitor(); 3086 setPosition(newPos); 3087 pCore->monitorManager()->activateMonitor(activeMonitor); 3088 return true; 3089 }; 3090 Fun undoPos = [this, currentPos]() { 3091 Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id(); 3092 pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); 3093 pCore->monitorManager()->refreshProjectMonitor(); 3094 setPosition(currentPos); 3095 pCore->monitorManager()->activateMonitor(activeMonitor); 3096 return true; 3097 }; 3098 redoPos(); 3099 UPDATE_UNDO_REDO_NOLOCK(redoPos, undoPos, undo, redo); 3100 pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone")); 3101 } else { 3102 pCore->displayMessage(i18n("Could not insert zone"), ErrorMessage); 3103 undo(); 3104 } 3105 return res; 3106 } 3107 3108 int TimelineController::insertZone(const QString &binId, QPoint zone, bool overwrite, Fun &undo, Fun &redo) 3109 { 3110 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(binId); 3111 int aTrack = -1; 3112 int vTrack = -1; 3113 if (clip->hasAudio() && !m_model->m_audioTarget.isEmpty()) { 3114 QList<int> audioTracks = m_model->m_audioTarget.keys(); 3115 for (int tid : qAsConst(audioTracks)) { 3116 if (m_model->getTrackById_const(tid)->shouldReceiveTimelineOp()) { 3117 aTrack = tid; 3118 break; 3119 } 3120 } 3121 } 3122 if (clip->hasVideo()) { 3123 vTrack = videoTarget(); 3124 } 3125 3126 int insertPoint; 3127 QPoint sourceZone; 3128 if (useRuler() && m_zone != QPoint()) { 3129 // We want to use timeline zone for in/out insert points 3130 insertPoint = m_zone.x(); 3131 sourceZone = QPoint(zone.x(), zone.x() + m_zone.y() - m_zone.x()); 3132 } else { 3133 // Use current timeline pos and clip zone for in/out 3134 insertPoint = pCore->getMonitorPosition(); 3135 sourceZone = zone; 3136 } 3137 QList<int> target_tracks; 3138 if (vTrack > -1) { 3139 target_tracks << vTrack; 3140 } 3141 if (aTrack > -1) { 3142 target_tracks << aTrack; 3143 } 3144 if (target_tracks.isEmpty()) { 3145 pCore->displayMessage(i18n("Please select a target track by clicking on a track's target zone"), ErrorMessage); 3146 return -1; 3147 } 3148 bool res = TimelineFunctions::insertZone(m_model, target_tracks, binId, insertPoint, sourceZone, overwrite, true, undo, redo); 3149 if (res) { 3150 int newPos = insertPoint + (sourceZone.y() - sourceZone.x()); 3151 int currentPos = pCore->getMonitorPosition(); 3152 Fun redoPos = [this, newPos]() { 3153 setPosition(newPos); 3154 pCore->getMonitor(Kdenlive::ProjectMonitor)->refreshMonitorIfActive(); 3155 return true; 3156 }; 3157 Fun undoPos = [this, currentPos]() { 3158 setPosition(currentPos); 3159 pCore->getMonitor(Kdenlive::ProjectMonitor)->refreshMonitorIfActive(); 3160 return true; 3161 }; 3162 redoPos(); 3163 UPDATE_UNDO_REDO_NOLOCK(redoPos, undoPos, undo, redo); 3164 } 3165 return res; 3166 } 3167 3168 void TimelineController::updateClip(int clipId, const QVector<int> &roles) 3169 { 3170 QModelIndex ix = m_model->makeClipIndexFromID(clipId); 3171 if (ix.isValid()) { 3172 Q_EMIT m_model->dataChanged(ix, ix, roles); 3173 } 3174 } 3175 3176 void TimelineController::showClipKeyframes(int clipId, bool value) 3177 { 3178 TimelineFunctions::showClipKeyframes(m_model, clipId, value); 3179 } 3180 3181 void TimelineController::showCompositionKeyframes(int clipId, bool value) 3182 { 3183 TimelineFunctions::showCompositionKeyframes(m_model, clipId, value); 3184 } 3185 3186 void TimelineController::switchEnableState(std::unordered_set<int> selection) 3187 { 3188 if (selection.empty()) { 3189 selection = m_model->getCurrentSelection(); 3190 // clipId = getMainSelectedItem(false, false); 3191 } 3192 if (selection.empty()) { 3193 return; 3194 } 3195 TimelineFunctions::switchEnableState(m_model, selection); 3196 } 3197 3198 void TimelineController::addCompositionToClip(const QString &assetId, int clipId, int offset) 3199 { 3200 if (clipId == -1) { 3201 clipId = getMainSelectedClip(); 3202 if (clipId == -1) { 3203 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 3204 return; 3205 } 3206 } 3207 if (offset == -1) { 3208 offset = m_root->property("clickFrame").toInt(); 3209 } 3210 int track = clipId > -1 ? m_model->getClipTrackId(clipId) : m_activeTrack; 3211 int compoId = -1; 3212 if (assetId.isEmpty()) { 3213 QStringList compositions = KdenliveSettings::favorite_transitions(); 3214 if (compositions.isEmpty()) { 3215 pCore->displayMessage(i18n("Select a favorite composition"), ErrorMessage, 500); 3216 return; 3217 } 3218 compoId = insertNewComposition(track, clipId, offset, compositions.first(), true); 3219 } else { 3220 compoId = insertNewComposition(track, clipId, offset, assetId, true); 3221 } 3222 if (compoId > 0) { 3223 m_model->requestSetSelection({compoId}); 3224 } 3225 } 3226 3227 void TimelineController::setEffectsEnabled(int clipId, bool enabled) 3228 { 3229 std::shared_ptr<ClipModel> clip = m_model->getClipPtr(clipId); 3230 clip->setTimelineEffectsEnabled(enabled); 3231 } 3232 3233 void TimelineController::addEffectToClip(const QString &assetId, int clipId) 3234 { 3235 if (clipId == -1) { 3236 clipId = getMainSelectedClip(); 3237 if (clipId == -1) { 3238 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 3239 return; 3240 } 3241 } 3242 if (m_model->addClipEffect(clipId, assetId).size() > 0 && KdenliveSettings::seekonaddeffect()) { 3243 // Move timeline cursor inside clip if it is not 3244 int in = m_model->getClipPosition(clipId); 3245 int out = in + m_model->getClipPlaytime(clipId); 3246 int position = pCore->getMonitorPosition(); 3247 if (position < in || position > out) { 3248 Q_EMIT seeked(in); 3249 } 3250 } 3251 } 3252 3253 bool TimelineController::splitAV() 3254 { 3255 int cid = *m_model->getCurrentSelection().begin(); 3256 if (m_model->isClip(cid)) { 3257 std::shared_ptr<ClipModel> clip = m_model->getClipPtr(cid); 3258 if (clip->clipState() == PlaylistState::AudioOnly) { 3259 return TimelineFunctions::requestSplitVideo(m_model, cid, videoTarget()); 3260 } else { 3261 QVariantList aTargets = audioTarget(); 3262 int targetTrack = aTargets.isEmpty() ? -1 : aTargets.first().toInt(); 3263 return TimelineFunctions::requestSplitAudio(m_model, cid, targetTrack); 3264 } 3265 } 3266 pCore->displayMessage(i18n("No clip found to perform AV split operation"), ErrorMessage, 500); 3267 return false; 3268 } 3269 3270 void TimelineController::splitAudio(int clipId) 3271 { 3272 QVariantList aTargets = audioTarget(); 3273 int targetTrack = aTargets.isEmpty() ? -1 : aTargets.first().toInt(); 3274 TimelineFunctions::requestSplitAudio(m_model, clipId, targetTrack); 3275 } 3276 3277 void TimelineController::splitVideo(int clipId) 3278 { 3279 TimelineFunctions::requestSplitVideo(m_model, clipId, videoTarget()); 3280 } 3281 3282 void TimelineController::setAudioRef(int clipId) 3283 { 3284 if (clipId == -1) { 3285 clipId = getMainSelectedClip(); 3286 if (clipId == -1) { 3287 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 3288 return; 3289 } 3290 } 3291 m_audioRef = clipId; 3292 std::unique_ptr<AudioEnvelope> envelope(new AudioEnvelope(getClipBinId(clipId), clipId)); 3293 m_audioCorrelator.reset(new AudioCorrelation(std::move(envelope))); 3294 connect(m_audioCorrelator.get(), &AudioCorrelation::gotAudioAlignData, this, [&](int cid, int shift) { 3295 // Ensure the clip was not deleted while processing calculations 3296 if (m_model->isClip(cid)) { 3297 int pos = m_model->getClipPosition(m_audioRef) + shift - m_model->getClipIn(m_audioRef); 3298 bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), pos, true, true, true); 3299 if (!result) { 3300 pCore->displayMessage(i18n("Cannot move clip to frame %1.", (pos + shift)), ErrorMessage, 500); 3301 } 3302 } else { 3303 // Clip was deleted, discard audio reference 3304 m_audioRef = -1; 3305 } 3306 }); 3307 connect(m_audioCorrelator.get(), &AudioCorrelation::displayMessage, pCore.get(), &Core::displayMessage); 3308 } 3309 3310 void TimelineController::alignAudio(int clipId) 3311 { 3312 // find other clip 3313 if (clipId == -1) { 3314 clipId = getMainSelectedClip(); 3315 if (clipId == -1) { 3316 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 3317 return; 3318 } 3319 } 3320 if (m_audioRef == -1 || m_audioRef == clipId || !m_model->isClip(m_audioRef)) { 3321 pCore->displayMessage(i18n("Set audio reference before attempting to align"), InformationMessage, 500); 3322 return; 3323 } 3324 const QString masterBinClipId = getClipBinId(m_audioRef); 3325 std::unordered_set<int> clipsToAnalyse; 3326 if (m_model->m_groups->isInGroup(clipId)) { 3327 clipsToAnalyse = m_model->getGroupElements(clipId); 3328 m_model->requestClearSelection(); 3329 } else { 3330 clipsToAnalyse.insert(clipId); 3331 } 3332 QList<int> processedGroups; 3333 int processed = 0; 3334 for (int cid : clipsToAnalyse) { 3335 if (!m_model->isClip(cid) || cid == m_audioRef) { 3336 continue; 3337 } 3338 const QString otherBinId = getClipBinId(cid); 3339 if (m_model->m_groups->isInGroup(cid)) { 3340 int parentGroup = m_model->m_groups->getRootId(cid); 3341 if (processedGroups.contains(parentGroup)) { 3342 continue; 3343 } 3344 // Only process one clip from the group 3345 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(otherBinId); 3346 if (clip->hasAudio()) { 3347 processedGroups << parentGroup; 3348 } else { 3349 continue; 3350 } 3351 } 3352 if (!pCore->bin()->getBinClip(otherBinId)->hasAudio()) { 3353 // Cannot process non audi clips 3354 continue; 3355 } 3356 if (otherBinId == masterBinClipId) { 3357 // easy, same clip. 3358 int newPos = m_model->getClipPosition(m_audioRef) - m_model->getClipIn(m_audioRef) + m_model->getClipIn(cid); 3359 if (newPos) { 3360 bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), newPos, true, true, true); 3361 processed++; 3362 if (!result) { 3363 pCore->displayMessage(i18n("Cannot move clip to frame %1.", newPos), ErrorMessage, 500); 3364 } 3365 continue; 3366 } 3367 } 3368 processed++; 3369 // Perform audio calculation 3370 auto *envelope = 3371 new AudioEnvelope(otherBinId, cid, size_t(m_model->getClipIn(cid)), size_t(m_model->getClipPlaytime(cid)), size_t(m_model->getClipPosition(cid))); 3372 m_audioCorrelator->addChild(envelope); 3373 } 3374 if (processed == 0) { 3375 // TODO: improve feedback message after freeze 3376 pCore->displayMessage(i18n("Select a clip to apply an effect"), ErrorMessage, 500); 3377 } 3378 } 3379 3380 void TimelineController::switchTrackActive(int trackId) 3381 { 3382 if (trackId == -1) { 3383 trackId = m_activeTrack; 3384 } 3385 if (trackId < 0) { 3386 return; 3387 } 3388 bool active = m_model->getTrackById_const(trackId)->isTimelineActive(); 3389 m_model->setTrackProperty(trackId, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1")); 3390 m_activeSnaps.clear(); 3391 } 3392 3393 void TimelineController::switchAllTrackActive() 3394 { 3395 auto it = m_model->m_allTracks.cbegin(); 3396 while (it != m_model->m_allTracks.cend()) { 3397 bool active = (*it)->isTimelineActive(); 3398 int target_track = (*it)->getId(); 3399 m_model->setTrackProperty(target_track, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1")); 3400 ++it; 3401 } 3402 m_activeSnaps.clear(); 3403 } 3404 3405 void TimelineController::makeAllTrackActive() 3406 { 3407 // Check current status 3408 auto it = m_model->m_allTracks.cbegin(); 3409 bool makeActive = false; 3410 while (it != m_model->m_allTracks.cend()) { 3411 if (!(*it)->isTimelineActive()) { 3412 // There is an inactive track, activate all 3413 makeActive = true; 3414 break; 3415 } 3416 ++it; 3417 } 3418 it = m_model->m_allTracks.cbegin(); 3419 while (it != m_model->m_allTracks.cend()) { 3420 int target_track = (*it)->getId(); 3421 m_model->setTrackProperty(target_track, QStringLiteral("kdenlive:timeline_active"), makeActive ? QStringLiteral("1") : QStringLiteral("0")); 3422 ++it; 3423 } 3424 m_activeSnaps.clear(); 3425 } 3426 3427 void TimelineController::switchTrackDisabled() 3428 { 3429 3430 if (m_model->isSubtitleTrack(m_activeTrack)) { 3431 // Subtitle track 3432 switchSubtitleDisable(); 3433 } else { 3434 bool isAudio = m_model->getTrackById_const(m_activeTrack)->isAudioTrack(); 3435 bool enabled = isAudio ? m_model->getTrackById_const(m_activeTrack)->isMute() : m_model->getTrackById_const(m_activeTrack)->isHidden(); 3436 hideTrack(m_activeTrack, enabled); 3437 } 3438 } 3439 3440 void TimelineController::switchTrackLock(bool applyToAll) 3441 { 3442 if (!applyToAll) { 3443 // apply to active track only 3444 if (m_model->isSubtitleTrack(m_activeTrack)) { 3445 // Subtitle track 3446 switchSubtitleLock(); 3447 } else { 3448 bool locked = m_model->getTrackById_const(m_activeTrack)->isLocked(); 3449 m_model->setTrackLockedState(m_activeTrack, !locked); 3450 } 3451 } else { 3452 // Invert track lock 3453 const auto ids = m_model->getAllTracksIds(); 3454 // count the number of tracks to be locked 3455 for (const int id : ids) { 3456 bool isLocked = m_model->getTrackById_const(id)->isLocked(); 3457 m_model->setTrackLockedState(id, !isLocked); 3458 } 3459 if (m_model->hasSubtitleModel()) { 3460 switchSubtitleLock(); 3461 } 3462 } 3463 } 3464 3465 void TimelineController::switchTargetTrack() 3466 { 3467 if (m_activeTrack < 0) { 3468 return; 3469 } 3470 bool isAudio = m_model->isAudioTrack(m_activeTrack); 3471 if (isAudio) { 3472 QMap<int, int> current = m_model->m_audioTarget; 3473 if (current.contains(m_activeTrack)) { 3474 current.remove(m_activeTrack); 3475 } else { 3476 int ix = getFirstUnassignedStream(); 3477 if (ix > -1) { 3478 current.insert(m_activeTrack, ix); 3479 } else if (current.size() == 1) { 3480 // If we only have one video stream, directly reassign it 3481 int stream = current.first(); 3482 current.clear(); 3483 current.insert(m_activeTrack, stream); 3484 } else { 3485 pCore->displayMessage(i18n("All streams already assigned, deselect another audio target first"), InformationMessage, 500); 3486 return; 3487 } 3488 } 3489 setAudioTarget(current); 3490 } else { 3491 setVideoTarget(videoTarget() == m_activeTrack ? -1 : m_activeTrack); 3492 } 3493 } 3494 3495 QVariantList TimelineController::audioTarget() const 3496 { 3497 QVariantList audioTracks; 3498 QMapIterator<int, int> i(m_model->m_audioTarget); 3499 while (i.hasNext()) { 3500 i.next(); 3501 audioTracks << i.key(); 3502 } 3503 return audioTracks; 3504 } 3505 3506 QVariantList TimelineController::lastAudioTarget() const 3507 { 3508 QVariantList audioTracks; 3509 QMapIterator<int, int> i(m_lastAudioTarget); 3510 while (i.hasNext()) { 3511 i.next(); 3512 audioTracks << i.key(); 3513 } 3514 return audioTracks; 3515 } 3516 3517 const QString TimelineController::audioTargetName(int tid) const 3518 { 3519 if (m_model->m_audioTarget.contains(tid) && m_model->m_binAudioTargets.size() > 1) { 3520 int streamIndex = m_model->m_audioTarget.value(tid); 3521 if (m_model->m_binAudioTargets.contains(streamIndex)) { 3522 QString targetName = m_model->m_binAudioTargets.value(streamIndex); 3523 return targetName.isEmpty() ? QChar('x') : targetName.section(QLatin1Char('|'), 0, 0); 3524 } else { 3525 qDebug() << "STREAM INDEX NOT IN TARGET : " << streamIndex << " = " << m_model->m_binAudioTargets; 3526 } 3527 } else { 3528 qDebug() << "TRACK NOT IN TARGET : " << tid << " = " << m_model->m_audioTarget.keys(); 3529 } 3530 return QString(); 3531 } 3532 3533 int TimelineController::videoTarget() const 3534 { 3535 return m_model->m_videoTarget; 3536 } 3537 3538 int TimelineController::hasAudioTarget() const 3539 { 3540 return m_hasAudioTarget; 3541 } 3542 3543 int TimelineController::clipTargets() const 3544 { 3545 return m_model->m_binAudioTargets.size(); 3546 } 3547 3548 bool TimelineController::hasVideoTarget() const 3549 { 3550 return m_hasVideoTarget; 3551 } 3552 3553 bool TimelineController::autoScroll() const 3554 { 3555 return !pCore->monitorManager()->projectMonitor()->isPlaying() || KdenliveSettings::autoscroll(); 3556 } 3557 3558 void TimelineController::resetTrackHeight() 3559 { 3560 int tracksCount = m_model->getTracksCount(); 3561 for (int track = tracksCount - 1; track >= 0; track--) { 3562 int trackIx = m_model->getTrackIndexFromPosition(track); 3563 m_model->getTrackById(trackIx)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight())); 3564 } 3565 QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); 3566 QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); 3567 Q_EMIT m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); 3568 } 3569 3570 void TimelineController::selectAll() 3571 { 3572 std::unordered_set<int> ids; 3573 for (const auto &clp : m_model->m_allClips) { 3574 ids.insert(clp.first); 3575 } 3576 for (const auto &clp : m_model->m_allCompositions) { 3577 ids.insert(clp.first); 3578 } 3579 // Subtitles 3580 for (const auto &sub : m_model->m_allSubtitles) { 3581 ids.insert(sub.first); 3582 } 3583 m_model->requestSetSelection(ids); 3584 } 3585 3586 void TimelineController::selectCurrentTrack() 3587 { 3588 if (m_activeTrack == -1) { 3589 return; 3590 } 3591 std::unordered_set<int> ids; 3592 if (m_model->isSubtitleTrack(m_activeTrack)) { 3593 for (const auto &sub : m_model->m_allSubtitles) { 3594 ids.insert(sub.first); 3595 } 3596 } else { 3597 for (const auto &clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) { 3598 ids.insert(clp.first); 3599 } 3600 for (const auto &clp : m_model->getTrackById_const(m_activeTrack)->m_allCompositions) { 3601 ids.insert(clp.first); 3602 } 3603 } 3604 m_model->requestSetSelection(ids); 3605 } 3606 3607 void TimelineController::deleteEffects(int targetId) 3608 { 3609 std::unordered_set<int> targetIds; 3610 std::unordered_set<int> sel; 3611 if (targetId == -1) { 3612 sel = m_model->getCurrentSelection(); 3613 } else { 3614 if (m_model->m_groups->isInGroup(targetId)) { 3615 sel = {m_model->m_groups->getRootId(targetId)}; 3616 } else { 3617 sel = {targetId}; 3618 } 3619 } 3620 if (sel.empty()) { 3621 pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); 3622 } 3623 for (int s : sel) { 3624 if (m_model->isGroup(s)) { 3625 std::unordered_set<int> sub = m_model->m_groups->getLeaves(s); 3626 for (int current_id : sub) { 3627 if (m_model->isClip(current_id)) { 3628 targetIds.insert(current_id); 3629 } 3630 } 3631 } else if (m_model->isClip(s)) { 3632 targetIds.insert(s); 3633 } 3634 } 3635 if (targetIds.empty()) { 3636 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 3637 } 3638 std::function<bool(void)> undo = []() { return true; }; 3639 std::function<bool(void)> redo = []() { return true; }; 3640 for (int target : targetIds) { 3641 std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(target); 3642 destStack->removeAllEffects(undo, redo); 3643 } 3644 pCore->pushUndo(undo, redo, i18n("Delete effects")); 3645 } 3646 3647 void TimelineController::pasteEffects(int targetId) 3648 { 3649 std::unordered_set<int> targetIds; 3650 std::unordered_set<int> sel; 3651 if (targetId == -1) { 3652 sel = m_model->getCurrentSelection(); 3653 } else { 3654 if (m_model->m_groups->isInGroup(targetId)) { 3655 sel = {m_model->m_groups->getRootId(targetId)}; 3656 } else { 3657 sel = {targetId}; 3658 } 3659 } 3660 if (sel.empty()) { 3661 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 3662 } 3663 for (int s : sel) { 3664 if (m_model->isGroup(s)) { 3665 std::unordered_set<int> sub = m_model->m_groups->getLeaves(s); 3666 for (int current_id : sub) { 3667 if (m_model->isClip(current_id)) { 3668 targetIds.insert(current_id); 3669 } 3670 } 3671 } else if (m_model->isClip(s)) { 3672 targetIds.insert(s); 3673 } 3674 } 3675 if (targetIds.empty()) { 3676 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 3677 } 3678 3679 QClipboard *clipboard = QApplication::clipboard(); 3680 QString txt = clipboard->text(); 3681 if (txt.isEmpty()) { 3682 pCore->displayMessage(i18n("No information in clipboard"), ErrorMessage, 500); 3683 return; 3684 } 3685 QDomDocument copiedItems; 3686 copiedItems.setContent(txt); 3687 if (copiedItems.documentElement().tagName() != QLatin1String("kdenlive-scene")) { 3688 pCore->displayMessage(i18n("No information in clipboard"), ErrorMessage, 500); 3689 return; 3690 } 3691 QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip")); 3692 if (clips.isEmpty()) { 3693 pCore->displayMessage(i18n("No information in clipboard"), ErrorMessage, 500); 3694 return; 3695 } 3696 std::function<bool(void)> undo = []() { return true; }; 3697 std::function<bool(void)> redo = []() { return true; }; 3698 QDomElement effects = clips.at(0).firstChildElement(QStringLiteral("effects")); 3699 effects.setAttribute(QStringLiteral("parentIn"), clips.at(0).toElement().attribute(QStringLiteral("in"))); 3700 for (int i = 1; i < clips.size(); i++) { 3701 QDomElement subeffects = clips.at(i).firstChildElement(QStringLiteral("effects")); 3702 QDomNodeList subs = subeffects.childNodes(); 3703 while (!subs.isEmpty()) { 3704 subs.at(0).toElement().setAttribute(QStringLiteral("parentIn"), clips.at(i).toElement().attribute(QStringLiteral("in"))); 3705 effects.appendChild(subs.at(0)); 3706 } 3707 } 3708 int insertedEffects = 0; 3709 for (int target : targetIds) { 3710 std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(target); 3711 if (destStack->fromXml(effects, undo, redo)) { 3712 insertedEffects++; 3713 } 3714 } 3715 if (insertedEffects > 0) { 3716 pCore->pushUndo(undo, redo, i18n("Paste effects")); 3717 } else { 3718 pCore->displayMessage(i18n("Cannot paste effect on selected clip"), ErrorMessage, 500); 3719 undo(); 3720 } 3721 } 3722 3723 double TimelineController::fps() const 3724 { 3725 return pCore->getCurrentFps(); 3726 } 3727 3728 void TimelineController::editItemDuration(int id) 3729 { 3730 if (id == -1) { 3731 id = m_root->property("mainItemId").toInt(); 3732 if (id == -1) { 3733 std::unordered_set<int> sel = m_model->getCurrentSelection(); 3734 if (!sel.empty()) { 3735 id = *sel.begin(); 3736 } 3737 if (id == -1) { 3738 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 3739 return; 3740 } 3741 } 3742 } 3743 if (id == -1 || !m_model->isItem(id)) { 3744 pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500); 3745 return; 3746 } 3747 int start = m_model->getItemPosition(id); 3748 int in = 0; 3749 int duration = m_model->getItemPlaytime(id); 3750 int maxLength = -1; 3751 bool isComposition = false; 3752 if (m_model->isClip(id)) { 3753 in = m_model->getClipIn(id); 3754 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(getClipBinId(id)); 3755 if (clip && clip->hasLimitedDuration()) { 3756 maxLength = clip->getProducerDuration(); 3757 } 3758 } else if (m_model->isComposition(id)) { 3759 // nothing to do 3760 isComposition = true; 3761 } else { 3762 pCore->displayMessage(i18n("No item to edit"), ErrorMessage, 500); 3763 return; 3764 } 3765 int trackId = m_model->getItemTrackId(id); 3766 int maxFrame = qMax(0, start + duration + 3767 (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, true) 3768 : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, true))); 3769 int minFrame = qMax(0, in - (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, false) 3770 : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, false))); 3771 int partner = isComposition ? -1 : m_model->getClipSplitPartner(id); 3772 QScopedPointer<ClipDurationDialog> dialog(new ClipDurationDialog(id, start, minFrame, in, in + duration, maxLength, maxFrame, qApp->activeWindow())); 3773 if (dialog->exec() == QDialog::Accepted) { 3774 std::function<bool(void)> undo = []() { return true; }; 3775 std::function<bool(void)> redo = []() { return true; }; 3776 int newPos = dialog->startPos().frames(pCore->getCurrentFps()); 3777 int newIn = dialog->cropStart().frames(pCore->getCurrentFps()); 3778 int newDuration = dialog->duration().frames(pCore->getCurrentFps()); 3779 bool result = true; 3780 if (newPos < start) { 3781 if (!isComposition) { 3782 result = m_model->requestClipMove(id, trackId, newPos, true, true, true, true, undo, redo); 3783 if (result && partner > -1) { 3784 result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, true, undo, redo); 3785 } 3786 } else { 3787 result = m_model->requestCompositionMove(id, trackId, m_model->m_allCompositions[id]->getForcedTrack(), newPos, true, true, undo, redo); 3788 } 3789 if (result && newIn != in) { 3790 int updatedDuration = duration + (in - newIn); 3791 result = m_model->requestItemResize(id, updatedDuration, false, true, undo, redo); 3792 if (result && partner > -1) { 3793 result = m_model->requestItemResize(partner, updatedDuration, false, true, undo, redo); 3794 } 3795 } 3796 if (newDuration != duration + (in - newIn)) { 3797 result = result && m_model->requestItemResize(id, newDuration, true, true, undo, redo); 3798 if (result && partner > -1) { 3799 result = m_model->requestItemResize(partner, newDuration, false, true, undo, redo); 3800 } 3801 } 3802 } else { 3803 // perform resize first 3804 if (newIn != in) { 3805 int updatedDuration = duration + (in - newIn); 3806 result = m_model->requestItemResize(id, updatedDuration, false, true, undo, redo); 3807 if (result && partner > -1) { 3808 result = m_model->requestItemResize(partner, updatedDuration, false, true, undo, redo); 3809 } 3810 } 3811 if (newDuration != duration + (in - newIn)) { 3812 result = result && m_model->requestItemResize(id, newDuration, start == newPos, true, undo, redo); 3813 if (result && partner > -1) { 3814 result = m_model->requestItemResize(partner, newDuration, start == newPos, true, undo, redo); 3815 } 3816 } 3817 if (start != newPos || newIn != in) { 3818 if (!isComposition) { 3819 result = result && m_model->requestClipMove(id, trackId, newPos, true, true, true, true, undo, redo); 3820 if (result && partner > -1) { 3821 result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, true, undo, redo); 3822 } 3823 } else { 3824 result = result && 3825 m_model->requestCompositionMove(id, trackId, m_model->m_allCompositions[id]->getForcedTrack(), newPos, true, true, undo, redo); 3826 } 3827 } 3828 } 3829 if (result) { 3830 pCore->pushUndo(undo, redo, i18n("Edit item")); 3831 } else { 3832 undo(); 3833 } 3834 } 3835 Q_EMIT regainFocus(); 3836 } 3837 3838 void TimelineController::focusTimelineSequence(int id) 3839 { 3840 std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(id)); 3841 if (binClip) { 3842 const QUuid uuid = binClip->getSequenceUuid(); 3843 int sequencePos = pCore->getMonitorPosition(); 3844 sequencePos -= m_model->getClipPosition(id); 3845 if (sequencePos < 0 || sequencePos > m_model->getClipPlaytime(id)) { 3846 sequencePos = -1; 3847 } else { 3848 sequencePos += m_model->getClipIn(id); 3849 } 3850 Fun local_redo = [uuid, binId = binClip->binId(), sequencePos]() { return pCore->projectManager()->openTimeline(binId, uuid, sequencePos); }; 3851 if (local_redo()) { 3852 Fun local_undo = [uuid]() { 3853 if (pCore->projectManager()->closeTimeline(uuid)) { 3854 pCore->window()->closeTimelineTab(uuid); 3855 } 3856 return true; 3857 }; 3858 pCore->pushUndo(local_undo, local_redo, i18n("Open sequence")); 3859 } 3860 } 3861 } 3862 3863 void TimelineController::editTitleClip(int id) 3864 { 3865 if (id == -1) { 3866 id = m_root->property("mainItemId").toInt(); 3867 if (id == -1) { 3868 std::unordered_set<int> sel = m_model->getCurrentSelection(); 3869 if (!sel.empty()) { 3870 id = *sel.begin(); 3871 } 3872 if (id == -1 || !m_model->isItem(id) || !m_model->isClip(id)) { 3873 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 3874 return; 3875 } 3876 } 3877 } 3878 std::shared_ptr<ProjectClip> binClip = pCore->projectItemModel()->getClipByBinID(getClipBinId(id)); 3879 if (binClip->clipType() != ClipType::Text && binClip->clipType() != ClipType::TextTemplate) { 3880 pCore->displayMessage(i18n("Item is not a title clip"), ErrorMessage, 500); 3881 return; 3882 } 3883 seekToMouse(); 3884 pCore->bin()->showTitleWidget(binClip); 3885 } 3886 3887 void TimelineController::editAnimationClip(int id) 3888 { 3889 if (id == -1) { 3890 id = m_root->property("mainItemId").toInt(); 3891 if (id == -1) { 3892 std::unordered_set<int> sel = m_model->getCurrentSelection(); 3893 if (!sel.empty()) { 3894 id = *sel.begin(); 3895 } 3896 if (id == -1 || !m_model->isItem(id) || !m_model->isClip(id)) { 3897 pCore->displayMessage(i18n("No clip selected"), ErrorMessage, 500); 3898 return; 3899 } 3900 } 3901 } 3902 GlaxnimateLauncher::instance().openClip(id); 3903 } 3904 3905 QPoint TimelineController::selectionInOut() const 3906 { 3907 std::unordered_set<int> ids = m_model->getCurrentSelection(); 3908 std::unordered_set<int> items_list; 3909 for (int i : ids) { 3910 if (m_model->isGroup(i)) { 3911 std::unordered_set<int> children = m_model->m_groups->getLeaves(i); 3912 items_list.insert(children.begin(), children.end()); 3913 } else { 3914 items_list.insert(i); 3915 } 3916 } 3917 int in = -1; 3918 int out = -1; 3919 for (int id : items_list) { 3920 if (m_model->isClip(id) || m_model->isComposition(id)) { 3921 int itemIn = m_model->getItemPosition(id); 3922 int itemOut = itemIn + m_model->getItemPlaytime(id) - 1; 3923 if (in < 0 || itemIn < in) { 3924 in = itemIn; 3925 } 3926 if (itemOut > out) { 3927 out = itemOut; 3928 } 3929 } 3930 } 3931 return QPoint(in, out); 3932 } 3933 3934 void TimelineController::updateClipActions() 3935 { 3936 if (m_model->getCurrentSelection().empty()) { 3937 for (QAction *act : qAsConst(clipActions)) { 3938 const QChar actionData = act->data().toChar(); 3939 if (actionData == QLatin1Char('P')) { 3940 // Position actions should stay enabled in clip monitor 3941 act->setEnabled(true); 3942 } else { 3943 act->setEnabled(false); 3944 } 3945 } 3946 Q_EMIT timelineClipSelected(false); 3947 // nothing selected 3948 Q_EMIT showItemEffectStack(QString(), nullptr, QSize(), false); 3949 pCore->timeRemapWidget()->selectedClip(-1, QUuid()); 3950 Q_EMIT showSubtitle(-1); 3951 pCore->displaySelectionMessage(QString()); 3952 return; 3953 } 3954 std::shared_ptr<ClipModel> clip(nullptr); 3955 std::unordered_set<int> selectedItems = m_model->getCurrentSelection(); 3956 int item = *selectedItems.begin(); 3957 int selectionSize = selectedItems.size(); 3958 if (selectionSize == 1) { 3959 if (m_model->isClip(item) || m_model->isComposition(item)) { 3960 showAsset(item); 3961 Q_EMIT showSubtitle(-1); 3962 } else if (m_model->isSubTitle(item)) { 3963 Q_EMIT showSubtitle(item); 3964 } 3965 pCore->displaySelectionMessage(QString()); 3966 } else { 3967 int min = -1; 3968 int max = -1; 3969 for (const auto &id : selectedItems) { 3970 int itemPos = m_model->getItemPosition(id); 3971 int itemOut = itemPos + m_model->getItemPlaytime(id); 3972 if (min == -1 || itemPos < min) { 3973 min = itemPos; 3974 } 3975 if (max == -1 || itemOut > max) { 3976 max = itemOut; 3977 } 3978 } 3979 pCore->displaySelectionMessage(i18n("%1 items selected (%2) |", selectionSize, simplifiedTC(max - min))); 3980 } 3981 if (m_model->isClip(item)) { 3982 clip = m_model->getClipPtr(item); 3983 if (clip->hasTimeRemap()) { 3984 Q_EMIT pCore->remapClip(item); 3985 } 3986 } 3987 bool isInGroup = m_model->m_groups->isInGroup(item); 3988 PlaylistState::ClipState state = PlaylistState::ClipState::Unknown; 3989 ClipType::ProducerType type = ClipType::Unknown; 3990 if (clip) { 3991 state = clip->clipState(); 3992 type = clip->clipType(); 3993 } 3994 for (QAction *act : qAsConst(clipActions)) { 3995 bool enableAction = true; 3996 const QChar actionData = act->data().toChar(); 3997 if (actionData == QLatin1Char('G')) { 3998 enableAction = isInSelection(item) && selectionSize > 1; 3999 } else if (actionData == QLatin1Char('U')) { 4000 enableAction = isInGroup; 4001 } else if (actionData == QLatin1Char('A')) { 4002 if (isInGroup && m_model->m_groups->getType(m_model->m_groups->getRootId(item)) == GroupType::AVSplit) { 4003 enableAction = true; 4004 } else { 4005 enableAction = state == PlaylistState::AudioOnly; 4006 } 4007 } else if (actionData == QLatin1Char('V')) { 4008 enableAction = state == PlaylistState::VideoOnly; 4009 } else if (actionData == QLatin1Char('D')) { 4010 enableAction = state == PlaylistState::Disabled; 4011 } else if (actionData == QLatin1Char('E')) { 4012 enableAction = state != PlaylistState::Disabled && state != PlaylistState::Unknown; 4013 } else if (actionData == QLatin1Char('X') || actionData == QLatin1Char('S')) { 4014 enableAction = clip && clip->canBeVideo() && clip->canBeAudio(); 4015 if (enableAction && actionData == QLatin1Char('S')) { 4016 if (isInGroup) { 4017 // Check if all clips in the group have have same state (audio or video) 4018 int targetRoot = m_model->m_groups->getRootId(item); 4019 if (m_model->isGroup(targetRoot)) { 4020 std::unordered_set<int> sub = m_model->m_groups->getLeaves(targetRoot); 4021 for (int current_id : sub) { 4022 if (current_id == item) { 4023 continue; 4024 } 4025 if (m_model->isClip(current_id) && m_model->getClipPtr(current_id)->clipState() != state) { 4026 // Group with audio and video clips, disable split action 4027 enableAction = false; 4028 break; 4029 } 4030 } 4031 } 4032 } 4033 act->setText(state == PlaylistState::AudioOnly ? i18n("Restore video") : i18n("Restore audio")); 4034 } 4035 } else if (actionData == QLatin1Char('W')) { 4036 enableAction = clip != nullptr; 4037 if (enableAction) { 4038 act->setText(clip->clipState() == PlaylistState::Disabled ? i18n("Enable clip") : i18n("Disable clip")); 4039 } 4040 } else if (actionData == QLatin1Char('C') && clip == nullptr) { 4041 enableAction = false; 4042 } else if (actionData == QLatin1Char('P')) { 4043 // Position actions should stay enabled in clip monitor 4044 enableAction = true; 4045 } else if (actionData == QLatin1Char('R')) { 4046 // Time remap action 4047 enableAction = clip != nullptr && type != ClipType::Color && type != ClipType::Image && qFuzzyCompare(1., m_model->m_allClips[item]->getSpeed()); 4048 if (enableAction) { 4049 act->setChecked(clip->hasTimeRemap()); 4050 } 4051 } else if (actionData == QLatin1Char('Q')) { 4052 // Speed change action 4053 enableAction = clip != nullptr && (clip->getSpeed() != 1. || (type != ClipType::Timeline && type != ClipType::Playlist && type != ClipType::Color && 4054 type != ClipType::Image && !clip->hasTimeRemap())); 4055 } 4056 act->setEnabled(enableAction); 4057 } 4058 Q_EMIT timelineClipSelected(clip != nullptr); 4059 } 4060 4061 const QString TimelineController::getAssetName(const QString &assetId, bool isTransition) 4062 { 4063 return isTransition ? TransitionsRepository::get()->getName(assetId) : EffectsRepository::get()->getName(assetId); 4064 } 4065 4066 void TimelineController::grabCurrent() 4067 { 4068 if (trimmingActive()) { 4069 return; 4070 } 4071 std::unordered_set<int> ids = m_model->getCurrentSelection(); 4072 std::unordered_set<int> items_list; 4073 int mainId = -1; 4074 for (int i : ids) { 4075 if (m_model->isGroup(i)) { 4076 std::unordered_set<int> children = m_model->m_groups->getLeaves(i); 4077 items_list.insert(children.begin(), children.end()); 4078 } else { 4079 items_list.insert(i); 4080 } 4081 } 4082 for (int id : items_list) { 4083 if (mainId == -1 && m_model->getItemTrackId(id) == m_activeTrack) { 4084 mainId = id; 4085 continue; 4086 } 4087 if (m_model->isClip(id)) { 4088 std::shared_ptr<ClipModel> clip = m_model->getClipPtr(id); 4089 clip->setGrab(!clip->isGrabbed()); 4090 } else if (m_model->isComposition(id)) { 4091 std::shared_ptr<CompositionModel> clip = m_model->getCompositionPtr(id); 4092 clip->setGrab(!clip->isGrabbed()); 4093 } else if (m_model->isSubTitle(id)) { 4094 m_model->getSubtitleModel()->switchGrab(id); 4095 } 4096 } 4097 if (mainId > -1) { 4098 if (m_model->isClip(mainId)) { 4099 std::shared_ptr<ClipModel> clip = m_model->getClipPtr(mainId); 4100 clip->setGrab(!clip->isGrabbed()); 4101 } else if (m_model->isComposition(mainId)) { 4102 std::shared_ptr<CompositionModel> clip = m_model->getCompositionPtr(mainId); 4103 clip->setGrab(!clip->isGrabbed()); 4104 } else if (m_model->isSubTitle(mainId)) { 4105 m_model->getSubtitleModel()->switchGrab(mainId); 4106 } 4107 } 4108 } 4109 4110 int TimelineController::getItemMovingTrack(int itemId) const 4111 { 4112 if (m_model->isClip(itemId)) { 4113 int trackId = -1; 4114 if (m_model->m_editMode != TimelineMode::NormalEdit) { 4115 trackId = m_model->m_allClips[itemId]->getFakeTrackId(); 4116 } 4117 return trackId < 0 ? m_model->m_allClips[itemId]->getCurrentTrackId() : trackId; 4118 } 4119 return m_model->m_allCompositions[itemId]->getCurrentTrackId(); 4120 } 4121 4122 bool TimelineController::endFakeMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline) 4123 { 4124 Q_ASSERT(m_model->m_allClips.count(clipId) > 0); 4125 int trackId = m_model->m_allClips[clipId]->getFakeTrackId(); 4126 if (m_model->getClipPosition(clipId) == position && m_model->getClipTrackId(clipId) == trackId) { 4127 qDebug() << "* * ** END FAKE; NO MOVE RQSTED"; 4128 // Ensure clip height binds again with parent track height 4129 if (m_model->m_groups->isInGroup(clipId)) { 4130 int groupId = m_model->m_groups->getRootId(clipId); 4131 auto all_items = m_model->m_groups->getLeaves(groupId); 4132 for (int item : all_items) { 4133 if (m_model->isClip(item)) { 4134 m_model->m_allClips[item]->setFakeTrackId(-1); 4135 QModelIndex modelIndex = m_model->makeClipIndexFromID(item); 4136 if (modelIndex.isValid()) { 4137 m_model->notifyChange(modelIndex, modelIndex, TimelineModel::FakeTrackIdRole); 4138 } 4139 } 4140 } 4141 } else { 4142 m_model->m_allClips[clipId]->setFakeTrackId(-1); 4143 QModelIndex modelIndex = m_model->makeClipIndexFromID(clipId); 4144 if (modelIndex.isValid()) { 4145 m_model->notifyChange(modelIndex, modelIndex, TimelineModel::FakeTrackIdRole); 4146 } 4147 } 4148 return true; 4149 } 4150 if (m_model->m_groups->isInGroup(clipId)) { 4151 // element is in a group. 4152 int groupId = m_model->m_groups->getRootId(clipId); 4153 int current_trackId = m_model->getClipTrackId(clipId); 4154 int track_pos1 = m_model->getTrackPosition(trackId); 4155 int track_pos2 = m_model->getTrackPosition(current_trackId); 4156 int delta_track = track_pos1 - track_pos2; 4157 int delta_pos = position - m_model->m_allClips[clipId]->getPosition(); 4158 return endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); 4159 } 4160 qDebug() << "//////\n//////\nENDING FAKE MOVE: " << trackId << ", POS: " << position; 4161 std::function<bool(void)> undo = []() { return true; }; 4162 std::function<bool(void)> redo = []() { return true; }; 4163 int startPos = m_model->getClipPosition(clipId); 4164 int duration = m_model->getClipPlaytime(clipId); 4165 int currentTrack = m_model->m_allClips[clipId]->getCurrentTrackId(); 4166 bool res = true; 4167 if (currentTrack > -1) { 4168 std::pair<MixInfo, MixInfo> mixData = m_model->getTrackById_const(currentTrack)->getMixInfo(clipId); 4169 if (mixData.first.firstClipId > -1) { 4170 m_model->removeMixWithUndo(mixData.first.secondClipId, undo, redo); 4171 } 4172 if (mixData.second.firstClipId > -1) { 4173 m_model->removeMixWithUndo(mixData.second.secondClipId, undo, redo); 4174 } 4175 res = m_model->getTrackById(currentTrack)->requestClipDeletion(clipId, updateView, invalidateTimeline, undo, redo, false, false); 4176 } 4177 if (m_model->m_editMode == TimelineMode::OverwriteEdit) { 4178 res = res && TimelineFunctions::liftZone(m_model, trackId, QPoint(position, position + duration), undo, redo); 4179 } else if (m_model->m_editMode == TimelineMode::InsertEdit) { 4180 // Remove space from previous location 4181 if (currentTrack > -1) { 4182 res = res && TimelineFunctions::removeSpace(m_model, {startPos, startPos + duration}, undo, redo, {currentTrack}, false); 4183 } 4184 int startClipId = m_model->getClipByPosition(trackId, position); 4185 if (startClipId > -1) { 4186 // There is a clip at insert pos 4187 if (m_model->getClipPosition(startClipId) != position) { 4188 // If position is in the middle of the clip, cut it 4189 res = res && TimelineFunctions::requestClipCut(m_model, startClipId, position, undo, redo); 4190 } 4191 } 4192 res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(position, position + duration), undo, redo, {trackId}); 4193 } 4194 res = res && m_model->getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, undo, redo, false, false); 4195 if (res) { 4196 // Terminate fake move 4197 if (m_model->isClip(clipId)) { 4198 m_model->m_allClips[clipId]->setFakeTrackId(-1); 4199 } 4200 if (logUndo) { 4201 pCore->pushUndo(undo, redo, i18n("Move item")); 4202 } 4203 } else { 4204 qDebug() << "//// FAKE FAILED"; 4205 undo(); 4206 } 4207 return res; 4208 } 4209 4210 bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) 4211 { 4212 std::function<bool(void)> undo = []() { return true; }; 4213 std::function<bool(void)> redo = []() { return true; }; 4214 bool res = endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); 4215 if (res && logUndo) { 4216 // Terminate fake move 4217 if (m_model->isClip(clipId)) { 4218 m_model->m_allClips[clipId]->setFakeTrackId(-1); 4219 } 4220 pCore->pushUndo(undo, redo, i18n("Move group")); 4221 } 4222 return res; 4223 } 4224 4225 bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo) 4226 { 4227 Q_ASSERT(m_model->m_allGroups.count(groupId) > 0); 4228 bool ok = true; 4229 auto all_items = m_model->m_groups->getLeaves(groupId); 4230 Q_ASSERT(all_items.size() > 1); 4231 Fun local_undo = []() { return true; }; 4232 Fun local_redo = []() { return true; }; 4233 4234 // Sort clips. We need to delete from right to left to avoid confusing the view 4235 std::vector<int> sorted_clips{std::make_move_iterator(std::begin(all_items)), std::make_move_iterator(std::end(all_items))}; 4236 std::sort(sorted_clips.begin(), sorted_clips.end(), [this](const int &clipId1, const int &clipId2) { 4237 int p1 = m_model->isClip(clipId1) ? m_model->m_allClips[clipId1]->getPosition() : m_model->m_allCompositions[clipId1]->getPosition(); 4238 int p2 = m_model->isClip(clipId2) ? m_model->m_allClips[clipId2]->getPosition() : m_model->m_allCompositions[clipId2]->getPosition(); 4239 return p2 < p1; 4240 }); 4241 4242 // Moving groups is a two stage process: first we remove the clips from the tracks, and then try to insert them back at their calculated new positions. 4243 // This way, we ensure that no conflict will arise with clips inside the group being moved 4244 // We are moving a group on another track, delete and re-add 4245 4246 // First, remove clips 4247 int audio_delta, video_delta; 4248 audio_delta = video_delta = delta_track; 4249 int master_trackId = m_model->getItemTrackId(clipId); 4250 if (m_model->getTrackById_const(master_trackId)->isAudioTrack()) { 4251 // Master clip is audio, so reverse delta for video clips 4252 video_delta = -delta_track; 4253 } else { 4254 audio_delta = -delta_track; 4255 } 4256 int min = -1; 4257 int max = -1; 4258 std::unordered_map<int, int> old_track_ids, old_position, old_forced_track, new_track_ids; 4259 std::vector<int> affected_trackIds; 4260 std::unordered_map<int, std::pair<QString, QVector<QPair<QString, QVariant>>>> mixToMove; 4261 std::unordered_map<int, MixInfo> mixInfoToMove; 4262 std::unordered_map<int, std::pair<int, int>> mixTracksToMove; 4263 // Remove mixes not part of the group move 4264 for (int item : sorted_clips) { 4265 if (m_model->isClip(item)) { 4266 int tid = m_model->getItemTrackId(item); 4267 affected_trackIds.emplace_back(tid); 4268 std::pair<MixInfo, MixInfo> mixData = m_model->getTrackById_const(tid)->getMixInfo(item); 4269 if (mixData.first.firstClipId > -1) { 4270 if (std::find(sorted_clips.begin(), sorted_clips.end(), mixData.first.firstClipId) == sorted_clips.end()) { 4271 // Clip has startMix 4272 m_model->removeMixWithUndo(mixData.first.secondClipId, undo, redo); 4273 } else { 4274 // Get mix properties 4275 std::pair<QString, QVector<QPair<QString, QVariant>>> mixParams = 4276 m_model->getTrackById_const(tid)->getMixParams(mixData.first.secondClipId); 4277 mixToMove[item] = mixParams; 4278 mixTracksToMove[item] = m_model->getTrackById_const(tid)->getMixTracks(mixData.first.secondClipId); 4279 mixInfoToMove[item] = mixData.first; 4280 } 4281 } 4282 if (mixData.second.firstClipId > -1 && std::find(sorted_clips.begin(), sorted_clips.end(), mixData.second.secondClipId) == sorted_clips.end()) { 4283 m_model->removeMixWithUndo(mixData.second.secondClipId, undo, redo); 4284 } 4285 } 4286 } 4287 for (int item : sorted_clips) { 4288 int old_trackId = m_model->getItemTrackId(item); 4289 old_track_ids[item] = old_trackId; 4290 if (old_trackId != -1) { 4291 if (m_model->isClip(item)) { 4292 int current_track_position = m_model->getTrackPosition(old_trackId); 4293 int d = m_model->getTrackById_const(old_trackId)->isAudioTrack() ? audio_delta : video_delta; 4294 int target_track_position = current_track_position + d; 4295 auto it = m_model->m_allTracks.cbegin(); 4296 std::advance(it, target_track_position); 4297 int target_track = (*it)->getId(); 4298 new_track_ids[item] = target_track; 4299 affected_trackIds.emplace_back(target_track); 4300 old_position[item] = m_model->m_allClips[item]->getPosition(); 4301 int duration = m_model->m_allClips[item]->getPlaytime(); 4302 min = min < 0 ? old_position[item] + delta_pos : qMin(min, old_position[item] + delta_pos); 4303 max = max < 0 ? old_position[item] + delta_pos + duration : qMax(max, old_position[item] + delta_pos + duration); 4304 ok = ok && m_model->getTrackById(old_trackId)->requestClipDeletion(item, true, finalMove, undo, redo, false, false); 4305 if (m_model->m_editMode == TimelineMode::InsertEdit) { 4306 // Lift space left by removed clip 4307 ok = ok && TimelineFunctions::removeSpace(m_model, {old_position[item], old_position[item] + duration}, undo, redo, {old_trackId}, false); 4308 } 4309 } else { 4310 // ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, local_undo, local_redo); 4311 old_position[item] = m_model->m_allCompositions[item]->getPosition(); 4312 old_forced_track[item] = m_model->m_allCompositions[item]->getForcedTrack(); 4313 } 4314 if (!ok) { 4315 bool undone = undo(); 4316 Q_ASSERT(undone); 4317 return false; 4318 } 4319 } 4320 } 4321 bool res = true; 4322 if (m_model->m_editMode == TimelineMode::OverwriteEdit) { 4323 for (int item : sorted_clips) { 4324 if (m_model->isClip(item) && new_track_ids.count(item) > 0) { 4325 int target_track = new_track_ids[item]; 4326 int target_position = old_position[item] + delta_pos; 4327 int duration = m_model->m_allClips[item]->getPlaytime(); 4328 res = res && TimelineFunctions::liftZone(m_model, target_track, QPoint(target_position, target_position + duration), undo, redo); 4329 } 4330 } 4331 } else if (m_model->m_editMode == TimelineMode::InsertEdit) { 4332 QVector<int> processedTracks; 4333 for (int item : sorted_clips) { 4334 int target_track = new_track_ids[item]; 4335 if (processedTracks.contains(target_track)) { 4336 // already processed 4337 continue; 4338 } 4339 processedTracks << target_track; 4340 int target_position = min; 4341 int startClipId = m_model->getClipByPosition(target_track, target_position); 4342 if (startClipId > -1) { 4343 // There is a clip, cut 4344 res = res && TimelineFunctions::requestClipCut(m_model, startClipId, target_position, undo, redo); 4345 } 4346 } 4347 res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(min, max), undo, redo, processedTracks); 4348 } 4349 for (int item : sorted_clips) { 4350 if (m_model->isClip(item)) { 4351 int target_track = new_track_ids[item]; 4352 int target_position = old_position[item] + delta_pos; 4353 ok = ok && m_model->requestClipMove(item, target_track, target_position, true, updateView, finalMove, finalMove, undo, redo); 4354 } else { 4355 // ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, local_undo, local_redo); 4356 } 4357 if (!ok) { 4358 bool undone = undo(); 4359 Q_ASSERT(undone); 4360 return false; 4361 } 4362 } 4363 4364 if (delta_track == 0) { 4365 Fun sync_mix = [this, affected_trackIds, finalMove]() { 4366 for (int t : affected_trackIds) { 4367 m_model->getTrackById_const(t)->syncronizeMixes(finalMove); 4368 } 4369 return true; 4370 }; 4371 sync_mix(); 4372 PUSH_LAMBDA(sync_mix, redo); 4373 return true; 4374 } 4375 for (int item : sorted_clips) { 4376 if (mixToMove.find(item) == mixToMove.end()) { 4377 continue; 4378 } 4379 int trackId = new_track_ids[item]; 4380 int previous_track = old_track_ids[item]; 4381 MixInfo mixData = mixInfoToMove[item]; 4382 std::pair<int, int> mixTracks = mixTracksToMove[item]; 4383 std::pair<QString, QVector<QPair<QString, QVariant>>> mixParams = mixToMove[item]; 4384 Fun simple_move_mix = [this, previous_track, trackId, finalMove, mixData, mixTracks, mixParams]() { 4385 // Insert mix on new track 4386 bool result = m_model->getTrackById_const(trackId)->createMix(mixData, mixParams, mixTracks, finalMove); 4387 // Remove mix on old track 4388 m_model->getTrackById_const(previous_track)->removeMix(mixData); 4389 return result; 4390 }; 4391 Fun simple_restore_mix = [this, previous_track, trackId, finalMove, mixData, mixTracks, mixParams]() { 4392 bool result = m_model->getTrackById_const(previous_track)->createMix(mixData, mixParams, mixTracks, finalMove); 4393 // Remove mix on old track 4394 m_model->getTrackById_const(trackId)->removeMix(mixData); 4395 return result; 4396 }; 4397 simple_move_mix(); 4398 if (finalMove) { 4399 PUSH_LAMBDA(simple_restore_mix, undo); 4400 PUSH_LAMBDA(simple_move_mix, redo); 4401 } 4402 } 4403 return true; 4404 } 4405 4406 const std::unordered_map<QString, std::vector<int>> TimelineController::getThumbKeys() 4407 { 4408 std::unordered_map<QString, std::vector<int>> framesToStore; 4409 for (const auto &clp : m_model->m_allClips) { 4410 if (clp.second->isAudioOnly()) { 4411 // Don't process audio clips 4412 continue; 4413 } 4414 const QString binId = getClipBinId(clp.first); 4415 framesToStore[binId].push_back(clp.second->getIn()); 4416 framesToStore[binId].push_back(clp.second->getOut()); 4417 } 4418 return framesToStore; 4419 } 4420 4421 bool TimelineController::isInSelection(int itemId) 4422 { 4423 return m_model->getCurrentSelection().count(itemId) > 0; 4424 } 4425 4426 bool TimelineController::exists(int itemId) 4427 { 4428 return m_model->isClip(itemId) || m_model->isComposition(itemId); 4429 } 4430 4431 void TimelineController::slotMultitrackView(bool enable, bool refresh) 4432 { 4433 QStringList trackNames = TimelineFunctions::enableMultitrackView(m_model, enable, refresh); 4434 if (!refresh) { 4435 // This is just a temporary state (disable multitrack view for playlist save, don't change scene 4436 return; 4437 } 4438 pCore->monitorManager()->projectMonitor()->slotShowEffectScene(enable ? MonitorSplitTrack : MonitorSceneNone, false, QVariant(trackNames)); 4439 QObject::disconnect(m_connection); 4440 if (enable) { 4441 connect(m_model.get(), &TimelineItemModel::trackVisibilityChanged, this, &TimelineController::updateMultiTrack, Qt::UniqueConnection); 4442 m_connection = connect(this, &TimelineController::activeTrackChanged, [this]() { 4443 int ix = 0; 4444 auto it = m_model->m_allTracks.cbegin(); 4445 while (it != m_model->m_allTracks.cend()) { 4446 int target_track = (*it)->getId(); 4447 ++it; 4448 if (target_track == m_activeTrack) { 4449 break; 4450 } 4451 if (m_model->getTrackById_const(target_track)->isAudioTrack() || m_model->getTrackById_const(target_track)->isHidden()) { 4452 continue; 4453 } 4454 ++ix; 4455 } 4456 pCore->monitorManager()->projectMonitor()->updateMultiTrackView(ix); 4457 }); 4458 int ix = 0; 4459 auto it = m_model->m_allTracks.cbegin(); 4460 while (it != m_model->m_allTracks.cend()) { 4461 int target_track = (*it)->getId(); 4462 ++it; 4463 if (target_track == m_activeTrack) { 4464 break; 4465 } 4466 if (m_model->getTrackById_const(target_track)->isAudioTrack() || m_model->getTrackById_const(target_track)->isHidden()) { 4467 continue; 4468 } 4469 ++ix; 4470 } 4471 pCore->monitorManager()->projectMonitor()->updateMultiTrackView(ix); 4472 } else { 4473 disconnect(m_model.get(), &TimelineItemModel::trackVisibilityChanged, this, &TimelineController::updateMultiTrack); 4474 } 4475 } 4476 4477 void TimelineController::updateMultiTrack() 4478 { 4479 QStringList trackNames = TimelineFunctions::enableMultitrackView(m_model, true, true); 4480 pCore->monitorManager()->projectMonitor()->slotShowEffectScene(MonitorSplitTrack, false, QVariant(trackNames)); 4481 } 4482 4483 void TimelineController::activateTrackAndSelect(int trackPosition, bool notesMode) 4484 { 4485 int tid = -1; 4486 int ix = 0; 4487 if (notesMode && trackPosition == -2) { 4488 m_activeTrack = -2; 4489 Q_EMIT activeTrackChanged(); 4490 return; 4491 } 4492 auto it = m_model->m_allTracks.cbegin(); 4493 while (it != m_model->m_allTracks.cend()) { 4494 tid = (*it)->getId(); 4495 ++it; 4496 if (!notesMode && (m_model->getTrackById_const(tid)->isAudioTrack() || m_model->getTrackById_const(tid)->isHidden())) { 4497 continue; 4498 } 4499 if (trackPosition == ix) { 4500 break; 4501 } 4502 ++ix; 4503 } 4504 if (tid > -1) { 4505 m_activeTrack = tid; 4506 Q_EMIT activeTrackChanged(); 4507 if (!notesMode && pCore->window()->getCurrentTimeline()->activeTool() != ToolType::MulticamTool) { 4508 selectCurrentItem(KdenliveObjectType::TimelineClip, true); 4509 } 4510 } 4511 } 4512 4513 void TimelineController::saveTimelineSelection(const QDir &targetDir) 4514 { 4515 std::unordered_set<int> ids = m_model->getCurrentSelection(); 4516 std::unordered_set<int> items_list; 4517 for (int i : ids) { 4518 if (m_model->isGroup(i)) { 4519 std::unordered_set<int> children = m_model->m_groups->getLeaves(i); 4520 items_list.insert(children.begin(), children.end()); 4521 } else { 4522 items_list.insert(i); 4523 } 4524 } 4525 int startPos = 0; 4526 int endPos = 0; 4527 for (int id : items_list) { 4528 int start = m_model->getItemPosition(id); 4529 int end = start + m_model->getItemPlaytime(id); 4530 if (startPos == 0 || start < startPos) { 4531 startPos = start; 4532 } 4533 if (end > endPos) { 4534 endPos = end; 4535 } 4536 } 4537 TimelineFunctions::saveTimelineSelection(m_model, m_model->getCurrentSelection(), targetDir, endPos - startPos - 1); 4538 } 4539 4540 void TimelineController::addEffectKeyframe(int cid, int frame, double val) 4541 { 4542 if (m_model->isClip(cid)) { 4543 std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(cid); 4544 destStack->addEffectKeyFrame(frame, val); 4545 } else if (m_model->isComposition(cid)) { 4546 std::shared_ptr<KeyframeModelList> listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); 4547 listModel->addKeyframe(frame, val); 4548 } 4549 } 4550 4551 void TimelineController::removeEffectKeyframe(int cid, int frame) 4552 { 4553 if (m_model->isClip(cid)) { 4554 std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(cid); 4555 destStack->removeKeyFrame(frame); 4556 } else if (m_model->isComposition(cid)) { 4557 std::shared_ptr<KeyframeModelList> listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); 4558 listModel->removeKeyframe(GenTime(frame, pCore->getCurrentFps())); 4559 } 4560 } 4561 4562 void TimelineController::updateEffectKeyframe(int cid, int oldFrame, int newFrame, const QVariant &normalizedValue) 4563 { 4564 if (m_model->isClip(cid)) { 4565 std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(cid); 4566 destStack->updateKeyFrame(oldFrame, newFrame, normalizedValue); 4567 } else if (m_model->isComposition(cid)) { 4568 std::shared_ptr<KeyframeModelList> listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); 4569 listModel->updateKeyframe(GenTime(oldFrame, pCore->getCurrentFps()), GenTime(newFrame, pCore->getCurrentFps()), normalizedValue); 4570 } 4571 } 4572 4573 bool TimelineController::hasKeyframeAt(int cid, int frame) 4574 { 4575 if (m_model->isClip(cid)) { 4576 std::shared_ptr<EffectStackModel> destStack = m_model->getClipEffectStackModel(cid); 4577 return destStack->hasKeyFrame(frame); 4578 } else if (m_model->isComposition(cid)) { 4579 std::shared_ptr<KeyframeModelList> listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); 4580 return listModel->hasKeyframe(frame); 4581 } 4582 return false; 4583 } 4584 4585 QColor TimelineController::videoColor() const 4586 { 4587 KColorScheme scheme(QApplication::palette().currentColorGroup()); 4588 return scheme.foreground(KColorScheme::LinkText).color(); 4589 } 4590 4591 QColor TimelineController::targetColor() const 4592 { 4593 KColorScheme scheme(QApplication::palette().currentColorGroup()); 4594 QColor base = scheme.foreground(KColorScheme::PositiveText).color(); 4595 QColor high = QApplication::palette().highlightedText().color(); 4596 double factor = 0.3; 4597 QColor res = QColor(qBound(0, base.red() + int(factor * (high.red() - 128)), 255), qBound(0, base.green() + int(factor * (high.green() - 128)), 255), 4598 qBound(0, base.blue() + int(factor * (high.blue() - 128)), 255), 255); 4599 return res; 4600 } 4601 4602 QColor TimelineController::targetTextColor() const 4603 { 4604 KColorScheme scheme(QApplication::palette().currentColorGroup()); 4605 return scheme.background(KColorScheme::PositiveBackground).color(); 4606 } 4607 4608 QColor TimelineController::audioColor() const 4609 { 4610 KColorScheme scheme(QApplication::palette().currentColorGroup()); 4611 return scheme.foreground(KColorScheme::PositiveText).color().darker(200); 4612 } 4613 4614 QColor TimelineController::titleColor() const 4615 { 4616 KColorScheme scheme(QApplication::palette().currentColorGroup()); 4617 QColor base = scheme.foreground(KColorScheme::LinkText).color(); 4618 QColor high = scheme.foreground(KColorScheme::NegativeText).color(); 4619 QColor title = QColor(qBound(0, base.red() + int(high.red() - 128), 255), qBound(0, base.green() + int(high.green() - 128), 255), 4620 qBound(0, base.blue() + int(high.blue() - 128), 255), 255); 4621 return title; 4622 } 4623 4624 QColor TimelineController::imageColor() const 4625 { 4626 KColorScheme scheme(QApplication::palette().currentColorGroup()); 4627 return scheme.foreground(KColorScheme::NeutralText).color(); 4628 } 4629 4630 QColor TimelineController::thumbColor1() const 4631 { 4632 return KdenliveSettings::thumbColor1(); 4633 } 4634 4635 QColor TimelineController::thumbColor2() const 4636 { 4637 return KdenliveSettings::thumbColor2(); 4638 } 4639 4640 QColor TimelineController::slideshowColor() const 4641 { 4642 KColorScheme scheme(QApplication::palette().currentColorGroup()); 4643 QColor base = scheme.foreground(KColorScheme::LinkText).color(); 4644 QColor high = scheme.foreground(KColorScheme::NeutralText).color(); 4645 QColor slide = QColor(qBound(0, base.red() + int(high.red() - 128), 255), qBound(0, base.green() + int(high.green() - 128), 255), 4646 qBound(0, base.blue() + int(high.blue() - 128), 255), 255); 4647 return slide; 4648 } 4649 4650 QColor TimelineController::lockedColor() const 4651 { 4652 KColorScheme scheme(QApplication::palette().currentColorGroup()); 4653 return scheme.foreground(KColorScheme::NegativeText).color(); 4654 } 4655 4656 QColor TimelineController::groupColor() const 4657 { 4658 KColorScheme scheme(QApplication::palette().currentColorGroup()); 4659 return scheme.foreground(KColorScheme::ActiveText).color(); 4660 } 4661 4662 QColor TimelineController::selectionColor() const 4663 { 4664 KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::Complementary); 4665 if (m_model && m_model->singleSelectionMode()) { 4666 return Qt::red; 4667 } 4668 return scheme.foreground(KColorScheme::NeutralText).color(); 4669 } 4670 4671 void TimelineController::switchRecording(int trackId, bool record) 4672 { 4673 if (trackId == -1) { 4674 trackId = pCore->mixer()->recordTrack(); 4675 } 4676 if (record) { 4677 if (pCore->isMediaCapturing()) { 4678 // Already recording, abort 4679 return; 4680 } 4681 qDebug() << "start recording" << trackId; 4682 if (!m_model->isTrack(trackId)) { 4683 qDebug() << "ERROR: Starting to capture on invalid track " << trackId; 4684 } 4685 if (m_model->getTrackById_const(trackId)->isLocked()) { 4686 pCore->displayMessage(i18n("Impossible to capture on a locked track"), ErrorMessage, 500); 4687 return; 4688 } 4689 m_recordStart.first = pCore->getMonitorPosition(); 4690 m_recordTrack = trackId; 4691 int maximumSpace = m_model->getTrackById_const(trackId)->getBlankEnd(m_recordStart.first); 4692 if (maximumSpace == INT_MAX) { 4693 m_recordStart.second = 0; 4694 } else { 4695 m_recordStart.second = maximumSpace - m_recordStart.first; 4696 if (m_recordStart.second < 8) { 4697 pCore->displayMessage(i18n("Impossible to capture here: the capture could override clips. Please remove clips after the current position or " 4698 "choose a different track"), 4699 ErrorMessage, 500); 4700 return; 4701 } 4702 } 4703 pCore->monitorManager()->slotSwitchMonitors(false); 4704 pCore->startMediaCapture(trackId, true, false); 4705 if (KdenliveSettings::disablereccountdown()) { 4706 pCore->startRecording(); 4707 } else { 4708 pCore->getMonitor(Kdenlive::ProjectMonitor)->startCountDown(); 4709 } 4710 4711 } else { 4712 pCore->getMonitor(Kdenlive::ProjectMonitor)->stopCountDown(); 4713 pCore->stopMediaCapture(trackId, true, false); 4714 Q_EMIT stopAudioRecord(); 4715 pCore->monitorManager()->slotPause(); 4716 } 4717 } 4718 4719 void TimelineController::urlDropped(QStringList droppedFile, int frame, int tid) 4720 { 4721 if (droppedFile.isEmpty()) { 4722 // Empty url passed, abort 4723 return; 4724 } 4725 m_recordTrack = tid; 4726 m_recordStart = {frame, -1}; 4727 qDebug() << "=== GOT DROPPED FILED: " << droppedFile << "\n======"; 4728 if (droppedFile.first().endsWith(QLatin1String(".ass")) || droppedFile.first().endsWith(QLatin1String(".srt"))) { 4729 // Subtitle dropped, import 4730 pCore->window()->showSubtitleTrack(); 4731 importSubtitle(QUrl(droppedFile.first()).toLocalFile()); 4732 } else { 4733 finishRecording(QUrl(droppedFile.first()).toLocalFile()); 4734 } 4735 } 4736 4737 void TimelineController::finishRecording(const QString &recordedFile) 4738 { 4739 if (recordedFile.isEmpty()) { 4740 return; 4741 } 4742 4743 Fun undo = []() { return true; }; 4744 Fun redo = []() { return true; }; 4745 std::function<void(const QString &)> callBack = [this](const QString &binId) { 4746 int id = -1; 4747 if (m_recordTrack == -1) { 4748 return; 4749 } 4750 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(binId); 4751 if (!clip) { 4752 return; 4753 } 4754 pCore->activeBin()->selectClipById(binId); 4755 qDebug() << "callback " << binId << " " << m_recordTrack << ", MAXIMUM SPACE: " << m_recordStart.second; 4756 if (m_recordStart.second > 0) { 4757 // Limited space on track 4758 m_recordStart.second = qMin(int(clip->frameDuration() - 1), m_recordStart.second); 4759 QString binClipId = QString("%1/%2/%3").arg(binId).arg(0).arg(m_recordStart.second); 4760 m_model->requestClipInsertion(binClipId, m_recordTrack, m_recordStart.first, id, true, true, false); 4761 m_recordStart.second++; 4762 } else { 4763 m_recordStart.second = clip->frameDuration(); 4764 m_model->requestClipInsertion(binId, m_recordTrack, m_recordStart.first, id, true, true, false); 4765 } 4766 setPosition(m_recordStart.first + m_recordStart.second); 4767 }; 4768 std::shared_ptr<ProjectItemModel> itemModel = pCore->projectItemModel(); 4769 std::shared_ptr<ProjectFolder> targetFolder = itemModel->getRootFolder(); 4770 if (itemModel->defaultAudioCaptureFolder() > -1) { 4771 const QString audioCaptureFolder = QString::number(itemModel->defaultAudioCaptureFolder()); 4772 std::shared_ptr<ProjectFolder> folderItem = itemModel->getFolderByBinId(audioCaptureFolder); 4773 if (folderItem) { 4774 targetFolder = folderItem; 4775 } 4776 } 4777 4778 QString binId = 4779 ClipCreator::createClipFromFile(recordedFile, targetFolder->clipId(), pCore->projectItemModel(), undo, redo, callBack); 4780 pCore->window()->raiseBin(); 4781 if (binId != QStringLiteral("-1")) { 4782 pCore->pushUndo(undo, redo, i18n("Record audio")); 4783 } 4784 } 4785 4786 void TimelineController::updateVideoTarget() 4787 { 4788 if (videoTarget() > -1) { 4789 m_lastVideoTarget = videoTarget(); 4790 m_videoTargetActive = true; 4791 Q_EMIT lastVideoTargetChanged(); 4792 } else { 4793 m_videoTargetActive = false; 4794 } 4795 } 4796 4797 void TimelineController::updateAudioTarget() 4798 { 4799 if (!audioTarget().isEmpty()) { 4800 m_lastAudioTarget = m_model->m_audioTarget; 4801 m_audioTargetActive = true; 4802 Q_EMIT lastAudioTargetChanged(); 4803 } else { 4804 m_audioTargetActive = false; 4805 } 4806 } 4807 4808 bool TimelineController::hasActiveTracks() const 4809 { 4810 auto it = m_model->m_allTracks.cbegin(); 4811 while (it != m_model->m_allTracks.cend()) { 4812 int target_track = (*it)->getId(); 4813 if (m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { 4814 return true; 4815 } 4816 ++it; 4817 } 4818 return false; 4819 } 4820 4821 void TimelineController::showMasterEffects() 4822 { 4823 Q_EMIT showItemEffectStack(i18n("Master effects"), m_model->getMasterEffectStackModel(), pCore->getCurrentFrameSize(), false); 4824 } 4825 4826 bool TimelineController::refreshIfVisible(int cid) 4827 { 4828 auto it = m_model->m_allTracks.cbegin(); 4829 while (it != m_model->m_allTracks.cend()) { 4830 int target_track = (*it)->getId(); 4831 if (m_model->getTrackById_const(target_track)->isAudioTrack() || m_model->getTrackById_const(target_track)->isHidden()) { 4832 ++it; 4833 continue; 4834 } 4835 int child = m_model->getClipByPosition(target_track, pCore->getMonitorPosition()); 4836 if (child > 0) { 4837 if (m_model->m_allClips[child]->binId().toInt() == cid) { 4838 return true; 4839 } 4840 } 4841 ++it; 4842 } 4843 return false; 4844 } 4845 4846 void TimelineController::collapseActiveTrack() 4847 { 4848 if (m_activeTrack == -1) { 4849 return; 4850 } 4851 if (m_model->isSubtitleTrack(m_activeTrack)) { 4852 // Subtitle track 4853 QMetaObject::invokeMethod(m_root, "switchSubtitleTrack", Qt::QueuedConnection); 4854 return; 4855 } 4856 int collapsed = m_model->getTrackProperty(m_activeTrack, QStringLiteral("kdenlive:collapsed")).toInt(); 4857 // Default unit for timeline.qml objects size 4858 int baseUnit = qMax(28, qRound(QFontInfo(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)).pixelSize() * 1.8)); 4859 m_model->setTrackProperty(m_activeTrack, QStringLiteral("kdenlive:collapsed"), collapsed > 0 ? QStringLiteral("0") : QString::number(baseUnit)); 4860 } 4861 4862 void TimelineController::setActiveTrackProperty(const QString &name, const QString &value) 4863 { 4864 if (m_activeTrack > -1) { 4865 m_model->setTrackProperty(m_activeTrack, name, value); 4866 } 4867 } 4868 4869 bool TimelineController::isActiveTrackAudio() const 4870 { 4871 if (m_activeTrack > -1) { 4872 if (m_model->getTrackById_const(m_activeTrack)->isAudioTrack()) { 4873 return true; 4874 } 4875 } 4876 return false; 4877 } 4878 4879 const QVariant TimelineController::getActiveTrackProperty(const QString &name) const 4880 { 4881 if (m_activeTrack > -1) { 4882 return m_model->getTrackProperty(m_activeTrack, name); 4883 } 4884 return QVariant(); 4885 } 4886 4887 void TimelineController::expandActiveClip() 4888 { 4889 std::unordered_set<int> ids = m_model->getCurrentSelection(); 4890 std::unordered_set<int> items_list; 4891 for (int i : ids) { 4892 if (m_model->isGroup(i)) { 4893 std::unordered_set<int> children = m_model->m_groups->getLeaves(i); 4894 items_list.insert(children.begin(), children.end()); 4895 } else { 4896 items_list.insert(i); 4897 } 4898 } 4899 m_model->requestClearSelection(); 4900 bool result = true; 4901 for (int id : items_list) { 4902 if (result && m_model->isClip(id)) { 4903 std::shared_ptr<ClipModel> clip = m_model->getClipPtr(id); 4904 if (clip->clipType() == ClipType::Playlist || clip->clipType() == ClipType::Timeline) { 4905 std::function<bool(void)> undo = []() { return true; }; 4906 std::function<bool(void)> redo = []() { return true; }; 4907 if (m_model->m_groups->isInGroup(id)) { 4908 int targetRoot = m_model->m_groups->getRootId(id); 4909 if (m_model->isGroup(targetRoot)) { 4910 m_model->requestClipUngroup(targetRoot, undo, redo); 4911 } 4912 } 4913 int pos = clip->getPosition(); 4914 int inPos = m_model->getClipIn(id); 4915 int duration = m_model->getClipPlaytime(id); 4916 QDomDocument doc = TimelineFunctions::extractClip(m_model, id, getClipBinId(id)); 4917 m_model->requestClipDeletion(id, undo, redo); 4918 result = TimelineFunctions::pasteClips(m_model, doc.toString(), m_activeTrack, pos, undo, redo, inPos, duration); 4919 if (result) { 4920 pCore->pushUndo(undo, redo, i18n("Expand clip")); 4921 } else { 4922 undo(); 4923 pCore->displayMessage(i18n("Could not expand clip"), ErrorMessage, 500); 4924 } 4925 } 4926 } 4927 } 4928 } 4929 4930 QMap<int, QString> TimelineController::getCurrentTargets(int trackId, int &activeTargetStream) 4931 { 4932 if (m_model->m_binAudioTargets.size() < 2) { 4933 activeTargetStream = -1; 4934 return QMap<int, QString>(); 4935 } 4936 if (m_model->m_audioTarget.contains(trackId)) { 4937 activeTargetStream = m_model->m_audioTarget.value(trackId); 4938 } else { 4939 activeTargetStream = -1; 4940 } 4941 return m_model->m_binAudioTargets; 4942 } 4943 4944 void TimelineController::addTracks(int videoTracks, int audioTracks) 4945 { 4946 bool result = false; 4947 int total = videoTracks + audioTracks; 4948 Fun undo = []() { return true; }; 4949 Fun redo = []() { return true; }; 4950 while (videoTracks + audioTracks > 0) { 4951 int newTid; 4952 if (audioTracks > 0) { 4953 result = m_model->requestTrackInsertion(0, newTid, QString(), true, undo, redo); 4954 audioTracks--; 4955 } else { 4956 result = m_model->requestTrackInsertion(-1, newTid, QString(), false, undo, redo); 4957 videoTracks--; 4958 } 4959 if (!result) { 4960 break; 4961 } 4962 } 4963 if (result) { 4964 pCore->pushUndo(undo, redo, i18np("Insert Track", "Insert Tracks", total)); 4965 } else { 4966 pCore->displayMessage(i18n("Could not insert track"), ErrorMessage, 500); 4967 undo(); 4968 } 4969 } 4970 4971 void TimelineController::mixClip(int cid, int delta) 4972 { 4973 if (cid == -1) { 4974 std::unordered_set<int> selectedIds = m_model->getCurrentSelection(); 4975 if (selectedIds.empty() && m_model->isTrack(m_activeTrack)) { 4976 // Check if timeline playhead is on a cut 4977 int timelinePos = pCore->getMonitorPosition(); 4978 int nextClip = m_model->getTrackById_const(m_activeTrack)->getClipByPosition(timelinePos); 4979 int prevClip = m_model->getTrackById_const(m_activeTrack)->getClipByPosition(timelinePos - 1); 4980 if (m_model->isClip(prevClip) && m_model->isClip(nextClip) && prevClip != nextClip) { 4981 cid = nextClip; 4982 } 4983 } 4984 } 4985 m_model->mixClip(cid, QStringLiteral("luma"), delta); 4986 } 4987 4988 void TimelineController::temporaryUnplug(const QList<int> &clipIds, bool hide) 4989 { 4990 for (auto &cid : clipIds) { 4991 int tid = m_model->getItemTrackId(cid); 4992 if (tid == -1) { 4993 continue; 4994 } 4995 if (hide) { 4996 m_model->getTrackById_const(tid)->temporaryUnplugClip(cid); 4997 } else { 4998 m_model->getTrackById_const(tid)->temporaryReplugClip(cid); 4999 } 5000 } 5001 } 5002 5003 void TimelineController::importSubtitle(const QString &path) 5004 { 5005 QScopedPointer<ImportSubtitle> d(new ImportSubtitle(path, pCore->window())); 5006 if (d->exec() == QDialog::Accepted && !d->subtitle_url->url().isEmpty()) { 5007 auto subtitleModel = m_model->getSubtitleModel(); 5008 if (d->create_track->isChecked()) { 5009 // Create a new subtitle entry 5010 int ix = subtitleModel->createNewSubtitle(d->track_name->text()); 5011 subtitlesListChanged(); 5012 // Activate the newly created subtitle track 5013 subtitlesMenuActivated(ix - 1); 5014 } 5015 int offset = 0, startFramerate = 30.00, targetFramerate = 30.00; 5016 if (d->cursor_pos->isChecked()) { 5017 offset = pCore->getMonitorPosition(); 5018 } 5019 if (d->transform_framerate_check_box->isChecked()) { 5020 startFramerate = d->caption_original_framerate->value(); 5021 targetFramerate = d->caption_target_framerate->value(); 5022 } 5023 subtitleModel->importSubtitle(d->subtitle_url->url().toLocalFile(), offset, true, startFramerate, targetFramerate, 5024 d->codecs_list->currentText().toUtf8()); 5025 } 5026 Q_EMIT regainFocus(); 5027 } 5028 5029 void TimelineController::exportSubtitle() 5030 { 5031 if (!m_model->hasSubtitleModel()) { 5032 return; 5033 } 5034 QString currentSub = m_model->getSubtitleModel()->getUrl(); 5035 if (currentSub.isEmpty()) { 5036 pCore->displayMessage(i18n("No subtitles in current project"), ErrorMessage); 5037 return; 5038 } 5039 QString url = QFileDialog::getSaveFileName(qApp->activeWindow(), i18n("Export subtitle file"), pCore->currentDoc()->url().toLocalFile(), 5040 i18n("Subtitle File (*.srt)")); 5041 if (url.isEmpty()) { 5042 return; 5043 } 5044 if (!url.endsWith(QStringLiteral(".srt"))) { 5045 url.append(QStringLiteral(".srt")); 5046 } 5047 QFile srcFile(url); 5048 if (srcFile.exists()) { 5049 srcFile.remove(); 5050 } 5051 QFile src(currentSub); 5052 if (!src.copy(srcFile.fileName())) { 5053 KMessageBox::error(qApp->activeWindow(), i18n("Cannot write to file %1", srcFile.fileName())); 5054 } 5055 } 5056 5057 void TimelineController::subtitleSpeechRecognition() 5058 { 5059 SpeechDialog d(m_model, m_zone, m_activeTrack, false, false, qApp->activeWindow()); 5060 d.exec(); 5061 } 5062 5063 bool TimelineController::subtitlesWarning() const 5064 { 5065 return !EffectsRepository::get()->hasInternalEffect("avfilter.subtitles"); 5066 } 5067 5068 void TimelineController::subtitlesWarningDetails() 5069 { 5070 KMessageBox::error(nullptr, i18n("The avfilter.subtitles filter is required, but was not found." 5071 " The subtitles feature will probably not work as expected.")); 5072 Q_EMIT regainFocus(); 5073 } 5074 5075 void TimelineController::switchSubtitleDisable() 5076 { 5077 if (m_model->hasSubtitleModel()) { 5078 auto subtitleModel = m_model->getSubtitleModel(); 5079 bool disabled = subtitleModel->isDisabled(); 5080 Fun local_switch = [this, subtitleModel]() { 5081 subtitleModel->switchDisabled(); 5082 Q_EMIT subtitlesDisabledChanged(); 5083 pCore->refreshProjectMonitorOnce(); 5084 return true; 5085 }; 5086 local_switch(); 5087 pCore->pushUndo(local_switch, local_switch, disabled ? i18n("Show subtitle track") : i18n("Hide subtitle track")); 5088 } 5089 } 5090 5091 bool TimelineController::subtitlesDisabled() const 5092 { 5093 if (m_model->hasSubtitleModel()) { 5094 return m_model->getSubtitleModel()->isDisabled(); 5095 } 5096 return false; 5097 } 5098 5099 void TimelineController::switchSubtitleLock() 5100 { 5101 if (m_model->hasSubtitleModel()) { 5102 auto subtitleModel = m_model->getSubtitleModel(); 5103 bool locked = subtitleModel->isLocked(); 5104 Fun local_switch = [this, subtitleModel]() { 5105 subtitleModel->switchLocked(); 5106 Q_EMIT subtitlesLockedChanged(); 5107 return true; 5108 }; 5109 local_switch(); 5110 pCore->pushUndo(local_switch, local_switch, locked ? i18n("Unlock subtitle track") : i18n("Lock subtitle track")); 5111 } 5112 } 5113 bool TimelineController::subtitlesLocked() const 5114 { 5115 if (m_model->hasSubtitleModel()) { 5116 return m_model->getSubtitleModel()->isLocked(); 5117 } 5118 return false; 5119 } 5120 5121 bool TimelineController::guidesLocked() const 5122 { 5123 return KdenliveSettings::lockedGuides(); 5124 } 5125 5126 void TimelineController::showToolTip(const QString &info) const 5127 { 5128 pCore->displayMessage(info, TooltipMessage); 5129 } 5130 5131 void TimelineController::showKeyBinding(const QString &info) const 5132 { 5133 pCore->window()->showKeyBinding(info); 5134 } 5135 5136 void TimelineController::showTimelineToolInfo(bool show) const 5137 { 5138 if (show) { 5139 pCore->window()->showToolMessage(); 5140 } else { 5141 pCore->window()->setWidgetKeyBinding(); 5142 } 5143 } 5144 5145 void TimelineController::showRulerEffectZone(QPair<int, int> inOut, bool checked) 5146 { 5147 m_effectZone = checked ? QPoint(inOut.first, inOut.second) : QPoint(); 5148 Q_EMIT effectZoneChanged(); 5149 } 5150 5151 void TimelineController::updateMasterZones(const QVariantList &zones) 5152 { 5153 m_masterEffectZones = zones; 5154 Q_EMIT masterZonesChanged(); 5155 } 5156 5157 int TimelineController::clipMaxDuration(int cid) 5158 { 5159 if (!m_model->isClip(cid)) { 5160 return -1; 5161 } 5162 return m_model->m_allClips[cid]->getMaxDuration(); 5163 } 5164 5165 void TimelineController::resizeMix(int cid, int duration, MixAlignment align, int leftFrames) 5166 { 5167 if (cid > -1) { 5168 m_model->requestResizeMix(cid, duration, align, leftFrames); 5169 } 5170 } 5171 5172 int TimelineController::getMixCutPos(int cid) const 5173 { 5174 return m_model->getMixCutPos(cid); 5175 } 5176 5177 MixAlignment TimelineController::getMixAlign(int cid) const 5178 { 5179 return m_model->getMixAlign(cid); 5180 } 5181 5182 void TimelineController::processMultitrackOperation(int tid, int in) 5183 { 5184 int out = pCore->getMonitorPosition(); 5185 if (out == in) { 5186 // Simply change the reference track, nothing to do here 5187 return; 5188 } 5189 QVector<int> tracks; 5190 auto it = m_model->m_allTracks.cbegin(); 5191 // Lift all tracks except tid 5192 while (it != m_model->m_allTracks.cend()) { 5193 int target_track = (*it)->getId(); 5194 if (target_track != tid && !(*it)->isAudioTrack() && m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { 5195 tracks << target_track; 5196 } 5197 ++it; 5198 } 5199 if (tracks.isEmpty()) { 5200 pCore->displayMessage(i18n("Please activate a track for this operation by clicking on its label"), ErrorMessage); 5201 } 5202 TimelineFunctions::extractZone(m_model, tracks, QPoint(in, out), true); 5203 } 5204 5205 void TimelineController::setMulticamIn(int pos) 5206 { 5207 if (multicamIn != -1) { 5208 // remove previous snap 5209 m_model->removeSnap(multicamIn); 5210 } 5211 multicamIn = pos; 5212 m_model->addSnap(multicamIn); 5213 Q_EMIT multicamInChanged(); 5214 } 5215 5216 void TimelineController::checkClipPosition(const QModelIndex &topLeft, const QModelIndex &, const QVector<int> &roles) 5217 { 5218 if (roles.contains(TimelineModel::StartRole)) { 5219 int id = int(topLeft.internalId()); 5220 if (m_model->isComposition(id) || m_model->isClip(id)) { 5221 Q_EMIT updateAssetPosition(id, m_model->uuid()); 5222 } 5223 } 5224 if (roles.contains(TimelineModel::ResourceRole)) { 5225 int id = int(topLeft.internalId()); 5226 if (m_model->isComposition(id) || m_model->isClip(id)) { 5227 int in = m_model->getItemPosition(id); 5228 int out = in + m_model->getItemPlaytime(id); 5229 pCore->refreshProjectRange({in, out}); 5230 } 5231 } 5232 } 5233 5234 void TimelineController::autofitTrackHeight(int timelineHeight, int collapsedHeight) 5235 { 5236 int tracksCount = m_model->getTracksCount(); 5237 if (tracksCount < 1) { 5238 return; 5239 } 5240 // Check how many collapsed tracks we have 5241 int collapsed = 0; 5242 auto it = m_model->m_allTracks.cbegin(); 5243 while (it != m_model->m_allTracks.cend()) { 5244 if ((*it)->getProperty(QStringLiteral("kdenlive:collapsed")).toInt() > 0) { 5245 collapsed++; 5246 } 5247 ++it; 5248 } 5249 if (collapsed == tracksCount) { 5250 // All tracks are collapsed, do nothing 5251 return; 5252 } 5253 int trackHeight = qMax(collapsedHeight, (timelineHeight - (collapsed * collapsedHeight)) / (tracksCount - collapsed)); 5254 it = m_model->m_allTracks.cbegin(); 5255 while (it != m_model->m_allTracks.cend()) { 5256 if ((*it)->getProperty(QStringLiteral("kdenlive:collapsed")).toInt() == 0) { 5257 (*it)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(trackHeight)); 5258 } 5259 ++it; 5260 } 5261 // m_model->setTrackProperty(trackId, "kdenlive:collapsed", QStringLiteral("0")); 5262 QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); 5263 QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); 5264 Q_EMIT m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); 5265 } 5266 5267 QVariantList TimelineController::subtitlesList() const 5268 { 5269 QVariantList result; 5270 auto subtitleModel = m_model->getSubtitleModel(); 5271 if (subtitleModel) { 5272 QMap<std::pair<int, QString>, QString> currentSubs = subtitleModel->getSubtitlesList(); 5273 if (currentSubs.isEmpty()) { 5274 result << i18nc("@item:inlistbox name for subtitle track", "Subtitles"); 5275 } else { 5276 QMapIterator<std::pair<int, QString>, QString> i(currentSubs); 5277 while (i.hasNext()) { 5278 i.next(); 5279 result << i.key().second; 5280 } 5281 } 5282 } else { 5283 result << i18nc("@item:inlistbox name for subtitle track", "Subtitles"); 5284 } 5285 result << i18nc("@item:inlistbox", "Manage Subtitles"); 5286 return result; 5287 } 5288 5289 void TimelineController::subtitlesMenuActivatedAsync(int ix) 5290 { 5291 // This method needs a timer otherwise the qml combobox crashes because we try to chenge its index while it is processing an activated event 5292 QTimer::singleShot(100, this, [&, ix]() { subtitlesMenuActivated(ix); }); 5293 } 5294 5295 void TimelineController::refreshSubtitlesComboIndex() 5296 { 5297 int ix = m_activeSubPosition; 5298 m_activeSubPosition = 0; 5299 Q_EMIT activeSubtitlePositionChanged(); 5300 m_activeSubPosition = ix; 5301 Q_EMIT activeSubtitlePositionChanged(); 5302 } 5303 5304 void TimelineController::subtitlesMenuActivated(int ix) 5305 { 5306 auto subtitleModel = m_model->getSubtitleModel(); 5307 QMap<std::pair<int, QString>, QString> currentSubs = subtitleModel->getSubtitlesList(); 5308 if (subtitleModel) { 5309 if (ix != -1 && ix < currentSubs.size()) { 5310 // Clear selection if a subtitle item is selected 5311 std::unordered_set<int> selectedIds = m_model->getCurrentSelection(); 5312 for (auto &id : selectedIds) { 5313 int tid = m_model->getItemTrackId(id); 5314 if (m_model->isSubtitleTrack(tid)) { 5315 m_model->requestClearSelection(); 5316 break; 5317 } 5318 } 5319 QMapIterator<std::pair<int, QString>, QString> i(currentSubs); 5320 m_activeSubPosition = 0; 5321 int counter = 0; 5322 while (i.hasNext()) { 5323 i.next(); 5324 ix--; 5325 if (ix < 0) { 5326 // Match, switch to another subtitle 5327 int index = i.key().first; 5328 m_activeSubPosition = counter; 5329 subtitleModel->activateSubtitle(index); 5330 break; 5331 } 5332 counter++; 5333 } 5334 Q_EMIT activeSubtitlePositionChanged(); 5335 return; 5336 } 5337 } 5338 int currentIx = pCore->currentDoc()->getSequenceProperty(m_model->uuid(), QStringLiteral("kdenlive:activeSubtitleIndex"), QStringLiteral("0")).toInt(); 5339 if (ix > -1) { 5340 m_activeSubPosition = currentSubs.size(); 5341 Q_EMIT activeSubtitlePositionChanged(); 5342 // Reselect last active subtitle in combobox 5343 QMapIterator<std::pair<int, QString>, QString> i(currentSubs); 5344 int counter = 0; 5345 while (i.hasNext()) { 5346 i.next(); 5347 if (i.key().first == currentIx) { 5348 m_activeSubPosition = counter; 5349 break; 5350 } 5351 counter++; 5352 } 5353 Q_EMIT activeSubtitlePositionChanged(); 5354 } 5355 5356 // Show manage dialog 5357 ManageSubtitles *d = new ManageSubtitles(subtitleModel, this, currentIx, qApp->activeWindow()); 5358 d->exec(); 5359 }