diff --git a/src/timeline2/view/timelinecontroller.cpp b/src/timeline2/view/timelinecontroller.cpp index 84ad6f6fb..ba1037a37 100644 --- a/src/timeline2/view/timelinecontroller.cpp +++ b/src/timeline2/view/timelinecontroller.cpp @@ -1,3549 +1,3540 @@ /*************************************************************************** * Copyright (C) 2017 by Jean-Baptiste Mardelle * * This file is part of Kdenlive. See www.kdenlive.org. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) version 3 or any later version accepted by the * * membership of KDE e.V. (or its successor approved by the membership * * of KDE e.V.), which shall act as a proxy defined in Section 14 of * * version 3 of the license. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * ***************************************************************************/ #include "timelinecontroller.h" #include "../model/timelinefunctions.hpp" #include "assets/keyframes/model/keyframemodellist.hpp" #include "bin/bin.h" #include "bin/clipcreator.hpp" #include "bin/model/markerlistmodel.hpp" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "core.h" #include "dialogs/spacerdialog.h" #include "dialogs/speeddialog.h" #include "doc/kdenlivedoc.h" #include "effects/effectsrepository.hpp" #include "effects/effectstack/model/effectstackmodel.hpp" #include "kdenlivesettings.h" #include "lib/audio/audioEnvelope.h" #include "mainwindow.h" #include "monitor/monitormanager.h" #include "previewmanager.h" #include "project/projectmanager.h" #include "timeline2/model/clipmodel.hpp" #include "timeline2/model/compositionmodel.hpp" #include "timeline2/model/groupsmodel.hpp" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/model/trackmodel.hpp" #include "timeline2/view/dialogs/clipdurationdialog.h" #include "timeline2/view/dialogs/trackdialog.h" #include "transitions/transitionsrepository.hpp" #include "audiomixer/mixermanager.hpp" #include #include #include #include #include #include int TimelineController::m_duration = 0; TimelineController::TimelineController(QObject *parent) : QObject(parent) , m_root(nullptr) , m_usePreview(false) , m_activeTrack(-1) , m_audioRef(-1) , m_zone(-1, -1) , m_scale(QFontMetrics(QApplication::font()).maxWidth() / 250) , m_timelinePreview(nullptr) , m_ready(false) , m_snapStackIndex(-1) { m_disablePreview = pCore->currentDoc()->getAction(QStringLiteral("disable_preview")); connect(m_disablePreview, &QAction::triggered, this, &TimelineController::disablePreview); connect(this, &TimelineController::selectionChanged, this, &TimelineController::updateClipActions); connect(this, &TimelineController::videoTargetChanged, this, &TimelineController::updateVideoTarget); connect(this, &TimelineController::audioTargetChanged, this, &TimelineController::updateAudioTarget); m_disablePreview->setEnabled(false); connect(pCore.get(), &Core::finalizeRecording, this, &TimelineController::finishRecording); connect(pCore.get(), &Core::autoScrollChanged, this, &TimelineController::autoScrollChanged); connect(pCore->mixer(), &MixerManager::recordAudio, this, &TimelineController::switchRecording); } TimelineController::~TimelineController() { prepareClose(); } void TimelineController::prepareClose() { // Clear roor so we don't call its methods anymore m_ready = false; m_root = nullptr; // Delete timeline preview before resetting model so that removing clips from timeline doesn't invalidate delete m_timelinePreview; m_timelinePreview = nullptr; } void TimelineController::setModel(std::shared_ptr model) { delete m_timelinePreview; m_zone = QPoint(-1, -1); m_timelinePreview = nullptr; m_model = std::move(model); m_activeSnaps.clear(); connect(m_model.get(), &TimelineItemModel::requestClearAssetView, pCore.get(), &Core::clearAssetPanel); connect(m_model.get(), &TimelineItemModel::checkItemDeletion, [this] (int id) { if (m_ready) { QMetaObject::invokeMethod(m_root, "checkDeletion", Qt::QueuedConnection, Q_ARG(QVariant, id)); } }); connect(m_model.get(), &TimelineItemModel::requestMonitorRefresh, [&]() { pCore->requestMonitorRefresh(); }); connect(m_model.get(), &TimelineModel::invalidateZone, this, &TimelineController::invalidateZone, Qt::DirectConnection); connect(m_model.get(), &TimelineModel::durationUpdated, this, &TimelineController::checkDuration); connect(m_model.get(), &TimelineModel::selectionChanged, this, &TimelineController::selectionChanged); connect(m_model.get(), &TimelineModel::checkTrackDeletion, this, &TimelineController::checkTrackDeletion, Qt::DirectConnection); } void TimelineController::setTargetTracks(bool hasVideo, QMap audioTargets) { int videoTrack = -1; m_model->m_binAudioTargets = audioTargets; QMap audioTracks; m_hasVideoTarget = hasVideo; m_hasAudioTarget = audioTargets.size(); if (m_hasVideoTarget) { videoTrack = m_model->getFirstVideoTrackIndex(); } if (m_hasAudioTarget > 0) { QVector tracks; auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { if ((*it)->isAudioTrack()) { tracks << (*it)->getId(); } ++it; } if (KdenliveSettings::multistream_checktrack() && audioTargets.count() > tracks.count()) { pCore->bin()->checkProjectAudioTracks(QString(), audioTargets.count()); } QMapIterator st(audioTargets); while (st.hasNext()) { st.next(); if (tracks.isEmpty()) { break; } audioTracks.insert(tracks.takeLast(), st.key()); } } emit hasAudioTargetChanged(); emit hasVideoTargetChanged(); setVideoTarget(m_hasVideoTarget && (m_lastVideoTarget > -1) ? m_lastVideoTarget : videoTrack); setAudioTarget(audioTracks); } std::shared_ptr TimelineController::getModel() const { return m_model; } void TimelineController::setRoot(QQuickItem *root) { m_root = root; m_ready = true; } Mlt::Tractor *TimelineController::tractor() { return m_model->tractor(); } Mlt::Producer TimelineController::trackProducer(int tid) { return *(m_model->getTrackById(tid).get()); } double TimelineController::scaleFactor() const { return m_scale; } const QString TimelineController::getTrackNameFromMltIndex(int trackPos) { if (trackPos == -1) { return i18n("unknown"); } if (trackPos == 0) { return i18n("Black"); } return m_model->getTrackTagById(m_model->getTrackIndexFromPosition(trackPos - 1)); } const QString TimelineController::getTrackNameFromIndex(int trackIndex) { QString trackName = m_model->getTrackFullName(trackIndex); return trackName.isEmpty() ? m_model->getTrackTagById(trackIndex) : trackName; } QMap TimelineController::getTrackNames(bool videoOnly) { QMap names; for (const auto &track : m_model->m_iteratorTable) { if (videoOnly && m_model->getTrackById_const(track.first)->isAudioTrack()) { continue; } QString trackName = m_model->getTrackFullName(track.first); names[m_model->getTrackMltIndex(track.first)] = trackName; } return names; } void TimelineController::setScaleFactorOnMouse(double scale, bool zoomOnMouse) { if (m_root) { m_root->setProperty("zoomOnMouse", zoomOnMouse ? qBound(0, getMousePos(), duration()) : -1); m_scale = scale; emit scaleFactorChanged(); } else { qWarning("Timeline root not created, impossible to zoom in"); } } void TimelineController::setScaleFactor(double scale) { m_scale = scale; // Update mainwindow's zoom slider emit updateZoom(scale); // inform qml emit scaleFactorChanged(); } int TimelineController::duration() const { return m_duration; } int TimelineController::fullDuration() const { return m_duration + TimelineModel::seekDuration; } void TimelineController::checkDuration() { int currentLength = m_model->duration(); if (currentLength != m_duration) { m_duration = currentLength; emit durationChanged(); } } int TimelineController::selectedTrack() const { std::unordered_set sel = m_model->getCurrentSelection(); if (sel.empty()) return -1; std::vector> selected_tracks; // contains pairs of (track position, track id) for each selected item for (int s : sel) { int tid = m_model->getItemTrackId(s); selected_tracks.push_back({m_model->getTrackPosition(tid), tid}); } // sort by track position std::sort(selected_tracks.begin(), selected_tracks.begin(), [](const auto &a, const auto &b) { return a.first < b.first; }); return selected_tracks.front().second; } void TimelineController::selectCurrentItem(ObjectType type, bool select, bool addToCurrent) { QList toSelect; int currentClip = type == ObjectType::TimelineClip ? m_model->getClipByPosition(m_activeTrack, pCore->getTimelinePosition()) : m_model->getCompositionByPosition(m_activeTrack, pCore->getTimelinePosition()); if (currentClip == -1) { pCore->displayMessage(i18n("No item under timeline cursor in active track"), InformationMessage, 500); return; } if (!select) { m_model->requestRemoveFromSelection(currentClip); } else { m_model->requestAddToSelection(currentClip, !addToCurrent); } } QList TimelineController::selection() const { if (!m_root) return QList(); std::unordered_set sel = m_model->getCurrentSelection(); QList items; for (int id : sel) { items << id; } return items; } void TimelineController::selectItems(const QList &ids) { std::unordered_set ids_s(ids.begin(), ids.end()); m_model->requestSetSelection(ids_s); } void TimelineController::setScrollPos(int pos) { if (pos > 0 && m_root) { QMetaObject::invokeMethod(m_root, "setScrollPos", Qt::QueuedConnection, Q_ARG(QVariant, pos)); } } void TimelineController::resetView() { m_model->_resetView(); if (m_root) { QMetaObject::invokeMethod(m_root, "updatePalette"); } emit colorsChanged(); } bool TimelineController::snap() { return KdenliveSettings::snaptopoints(); } bool TimelineController::ripple() { return false; } bool TimelineController::scrub() { return false; } int TimelineController::insertClip(int tid, int position, const QString &data_str, bool logUndo, bool refreshView, bool useTargets) { int id; if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = pCore->getTimelinePosition(); } if (!m_model->requestClipInsertion(data_str, tid, position, id, logUndo, refreshView, useTargets)) { id = -1; } return id; } QList TimelineController::insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView) { QList clipIds; if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = pCore->getTimelinePosition(); } TimelineFunctions::requestMultipleClipsInsertion(m_model, binIds, tid, position, clipIds, logUndo, refreshView); // 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. return clipIds; } int TimelineController::insertNewComposition(int tid, int position, const QString &transitionId, bool logUndo) { int clipId = m_model->getTrackById_const(tid)->getClipByPosition(position); if (clipId > 0) { int minimum = m_model->getClipPosition(clipId); return insertNewComposition(tid, clipId, position - minimum, transitionId, logUndo); } return insertComposition(tid, position, transitionId, logUndo); } int TimelineController::insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo) { int id; int minimumPos = clipId > -1 ? m_model->getClipPosition(clipId) : offset; int clip_duration = clipId > -1 ? m_model->getClipPlaytime(clipId) : pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); int endPos = minimumPos + clip_duration; int position = minimumPos; int duration = qMin(clip_duration, pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration())); int lowerVideoTrackId = m_model->getPreviousVideoTrackIndex(tid); bool revert = offset > clip_duration / 2; int bottomId = 0; if (lowerVideoTrackId > 0) { bottomId = m_model->getTrackById_const(lowerVideoTrackId)->getClipByPosition(position + offset); } if (bottomId <= 0) { // No video track underneath if (offset < duration && duration < 2 * clip_duration) { // Composition dropped close to start, keep default composition duration } else if (clip_duration - offset < duration * 1.2 && duration < 2 * clip_duration) { // Composition dropped close to end, keep default composition duration position = endPos - duration; } else { // Use full clip length for duration duration = m_model->getTrackById_const(tid)->suggestCompositionLength(position); } } else { duration = qMin(duration, m_model->getTrackById_const(tid)->suggestCompositionLength(position)); QPair bottom(m_model->m_allClips[bottomId]->getPosition(), m_model->m_allClips[bottomId]->getPlaytime()); if (bottom.first > minimumPos) { // Lower clip is after top clip if (position + offset > bottom.first) { int test_duration = m_model->getTrackById_const(tid)->suggestCompositionLength(bottom.first); if (test_duration > 0) { offset -= (bottom.first - position); position = bottom.first; duration = test_duration; revert = position > minimumPos; } } } else if (position >= bottom.first) { // Lower clip is before or at same pos as top clip int test_duration = m_model->getTrackById_const(lowerVideoTrackId)->suggestCompositionLength(position); if (test_duration > 0) { duration = qMin(test_duration, clip_duration); } } } int defaultLength = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); bool isShortComposition = TransitionsRepository::get()->getType(transitionId) == AssetListType::AssetType::VideoShortComposition; if (duration < 0 || (isShortComposition && duration > 1.5 * defaultLength)) { duration = defaultLength; } else if (duration <= 1) { // if suggested composition duration is lower than 4 frames, use default duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); if (minimumPos + clip_duration - position < 3) { position = minimumPos + clip_duration - duration; } } QPair finalPos = m_model->getTrackById_const(tid)->validateCompositionLength(position, offset, duration, endPos); position = finalPos.first; duration = finalPos.second; std::unique_ptr props(nullptr); if (revert) { props = std::make_unique(); if (transitionId == QLatin1String("dissolve")) { props->set("reverse", 1); } else if (transitionId == QLatin1String("composite") || transitionId == QLatin1String("slide")) { props->set("invert", 1); } else if (transitionId == QLatin1String("wipe")) { props->set("geometry", "0%/0%:100%x100%:100;-1=0%/0%:100%x100%:0"); } } if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, std::move(props), id, logUndo)) { id = -1; pCore->displayMessage(i18n("Could not add composition at selected position"), InformationMessage, 500); } return id; } int TimelineController::insertComposition(int tid, int position, const QString &transitionId, bool logUndo) { int id; int duration = pCore->currentDoc()->getFramePos(KdenliveSettings::transition_duration()); if (!m_model->requestCompositionInsertion(transitionId, tid, position, duration, nullptr, id, logUndo)) { id = -1; } return id; } void TimelineController::deleteSelectedClips() { auto sel = m_model->getCurrentSelection(); if (sel.empty()) { return; } // only need to delete the first item, the others will be deleted in cascade m_model->requestItemDeletion(*sel.begin()); } int TimelineController::getMainSelectedItem(bool restrictToCurrentPos, bool allowComposition) { auto sel = m_model->getCurrentSelection(); if (sel.empty() || sel.size() > 2) { return -1; } int itemId = *(sel.begin()); if (sel.size() == 2) { int parentGroup = m_model->m_groups->getRootId(itemId); if (parentGroup == -1 || m_model->m_groups->getType(parentGroup) != GroupType::AVSplit) { return -1; } } if (!restrictToCurrentPos) { if (m_model->isClip(itemId) || (allowComposition && m_model->isComposition(itemId))) { return itemId; } } if (m_model->isClip(itemId)) { int position = pCore->getTimelinePosition(); int start = m_model->getClipPosition(itemId); int end = start + m_model->getClipPlaytime(itemId); if (position >= start && position <= end) { return itemId; } } return -1; } void TimelineController::copyItem() { std::unordered_set selectedIds = m_model->getCurrentSelection(); if (selectedIds.empty()) { return; } int clipId = *(selectedIds.begin()); QString copyString = TimelineFunctions::copyClips(m_model, selectedIds); QClipboard *clipboard = QApplication::clipboard(); clipboard->setText(copyString); m_root->setProperty("copiedClip", clipId); m_model->requestSetSelection(selectedIds); } bool TimelineController::pasteItem(int position, int tid) { QClipboard *clipboard = QApplication::clipboard(); QString txt = clipboard->text(); if (tid == -1) { tid = getMouseTrack(); } if (position == -1) { position = getMousePos(); } if (tid == -1) { tid = m_activeTrack; } if (position == -1) { position = pCore->getTimelinePosition(); } return TimelineFunctions::pasteClips(m_model, txt, tid, position); } void TimelineController::triggerAction(const QString &name) { pCore->triggerAction(name); } QString TimelineController::timecode(int frames) const { return KdenliveSettings::frametimecode() ? QString::number(frames) : m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df); } QString TimelineController::framesToClock(int frames) const { return m_model->tractor()->frames_to_time(frames, mlt_time_clock); } QString TimelineController::simplifiedTC(int frames) const { if (KdenliveSettings::frametimecode()) { return QString::number(frames); } QString s = m_model->tractor()->frames_to_time(frames, mlt_time_smpte_df); return s.startsWith(QLatin1String("00:")) ? s.remove(0, 3) : s; } bool TimelineController::showThumbnails() const { return KdenliveSettings::videothumbnails(); } bool TimelineController::showAudioThumbnails() const { return KdenliveSettings::audiothumbnails(); } bool TimelineController::showMarkers() const { return KdenliveSettings::showmarkers(); } bool TimelineController::audioThumbFormat() const { return KdenliveSettings::displayallchannels(); } bool TimelineController::showWaveforms() const { return KdenliveSettings::audiothumbnails(); } void TimelineController::addTrack(int tid) { if (tid == -1) { tid = m_activeTrack; } QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow()); if (d->exec() == QDialog::Accepted) { bool audioRecTrack = d->addRecTrack(); bool addAVTrack = d->addAVTrack(); int tracksCount = d->tracksCount(); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = true; for (int ix = 0; ix < tracksCount; ++ix) { int newTid; result = m_model->requestTrackInsertion(d->selectedTrackPosition(), newTid, d->trackName(), d->addAudioTrack(), undo, redo); if (result) { m_model->setTrackProperty(newTid, "kdenlive:timeline_active", QStringLiteral("1")); if (addAVTrack) { int newTid2; int mirrorPos = 0; int mirrorId = m_model->getMirrorAudioTrackId(newTid); if (mirrorId > -1) { mirrorPos = m_model->getTrackMltIndex(mirrorId); } result = m_model->requestTrackInsertion(mirrorPos, newTid2, d->trackName(), true, undo, redo); if (result) { m_model->setTrackProperty(newTid2, "kdenlive:timeline_active", QStringLiteral("1")); } } if (audioRecTrack) { m_model->setTrackProperty(newTid, "kdenlive:audio_rec", QStringLiteral("1")); } } else { break; } } if (result) { pCore->pushUndo(undo, redo, addAVTrack || tracksCount > 1 ? i18n("Insert Tracks") : i18n("Insert Track")); } else { pCore->displayMessage(i18n("Could not insert track"), InformationMessage, 500); undo(); } } } void TimelineController::deleteTrack(int tid) { if (tid == -1) { tid = m_activeTrack; } QPointer d = new TrackDialog(m_model, tid, qApp->activeWindow(), true); if (d->exec() == QDialog::Accepted) { int selectedTrackIx = d->selectedTrackId(); m_model->requestTrackDeletion(selectedTrackIx); if (m_activeTrack == -1) { setActiveTrack(m_model->getTrackIndexFromPosition(m_model->getTracksCount() - 1)); } } } void TimelineController::switchTrackRecord(int tid) { if (tid == -1) { tid = m_activeTrack; } if (!m_model->getTrackById_const(tid)->isAudioTrack()) { pCore->displayMessage(i18n("Select an audio track to display record controls"), InformationMessage, 500); } int recDisplayed = m_model->getTrackProperty(tid, QStringLiteral("kdenlive:audio_rec")).toInt(); if (recDisplayed == 1) { // Disable rec controls m_model->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), QStringLiteral("0")); } else { // Enable rec controls m_model->setTrackProperty(tid, QStringLiteral("kdenlive:audio_rec"), QStringLiteral("1")); } QModelIndex ix = m_model->makeTrackIndexFromID(tid); if (ix.isValid()) { m_model->dataChanged(ix, ix, {TimelineModel::AudioRecordRole}); } } void TimelineController::checkTrackDeletion(int selectedTrackIx) { if (m_activeTrack == selectedTrackIx) { // Make sure we don't keep an index on a deleted track m_activeTrack = -1; emit activeTrackChanged(); } if (m_model->m_audioTarget.contains(selectedTrackIx)) { QMap selection = m_model->m_audioTarget; selection.remove(selectedTrackIx); setAudioTarget(selection); } if (m_model->m_videoTarget == selectedTrackIx) { setVideoTarget(-1); } if (m_lastAudioTarget.contains(selectedTrackIx)) { m_lastAudioTarget.remove(selectedTrackIx); emit lastAudioTargetChanged(); } if (m_lastVideoTarget == selectedTrackIx) { m_lastVideoTarget = -1; emit lastVideoTargetChanged(); } } void TimelineController::showConfig(int page, int tab) { pCore->showConfigDialog(page, tab); } void TimelineController::gotoNextSnap() { if (m_activeSnaps.empty() || pCore->undoIndex() != m_snapStackIndex) { m_snapStackIndex = pCore->undoIndex(); m_activeSnaps.clear(); m_activeSnaps = pCore->projectManager()->current()->getGuideModel()->getSnapPoints(); m_activeSnaps.push_back(m_zone.x()); m_activeSnaps.push_back(m_zone.y() - 1); } int nextSnap = m_model->getNextSnapPos(pCore->getTimelinePosition(), m_activeSnaps); if (nextSnap > pCore->getTimelinePosition()) { setPosition(nextSnap); } } void TimelineController::gotoPreviousSnap() { if (pCore->getTimelinePosition() > 0) { if (m_activeSnaps.empty() || pCore->undoIndex() != m_snapStackIndex) { m_snapStackIndex = pCore->undoIndex(); m_activeSnaps.clear(); m_activeSnaps = pCore->projectManager()->current()->getGuideModel()->getSnapPoints(); m_activeSnaps.push_back(m_zone.x()); m_activeSnaps.push_back(m_zone.y() - 1); } setPosition(m_model->getPreviousSnapPos(pCore->getTimelinePosition(), m_activeSnaps)); } } void TimelineController::gotoNextGuide() { QList guides = pCore->projectManager()->current()->getGuideModel()->getAllMarkers(); int pos = pCore->getTimelinePosition(); double fps = pCore->getCurrentFps(); for (auto &guide : guides) { if (guide.time().frames(fps) > pos) { setPosition(guide.time().frames(fps)); return; } } setPosition(m_duration - 1); } void TimelineController::gotoPreviousGuide() { if (pCore->getTimelinePosition() > 0) { QList guides = pCore->projectManager()->current()->getGuideModel()->getAllMarkers(); int pos = pCore->getTimelinePosition(); double fps = pCore->getCurrentFps(); int lastGuidePos = 0; for (auto &guide : guides) { if (guide.time().frames(fps) >= pos) { setPosition(lastGuidePos); return; } lastGuidePos = guide.time().frames(fps); } setPosition(lastGuidePos); } } void TimelineController::groupSelection() { const auto selection = m_model->getCurrentSelection(); if (selection.size() < 2) { pCore->displayMessage(i18n("Select at least 2 items to group"), InformationMessage, 500); return; } m_model->requestClearSelection(); m_model->requestClipsGroup(selection); m_model->requestSetSelection(selection); } void TimelineController::unGroupSelection(int cid) { auto ids = m_model->getCurrentSelection(); // ask to unselect if needed m_model->requestClearSelection(); if (cid > -1) { ids.insert(cid); } if (!ids.empty()) { m_model->requestClipsUngroup(ids); } } bool TimelineController::dragOperationRunning() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "isDragging", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toBool(); } void TimelineController::setInPoint() { if (dragOperationRunning()) { // Don't allow timeline operation while drag in progress qDebug() << "Cannot operate while dragging"; return; } int cursorPos = pCore->getTimelinePosition(); const auto selection = m_model->getCurrentSelection(); bool selectionFound = false; if (!selection.empty()) { for (int id : selection) { int start = m_model->getItemPosition(id); if (start == cursorPos) { continue; } int size = start + m_model->getItemPlaytime(id) - cursorPos; m_model->requestItemResize(id, size, false, true, 0, false); selectionFound = true; } } if (!selectionFound) { if (m_activeTrack >= 0) { int cid = m_model->getClipByPosition(m_activeTrack, cursorPos); if (cid < 0) { // Check first item after timeline position int maximumSpace = m_model->getTrackById_const(m_activeTrack)->getBlankEnd(cursorPos); if (maximumSpace < INT_MAX) { cid = m_model->getClipByPosition(m_activeTrack, maximumSpace + 1); } } if (cid >= 0) { int start = m_model->getItemPosition(cid); if (start != cursorPos) { int size = start + m_model->getItemPlaytime(cid) - cursorPos; m_model->requestItemResize(cid, size, false, true, 0, false); } } } } } void TimelineController::setOutPoint() { if (dragOperationRunning()) { // Don't allow timeline operation while drag in progress qDebug() << "Cannot operate while dragging"; return; } int cursorPos = pCore->getTimelinePosition(); const auto selection = m_model->getCurrentSelection(); bool selectionFound = false; if (!selection.empty()) { for (int id : selection) { int start = m_model->getItemPosition(id); if (start + m_model->getItemPlaytime(id) == cursorPos) { continue; } int size = cursorPos - start; m_model->requestItemResize(id, size, true, true, 0, false); selectionFound = true; } } if (!selectionFound) { if (m_activeTrack >= 0) { int cid = m_model->getClipByPosition(m_activeTrack, cursorPos); if (cid < 0) { // Check first item after timeline position int minimumSpace = m_model->getTrackById_const(m_activeTrack)->getBlankStart(cursorPos); cid = m_model->getClipByPosition(m_activeTrack, qMax(0, minimumSpace - 1)); } if (cid >= 0) { int start = m_model->getItemPosition(cid); if (start + m_model->getItemPlaytime(cid) != cursorPos) { int size = cursorPos - start; m_model->requestItemResize(cid, size, true, true, 0, false); } } } } } void TimelineController::editMarker(int cid, int position) { if (cid == -1) { cid = getMainSelectedClip(); if (cid == -1) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); return; } } Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = pCore->getTimelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); if (clip->getMarkerModel()->hasMarker(position)) { GenTime pos(position, pCore->getCurrentFps()); clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), false, clip.get()); } else { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); } } void TimelineController::addMarker(int cid, int position) { if (cid == -1) { cid = getMainSelectedClip(); if (cid == -1) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); return; } } Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = pCore->getTimelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); GenTime pos(position, pCore->getCurrentFps()); clip->getMarkerModel()->editMarkerGui(pos, qApp->activeWindow(), true, clip.get()); } int TimelineController::getMainSelectedClip() const { int clipId = m_root->property("mainItemId").toInt(); if (clipId == -1) { std::unordered_set sel = m_model->getCurrentSelection(); for (int i : sel) { if (m_model->isClip(i)) { clipId = i; break; } } } return clipId; } void TimelineController::addQuickMarker(int cid, int position) { if (cid == -1) { cid = getMainSelectedClip(); if (cid == -1) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); return; } } Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = pCore->getTimelinePosition() - m_model->getClipPosition(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > ((m_model->getClipIn(cid) + m_model->getClipPlaytime(cid) * speed))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); GenTime pos(position, pCore->getCurrentFps()); CommentedTime marker(pos, pCore->currentDoc()->timecode().getDisplayTimecode(pos, false), KdenliveSettings::default_marker_type()); clip->getMarkerModel()->addMarker(marker.time(), marker.comment(), marker.markerType()); } void TimelineController::deleteMarker(int cid, int position) { if (cid == -1) { cid = getMainSelectedClip(); if (cid == -1) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); return; } } Q_ASSERT(m_model->isClip(cid)); double speed = m_model->getClipSpeed(cid); if (position == -1) { // Calculate marker position relative to timeline cursor position = pCore->getTimelinePosition() - m_model->getClipPosition(cid) + m_model->getClipIn(cid); position = position * speed; } if (position < (m_model->getClipIn(cid) * speed) || position > (m_model->getClipIn(cid) * speed + m_model->getClipPlaytime(cid))) { pCore->displayMessage(i18n("Cannot find clip to edit marker"), InformationMessage, 500); return; } std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); GenTime pos(position, pCore->getCurrentFps()); clip->getMarkerModel()->removeMarker(pos); } void TimelineController::deleteAllMarkers(int cid) { if (cid == -1) { cid = getMainSelectedClip(); if (cid == -1) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); return; } } Q_ASSERT(m_model->isClip(cid)); std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(cid)); clip->getMarkerModel()->removeAllMarkers(); } void TimelineController::editGuide(int frame) { if (frame == -1) { frame = pCore->getTimelinePosition(); } auto guideModel = pCore->projectManager()->current()->getGuideModel(); GenTime pos(frame, pCore->getCurrentFps()); guideModel->editMarkerGui(pos, qApp->activeWindow(), false); } void TimelineController::moveGuide(int frame, int newFrame) { auto guideModel = pCore->projectManager()->current()->getGuideModel(); GenTime pos(frame, pCore->getCurrentFps()); GenTime newPos(newFrame, pCore->getCurrentFps()); guideModel->editMarker(pos, newPos); } void TimelineController::switchGuide(int frame, bool deleteOnly) { bool markerFound = false; if (frame == -1) { frame = pCore->getTimelinePosition(); } CommentedTime marker = pCore->projectManager()->current()->getGuideModel()->getMarker(GenTime(frame, pCore->getCurrentFps()), &markerFound); if (!markerFound) { if (deleteOnly) { pCore->displayMessage(i18n("No guide found at current position"), InformationMessage, 500); return; } GenTime pos(frame, pCore->getCurrentFps()); pCore->projectManager()->current()->getGuideModel()->addMarker(pos, i18n("guide")); } else { pCore->projectManager()->current()->getGuideModel()->removeMarker(marker.time()); } } void TimelineController::addAsset(const QVariantMap &data) { QString effect = data.value(QStringLiteral("kdenlive/effect")).toString(); const auto selection = m_model->getCurrentSelection(); if (!selection.empty()) { QList effectSelection; for (int id : selection) { if (m_model->isClip(id)) { effectSelection << id; } } bool foundMatch = false; for (int id : effectSelection) { if (m_model->addClipEffect(id, effect, false)) { foundMatch = true; } } if (!foundMatch) { QString effectName = EffectsRepository::get()->getName(effect); pCore->displayMessage(i18n("Cannot add effect %1 to selected clip", effectName), InformationMessage, 500); } } else { pCore->displayMessage(i18n("Select a clip to apply an effect"), InformationMessage, 500); } } void TimelineController::requestRefresh() { pCore->requestMonitorRefresh(); } void TimelineController::showAsset(int id) { if (m_model->isComposition(id)) { emit showTransitionModel(id, m_model->getCompositionParameterModel(id)); } else if (m_model->isClip(id)) { QModelIndex clipIx = m_model->makeClipIndexFromID(id); QString clipName = m_model->data(clipIx, Qt::DisplayRole).toString(); bool showKeyframes = m_model->data(clipIx, TimelineModel::ShowKeyframesRole).toInt(); qDebug() << "-----\n// SHOW KEYFRAMES: " << showKeyframes; emit showItemEffectStack(clipName, m_model->getClipEffectStackModel(id), m_model->getClipFrameSize(id), showKeyframes); } } void TimelineController::showTrackAsset(int trackId) { emit showItemEffectStack(getTrackNameFromIndex(trackId), m_model->getTrackEffectStackModel(trackId), pCore->getCurrentFrameSize(), false); } void TimelineController::adjustAllTrackHeight(int trackId, int height) { bool isAudio = m_model->getTrackById_const(trackId)->isAudioTrack(); auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (target_track != trackId && m_model->getTrackById_const(target_track)->isAudioTrack() == isAudio) { m_model->getTrackById(target_track)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(height)); } ++it; } int tracksCount = m_model->getTracksCount(); QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); } void TimelineController::collapseAllTrackHeight(int trackId, bool collapse, int collapsedHeight) { bool isAudio = m_model->getTrackById_const(trackId)->isAudioTrack(); auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (m_model->getTrackById_const(target_track)->isAudioTrack() == isAudio) { if (collapse) { m_model->setTrackProperty(target_track, "kdenlive:collapsed", QString::number(collapsedHeight)); } else { m_model->setTrackProperty(target_track, "kdenlive:collapsed", QStringLiteral("0")); } } ++it; } int tracksCount = m_model->getTracksCount(); QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); } void TimelineController::defaultTrackHeight(int trackId) { if (trackId > -1) { m_model->getTrackById(trackId)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight())); QModelIndex modelStart = m_model->makeTrackIndexFromID(trackId); m_model->dataChanged(modelStart, modelStart, {TimelineModel::HeightRole}); return; } auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); m_model->getTrackById(target_track)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight())); ++it; } int tracksCount = m_model->getTracksCount(); QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); } void TimelineController::setPosition(int position) { // Process seek request emit seeked(position); } void TimelineController::setAudioTarget(QMap tracks) { if ((!tracks.isEmpty() && !m_model->isTrack(tracks.firstKey())) || m_hasAudioTarget == 0) { return; } // Clear targets before re-adding to trigger qml refresh m_model->m_audioTarget.clear(); emit audioTargetChanged(); m_model->m_audioTarget = tracks; emit audioTargetChanged(); } void TimelineController::switchAudioTarget(int trackId) { if (m_model->m_audioTarget.contains(trackId)) { m_model->m_audioTarget.remove(trackId); } else { //TODO: use track description if (m_model->m_binAudioTargets.count() == 1) { // Only one audio stream, remove previous and switch m_model->m_audioTarget.clear(); } int ix = getFirstUnassignedStream(); if (ix > -1) { m_model->m_audioTarget.insert(trackId, ix); } } emit audioTargetChanged(); } void TimelineController::assignAudioTarget(int trackId, int stream) { QList assignedStreams = m_model->m_audioTarget.values(); if (assignedStreams.contains(stream)) { // This stream was assigned to another track, remove m_model->m_audioTarget.remove(m_model->m_audioTarget.key(stream)); } //Remove and re-add target track to trigger a refresh in qml track headers m_model->m_audioTarget.remove(trackId); emit audioTargetChanged(); m_model->m_audioTarget.insert(trackId, stream); emit audioTargetChanged(); } int TimelineController::getFirstUnassignedStream() const { QList keys = m_model->m_binAudioTargets.keys(); QList assigned = m_model->m_audioTarget.values(); for (int k : keys) { if (!assigned.contains(k)) { return k; } } return -1; } void TimelineController::setVideoTarget(int track) { if ((track > -1 && !m_model->isTrack(track)) || !m_hasVideoTarget) { return; } m_model->m_videoTarget = track; emit videoTargetChanged(); } void TimelineController::setActiveTrack(int track) { if (track > -1 && !m_model->isTrack(track)) { return; } m_activeTrack = track; emit activeTrackChanged(); } void TimelineController::setZone(const QPoint &zone, bool withUndo) { if (m_zone.x() > 0) { m_model->removeSnap(m_zone.x()); } if (m_zone.y() > 0) { m_model->removeSnap(m_zone.y() - 1); } if (zone.x() > 0) { m_model->addSnap(zone.x()); } if (zone.y() > 0) { m_model->addSnap(zone.y() - 1); } updateZone(m_zone, zone, withUndo); } void TimelineController::updateZone(const QPoint oldZone, const QPoint newZone, bool withUndo) { if (!withUndo) { m_zone = newZone; emit zoneChanged(); // Update monitor zone emit zoneMoved(m_zone); return; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; Fun undo_zone = [this, oldZone]() { setZone(oldZone, false); return true; }; Fun redo_zone = [this, newZone]() { setZone(newZone, false); return true; }; redo_zone(); UPDATE_UNDO_REDO_NOLOCK(redo_zone, undo_zone, undo, redo); pCore->pushUndo(undo, redo, i18n("Set Zone")); } void TimelineController::setZoneIn(int inPoint) { if (m_zone.x() > 0) { m_model->removeSnap(m_zone.x()); } if (inPoint > 0) { m_model->addSnap(inPoint); } m_zone.setX(inPoint); emit zoneChanged(); // Update monitor zone emit zoneMoved(m_zone); } void TimelineController::setZoneOut(int outPoint) { if (m_zone.y() > 0) { m_model->removeSnap(m_zone.y() - 1); } if (outPoint > 0) { m_model->addSnap(outPoint - 1); } m_zone.setY(outPoint); emit zoneChanged(); emit zoneMoved(m_zone); } void TimelineController::selectItems(const QVariantList &tracks, int startFrame, int endFrame, bool addToSelect, bool selectBottomCompositions) { std::unordered_set itemsToSelect; if (addToSelect) { itemsToSelect = m_model->getCurrentSelection(); } for (int i = 0; i < tracks.count(); i++) { if (m_model->getTrackById_const(tracks.at(i).toInt())->isLocked()) { continue; } auto currentClips = m_model->getItemsInRange(tracks.at(i).toInt(), startFrame, endFrame, i < tracks.count() - 1 ? true : selectBottomCompositions); itemsToSelect.insert(currentClips.begin(), currentClips.end()); } m_model->requestSetSelection(itemsToSelect); } void TimelineController::requestClipCut(int clipId, int position) { if (position == -1) { position = pCore->getTimelinePosition(); } TimelineFunctions::requestClipCut(m_model, clipId, position); } void TimelineController::cutClipUnderCursor(int position, int track) { if (position == -1) { position = pCore->getTimelinePosition(); } QMutexLocker lk(&m_metaMutex); bool foundClip = false; const auto selection = m_model->getCurrentSelection(); if (track == -1) { for (int cid : selection) { if (m_model->isClip(cid) && positionIsInItem(cid)) { if (TimelineFunctions::requestClipCut(m_model, cid, position)) { foundClip = true; // Cutting clips in the selection group is handled in TimelineFunctions break; } } } } if (!foundClip) { if (track == -1) { track = m_activeTrack; } if (track >= 0) { int cid = m_model->getClipByPosition(track, position); if (cid >= 0 && TimelineFunctions::requestClipCut(m_model, cid, position)) { foundClip = true; } } } if (!foundClip) { pCore->displayMessage(i18n("No clip to cut"), InformationMessage, 500); } } void TimelineController::cutAllClipsUnderCursor(int position) { if (position == -1) { position = pCore->getTimelinePosition(); } QMutexLocker lk(&m_metaMutex); TimelineFunctions::requestClipCutAll(m_model, position); } int TimelineController::requestSpacerStartOperation(int trackId, int position) { QMutexLocker lk(&m_metaMutex); int itemId = TimelineFunctions::requestSpacerStartOperation(m_model, trackId, position); return itemId; } bool TimelineController::requestSpacerEndOperation(int clipId, int startPosition, int endPosition) { QMutexLocker lk(&m_metaMutex); bool result = TimelineFunctions::requestSpacerEndOperation(m_model, clipId, startPosition, endPosition); return result; } void TimelineController::seekCurrentClip(bool seekToEnd) { const auto selection = m_model->getCurrentSelection(); if (!selection.empty()) { int cid = *selection.begin(); int start = m_model->getItemPosition(cid); if (seekToEnd) { start += m_model->getItemPlaytime(cid); } setPosition(start); } } void TimelineController::seekToClip(int cid, bool seekToEnd) { int start = m_model->getItemPosition(cid); if (seekToEnd) { start += m_model->getItemPlaytime(cid); } setPosition(start); } void TimelineController::seekToMouse() { int mousePos = getMousePos(); if (mousePos > -1) { setPosition(mousePos); } } int TimelineController::getMousePos() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMousePos", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toInt(); } int TimelineController::getMouseTrack() { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getMouseTrack", Q_RETURN_ARG(QVariant, returnedValue)); return returnedValue.toInt(); } bool TimelineController::positionIsInItem(int id) { int in = m_model->getItemPosition(id); int position = pCore->getTimelinePosition(); if (in > position) { return false; } if (position <= in + m_model->getItemPlaytime(id)) { return true; } return false; } void TimelineController::refreshItem(int id) { if (m_model->isClip(id) && m_model->m_allClips[id]->isAudioOnly()) { return; } if (positionIsInItem(id)) { pCore->requestMonitorRefresh(); } } QPair TimelineController::getTracksCount() const { return m_model->getAVtracksCount(); } QStringList TimelineController::extractCompositionLumas() const { return m_model->extractCompositionLumas(); } void TimelineController::addEffectToCurrentClip(const QStringList &effectData) { QList activeClips; for (int track = m_model->getTracksCount() - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); int cid = m_model->getClipByPosition(trackIx, pCore->getTimelinePosition()); if (cid > -1) { activeClips << cid; } } if (!activeClips.isEmpty()) { if (effectData.count() == 4) { QString effectString = effectData.at(1) + QStringLiteral("-") + effectData.at(2) + QStringLiteral("-") + effectData.at(3); m_model->copyClipEffect(activeClips.first(), effectString); } else { m_model->addClipEffect(activeClips.first(), effectData.constFirst()); } } } void TimelineController::adjustFade(int cid, const QString &effectId, int duration, int initialDuration) { if (initialDuration == -2) { // Add default fade duration = pCore->currentDoc()->getFramePos(KdenliveSettings::fade_duration()); initialDuration = 0; } if (duration <= 0) { // remove fade m_model->removeFade(cid, effectId == QLatin1String("fadein")); } else { m_model->adjustEffectLength(cid, effectId, duration, initialDuration); } } QPair TimelineController::getCompositionATrack(int cid) const { QPair result; std::shared_ptr compo = m_model->getCompositionPtr(cid); if (compo) { result = QPair(compo->getATrack(), m_model->getTrackMltIndex(compo->getCurrentTrackId())); } return result; } void TimelineController::setCompositionATrack(int cid, int aTrack) { TimelineFunctions::setCompositionATrack(m_model, cid, aTrack); } bool TimelineController::compositionAutoTrack(int cid) const { std::shared_ptr compo = m_model->getCompositionPtr(cid); return compo && compo->getForcedTrack() == -1; } const QString TimelineController::getClipBinId(int clipId) const { return m_model->getClipBinId(clipId); } void TimelineController::focusItem(int itemId) { int start = m_model->getItemPosition(itemId); setPosition(start); } int TimelineController::headerWidth() const { return qMax(10, KdenliveSettings::headerwidth()); } void TimelineController::setHeaderWidth(int width) { KdenliveSettings::setHeaderwidth(width); } bool TimelineController::createSplitOverlay(int clipId, std::shared_ptr filter) { if (m_timelinePreview && m_timelinePreview->hasOverlayTrack()) { return true; } if (clipId == -1) { pCore->displayMessage(i18n("Select a clip to compare effect"), InformationMessage, 500); return false; } std::shared_ptr clip = m_model->getClipPtr(clipId); const QString binId = clip->binId(); // Get clean bin copy of the clip std::shared_ptr binClip = pCore->projectItemModel()->getClipByBinID(binId); std::shared_ptr binProd(binClip->masterProducer()->cut(clip->getIn(), clip->getOut())); // Get copy of timeline producer std::shared_ptr clipProducer(new Mlt::Producer(*clip)); // Built tractor and compositing Mlt::Tractor trac(*m_model->m_tractor->profile()); Mlt::Playlist play(*m_model->m_tractor->profile()); Mlt::Playlist play2(*m_model->m_tractor->profile()); play.append(*clipProducer.get()); play2.append(*binProd); trac.set_track(play, 0); trac.set_track(play2, 1); play2.attach(*filter.get()); QString splitTransition = TransitionsRepository::get()->getCompositingTransition(); Mlt::Transition t(*m_model->m_tractor->profile(), splitTransition.toUtf8().constData()); t.set("always_active", 1); trac.plant_transition(t, 0, 1); int startPos = m_model->getClipPosition(clipId); // plug in overlay playlist auto *overlay = new Mlt::Playlist(*m_model->m_tractor->profile()); overlay->insert_blank(0, startPos); Mlt::Producer split(trac.get_producer()); overlay->insert_at(startPos, &split, 1); // insert in tractor if (!m_timelinePreview) { initializePreview(); } m_timelinePreview->setOverlayTrack(overlay); m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); return true; } void TimelineController::removeSplitOverlay() { if (!m_timelinePreview || !m_timelinePreview->hasOverlayTrack()) { return; } // disconnect m_timelinePreview->removeOverlayTrack(); m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } void TimelineController::addPreviewRange(bool add) { if (m_zone.isNull()) { return; } if (!m_timelinePreview) { initializePreview(); } if (m_timelinePreview) { m_timelinePreview->addPreviewRange(m_zone, add); } } void TimelineController::clearPreviewRange(bool resetZones) { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(resetZones); } } void TimelineController::startPreviewRender() { // Timeline preview stuff if (!m_timelinePreview) { initializePreview(); } else if (m_disablePreview->isChecked()) { m_disablePreview->setChecked(false); disablePreview(false); } if (m_timelinePreview) { if (!m_usePreview) { m_timelinePreview->buildPreviewTrack(); m_usePreview = true; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } m_timelinePreview->startPreviewRender(); } } void TimelineController::stopPreviewRender() { if (m_timelinePreview) { m_timelinePreview->abortRendering(); } } void TimelineController::initializePreview() { if (m_timelinePreview) { // Update parameters if (!m_timelinePreview->loadParams()) { if (m_usePreview) { // Disconnect preview track m_timelinePreview->disconnectTrack(); m_usePreview = false; } delete m_timelinePreview; m_timelinePreview = nullptr; } } else { m_timelinePreview = new PreviewManager(this, m_model->m_tractor.get()); if (!m_timelinePreview->initialize()) { // TODO warn user delete m_timelinePreview; m_timelinePreview = nullptr; } else { } } QAction *previewRender = pCore->currentDoc()->getAction(QStringLiteral("prerender_timeline_zone")); if (previewRender) { previewRender->setEnabled(m_timelinePreview != nullptr); } m_disablePreview->setEnabled(m_timelinePreview != nullptr); m_disablePreview->blockSignals(true); m_disablePreview->setChecked(false); m_disablePreview->blockSignals(false); } bool TimelineController::hasPreviewTrack() const { return (m_timelinePreview && (m_timelinePreview->hasOverlayTrack() || m_timelinePreview->hasPreviewTrack())); } void TimelineController::updatePreviewConnection(bool enable) { if (m_timelinePreview) { if (enable) { m_timelinePreview->enable(); } else { m_timelinePreview->disable(); } } } void TimelineController::disablePreview(bool disable) { if (disable) { m_timelinePreview->deletePreviewTrack(); m_usePreview = false; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } else { if (!m_usePreview) { if (!m_timelinePreview->buildPreviewTrack()) { // preview track already exists, reconnect m_model->m_tractor->lock(); m_timelinePreview->reconnectTrack(); m_model->m_tractor->unlock(); } m_timelinePreview->loadChunks(QVariantList(), QVariantList(), QDateTime()); m_usePreview = true; } } m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } QVariantList TimelineController::dirtyChunks() const { return m_timelinePreview ? m_timelinePreview->m_dirtyChunks : QVariantList(); } QVariantList TimelineController::renderedChunks() const { return m_timelinePreview ? m_timelinePreview->m_renderedChunks : QVariantList(); } int TimelineController::workingPreview() const { return m_timelinePreview ? m_timelinePreview->workingPreview : -1; } bool TimelineController::useRuler() const { return pCore->currentDoc()->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1; } void TimelineController::resetPreview() { if (m_timelinePreview) { m_timelinePreview->clearPreviewRange(true); initializePreview(); } } void TimelineController::loadPreview(const QString &chunks, const QString &dirty, const QDateTime &documentDate, int enable) { if (chunks.isEmpty() && dirty.isEmpty()) { return; } if (!m_timelinePreview) { initializePreview(); } QVariantList renderedChunks; QVariantList dirtyChunks; #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) QStringList chunksList = chunks.split(QLatin1Char(','), QString::SkipEmptyParts); #else QStringList chunksList = chunks.split(QLatin1Char(','), Qt::SkipEmptyParts); #endif #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) QStringList dirtyList = dirty.split(QLatin1Char(','), QString::SkipEmptyParts); #else QStringList dirtyList = dirty.split(QLatin1Char(','), Qt::SkipEmptyParts); #endif for (const QString &frame : chunksList) { renderedChunks << frame.toInt(); } for (const QString &frame : dirtyList) { dirtyChunks << frame.toInt(); } m_disablePreview->blockSignals(true); m_disablePreview->setChecked(enable); m_disablePreview->blockSignals(false); if (!enable) { m_timelinePreview->buildPreviewTrack(); m_usePreview = true; m_model->m_overlayTrackCount = m_timelinePreview->addedTracks(); } m_timelinePreview->loadChunks(renderedChunks, dirtyChunks, documentDate); } QMap TimelineController::documentProperties() { QMap props = pCore->currentDoc()->documentProperties(); int audioTarget = m_model->m_audioTarget.isEmpty() ? -1 : m_model->getTrackPosition(m_model->m_audioTarget.firstKey()); int videoTarget = m_model->m_videoTarget == -1 ? -1 : m_model->getTrackPosition(m_model->m_videoTarget); int activeTrack = m_activeTrack == -1 ? -1 : m_model->getTrackPosition(m_activeTrack); props.insert(QStringLiteral("audioTarget"), QString::number(audioTarget)); props.insert(QStringLiteral("videoTarget"), QString::number(videoTarget)); props.insert(QStringLiteral("activeTrack"), QString::number(activeTrack)); props.insert(QStringLiteral("position"), QString::number(pCore->getTimelinePosition())); QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getScrollPos", Q_RETURN_ARG(QVariant, returnedValue)); int scrollPos = returnedValue.toInt(); props.insert(QStringLiteral("scrollPos"), QString::number(scrollPos)); props.insert(QStringLiteral("zonein"), QString::number(m_zone.x())); props.insert(QStringLiteral("zoneout"), QString::number(m_zone.y())); if (m_timelinePreview) { QPair chunks = m_timelinePreview->previewChunks(); props.insert(QStringLiteral("previewchunks"), chunks.first.join(QLatin1Char(','))); props.insert(QStringLiteral("dirtypreviewchunks"), chunks.second.join(QLatin1Char(','))); } props.insert(QStringLiteral("disablepreview"), QString::number((int)m_disablePreview->isChecked())); return props; } int TimelineController::getMenuOrTimelinePos() const { int frame = m_root->property("mainFrame").toInt(); if (frame == -1) { frame = pCore->getTimelinePosition(); } return frame; } void TimelineController::insertSpace(int trackId, int frame) { if (frame == -1) { frame = getMenuOrTimelinePos(); } if (trackId == -1) { trackId = m_activeTrack; } QPointer d = new SpacerDialog(GenTime(65, pCore->getCurrentFps()), pCore->currentDoc()->timecode(), qApp->activeWindow()); if (d->exec() != QDialog::Accepted) { delete d; return; } int cid = requestSpacerStartOperation(d->affectAllTracks() ? -1 : trackId, frame); int spaceDuration = d->selectedDuration().frames(pCore->getCurrentFps()); delete d; if (cid == -1) { pCore->displayMessage(i18n("No clips found to insert space"), InformationMessage, 500); return; } int start = m_model->getItemPosition(cid); requestSpacerEndOperation(cid, start, start + spaceDuration); } void TimelineController::removeSpace(int trackId, int frame, bool affectAllTracks) { if (frame == -1) { frame = getMenuOrTimelinePos(); } if (trackId == -1) { trackId = m_activeTrack; } bool res = TimelineFunctions::requestDeleteBlankAt(m_model, trackId, frame, affectAllTracks); if (!res) { pCore->displayMessage(i18n("Cannot remove space at given position"), InformationMessage, 500); } } void TimelineController::invalidateItem(int cid) { if (!m_timelinePreview || !m_model->isItem(cid)) { return; } const int tid = m_model->getItemTrackId(cid); if (tid == -1 || m_model->getTrackById_const(tid)->isAudioTrack()) { return; } int start = m_model->getItemPosition(cid); int end = start + m_model->getItemPlaytime(cid); m_timelinePreview->invalidatePreview(start, end); } void TimelineController::invalidateTrack(int tid) { if (!m_timelinePreview || !m_model->isTrack(tid) || m_model->getTrackById_const(tid)->isAudioTrack()) { return; } for (auto clp : m_model->getTrackById_const(tid)->m_allClips) { invalidateItem(clp.first); } } void TimelineController::invalidateZone(int in, int out) { if (!m_timelinePreview) { return; } m_timelinePreview->invalidatePreview(in, out == -1 ? m_duration : out); } void TimelineController::changeItemSpeed(int clipId, double speed) { /*if (clipId == -1) { clipId = getMainSelectedItem(false, true); }*/ if (clipId == -1) { clipId = getMainSelectedClip(); } if (clipId == -1) { pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500); return; } bool pitchCompensate = m_model->m_allClips[clipId]->getIntProperty(QStringLiteral("warp_pitch")); if (qFuzzyCompare(speed, -1)) { speed = 100 * m_model->getClipSpeed(clipId); double duration = m_model->getItemPlaytime(clipId); // this is the max speed so that the clip is at least one frame long double maxSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId)); // this is the min speed so that the clip doesn't bump into the next one on track double minSpeed = 100. * duration * qAbs(m_model->getClipSpeed(clipId)) / (duration + double(m_model->getBlankSizeNearClip(clipId, true))); // if there is a split partner, we must also take it into account int partner = m_model->getClipSplitPartner(clipId); if (partner != -1) { double duration2 = m_model->getItemPlaytime(partner); double maxSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)); double minSpeed2 = 100. * duration2 * qAbs(m_model->getClipSpeed(partner)) / (duration2 + double(m_model->getBlankSizeNearClip(partner, true))); minSpeed = std::max(minSpeed, minSpeed2); maxSpeed = std::min(maxSpeed, maxSpeed2); } QScopedPointer d(new SpeedDialog(QApplication::activeWindow(), std::abs(speed), minSpeed, maxSpeed, speed < 0, pitchCompensate)); if (d->exec() != QDialog::Accepted) { return; } speed = d->getValue(); pitchCompensate = d->getPitchCompensate(); qDebug() << "requesting speed " << speed; } m_model->requestClipTimeWarp(clipId, speed, pitchCompensate, true); } void TimelineController::switchCompositing(int mode) { // m_model->m_tractor->lock(); pCore->currentDoc()->setDocumentProperty(QStringLiteral("compositing"), QString::number(mode)); QScopedPointer service(m_model->m_tractor->field()); QScopedPointerfield(m_model->m_tractor->field()); field->lock(); while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); service.reset(service->producer()); QString serviceName = t.get("mlt_service"); if (t.get_int("internal_added") == 237 && serviceName != QLatin1String("mix")) { // remove all compositing transitions field->disconnect_service(t); t.disconnect_all_producers(); } } else { service.reset(service->producer()); } } if (mode > 0) { const QString compositeGeometry = QStringLiteral("0=0/0:%1x%2").arg(m_model->m_tractor->profile()->width()).arg(m_model->m_tractor->profile()->height()); // Loop through tracks for (int track = 0; track < m_model->getTracksCount(); track++) { if (m_model->getTrackById(m_model->getTrackIndexFromPosition(track))->getProperty("kdenlive:audio_track").toInt() == 0) { // This is a video track Mlt::Transition t(*m_model->m_tractor->profile(), mode == 1 ? "composite" : TransitionsRepository::get()->getCompositingTransition().toUtf8().constData()); t.set("always_active", 1); t.set_tracks(0, track + 1); if (mode == 1) { t.set("valign", "middle"); t.set("halign", "centre"); t.set("fill", 1); t.set("aligned", 0); t.set("geometry", compositeGeometry.toUtf8().constData()); } t.set("internal_added", 237); field->plant_transition(t, 0, track + 1); } } } field->unlock(); pCore->requestMonitorRefresh(); } void TimelineController::extractZone(QPoint zone, bool liftOnly) { QVector tracks; auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { tracks << target_track; } ++it; } if (tracks.isEmpty()) { pCore->displayMessage(i18n("Please activate a track for this operation by clicking on its label"), InformationMessage); } if (m_zone == QPoint()) { // Use current timeline position and clip zone length zone.setY(pCore->getTimelinePosition() + zone.y() - zone.x()); zone.setX(pCore->getTimelinePosition()); } TimelineFunctions::extractZone(m_model, tracks, m_zone == QPoint() ? zone : m_zone, liftOnly); } void TimelineController::extract(int clipId) { if (clipId == -1) { std::unordered_set sel = m_model->getCurrentSelection(); for (int i : sel) { if (m_model->isClip(i)) { clipId = i; break; } } if (clipId == -1) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); return; } } int in = m_model->getClipPosition(clipId); int out = in + m_model->getClipPlaytime(clipId); QVector tracks; tracks << m_model->getClipTrackId(clipId); if (m_model->m_groups->isInGroup(clipId)) { int targetRoot = m_model->m_groups->getRootId(clipId); if (m_model->isGroup(targetRoot)) { std::unordered_set sub = m_model->m_groups->getLeaves(targetRoot); for (int current_id : sub) { if (current_id == clipId) { continue; } if (m_model->isClip(current_id)) { int newIn = m_model->getClipPosition(current_id); int tk = m_model->getClipTrackId(current_id); in = qMin(in, newIn); out = qMax(out, newIn + m_model->getClipPlaytime(current_id)); if (!tracks.contains(tk)) { tracks << tk; } } } } } TimelineFunctions::extractZone(m_model, tracks, QPoint(in, out), false); } void TimelineController::saveZone(int clipId) { if (clipId == -1) { clipId = getMainSelectedClip(); if (clipId == -1) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); return; } } int in = m_model->getClipIn(clipId); int out = in + m_model->getClipPlaytime(clipId); QString id; pCore->projectItemModel()->requestAddBinSubClip(id, in, out, {}, m_model->m_allClips[clipId]->binId()); } bool TimelineController::insertClipZone(const QString &binId, int tid, int position) { QStringList binIdData = binId.split(QLatin1Char('/')); int in = 0; int out = -1; if (binIdData.size() >= 3) { in = binIdData.at(1).toInt(); out = binIdData.at(2).toInt(); } QString bid = binIdData.first(); // dropType indicates if we want a normal drop (disabled), audio only or video only drop PlaylistState::ClipState dropType = PlaylistState::Disabled; if (bid.startsWith(QLatin1Char('A'))) { dropType = PlaylistState::AudioOnly; bid = bid.remove(0, 1); } else if (bid.startsWith(QLatin1Char('V'))) { dropType = PlaylistState::VideoOnly; bid = bid.remove(0, 1); } QList audioTracks; int vTrack = -1; std::shared_ptr clip = pCore->bin()->getBinClip(bid); if (out <= in) { out = (int)clip->frameDuration() - 1; } QList audioStreams = m_model->m_binAudioTargets.keys(); if (dropType == PlaylistState::VideoOnly) { vTrack = tid; } else if (dropType == PlaylistState::AudioOnly) { audioTracks << tid; if (audioStreams.size() > 1) { // insert the other audio streams QList lower = m_model->getLowerTracksId(tid, TrackType::AudioTrack); while (audioStreams.size() > 1 && !lower.isEmpty()) { audioTracks << lower.takeFirst(); audioStreams.takeFirst(); } } } else { if (m_model->getTrackById_const(tid)->isAudioTrack()) { audioTracks << tid; if (audioStreams.size() > 1) { // insert the other audio streams QList lower = m_model->getLowerTracksId(tid, TrackType::AudioTrack); while (audioStreams.size() > 1 && !lower.isEmpty()) { audioTracks << lower.takeFirst(); audioStreams.takeFirst(); } } vTrack = clip->hasAudioAndVideo() ? m_model->getMirrorVideoTrackId(tid) : -1; } else { vTrack = tid; if (clip->hasAudioAndVideo()) { int firstAudio = m_model->getMirrorAudioTrackId(vTrack); audioTracks << firstAudio; if (audioStreams.size() > 1) { // insert the other audio streams QList lower = m_model->getLowerTracksId(firstAudio, TrackType::AudioTrack); while (audioStreams.size() > 1 && !lower.isEmpty()) { audioTracks << lower.takeFirst(); audioStreams.takeFirst(); } } } } } QList target_tracks; if (vTrack > -1) { target_tracks << vTrack; } if (!audioTracks.isEmpty()) { target_tracks << audioTracks; } qDebug()<<"=====================\n\nREADY TO INSERT IN TRACKS: "< undo = []() { return true; }; std::function redo = []() { return true; }; bool overwrite = m_model->m_editMode == TimelineMode::OverwriteEdit; QPoint zone(in, out + 1); bool res = TimelineFunctions::insertZone(m_model, target_tracks, binId, position, zone, overwrite, false, undo, redo); if (res) { int newPos = position + (zone.y() - zone.x()); int currentPos = pCore->getTimelinePosition(); Fun redoPos = [this, newPos]() { Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id(); pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); pCore->monitorManager()->refreshProjectMonitor(); setPosition(newPos); pCore->monitorManager()->activateMonitor(activeMonitor); return true; }; Fun undoPos = [this, currentPos]() { Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id(); pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); pCore->monitorManager()->refreshProjectMonitor(); setPosition(currentPos); pCore->monitorManager()->activateMonitor(activeMonitor); return true; }; redoPos(); UPDATE_UNDO_REDO_NOLOCK(redoPos, undoPos, undo, redo); pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone")); } else { pCore->displayMessage(i18n("Could not insert zone"), InformationMessage); undo(); } return res; } int TimelineController::insertZone(const QString &binId, QPoint zone, bool overwrite) { std::shared_ptr clip = pCore->bin()->getBinClip(binId); int aTrack = -1; int vTrack = -1; - if (clip->hasAudio()) { + if (clip->hasAudio() && !m_model->m_audioTarget.isEmpty()) { aTrack = m_model->m_audioTarget.firstKey(); } if (clip->hasVideo()) { vTrack = videoTarget(); } - /*if (aTrack == -1 && vTrack == -1) { - // No target tracks defined, use active track - if (m_model->getTrackById_const(m_activeTrack)->isAudioTrack()) { - aTrack = m_activeTrack; - vTrack = m_model->getMirrorVideoTrackId(aTrack); - } else { - vTrack = m_activeTrack; - aTrack = m_model->getMirrorAudioTrackId(vTrack); - } - }*/ + int insertPoint; QPoint sourceZone; if (useRuler() && m_zone != QPoint()) { // We want to use timeline zone for in/out insert points insertPoint = m_zone.x(); sourceZone = QPoint(zone.x(), zone.x() + m_zone.y() - m_zone.x()); } else { // Use current timeline pos and clip zone for in/out insertPoint = pCore->getTimelinePosition(); sourceZone = zone; } QList target_tracks; if (vTrack > -1) { target_tracks << vTrack; } if (aTrack > -1) { target_tracks << aTrack; } if (target_tracks.isEmpty()) { pCore->displayMessage(i18n("Please select a target track by clicking on a track's target zone"), InformationMessage); return -1; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = TimelineFunctions::insertZone(m_model, target_tracks, binId, insertPoint, sourceZone, overwrite, true, undo, redo); if (res) { int newPos = insertPoint + (sourceZone.y() - sourceZone.x()); int currentPos = pCore->getTimelinePosition(); Fun redoPos = [this, newPos]() { Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id(); pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); pCore->monitorManager()->refreshProjectMonitor(); setPosition(newPos); pCore->monitorManager()->activateMonitor(activeMonitor); return true; }; Fun undoPos = [this, currentPos]() { Kdenlive::MonitorId activeMonitor = pCore->monitorManager()->activeMonitor()->id(); pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); pCore->monitorManager()->refreshProjectMonitor(); setPosition(currentPos); pCore->monitorManager()->activateMonitor(activeMonitor); return true; }; redoPos(); UPDATE_UNDO_REDO_NOLOCK(redoPos, undoPos, undo, redo); pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone")); } else { pCore->displayMessage(i18n("Could not insert zone"), InformationMessage); undo(); } return res; } void TimelineController::updateClip(int clipId, const QVector &roles) { QModelIndex ix = m_model->makeClipIndexFromID(clipId); if (ix.isValid()) { m_model->dataChanged(ix, ix, roles); } } void TimelineController::showClipKeyframes(int clipId, bool value) { TimelineFunctions::showClipKeyframes(m_model, clipId, value); } void TimelineController::showCompositionKeyframes(int clipId, bool value) { TimelineFunctions::showCompositionKeyframes(m_model, clipId, value); } void TimelineController::switchEnableState(std::unordered_set selection) { if (selection.empty()) { selection = m_model->getCurrentSelection(); //clipId = getMainSelectedItem(false, false); } if (selection.empty()) { return; } TimelineFunctions::switchEnableState(m_model, selection); } void TimelineController::addCompositionToClip(const QString &assetId, int clipId, int offset) { if (clipId == -1) { clipId = getMainSelectedClip(); if (clipId == -1) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); return; } } if (offset == -1) { offset = m_root->property("mainFrame").toInt(); } int track = clipId > -1 ? m_model->getClipTrackId(clipId) : m_activeTrack; int compoId = -1; if (assetId.isEmpty()) { QStringList compositions = KdenliveSettings::favorite_transitions(); if (compositions.isEmpty()) { pCore->displayMessage(i18n("Select a favorite composition"), InformationMessage, 500); return; } compoId = insertNewComposition(track, clipId, offset, compositions.first(), true); } else { compoId = insertNewComposition(track, clipId, offset, assetId, true); } if (compoId > 0) { m_model->requestSetSelection({compoId}); } } void TimelineController::addEffectToClip(const QString &assetId, int clipId) { if (clipId == -1) { clipId = getMainSelectedClip(); if (clipId == -1) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); return; } } qDebug() << "/// ADDING ASSET: " << assetId; m_model->addClipEffect(clipId, assetId); } bool TimelineController::splitAV() { int cid = *m_model->getCurrentSelection().begin(); if (m_model->isClip(cid)) { std::shared_ptr clip = m_model->getClipPtr(cid); if (clip->clipState() == PlaylistState::AudioOnly) { return TimelineFunctions::requestSplitVideo(m_model, cid, videoTarget()); } else { return TimelineFunctions::requestSplitAudio(m_model, cid, m_model->m_audioTarget.firstKey()); } } pCore->displayMessage(i18n("No clip found to perform AV split operation"), InformationMessage, 500); return false; } void TimelineController::splitAudio(int clipId) { TimelineFunctions::requestSplitAudio(m_model, clipId, m_model->m_audioTarget.firstKey()); } void TimelineController::splitVideo(int clipId) { TimelineFunctions::requestSplitVideo(m_model, clipId, videoTarget()); } void TimelineController::setAudioRef(int clipId) { if (clipId == -1) { clipId = getMainSelectedClip(); if (clipId == -1) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); return; } } m_audioRef = clipId; std::unique_ptr envelope(new AudioEnvelope(getClipBinId(clipId), clipId)); m_audioCorrelator.reset(new AudioCorrelation(std::move(envelope))); connect(m_audioCorrelator.get(), &AudioCorrelation::gotAudioAlignData, [&](int cid, int shift) { int pos = m_model->getClipPosition(m_audioRef) + shift - m_model->getClipIn(m_audioRef); bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), pos, true, true, true); if (!result) { pCore->displayMessage(i18n("Cannot move clip to frame %1.", (pos + shift)), InformationMessage, 500); } }); connect(m_audioCorrelator.get(), &AudioCorrelation::displayMessage, pCore.get(), &Core::displayMessage); } void TimelineController::alignAudio(int clipId) { // find other clip if (clipId == -1) { clipId = getMainSelectedClip(); if (clipId == -1) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); return; } } if (m_audioRef == -1 || m_audioRef == clipId) { pCore->displayMessage(i18n("Set audio reference before attempting to align"), InformationMessage, 500); return; } const QString masterBinClipId = getClipBinId(m_audioRef); std::unordered_set clipsToAnalyse; if (m_model->m_groups->isInGroup(clipId)) { clipsToAnalyse = m_model->getGroupElements(clipId); m_model->requestClearSelection(); } else { clipsToAnalyse.insert(clipId); } QList processedGroups; int processed = 0; for (int cid : clipsToAnalyse) { if (!m_model->isClip(cid) || cid == m_audioRef) { continue; } const QString otherBinId = getClipBinId(cid); if (m_model->m_groups->isInGroup(cid)) { int parentGroup = m_model->m_groups->getRootId(cid); if (processedGroups.contains(parentGroup)) { continue; } // Only process one clip from the group std::shared_ptr clip = pCore->bin()->getBinClip(otherBinId); if (clip->hasAudio()) { processedGroups << parentGroup; } else { continue; } } if (!pCore->bin()->getBinClip(otherBinId)->hasAudio()) { // Cannot process non audi clips continue; } if (otherBinId == masterBinClipId) { // easy, same clip. int newPos = m_model->getClipPosition(m_audioRef) - m_model->getClipIn(m_audioRef) + m_model->getClipIn(cid); if (newPos) { bool result = m_model->requestClipMove(cid, m_model->getClipTrackId(cid), newPos, true, true, true); processed ++; if (!result) { pCore->displayMessage(i18n("Cannot move clip to frame %1.", newPos), InformationMessage, 500); } continue; } } processed ++; // Perform audio calculation AudioEnvelope *envelope = new AudioEnvelope(otherBinId, cid, (size_t)m_model->getClipIn(cid), (size_t)m_model->getClipPlaytime(cid), (size_t)m_model->getClipPosition(cid)); m_audioCorrelator->addChild(envelope); } if (processed == 0) { //TODO: improve feedback message after freeze pCore->displayMessage(i18n("Select a clip to apply an effect"), InformationMessage, 500); } } void TimelineController::switchTrackActive(int trackId) { if (trackId == -1) { trackId = m_activeTrack; } bool active = m_model->getTrackById_const(trackId)->isTimelineActive(); m_model->setTrackProperty(trackId, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1")); } void TimelineController::switchAllTrackActive() { auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { bool active = (*it)->isTimelineActive(); int target_track = (*it)->getId(); m_model->setTrackProperty(target_track, QStringLiteral("kdenlive:timeline_active"), active ? QStringLiteral("0") : QStringLiteral("1")); ++it; } } void TimelineController::makeAllTrackActive() { // Check current status auto it = m_model->m_allTracks.cbegin(); bool makeActive = false; while (it != m_model->m_allTracks.cend()) { if (!(*it)->isTimelineActive()) { // There is an inactive track, activate all makeActive = true; break; } ++it; } it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); m_model->setTrackProperty(target_track, QStringLiteral("kdenlive:timeline_active"), makeActive ? QStringLiteral("1") : QStringLiteral("0")); ++it; } } void TimelineController::switchTrackLock(bool applyToAll) { if (!applyToAll) { // apply to active track only bool locked = m_model->getTrackById_const(m_activeTrack)->isLocked(); m_model->setTrackLockedState(m_activeTrack, !locked); } else { // Invert track lock const auto ids = m_model->getAllTracksIds(); // count the number of tracks to be locked int toBeLockedCount = std::accumulate(ids.begin(), ids.end(), 0, [this](int s, int id) { return s + (m_model->getTrackById_const(id)->isLocked() ? 0 : 1); }); bool leaveOneUnlocked = toBeLockedCount == m_model->getTracksCount(); for (const int id : ids) { // leave active track unlocked if (leaveOneUnlocked && id == m_activeTrack) { continue; } bool isLocked = m_model->getTrackById_const(id)->isLocked(); m_model->setTrackLockedState(id, !isLocked); } } } void TimelineController::switchTargetTrack() { bool isAudio = m_model->getTrackById_const(m_activeTrack)->getProperty("kdenlive:audio_track").toInt() == 1; if (isAudio) { QMap current = m_model->m_audioTarget; if (current.contains(m_activeTrack)) { current.remove(m_activeTrack); } else { int ix = getFirstUnassignedStream(); if (ix > -1) { current.insert(m_activeTrack, ix); } } setAudioTarget(current); } else { setVideoTarget(videoTarget() == m_activeTrack ? -1 : m_activeTrack); } } QVariantList TimelineController::audioTarget() const { QVariantList audioTracks; QMapIterator i(m_model->m_audioTarget); while (i.hasNext()) { i.next(); audioTracks << i.key(); } return audioTracks; } QVariantList TimelineController::lastAudioTarget() const { QVariantList audioTracks; QMapIterator i(m_lastAudioTarget); while (i.hasNext()) { i.next(); audioTracks << i.key(); } return audioTracks; } const QString TimelineController::audioTargetName(int tid) const { if (m_model->m_audioTarget.contains(tid) && m_model->m_binAudioTargets.size() > 1) { int streamIndex = m_model->m_audioTarget.value(tid); if (m_model->m_binAudioTargets.contains(streamIndex)) { QString targetName = m_model->m_binAudioTargets.value(streamIndex); return targetName.isEmpty() ? QChar('x') : targetName.section(QLatin1Char('|'), 0, 0); } else { qDebug()<<"STREAM INDEX NOT IN TARGET : "<m_binAudioTargets; } } else { qDebug()<<"TRACK NOT IN TARGET : "<m_audioTarget.keys(); } return QString(); } int TimelineController::videoTarget() const { return m_model->m_videoTarget; } int TimelineController::hasAudioTarget() const { return m_hasAudioTarget; } int TimelineController::clipTargets() const { return m_model->m_binAudioTargets.size(); } bool TimelineController::hasVideoTarget() const { return m_hasVideoTarget; } bool TimelineController::autoScroll() const { return KdenliveSettings::autoscroll(); } void TimelineController::resetTrackHeight() { int tracksCount = m_model->getTracksCount(); for (int track = tracksCount - 1; track >= 0; track--) { int trackIx = m_model->getTrackIndexFromPosition(track); m_model->getTrackById(trackIx)->setProperty(QStringLiteral("kdenlive:trackheight"), QString::number(KdenliveSettings::trackheight())); } QModelIndex modelStart = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(0)); QModelIndex modelEnd = m_model->makeTrackIndexFromID(m_model->getTrackIndexFromPosition(tracksCount - 1)); m_model->dataChanged(modelStart, modelEnd, {TimelineModel::HeightRole}); } void TimelineController::selectAll() { std::unordered_set ids; for (auto clp : m_model->m_allClips) { ids.insert(clp.first); } for (auto clp : m_model->m_allCompositions) { ids.insert(clp.first); } m_model->requestSetSelection(ids); } void TimelineController::selectCurrentTrack() { std::unordered_set ids; for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allClips) { ids.insert(clp.first); } for (auto clp : m_model->getTrackById_const(m_activeTrack)->m_allCompositions) { ids.insert(clp.first); } m_model->requestSetSelection(ids); } void TimelineController::pasteEffects(int targetId) { std::unordered_set targetIds; if (targetId == -1) { std::unordered_set sel = m_model->getCurrentSelection(); if (sel.empty()) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); } for (int s : sel) { if (m_model->isGroup(s)) { std::unordered_set sub = m_model->m_groups->getLeaves(s); for (int current_id : sub) { if (m_model->isClip(current_id)) { targetIds.insert(current_id); } } } else if (m_model->isClip(s)) { targetIds.insert(s); } } } else { if (m_model->m_groups->isInGroup(targetId)) { targetId = m_model->m_groups->getRootId(targetId); } if (m_model->isGroup(targetId)) { std::unordered_set sub = m_model->m_groups->getLeaves(targetId); for (int current_id : sub) { if (m_model->isClip(current_id)) { targetIds.insert(current_id); } } } else if (m_model->isClip(targetId)) { targetIds.insert(targetId); } } if (targetIds.empty()) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); } QClipboard *clipboard = QApplication::clipboard(); QString txt = clipboard->text(); if (txt.isEmpty()) { pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500); return; } QDomDocument copiedItems; copiedItems.setContent(txt); if (copiedItems.documentElement().tagName() != QLatin1String("kdenlive-scene")) { pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500); return; } QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip")); if (clips.isEmpty()) { pCore->displayMessage(i18n("No information in clipboard"), InformationMessage, 500); return; } std::function undo = []() { return true; }; std::function redo = []() { return true; }; QDomElement effects = clips.at(0).firstChildElement(QStringLiteral("effects")); effects.setAttribute(QStringLiteral("parentIn"), clips.at(0).toElement().attribute(QStringLiteral("in"))); for (int i = 1; i < clips.size(); i++) { QDomElement subeffects = clips.at(i).firstChildElement(QStringLiteral("effects")); QDomNodeList subs = subeffects.childNodes(); while (!subs.isEmpty()) { subs.at(0).toElement().setAttribute(QStringLiteral("parentIn"), clips.at(i).toElement().attribute(QStringLiteral("in"))); effects.appendChild(subs.at(0)); } } int insertedEffects = 0; for (int target : targetIds) { std::shared_ptr destStack = m_model->getClipEffectStackModel(target); if (destStack->fromXml(effects, undo, redo)) { insertedEffects++; } } if (insertedEffects > 0) { pCore->pushUndo(undo, redo, i18n("Paste effects")); } else { pCore->displayMessage(i18n("Cannot paste effect on selected clip"), InformationMessage, 500); undo(); } } double TimelineController::fps() const { return pCore->getCurrentFps(); } void TimelineController::editItemDuration(int id) { if (id == -1) { id = m_root->property("mainItemId").toInt(); if (id == -1) { std::unordered_set sel = m_model->getCurrentSelection(); if (!sel.empty()) { id = *sel.begin(); } if (id == -1) { pCore->displayMessage(i18n("No clip selected"), InformationMessage, 500); return; } } } if (id == -1 || !m_model->isItem(id)) { pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500); return; } int start = m_model->getItemPosition(id); int in = 0; int duration = m_model->getItemPlaytime(id); int maxLength = -1; bool isComposition = false; if (m_model->isClip(id)) { in = m_model->getClipIn(id); std::shared_ptr clip = pCore->bin()->getBinClip(getClipBinId(id)); if (clip && clip->hasLimitedDuration()) { maxLength = clip->getProducerDuration(); } } else if (m_model->isComposition(id)) { // nothing to do isComposition = true; } else { pCore->displayMessage(i18n("No item to edit"), InformationMessage, 500); return; } int trackId = m_model->getItemTrackId(id); int maxFrame = qMax(0, start + duration + (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, true) : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, true))); int minFrame = qMax(0, in - (isComposition ? m_model->getTrackById(trackId)->getBlankSizeNearComposition(id, false) : m_model->getTrackById(trackId)->getBlankSizeNearClip(id, false))); int partner = isComposition ? -1 : m_model->getClipSplitPartner(id); QPointer dialog = new ClipDurationDialog(id, pCore->currentDoc()->timecode(), start, minFrame, in, in + duration, maxLength, maxFrame, qApp->activeWindow()); if (dialog->exec() == QDialog::Accepted) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; int newPos = dialog->startPos().frames(pCore->getCurrentFps()); int newIn = dialog->cropStart().frames(pCore->getCurrentFps()); int newDuration = dialog->duration().frames(pCore->getCurrentFps()); bool result = true; if (newPos < start) { if (!isComposition) { result = m_model->requestClipMove(id, trackId, newPos, true, true, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, true, undo, redo); } } else { result = m_model->requestCompositionMove(id, trackId, m_model->m_allCompositions[id]->getForcedTrack(), newPos, true, true, undo, redo); } if (result && newIn != in) { m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo); } } if (newDuration != duration + (in - newIn)) { result = result && m_model->requestItemResize(id, newDuration, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, newDuration, false, true, undo, redo); } } } else { // perform resize first if (newIn != in) { result = m_model->requestItemResize(id, duration + (in - newIn), false, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, duration + (in - newIn), false, true, undo, redo); } } if (newDuration != duration + (in - newIn)) { result = result && m_model->requestItemResize(id, newDuration, start == newPos, true, undo, redo); if (result && partner > -1) { result = m_model->requestItemResize(partner, newDuration, start == newPos, true, undo, redo); } } if (start != newPos || newIn != in) { if (!isComposition) { result = result && m_model->requestClipMove(id, trackId, newPos, true, true, true, true, undo, redo); if (result && partner > -1) { result = m_model->requestClipMove(partner, m_model->getItemTrackId(partner), newPos, true, true, true, true, undo, redo); } } else { result = result && m_model->requestCompositionMove(id, trackId, m_model->m_allCompositions[id]->getForcedTrack(), newPos, true, true, undo, redo); } } } if (result) { pCore->pushUndo(undo, redo, i18n("Edit item")); } else { undo(); } } } void TimelineController::updateClipActions() { if (m_model->getCurrentSelection().empty()) { for (QAction *act : clipActions) { act->setEnabled(false); } emit timelineClipSelected(false); // nothing selected emit showItemEffectStack(QString(), nullptr, QSize(), false); return; } std::shared_ptr clip(nullptr); int item = *m_model->getCurrentSelection().begin(); if (m_model->getCurrentSelection().size() == 1 && (m_model->isClip(item) || m_model->isComposition(item))) { showAsset(item); } if (m_model->isClip(item)) { clip = m_model->getClipPtr(item); } bool enablePositionActions = positionIsInItem(item); for (QAction *act : clipActions) { bool enableAction = true; const QChar actionData = act->data().toChar(); if (actionData == QLatin1Char('G')) { enableAction = isInSelection(item) && m_model->getCurrentSelection().size() > 1; } else if (actionData == QLatin1Char('U')) { enableAction = m_model->m_groups->isInGroup(item); } else if (actionData == QLatin1Char('A')) { if (m_model->m_groups->isInGroup(item) && m_model->m_groups->getType(m_model->m_groups->getRootId(item)) == GroupType::AVSplit) { enableAction = true; } else { enableAction = clip && clip->clipState() == PlaylistState::AudioOnly; } } else if (actionData == QLatin1Char('V')) { enableAction = clip && clip->clipState() == PlaylistState::VideoOnly; } else if (actionData == QLatin1Char('D')) { enableAction = clip && clip->clipState() == PlaylistState::Disabled; } else if (actionData == QLatin1Char('E')) { enableAction = clip && clip->clipState() != PlaylistState::Disabled; } else if (actionData == QLatin1Char('X') || actionData == QLatin1Char('S')) { enableAction = clip && clip->canBeVideo() && clip->canBeAudio(); if (enableAction && actionData == QLatin1Char('S')) { act->setText(clip->clipState() == PlaylistState::AudioOnly ? i18n("Split video") : i18n("Split audio")); } } else if (actionData == QLatin1Char('W')) { enableAction = clip != nullptr; if (enableAction) { act->setText(clip->clipState() == PlaylistState::Disabled ? i18n("Enable clip") : i18n("Disable clip")); } } else if (actionData == QLatin1Char('C') && clip == nullptr) { enableAction = false; } else if (actionData == QLatin1Char('P')) { // Position actions should stay enabled in clip monitor //enableAction = enablePositionActions; } act->setEnabled(enableAction); } emit timelineClipSelected(clip != nullptr); } const QString TimelineController::getAssetName(const QString &assetId, bool isTransition) { return isTransition ? TransitionsRepository::get()->getName(assetId) : EffectsRepository::get()->getName(assetId); } void TimelineController::grabCurrent() { std::unordered_set ids = m_model->getCurrentSelection(); std::unordered_set items_list; int mainId = -1; for (int i : ids) { if (m_model->isGroup(i)) { std::unordered_set children = m_model->m_groups->getLeaves(i); items_list.insert(children.begin(), children.end()); } else { items_list.insert(i); } } for (int id : items_list) { if (mainId == -1 && m_model->getItemTrackId(id) == m_activeTrack) { mainId = id; continue; } if (m_model->isClip(id)) { std::shared_ptr clip = m_model->getClipPtr(id); clip->setGrab(!clip->isGrabbed()); } else if (m_model->isComposition(id)) { std::shared_ptr clip = m_model->getCompositionPtr(id); clip->setGrab(!clip->isGrabbed()); } } if (mainId > -1) { if (m_model->isClip(mainId)) { std::shared_ptr clip = m_model->getClipPtr(mainId); clip->setGrab(!clip->isGrabbed()); } else if (m_model->isComposition(mainId)) { std::shared_ptr clip = m_model->getCompositionPtr(mainId); clip->setGrab(!clip->isGrabbed()); } } } int TimelineController::getItemMovingTrack(int itemId) const { if (m_model->isClip(itemId)) { int trackId = -1; if (m_model->m_editMode != TimelineMode::NormalEdit) { trackId = m_model->m_allClips[itemId]->getFakeTrackId(); } return trackId < 0 ? m_model->m_allClips[itemId]->getCurrentTrackId() : trackId; } return m_model->m_allCompositions[itemId]->getCurrentTrackId(); } bool TimelineController::endFakeMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline) { Q_ASSERT(m_model->m_allClips.count(clipId) > 0); int trackId = m_model->m_allClips[clipId]->getFakeTrackId(); if (m_model->getClipPosition(clipId) == position && m_model->getClipTrackId(clipId) == trackId) { qDebug() << "* * ** END FAKE; NO MOVE RQSTED"; return true; } if (m_model->m_groups->isInGroup(clipId)) { // element is in a group. int groupId = m_model->m_groups->getRootId(clipId); int current_trackId = m_model->getClipTrackId(clipId); int track_pos1 = m_model->getTrackPosition(trackId); int track_pos2 = m_model->getTrackPosition(current_trackId); int delta_track = track_pos1 - track_pos2; int delta_pos = position - m_model->m_allClips[clipId]->getPosition(); return endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo); } qDebug() << "//////\n//////\nENDING FAKE MNOVE: " << trackId << ", POS: " << position; std::function undo = []() { return true; }; std::function redo = []() { return true; }; int startPos = m_model->getClipPosition(clipId); int duration = m_model->getClipPlaytime(clipId); int currentTrack = m_model->m_allClips[clipId]->getCurrentTrackId(); bool res = true; if (currentTrack > -1) { res = m_model->getTrackById(currentTrack)->requestClipDeletion(clipId, updateView, invalidateTimeline, undo, redo, false, false); } if (m_model->m_editMode == TimelineMode::OverwriteEdit) { res = res && TimelineFunctions::liftZone(m_model, trackId, QPoint(position, position + duration), undo, redo); } else if (m_model->m_editMode == TimelineMode::InsertEdit) { // Remove space from previous location if (currentTrack > -1) { res = res && TimelineFunctions::removeSpace(m_model, {startPos,startPos + duration}, undo, redo, {currentTrack}, false); } int startClipId = m_model->getClipByPosition(trackId, position); if (startClipId > -1) { // There is a clip at insert pos if (m_model->getClipPosition(startClipId) != position) { // If position is in the middle of the clip, cut it res = res && TimelineFunctions::requestClipCut(m_model, startClipId, position, undo, redo); } } res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(position, position + duration), undo, redo, {trackId}); } res = res && m_model->getTrackById(trackId)->requestClipInsertion(clipId, position, updateView, invalidateTimeline, undo, redo); if (res) { // Terminate fake move if (m_model->isClip(clipId)) { m_model->m_allClips[clipId]->setFakeTrackId(-1); } if (logUndo) { pCore->pushUndo(undo, redo, i18n("Move item")); } } else { qDebug() << "//// FAKE FAILED"; undo(); } return res; } bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = endFakeGroupMove(clipId, groupId, delta_track, delta_pos, updateView, logUndo, undo, redo); if (res && logUndo) { // Terminate fake move if (m_model->isClip(clipId)) { m_model->m_allClips[clipId]->setFakeTrackId(-1); } pCore->pushUndo(undo, redo, i18n("Move group")); } return res; } bool TimelineController::endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo) { Q_ASSERT(m_model->m_allGroups.count(groupId) > 0); bool ok = true; auto all_items = m_model->m_groups->getLeaves(groupId); Q_ASSERT(all_items.size() > 1); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // Sort clips. We need to delete from right to left to avoid confusing the view std::vector sorted_clips{std::make_move_iterator(std::begin(all_items)), std::make_move_iterator(std::end(all_items))}; std::sort(sorted_clips.begin(), sorted_clips.end(), [this](const int &clipId1, const int &clipId2) { int p1 = m_model->isClip(clipId1) ? m_model->m_allClips[clipId1]->getPosition() : m_model->m_allCompositions[clipId1]->getPosition(); int p2 = m_model->isClip(clipId2) ? m_model->m_allClips[clipId2]->getPosition() : m_model->m_allCompositions[clipId2]->getPosition(); return p2 < p1; }); // 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. // This way, we ensure that no conflict will arise with clips inside the group being moved // First, remove clips int audio_delta, video_delta; audio_delta = video_delta = delta_track; int master_trackId = m_model->getItemTrackId(clipId); if (m_model->getTrackById_const(master_trackId)->isAudioTrack()) { // Master clip is audio, so reverse delta for video clips video_delta = -delta_track; } else { audio_delta = -delta_track; } int min = -1; int max = -1; std::unordered_map old_track_ids, old_position, old_forced_track, new_track_ids; for (int item : sorted_clips) { int old_trackId = m_model->getItemTrackId(item); old_track_ids[item] = old_trackId; if (old_trackId != -1) { bool updateThisView = true; if (m_model->isClip(item)) { int current_track_position = m_model->getTrackPosition(old_trackId); int d = m_model->getTrackById_const(old_trackId)->isAudioTrack() ? audio_delta : video_delta; int target_track_position = current_track_position + d; auto it = m_model->m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); new_track_ids[item] = target_track; old_position[item] = m_model->m_allClips[item]->getPosition(); int duration = m_model->m_allClips[item]->getPlaytime(); min = min < 0 ? old_position[item] + delta_pos : qMin(min, old_position[item] + delta_pos); max = max < 0 ? old_position[item] + delta_pos + duration : qMax(max, old_position[item] + delta_pos + duration); ok = ok && m_model->getTrackById(old_trackId)->requestClipDeletion(item, updateThisView, finalMove, undo, redo, false, false); if (m_model->m_editMode == TimelineMode::InsertEdit) { // Lift space left by removed clip ok = ok && TimelineFunctions::removeSpace(m_model, {old_position[item],old_position[item] + duration}, undo, redo, {old_trackId}, false); } } else { // ok = ok && getTrackById(old_trackId)->requestCompositionDeletion(item, updateThisView, local_undo, local_redo); old_position[item] = m_model->m_allCompositions[item]->getPosition(); old_forced_track[item] = m_model->m_allCompositions[item]->getForcedTrack(); } if (!ok) { bool undone = undo(); Q_ASSERT(undone); return false; } } } bool res = true; if (m_model->m_editMode == TimelineMode::OverwriteEdit) { for (int item : sorted_clips) { if (m_model->isClip(item) && new_track_ids.count(item) > 0) { int target_track = new_track_ids[item]; int target_position = old_position[item] + delta_pos; int duration = m_model->m_allClips[item]->getPlaytime(); res = res && TimelineFunctions::liftZone(m_model, target_track, QPoint(target_position, target_position + duration), undo, redo); } } } else if (m_model->m_editMode == TimelineMode::InsertEdit) { QVector processedTracks; for (int item : sorted_clips) { int target_track = new_track_ids[item]; if (processedTracks.contains(target_track)) { // already processed continue; } processedTracks << target_track; int target_position = min; int startClipId = m_model->getClipByPosition(target_track, target_position); if (startClipId > -1) { // There is a clip, cut res = res && TimelineFunctions::requestClipCut(m_model, startClipId, target_position, undo, redo); } } res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(min, max), undo, redo, processedTracks); } for (int item : sorted_clips) { if (m_model->isClip(item)) { int target_track = new_track_ids[item]; int target_position = old_position[item] + delta_pos; ok = ok && m_model->requestClipMove(item, target_track, target_position, true, updateView, finalMove, finalMove, undo, redo); } else { // ok = ok && requestCompositionMove(item, target_track, old_forced_track[item], target_position, updateThisView, local_undo, local_redo); } if (!ok) { bool undone = undo(); Q_ASSERT(undone); return false; } } return true; } QStringList TimelineController::getThumbKeys() { QStringList result; for (const auto &clp : m_model->m_allClips) { const QString binId = getClipBinId(clp.first); std::shared_ptr binClip = pCore->bin()->getBinClip(binId); result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getIn()) + QStringLiteral(".png"); result << binClip->hash() + QLatin1Char('#') + QString::number(clp.second->getOut()) + QStringLiteral(".png"); } result.removeDuplicates(); return result; } bool TimelineController::isInSelection(int itemId) { return m_model->getCurrentSelection().count(itemId) > 0; } bool TimelineController::exists(int itemId) { return m_model->isClip(itemId) || m_model->isComposition(itemId); } void TimelineController::slotMultitrackView(bool enable, bool refresh) { QStringList trackNames = TimelineFunctions::enableMultitrackView(m_model, enable, refresh); pCore->monitorManager()->projectMonitor()->slotShowEffectScene(enable ? MonitorSplitTrack : MonitorSceneNone, false, QVariant(trackNames)); QObject::disconnect( m_connection ); if (enable) { connect(m_model.get(), &TimelineItemModel::trackVisibilityChanged, this, &TimelineController::updateMultiTrack, Qt::UniqueConnection); m_connection = connect(this, &TimelineController::activeTrackChanged, [this]() { int ix = 0; auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); ++it; if (target_track == m_activeTrack) { break; } if (m_model->getTrackById_const(target_track)->isAudioTrack() || m_model->getTrackById_const(target_track)->isHidden()) { continue; } ++ix; } pCore->monitorManager()->projectMonitor()->updateMultiTrackView(ix); }); } else { disconnect(m_model.get(), &TimelineItemModel::trackVisibilityChanged, this, &TimelineController::updateMultiTrack); } } void TimelineController::updateMultiTrack() { QStringList trackNames = TimelineFunctions::enableMultitrackView(m_model, true, true); pCore->monitorManager()->projectMonitor()->slotShowEffectScene(MonitorSplitTrack, false, QVariant(trackNames)); } void TimelineController::activateTrackAndSelect(int trackPosition) { int tid = -1; int ix = 0; auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { tid = (*it)->getId(); ++it; if (m_model->getTrackById_const(tid)->isAudioTrack() || m_model->getTrackById_const(tid)->isHidden()) { continue; } if (trackPosition == ix) { break; } ++ix; } if (tid > -1) { m_activeTrack = tid; emit activeTrackChanged(); selectCurrentItem(ObjectType::TimelineClip, true); } } void TimelineController::saveTimelineSelection(const QDir &targetDir) { TimelineFunctions::saveTimelineSelection(m_model, m_model->getCurrentSelection(), targetDir); } void TimelineController::addEffectKeyframe(int cid, int frame, double val) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->addEffectKeyFrame(frame, val); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->addKeyframe(frame, val); } } void TimelineController::removeEffectKeyframe(int cid, int frame) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->removeKeyFrame(frame); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->removeKeyframe(GenTime(frame, pCore->getCurrentFps())); } } void TimelineController::updateEffectKeyframe(int cid, int oldFrame, int newFrame, const QVariant &normalizedValue) { if (m_model->isClip(cid)) { std::shared_ptr destStack = m_model->getClipEffectStackModel(cid); destStack->updateKeyFrame(oldFrame, newFrame, normalizedValue); } else if (m_model->isComposition(cid)) { std::shared_ptr listModel = m_model->m_allCompositions[cid]->getKeyframeModel(); listModel->updateKeyframe(GenTime(oldFrame, pCore->getCurrentFps()), GenTime(newFrame, pCore->getCurrentFps()), normalizedValue); } } bool TimelineController::darkBackground() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.background(KColorScheme::NormalBackground).color().value() < 0.5; } QColor TimelineController::videoColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::LinkText).color(); } QColor TimelineController::targetColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); QColor base = scheme.foreground(KColorScheme::PositiveText).color(); QColor high = QApplication::palette().highlightedText().color(); double factor = 0.3; QColor res = QColor(qBound(0, base.red() + (int)(factor*(high.red() - 128)), 255), qBound(0, base.green() + (int)(factor*(high.green() - 128)), 255), qBound(0, base.blue() + (int)(factor*(high.blue() - 128)), 255), 255); return res; } QColor TimelineController::targetTextColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.background(KColorScheme::PositiveBackground).color(); } QColor TimelineController::audioColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::PositiveText).color(); } QColor TimelineController::titleColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); QColor base = scheme.foreground(KColorScheme::LinkText).color(); QColor high = scheme.foreground(KColorScheme::NegativeText).color(); QColor title = QColor(qBound(0, base.red() + (int)(high.red() - 128), 255), qBound(0, base.green() + (int)(high.green() - 128), 255), qBound(0, base.blue() + (int)(high.blue() - 128), 255), 255); return title; } QColor TimelineController::imageColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::NeutralText).color(); } QColor TimelineController::slideshowColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); QColor base = scheme.foreground(KColorScheme::LinkText).color(); QColor high = scheme.foreground(KColorScheme::NeutralText).color(); QColor slide = QColor(qBound(0, base.red() + (int)(high.red() - 128), 255), qBound(0, base.green() + (int)(high.green() - 128), 255), qBound(0, base.blue() + (int)(high.blue() - 128), 255), 255); return slide; } QColor TimelineController::lockedColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::NegativeText).color(); } QColor TimelineController::groupColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup()); return scheme.foreground(KColorScheme::ActiveText).color(); } QColor TimelineController::selectionColor() const { KColorScheme scheme(QApplication::palette().currentColorGroup(), KColorScheme::Complementary); return scheme.foreground(KColorScheme::NeutralText).color(); } void TimelineController::switchRecording(int trackId) { if (!pCore->isMediaCapturing()) { qDebug() << "start recording" << trackId; if (!m_model->isTrack(trackId)) { qDebug() << "ERROR: Starting to capture on invalid track " << trackId; } if (m_model->getTrackById_const(trackId)->isLocked()) { pCore->displayMessage(i18n("Impossible to capture on a locked track"), ErrorMessage, 500); return; } m_recordStart.first = pCore->getTimelinePosition(); m_recordTrack = trackId; int maximumSpace = m_model->getTrackById_const(trackId)->getBlankEnd(m_recordStart.first); if (maximumSpace == INT_MAX) { m_recordStart.second = 0; } else { m_recordStart.second = maximumSpace - m_recordStart.first; if (m_recordStart.second < 8) { pCore->displayMessage(i18n("Impossible to capture here: the capture could override clips. Please remove clips after the current position or " "choose a different track"), ErrorMessage, 500); return; } } pCore->monitorManager()->slotSwitchMonitors(false); pCore->startMediaCapture(trackId, true, false); pCore->monitorManager()->slotPlay(); } else { pCore->stopMediaCapture(trackId, true, false); pCore->monitorManager()->slotPause(); } } void TimelineController::urlDropped(QStringList droppedFile, int frame, int tid) { m_recordTrack = tid; m_recordStart = {frame, -1}; qDebug()<<"=== GOT DROPPED FILED: "< callBack = [this](const QString &binId) { int id = -1; if (m_recordTrack == -1) { return; } qDebug() << "callback " << binId << " " << m_recordTrack << ", MAXIMUM SPACE: " << m_recordStart.second; if (m_recordStart.second > 0) { // Limited space on track std::shared_ptr clip = pCore->bin()->getBinClip(binId); if (!clip) { return; } int out = qMin((int)clip->frameDuration() - 1, m_recordStart.second - 1); QString binClipId = QString("%1/%2/%3").arg(binId).arg(0).arg(out); m_model->requestClipInsertion(binClipId, m_recordTrack, m_recordStart.first, id, true, true, false); } else { m_model->requestClipInsertion(binId, m_recordTrack, m_recordStart.first, id, true, true, false); } }; QString binId = ClipCreator::createClipFromFile(recordedFile, pCore->projectItemModel()->getRootFolder()->clipId(), pCore->projectItemModel(), undo, redo, callBack); if (binId != QStringLiteral("-1")) { pCore->pushUndo(undo, redo, i18n("Record audio")); } } void TimelineController::updateVideoTarget() { if (videoTarget() > -1) { m_lastVideoTarget = videoTarget(); m_videoTargetActive = true; emit lastVideoTargetChanged(); } else { m_videoTargetActive = false; } } void TimelineController::updateAudioTarget() { if (!audioTarget().isEmpty()) { m_lastAudioTarget = m_model->m_audioTarget; m_audioTargetActive = true; emit lastAudioTargetChanged(); } else { m_audioTargetActive = false; } } bool TimelineController::hasActiveTracks() const { auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (m_model->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { return true; } ++it; } return false; } void TimelineController::showMasterEffects() { emit showItemEffectStack(i18n("Master effects"), m_model->getMasterEffectStackModel(), pCore->getCurrentFrameSize(), false); } bool TimelineController::refreshIfVisible(int cid) { auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { int target_track = (*it)->getId(); if (m_model->getTrackById_const(target_track)->isAudioTrack() || m_model->getTrackById_const(target_track)->isHidden()) { ++it; continue; } int child = m_model->getClipByPosition(target_track, pCore->getTimelinePosition()); if (child > 0) { if (m_model->m_allClips[child]->binId().toInt() == cid) { return true; } } ++it; } return false; } void TimelineController::collapseActiveTrack() { if (m_activeTrack == -1) { return; } int collapsed = m_model->getTrackProperty(m_activeTrack, QStringLiteral("kdenlive:collapsed")).toInt(); m_model->setTrackProperty(m_activeTrack, QStringLiteral("kdenlive:collapsed"), collapsed > 0 ? QStringLiteral("0") : QStringLiteral("5")); } void TimelineController::setActiveTrackProperty(const QString &name, const QString &value) { if (m_activeTrack > -1) { m_model->setTrackProperty(m_activeTrack, name, value); } } bool TimelineController::isActiveTrackAudio() const { if (m_activeTrack > -1) { if (m_model->getTrackById_const(m_activeTrack)->isAudioTrack()) { return true; } } return false; } const QVariant TimelineController::getActiveTrackProperty(const QString &name) const { if (m_activeTrack > -1) { return m_model->getTrackProperty(m_activeTrack, name); } return QVariant(); } void TimelineController::expandActiveClip() { std::unordered_set ids = m_model->getCurrentSelection(); std::unordered_set items_list; for (int i : ids) { if (m_model->isGroup(i)) { std::unordered_set children = m_model->m_groups->getLeaves(i); items_list.insert(children.begin(), children.end()); } else { items_list.insert(i); } } m_model->requestClearSelection(); bool result = true; for (int id : items_list) { if (result && m_model->isClip(id)) { std::shared_ptr clip = m_model->getClipPtr(id); if (clip->clipType() == ClipType::Playlist) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; if (m_model->m_groups->isInGroup(id)) { int targetRoot = m_model->m_groups->getRootId(id); if (m_model->isGroup(targetRoot)) { m_model->requestClipUngroup(targetRoot, undo, redo); } } int pos = clip->getPosition(); QDomDocument doc = TimelineFunctions::extractClip(m_model, id, getClipBinId(id)); m_model->requestClipDeletion(id, undo, redo); result = TimelineFunctions::pasteClips(m_model, doc.toString(), m_activeTrack, pos, undo, redo); if (result) { pCore->pushUndo(undo, redo, i18n("Expand clip")); } else { undo(); pCore->displayMessage(i18n("Could not expand clip"), InformationMessage, 500); } } } } } QMap TimelineController::getCurrentTargets(int trackId, int &activeTargetStream) { if (m_model->m_binAudioTargets.size() < 2) { activeTargetStream = -1; return QMap (); } if (m_model->m_audioTarget.contains(trackId)) { activeTargetStream = m_model->m_audioTarget.value(trackId); } else { activeTargetStream = -1; } return m_model->m_binAudioTargets; } void TimelineController::addTracks(int videoTracks, int audioTracks) { bool result = false; int total = videoTracks + audioTracks; Fun undo = []() { return true; }; Fun redo = []() { return true; }; for (int ix = 0; videoTracks + audioTracks > 0; ++ix) { int newTid; if (audioTracks > 0) { result = m_model->requestTrackInsertion(0, newTid, QString(), true, undo, redo); audioTracks--; } else { result = m_model->requestTrackInsertion(-1, newTid, QString(), false, undo, redo); videoTracks--; } if (result) { m_model->setTrackProperty(newTid, "kdenlive:timeline_active", QStringLiteral("1")); } else { break; } } if (result) { pCore->pushUndo(undo, redo, i18np("Insert Track", "Insert Tracks", total)); } else { pCore->displayMessage(i18n("Could not insert track"), InformationMessage, 500); undo(); } }