diff --git a/src/core.cpp b/src/core.cpp index 1c8de5367..438762754 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -1,900 +1,905 @@ /* Copyright (C) 2014 Till Theato 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 3 of the License, or (at your option) any later version. */ #include "core.h" #include "bin/bin.h" #include "bin/projectitemmodel.h" #include "capture/mediacapture.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "jobs/jobmanager.h" #include "kdenlive_debug.h" #include "kdenlivesettings.h" #include "library/librarywidget.h" #include "audiomixer/mixermanager.hpp" #include "mainwindow.h" #include "mltconnection.h" #include "mltcontroller/clipcontroller.h" #include "monitor/monitormanager.h" #include "profiles/profilemodel.hpp" #include "profiles/profilerepository.hpp" #include "project/projectmanager.h" #include "timeline2/model/timelineitemmodel.hpp" #include "timeline2/view/timelinecontroller.h" #include "timeline2/view/timelinewidget.h" #include #include #include #include #include #include #include #ifdef Q_OS_MAC #include #endif std::unique_ptr Core::m_self; Core::Core() : m_thumbProfile(nullptr) , m_capture(new MediaCapture(this)) { } void Core::prepareShutdown() { m_guiConstructed = false; m_mainWindow->getCurrentTimeline()->controller()->prepareClose(); projectItemModel()->blockSignals(true); QThreadPool::globalInstance()->clear(); } Core::~Core() { qDebug() << "deleting core"; if (m_monitorManager) { delete m_monitorManager; } // delete m_binWidget; if (m_projectManager) { delete m_projectManager; } ClipController::mediaUnavailable.reset(); } void Core::build(bool isAppImage, const QString &MltPath) { if (m_self) { return; } m_self.reset(new Core()); m_self->initLocale(); qRegisterMetaType("audioShortVector"); qRegisterMetaType>("QVector"); qRegisterMetaType("MessageType"); qRegisterMetaType("stringMap"); qRegisterMetaType("audioByteArray"); qRegisterMetaType>("QList"); qRegisterMetaType>("std::shared_ptr"); qRegisterMetaType>(); qRegisterMetaType("QDomElement"); qRegisterMetaType("requestClipInfo"); if (isAppImage) { QString appPath = qApp->applicationDirPath(); KdenliveSettings::setFfmpegpath(QDir::cleanPath(appPath + QStringLiteral("/ffmpeg"))); KdenliveSettings::setFfplaypath(QDir::cleanPath(appPath + QStringLiteral("/ffplay"))); KdenliveSettings::setFfprobepath(QDir::cleanPath(appPath + QStringLiteral("/ffprobe"))); KdenliveSettings::setRendererpath(QDir::cleanPath(appPath + QStringLiteral("/melt"))); MltConnection::construct(QDir::cleanPath(appPath + QStringLiteral("/../share/mlt/profiles"))); } else { // Open connection with Mlt MltConnection::construct(MltPath); } // load the profile from disk ProfileRepository::get()->refresh(); // load default profile m_self->m_profile = KdenliveSettings::default_profile(); if (m_self->m_profile.isEmpty()) { m_self->m_profile = ProjectManager::getDefaultProjectFormat(); KdenliveSettings::setDefault_profile(m_self->m_profile); } // Init producer shown for unavailable media // TODO make it a more proper image, it currently causes a crash on exit ClipController::mediaUnavailable = std::make_shared(ProfileRepository::get()->getProfile(m_self->m_profile)->profile(), "color:blue"); ClipController::mediaUnavailable->set("length", 99999999); m_self->m_projectItemModel = ProjectItemModel::construct(); // Job manager must be created before bin to correctly connect m_self->m_jobManager.reset(new JobManager(m_self.get())); } void Core::initGUI(const QUrl &Url, const QString &clipsToLoad) { m_profile = KdenliveSettings::default_profile(); m_currentProfile = m_profile; profileChanged(); m_mainWindow = new MainWindow(); m_guiConstructed = true; QStringList styles = QQuickStyle::availableStyles(); if (styles.contains(QLatin1String("org.kde.desktop"))) { QQuickStyle::setStyle("org.kde.desktop"); } else if (styles.contains(QLatin1String("Fusion"))) { QQuickStyle::setStyle("Fusion"); } connect(this, &Core::showConfigDialog, m_mainWindow, &MainWindow::slotPreferences); // load default profile and ask user to select one if not found. if (m_profile.isEmpty()) { m_profile = ProjectManager::getDefaultProjectFormat(); profileChanged(); KdenliveSettings::setDefault_profile(m_profile); } if (!ProfileRepository::get()->profileExists(m_profile)) { KMessageBox::sorry(m_mainWindow, i18n("The default profile of Kdenlive is not set or invalid, press OK to set it to a correct value.")); // TODO this simple widget should be improved and probably use profileWidget // we get the list of profiles QVector> all_profiles = ProfileRepository::get()->getAllProfiles(); QStringList all_descriptions; for (const auto &profile : all_profiles) { all_descriptions << profile.first; } // ask the user bool ok; QString item = QInputDialog::getItem(m_mainWindow, i18n("Select Default Profile"), i18n("Profile:"), all_descriptions, 0, false, &ok); if (ok) { ok = false; for (const auto &profile : all_profiles) { if (profile.first == item) { m_profile = profile.second; ok = true; } } } if (!ok) { KMessageBox::error( m_mainWindow, i18n("The given profile is invalid. We default to the profile \"dv_pal\", but you can change this from Kdenlive's settings panel")); m_profile = QStringLiteral("dv_pal"); } KdenliveSettings::setDefault_profile(m_profile); profileChanged(); } m_projectManager = new ProjectManager(this); m_binWidget = new Bin(m_projectItemModel, m_mainWindow); m_library = new LibraryWidget(m_projectManager, m_mainWindow); m_mixerWidget = new MixerManager(m_mainWindow); connect(m_library, SIGNAL(addProjectClips(QList)), m_binWidget, SLOT(droppedUrls(QList))); connect(this, &Core::updateLibraryPath, m_library, &LibraryWidget::slotUpdateLibraryPath); connect(m_capture.get(), &MediaCapture::recordStateChanged, m_mixerWidget, &MixerManager::recordStateChanged); connect(m_mixerWidget, &MixerManager::updateRecVolume, m_capture.get(), &MediaCapture::setAudioVolume); m_monitorManager = new MonitorManager(this); connect(m_monitorManager, &MonitorManager::cleanMixer, m_mixerWidget, &MixerManager::clearMixers); // Producer queue, creating MLT::Producers on request /* m_producerQueue = new ProducerQueue(m_binController); connect(m_producerQueue, &ProducerQueue::gotFileProperties, m_binWidget, &Bin::slotProducerReady); connect(m_producerQueue, &ProducerQueue::replyGetImage, m_binWidget, &Bin::slotThumbnailReady); connect(m_producerQueue, &ProducerQueue::requestProxy, [this](const QString &id){ m_binWidget->startJob(id, AbstractClipJob::PROXYJOB);}); connect(m_producerQueue, &ProducerQueue::removeInvalidClip, m_binWidget, &Bin::slotRemoveInvalidClip, Qt::DirectConnection); connect(m_producerQueue, SIGNAL(addClip(QString, QMap)), m_binWidget, SLOT(slotAddUrl(QString, QMap))); connect(m_binController.get(), SIGNAL(createThumb(QDomElement, QString, int)), m_producerQueue, SLOT(getFileProperties(QDomElement, QString, int))); connect(m_binWidget, &Bin::producerReady, m_producerQueue, &ProducerQueue::slotProcessingDone, Qt::DirectConnection); // TODO connect(m_producerQueue, SIGNAL(removeInvalidProxy(QString,bool)), m_binWidget, SLOT(slotRemoveInvalidProxy(QString,bool)));*/ m_mainWindow->init(); projectManager()->init(Url, clipsToLoad); if (qApp->isSessionRestored()) { // NOTE: we are restoring only one window, because Kdenlive only uses one MainWindow m_mainWindow->restore(1, false); } QMetaObject::invokeMethod(pCore->projectManager(), "slotLoadOnOpen", Qt::QueuedConnection); m_mainWindow->show(); QThreadPool::globalInstance()->setMaxThreadCount(qMin(4, QThreadPool::globalInstance()->maxThreadCount())); } void Core::buildLumaThumbs(const QStringList &values) { for (auto &entry : values) { if (MainWindow::m_lumacache.contains(entry)) { continue; } QImage pix(entry); if (!pix.isNull()) { MainWindow::m_lumacache.insert(entry, pix.scaled(50, 30, Qt::KeepAspectRatio, Qt::SmoothTransformation)); } } } std::unique_ptr &Core::self() { if (!m_self) { qDebug() << "Error : Core has not been created"; } return m_self; } MainWindow *Core::window() { return m_mainWindow; } ProjectManager *Core::projectManager() { return m_projectManager; } MonitorManager *Core::monitorManager() { return m_monitorManager; } Monitor *Core::getMonitor(int id) { if (id == Kdenlive::ClipMonitor) { return m_monitorManager->clipMonitor(); } return m_monitorManager->projectMonitor(); } Bin *Core::bin() { return m_binWidget; } void Core::selectBinClip(const QString &clipId, int frame, const QPoint &zone) { m_binWidget->selectClipById(clipId, frame, zone); } std::shared_ptr Core::jobManager() { return m_jobManager; } LibraryWidget *Core::library() { return m_library; } MixerManager *Core::mixer() { return m_mixerWidget; } void Core::initLocale() { QLocale systemLocale = QLocale(); #ifndef Q_OS_MAC setlocale(LC_NUMERIC, nullptr); #else setlocale(LC_NUMERIC_MASK, nullptr); #endif // localeconv()->decimal_point does not give reliable results on Windows #ifndef Q_OS_WIN char *separator = localeconv()->decimal_point; if (QString::fromUtf8(separator) != QChar(systemLocale.decimalPoint())) { // qCDebug(KDENLIVE_LOG)<<"------\n!!! system locale is not similar to Qt's locale... be prepared for bugs!!!\n------"; // HACK: There is a locale conflict, so set locale to C // Make sure to override exported values or it won't work qputenv("LANG", "C"); #ifndef Q_OS_MAC setlocale(LC_NUMERIC, "C"); #else setlocale(LC_NUMERIC_MASK, "C"); #endif systemLocale = QLocale::c(); } #endif systemLocale.setNumberOptions(QLocale::OmitGroupSeparator); QLocale::setDefault(systemLocale); } std::unique_ptr &Core::getMltRepository() { return MltConnection::self()->getMltRepository(); } std::unique_ptr &Core::getCurrentProfile() const { return ProfileRepository::get()->getProfile(m_currentProfile); } Mlt::Profile *Core::getProjectProfile() { if (!m_projectProfile) { m_projectProfile = std::make_unique(m_currentProfile.toStdString().c_str()); m_projectProfile->set_explicit(1); } return m_projectProfile.get(); } const QString &Core::getCurrentProfilePath() const { return m_currentProfile; } bool Core::setCurrentProfile(const QString &profilePath) { if (m_currentProfile == profilePath) { // no change required return true; } if (ProfileRepository::get()->profileExists(profilePath)) { m_currentProfile = profilePath; m_thumbProfile.reset(); if (m_projectProfile) { m_projectProfile->set_colorspace(getCurrentProfile()->colorspace()); m_projectProfile->set_frame_rate(getCurrentProfile()->frame_rate_num(), getCurrentProfile()->frame_rate_den()); m_projectProfile->set_height(getCurrentProfile()->height()); m_projectProfile->set_progressive(getCurrentProfile()->progressive()); m_projectProfile->set_sample_aspect(getCurrentProfile()->sample_aspect_num(), getCurrentProfile()->sample_aspect_den()); m_projectProfile->set_display_aspect(getCurrentProfile()->display_aspect_num(), getCurrentProfile()->display_aspect_den()); m_projectProfile->set_width(getCurrentProfile()->width()); m_projectProfile->set_explicit(true); } // inform render widget m_timecode.setFormat(getCurrentProfile()->fps()); profileChanged(); m_mainWindow->updateRenderWidgetProfile(); pCore->monitorManager()->resetProfiles(); pCore->monitorManager()->updatePreviewScaling(); if (m_guiConstructed && m_mainWindow->getCurrentTimeline()->controller()->getModel()) { m_mainWindow->getCurrentTimeline()->controller()->getModel()->updateProfile(getProjectProfile()); checkProfileValidity(); } return true; } return false; } void Core::checkProfileValidity() { int offset = (getCurrentProfile()->profile().width() % 8) + (getCurrentProfile()->profile().height() % 2); if (offset > 0) { // Profile is broken, warn user if (m_binWidget) { m_binWidget->displayBinMessage(i18n("Your project profile is invalid, rendering might fail."), KMessageWidget::Warning); } } } double Core::getCurrentSar() const { return getCurrentProfile()->sar(); } double Core::getCurrentDar() const { return getCurrentProfile()->dar(); } double Core::getCurrentFps() const { return getCurrentProfile()->fps(); } QSize Core::getCurrentFrameDisplaySize() const { return {(int)(getCurrentProfile()->height() * getCurrentDar() + 0.5), getCurrentProfile()->height()}; } QSize Core::getCurrentFrameSize() const { return {getCurrentProfile()->width(), getCurrentProfile()->height()}; } void Core::requestMonitorRefresh() { if (!m_guiConstructed) return; m_monitorManager->refreshProjectMonitor(); } void Core::refreshProjectRange(QSize range) { if (!m_guiConstructed) return; m_monitorManager->refreshProjectRange(range); } int Core::getItemPosition(const ObjectId &id) { if (!m_guiConstructed) return 0; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipPosition(id.second); } break; case ObjectType::TimelineComposition: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPosition(id.second); } break; case ObjectType::BinClip: case ObjectType::TimelineTrack: case ObjectType::Master: return 0; break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } int Core::getItemIn(const ObjectId &id) { if (!m_guiConstructed || !m_mainWindow->getCurrentTimeline() || !m_mainWindow->getCurrentTimeline()->controller()->getModel()) { qDebug() << "/ / // QUERYING ITEM IN BUT GUI NOT BUILD!!"; return 0; } switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipIn(id.second); } else { qDebug()<<"// ERROR QUERYING NON CLIP PROPERTIES\n\n!!!!!!!!!!!!!!!!!!!!!!!!!!"; } break; case ObjectType::TimelineComposition: case ObjectType::BinClip: case ObjectType::TimelineTrack: case ObjectType::Master: return 0; break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } PlaylistState::ClipState Core::getItemState(const ObjectId &id) { if (!m_guiConstructed) return PlaylistState::Disabled; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipState(id.second); } break; case ObjectType::TimelineComposition: return PlaylistState::VideoOnly; break; case ObjectType::BinClip: return m_binWidget->getClipState(id.second); break; case ObjectType::TimelineTrack: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->isAudioTrack(id.second) ? PlaylistState::AudioOnly : PlaylistState::VideoOnly; case ObjectType::Master: return PlaylistState::Disabled; break; default: qDebug() << "ERROR: unhandled object type"; break; } return PlaylistState::Disabled; } int Core::getItemDuration(const ObjectId &id) { if (!m_guiConstructed) return 0; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipPlaytime(id.second); } break; case ObjectType::TimelineComposition: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getCompositionPlaytime(id.second); } break; case ObjectType::BinClip: return (int)m_binWidget->getClipDuration(id.second); break; case ObjectType::TimelineTrack: case ObjectType::Master: return m_mainWindow->getCurrentTimeline()->controller()->duration(); default: qDebug() << "ERROR: unhandled object type"; } return 0; } int Core::getItemTrack(const ObjectId &id) { if (!m_guiConstructed) return 0; switch (id.first) { case ObjectType::TimelineClip: case ObjectType::TimelineComposition: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getItemTrackId(id.second); break; default: qDebug() << "ERROR: unhandled object type"; } return 0; } void Core::refreshProjectItem(const ObjectId &id) { if (!m_guiConstructed || m_mainWindow->getCurrentTimeline()->loading) return; switch (id.first) { case ObjectType::TimelineClip: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(id.second)) { m_mainWindow->getCurrentTimeline()->controller()->refreshItem(id.second); } break; case ObjectType::TimelineComposition: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isComposition(id.second)) { m_mainWindow->getCurrentTimeline()->controller()->refreshItem(id.second); } break; case ObjectType::TimelineTrack: if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isTrack(id.second)) { requestMonitorRefresh(); } break; case ObjectType::BinClip: m_monitorManager->activateMonitor(Kdenlive::ClipMonitor); m_monitorManager->refreshClipMonitor(); if (m_monitorManager->projectMonitorVisible() && m_mainWindow->getCurrentTimeline()->controller()->refreshIfVisible(id.second)) { m_monitorManager->refreshTimer.start(); } break; case ObjectType::Master: requestMonitorRefresh(); break; default: qDebug() << "ERROR: unhandled object type"; } } bool Core::hasTimelinePreview() const { if (!m_guiConstructed) { return false; } return m_mainWindow->getCurrentTimeline()->controller()->renderedChunks().size() > 0; } KdenliveDoc *Core::currentDoc() { return m_projectManager->current(); } Timecode Core::timecode() const { return m_timecode; } void Core::setDocumentModified() { m_projectManager->current()->setModified();; } int Core::projectDuration() const { if (!m_guiConstructed) { return 0; } return m_mainWindow->getCurrentTimeline()->controller()->duration(); } void Core::profileChanged() { GenTime::setFps(getCurrentFps()); } void Core::pushUndo(const Fun &undo, const Fun &redo, const QString &text) { undoStack()->push(new FunctionalUndoCommand(undo, redo, text)); } void Core::pushUndo(QUndoCommand *command) { undoStack()->push(command); } +int Core::undoIndex() const +{ + return m_projectManager->undoStack()->index(); +} + void Core::displayMessage(const QString &message, MessageType type, int timeout) { if (m_mainWindow) { if (type == ProcessingJobMessage || type == OperationCompletedMessage) { m_mainWindow->displayProgressMessage(message, type, timeout); } else { m_mainWindow->displayMessage(message, type, timeout); } } else { qDebug() << message; } } void Core::displayBinMessage(const QString &text, int type, const QList &actions) { m_binWidget->doDisplayMessage(text, (KMessageWidget::MessageType)type, actions); } void Core::displayBinLogMessage(const QString &text, int type, const QString &logInfo) { m_binWidget->doDisplayMessage(text, (KMessageWidget::MessageType)type, logInfo); } void Core::clearAssetPanel(int itemId) { if (m_guiConstructed) m_mainWindow->clearAssetPanel(itemId); } std::shared_ptr Core::getItemEffectStack(int itemType, int itemId) { if (!m_guiConstructed) return nullptr; switch (itemType) { case (int)ObjectType::TimelineClip: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipEffectStack(itemId); case (int)ObjectType::TimelineTrack: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getTrackEffectStackModel(itemId); break; case (int)ObjectType::BinClip: return m_binWidget->getClipEffectStack(itemId); case (int)ObjectType::Master: return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getMasterEffectStackModel(); default: return nullptr; } } std::shared_ptr Core::undoStack() { return projectManager()->undoStack(); } QMap Core::getTrackNames(bool videoOnly) { if (!m_guiConstructed) return QMap(); return m_mainWindow->getCurrentTimeline()->controller()->getTrackNames(videoOnly); } QPair Core::getCompositionATrack(int cid) const { if (!m_guiConstructed) return {}; return m_mainWindow->getCurrentTimeline()->controller()->getCompositionATrack(cid); } bool Core::compositionAutoTrack(int cid) const { return m_mainWindow->getCurrentTimeline()->controller()->compositionAutoTrack(cid); } void Core::setCompositionATrack(int cid, int aTrack) { if (!m_guiConstructed) return; m_mainWindow->getCurrentTimeline()->controller()->setCompositionATrack(cid, aTrack); } std::shared_ptr Core::projectItemModel() { return m_projectItemModel; } void Core::invalidateRange(QSize range) { if (!m_guiConstructed || m_mainWindow->getCurrentTimeline()->loading) return; m_mainWindow->getCurrentTimeline()->controller()->invalidateZone(range.width(), range.height()); } void Core::invalidateItem(ObjectId itemId) { if (!m_guiConstructed || !m_mainWindow->getCurrentTimeline() || m_mainWindow->getCurrentTimeline()->loading) return; switch (itemId.first) { case ObjectType::TimelineClip: case ObjectType::TimelineComposition: m_mainWindow->getCurrentTimeline()->controller()->invalidateItem(itemId.second); break; case ObjectType::TimelineTrack: m_mainWindow->getCurrentTimeline()->controller()->invalidateTrack(itemId.second); break; case ObjectType::BinClip: m_binWidget->invalidateClip(QString::number(itemId.second)); break; case ObjectType::Master: m_mainWindow->getCurrentTimeline()->controller()->invalidateZone(0, -1); break; default: // compositions should not have effects break; } } double Core::getClipSpeed(int id) const { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipSpeed(id); } void Core::updateItemKeyframes(ObjectId id) { if (id.first == ObjectType::TimelineClip && m_guiConstructed) { m_mainWindow->getCurrentTimeline()->controller()->updateClip(id.second, {TimelineModel::KeyframesRole}); } } void Core::updateItemModel(ObjectId id, const QString &service) { if (m_guiConstructed && id.first == ObjectType::TimelineClip && !m_mainWindow->getCurrentTimeline()->loading && service.startsWith(QLatin1String("fade"))) { bool startFade = service == QLatin1String("fadein") || service == QLatin1String("fade_from_black"); m_mainWindow->getCurrentTimeline()->controller()->updateClip(id.second, {startFade ? TimelineModel::FadeInRole : TimelineModel::FadeOutRole}); } } void Core::showClipKeyframes(ObjectId id, bool enable) { if (id.first == ObjectType::TimelineClip) { m_mainWindow->getCurrentTimeline()->controller()->showClipKeyframes(id.second, enable); } else if (id.first == ObjectType::TimelineComposition) { m_mainWindow->getCurrentTimeline()->controller()->showCompositionKeyframes(id.second, enable); } } Mlt::Profile *Core::thumbProfile() { QMutexLocker lck(&m_thumbProfileMutex); if (!m_thumbProfile) { m_thumbProfile = std::make_unique(m_currentProfile.toStdString().c_str()); double factor = 144. / m_thumbProfile->height(); m_thumbProfile->set_height(144); int width = m_thumbProfile->width() * factor + 0.5; if (width % 2 > 0) { width ++; } m_thumbProfile->set_width(width); } return m_thumbProfile.get(); } int Core::getTimelinePosition() const { if (m_guiConstructed) { return m_monitorManager->projectMonitor()->position(); } return 0; } void Core::triggerAction(const QString &name) { QAction *action = m_mainWindow->actionCollection()->action(name); if (action) { action->trigger(); } } void Core::clean() { m_self.reset(); } void Core::startMediaCapture(int tid, bool checkAudio, bool checkVideo) { if (checkAudio && checkVideo) { m_capture->recordVideo(tid, true); } else if (checkAudio) { m_capture->recordAudio(tid, true); } m_mediaCaptureFile = m_capture->getCaptureOutputLocation(); } void Core::stopMediaCapture(int tid, bool checkAudio, bool checkVideo) { if (checkAudio && checkVideo) { m_capture->recordVideo(tid, false); } else if (checkAudio) { m_capture->recordAudio(tid, false); } } QStringList Core::getAudioCaptureDevices() { return m_capture->getAudioCaptureDevices(); } int Core::getMediaCaptureState() { return m_capture->getState(); } bool Core::isMediaCapturing() { return m_capture->isRecording(); } MediaCapture *Core::getAudioDevice() { return m_capture.get(); } QString Core::getProjectFolderName() { if (currentDoc()) { return currentDoc()->projectDataFolder() + QDir::separator(); } return QString(); } QString Core::getTimelineClipBinId(int cid) { if (!m_guiConstructed) { return QString(); } if (m_mainWindow->getCurrentTimeline()->controller()->getModel()->isClip(cid)) { return m_mainWindow->getCurrentTimeline()->controller()->getModel()->getClipBinId(cid); } return QString(); } int Core::getDurationFromString(const QString &time) { if (!m_guiConstructed) { return 0; } const QString duration = currentDoc()->timecode().reformatSeparators(time); return currentDoc()->timecode().getFrameCount(duration); } void Core::processInvalidFilter(const QString service, const QString id, const QString message) { if (m_guiConstructed) m_mainWindow->assetPanelWarning(service, id, message); } void Core::updateProjectTags(QMap tags) { // Clear previous tags for (int i = 1 ; i< 20; i++) { QString current = currentDoc()->getDocumentProperty(QString("tag%1").arg(i)); if (current.isEmpty()) { break; } else { currentDoc()->setDocumentProperty(QString("tag%1").arg(i), QString()); } } QMapIterator j(tags); int i = 1; while (j.hasNext()) { j.next(); currentDoc()->setDocumentProperty(QString("tag%1").arg(i), QString("%1:%2").arg(j.key()).arg(j.value())); i++; } } std::unique_ptr Core::getMasterProducerInstance() { if (m_guiConstructed && m_mainWindow->getCurrentTimeline()) { std::unique_ptr producer(m_mainWindow->getCurrentTimeline()->controller()->tractor()->cut(0, m_mainWindow->getCurrentTimeline()->controller()->duration() - 1)); return producer; } return nullptr; } std::unique_ptr Core::getTrackProducerInstance(int tid) { if (m_guiConstructed && m_mainWindow->getCurrentTimeline()) { std::unique_ptr producer(new Mlt::Producer(m_mainWindow->getCurrentTimeline()->controller()->trackProducer(tid))); return producer; } return nullptr; } diff --git a/src/core.h b/src/core.h index 9d9bc8f8d..3ad0eacb4 100644 --- a/src/core.h +++ b/src/core.h @@ -1,269 +1,271 @@ /* Copyright (C) 2014 Till Theato 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 3 of the License, or (at your option) any later version. */ #ifndef CORE_H #define CORE_H #include "definitions.h" #include "kdenlivecore_export.h" #include "undohelper.hpp" #include #include #include #include #include #include "timecode.h" class Bin; class DocUndoStack; class EffectStackModel; class JobManager; class KdenliveDoc; class LibraryWidget; class MainWindow; class MediaCapture; class MixerManager; class Monitor; class MonitorManager; class ProfileModel; class ProjectItemModel; class ProjectManager; namespace Mlt { class Repository; class Producer; class Profile; } // namespace Mlt #define EXIT_RESTART (42) #define EXIT_CLEAN_RESTART (43) #define pCore Core::self() /** * @class Core * @brief Singleton that provides access to the different parts of Kdenlive * * Needs to be initialize before any widgets are created in MainWindow. * Plugins should be loaded after the widget setup. */ class /*KDENLIVECORE_EXPORT*/ Core : public QObject { Q_OBJECT public: Core(const Core &) = delete; Core &operator=(const Core &) = delete; Core(Core &&) = delete; Core &operator=(Core &&) = delete; ~Core() override; /** * @brief Setup the basics of the application, in particular the connection * with Mlt * @param isAppImage do we expect an AppImage (if yes, we use App path to deduce * other binaries paths (melt, ffmpeg, etc) * @param MltPath (optional) path to MLT environment */ static void build(bool isAppImage, const QString &MltPath = QString()); /** * @brief Init the GUI part of the app and show the main window * @param Url (optional) file to open * If Url is present, it will be opened, otherwise, if openlastproject is * set, latest project will be opened. If no file is open after trying this, * a default new file will be created. */ void initGUI(const QUrl &Url, const QString &clipsToLoad = QString()); /** @brief Returns a pointer to the singleton object. */ static std::unique_ptr &self(); /** @brief Delete the global core instance */ static void clean(); /** @brief Returns a pointer to the main window. */ MainWindow *window(); /** @brief Returns a pointer to the project manager. */ ProjectManager *projectManager(); /** @brief Returns a pointer to the current project. */ KdenliveDoc *currentDoc(); /** @brief Returns project's timecode. */ Timecode timecode() const; /** @brief Set current project modified. */ void setDocumentModified(); /** @brief Returns a pointer to the monitor manager. */ MonitorManager *monitorManager(); /** @brief Returns a pointer to the view of the project bin. */ Bin *bin(); /** @brief Select a clip in the Bin from its id. */ void selectBinClip(const QString &id, int frame = -1, const QPoint &zone = QPoint()); /** @brief Returns a pointer to the model of the project bin. */ std::shared_ptr projectItemModel(); /** @brief Returns a pointer to the job manager. Please do not store it. */ std::shared_ptr jobManager(); /** @brief Returns a pointer to the library. */ LibraryWidget *library(); /** @brief Returns a pointer to the audio mixer. */ MixerManager *mixer(); /** @brief Returns a pointer to MLT's repository */ std::unique_ptr &getMltRepository(); /** @brief Returns a pointer to the current profile */ std::unique_ptr &getCurrentProfile() const; const QString &getCurrentProfilePath() const; /** @brief Define the active profile * @returns true if profile exists, false if not found */ bool setCurrentProfile(const QString &profilePath); /** @brief Returns Sample Aspect Ratio of current profile */ double getCurrentSar() const; /** @brief Returns Display Aspect Ratio of current profile */ double getCurrentDar() const; /** @brief Returns frame rate of current profile */ double getCurrentFps() const; /** @brief Returns the frame size (width x height) of current profile */ QSize getCurrentFrameSize() const; /** @brief Returns the frame display size (width x height) of current profile */ QSize getCurrentFrameDisplaySize() const; /** @brief Request project monitor refresh */ void requestMonitorRefresh(); /** @brief Request project monitor refresh if current position is inside range*/ void refreshProjectRange(QSize range); /** @brief Request project monitor refresh if referenced item is under cursor */ void refreshProjectItem(const ObjectId &id); /** @brief Returns a reference to a monitor (clip or project monitor) */ Monitor *getMonitor(int id); /** @brief This function must be called whenever the profile used changes */ void profileChanged(); /** @brief Create and push and undo object based on the corresponding functions Note that if you class permits and requires it, you should use the macro PUSH_UNDO instead*/ void pushUndo(const Fun &undo, const Fun &redo, const QString &text); void pushUndo(QUndoCommand *command); /** @brief display a user info/warning message in statusbar */ void displayMessage(const QString &message, MessageType type, int timeout = -1); /** @brief Clear asset view if itemId is displayed. */ void clearAssetPanel(int itemId); /** @brief Returns the effectstack of a given bin clip. */ std::shared_ptr getItemEffectStack(int itemType, int itemId); int getItemPosition(const ObjectId &id); int getItemIn(const ObjectId &id); int getItemTrack(const ObjectId &id); int getItemDuration(const ObjectId &id); /** @brief Returns the capabilities of a clip: AudioOnly, VideoOnly or Disabled if both are allowed */ PlaylistState::ClipState getItemState(const ObjectId &id); /** @brief Get a list of video track names with indexes */ QMap getTrackNames(bool videoOnly); /** @brief Returns the composition A track (MLT index / Track id) */ QPair getCompositionATrack(int cid) const; void setCompositionATrack(int cid, int aTrack); /* @brief Return true if composition's a_track is automatic (no forced track) */ bool compositionAutoTrack(int cid) const; std::shared_ptr undoStack(); double getClipSpeed(int id) const; /** @brief Mark an item as invalid for timeline preview */ void invalidateItem(ObjectId itemId); void invalidateRange(QSize range); void prepareShutdown(); /** the keyframe model changed (effect added, deleted, active effect changed), inform timeline */ void updateItemKeyframes(ObjectId id); /** A fade for clip id changed, update timeline */ void updateItemModel(ObjectId id, const QString &service); /** Show / hide keyframes for a timeline clip */ void showClipKeyframes(ObjectId id, bool enable); Mlt::Profile *thumbProfile(); /** @brief Returns the current project duration */ int projectDuration() const; /** @brief Returns true if current project has some rendered timeline preview */ bool hasTimelinePreview() const; /** @brief Returns current timeline cursor position */ int getTimelinePosition() const; /** @brief Handles audio and video capture **/ void startMediaCapture(int tid, bool, bool); void stopMediaCapture(int tid, bool, bool); QStringList getAudioCaptureDevices(); int getMediaCaptureState(); bool isMediaCapturing(); MediaCapture *getAudioDevice(); /** @brief Returns Project Folder name for capture output location */ QString getProjectFolderName(); /** @brief Returns a timeline clip's bin id */ QString getTimelineClipBinId(int cid); /** @brief Returns a frame duration from a timecode */ int getDurationFromString(const QString &time); /** @brief An error occurred within a filter, inform user */ void processInvalidFilter(const QString service, const QString id, const QString message); /** @brief Update current project's tags */ void updateProjectTags(QMap tags); /** @brief Returns the consumer profile, that will be scaled * according to preview settings. Should only be used on the consumer */ Mlt::Profile *getProjectProfile(); /** @brief Returns a copy of current timeline's master playlist */ std::unique_ptr getMasterProducerInstance(); /** @brief Returns a copy of a track's playlist */ std::unique_ptr getTrackProducerInstance(int tid); + /** @brief Returns the undo stack index (position). */ + int undoIndex() const; private: explicit Core(); static std::unique_ptr m_self; /** @brief Makes sure Qt's locale and system locale settings match. */ void initLocale(); MainWindow *m_mainWindow{nullptr}; ProjectManager *m_projectManager{nullptr}; MonitorManager *m_monitorManager{nullptr}; std::shared_ptr m_projectItemModel; std::shared_ptr m_jobManager; Bin *m_binWidget{nullptr}; LibraryWidget *m_library{nullptr}; MixerManager *m_mixerWidget{nullptr}; /** @brief Current project's profile path */ QString m_currentProfile; QString m_profile; Timecode m_timecode; std::unique_ptr m_thumbProfile; /** @brief Mlt profile used in the consumer 's monitors */ std::unique_ptr m_projectProfile; bool m_guiConstructed = false; /** @brief Check that the profile is valid (width is a multiple of 8 and height a multiple of 2 */ void checkProfileValidity(); std::unique_ptr m_capture; QUrl m_mediaCaptureFile; QMutex m_thumbProfileMutex; public slots: void triggerAction(const QString &name); /** @brief display a user info/warning message in the project bin */ void displayBinMessage(const QString &text, int type, const QList &actions = QList()); void displayBinLogMessage(const QString &text, int type, const QString &logInfo); /** @brief Create small thumbnails for luma used in compositions */ void buildLumaThumbs(const QStringList &values); signals: void coreIsReady(); void updateLibraryPath(); void updateMonitorProfile(); /** @brief Call config dialog on a selected page / tab */ void showConfigDialog(int, int); void finalizeRecording(const QString &captureFile); void autoScrollChanged(); }; #endif diff --git a/src/monitor/monitor.cpp b/src/monitor/monitor.cpp index 379848089..a05df7978 100644 --- a/src/monitor/monitor.cpp +++ b/src/monitor/monitor.cpp @@ -1,2231 +1,2232 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@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) any later version. * * * * 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #include "monitor.h" #include "bin/bin.h" #include "bin/projectclip.h" #include "core.h" #include "dialogs/profilesdialog.h" #include "doc/kdenlivedoc.h" #include "doc/kthumb.h" #include "glwidget.h" #include "kdenlivesettings.h" #include "lib/audio/audioStreamInfo.h" #include "mainwindow.h" #include "mltcontroller/clipcontroller.h" #include "monitorproxy.h" #include "profiles/profilemodel.hpp" #include "project/projectmanager.h" #include "qmlmanager.h" #include "recmanager.h" #include "jobs/jobmanager.h" #include "jobs/cutclipjob.h" #include "scopes/monitoraudiolevel.h" #include "timeline2/model/snapmodel.hpp" #include "transitions/transitionsrepository.hpp" #include "utils/thumbnailcache.hpp" #include "klocalizedstring.h" #include #include #include #include #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #define SEEK_INACTIVE (-1) QuickEventEater::QuickEventEater(QObject *parent) : QObject(parent) { } bool QuickEventEater::eventFilter(QObject *obj, QEvent *event) { switch (event->type()) { case QEvent::DragEnter: { auto *ev = reinterpret_cast(event); if (ev->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) { ev->acceptProposedAction(); return true; } break; } case QEvent::DragMove: { auto *ev = reinterpret_cast(event); if (ev->mimeData()->hasFormat(QStringLiteral("kdenlive/effect"))) { ev->acceptProposedAction(); return true; } break; } case QEvent::Drop: { auto *ev = static_cast(event); if (ev) { QStringList effectData; effectData << QString::fromUtf8(ev->mimeData()->data(QStringLiteral("kdenlive/effect"))); QStringList source = QString::fromUtf8(ev->mimeData()->data(QStringLiteral("kdenlive/effectsource"))).split(QLatin1Char('-')); effectData << source; emit addEffect(effectData); ev->accept(); return true; } break; } default: break; } return QObject::eventFilter(obj, event); } QuickMonitorEventEater::QuickMonitorEventEater(QWidget *parent) : QObject(parent) { } bool QuickMonitorEventEater::eventFilter(QObject *obj, QEvent *event) { if (event->type() == QEvent::KeyPress) { auto *ev = static_cast(event); if (ev) { emit doKeyPressEvent(ev); return true; } } return QObject::eventFilter(obj, event); } Monitor::Monitor(Kdenlive::MonitorId id, MonitorManager *manager, QWidget *parent) : AbstractMonitor(id, manager, parent) , m_controller(nullptr) , m_glMonitor(nullptr) , m_snaps(new SnapModel()) , m_splitEffect(nullptr) , m_splitProducer(nullptr) , m_dragStarted(false) , m_recManager(nullptr) , m_loopClipAction(nullptr) , m_sceneVisibilityAction(nullptr) , m_contextMenu(nullptr) , m_markerMenu(nullptr) , m_loopClipTransition(true) , m_editMarker(nullptr) , m_forceSizeFactor(0) , m_offset(id == Kdenlive::ClipMonitor ? 0 : TimelineModel::seekDuration) , m_lastMonitorSceneType(MonitorSceneDefault) { auto *layout = new QVBoxLayout; layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); // Create container widget m_glWidget = new QWidget; m_glWidget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); auto *glayout = new QGridLayout(m_glWidget); glayout->setSpacing(0); glayout->setContentsMargins(0, 0, 0, 0); // Create QML OpenGL widget m_glMonitor = new GLWidget((int)id); connect(m_glMonitor, &GLWidget::passKeyEvent, this, &Monitor::doKeyPressEvent); connect(m_glMonitor, &GLWidget::panView, this, &Monitor::panView); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::requestSeek, this, &Monitor::processSeek, Qt::DirectConnection); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::positionChanged, this, &Monitor::slotSeekPosition); connect(m_glMonitor, &GLWidget::activateMonitor, this, &AbstractMonitor::slotActivateMonitor, Qt::DirectConnection); m_videoWidget = QWidget::createWindowContainer(qobject_cast(m_glMonitor)); m_videoWidget->setAcceptDrops(true); auto *leventEater = new QuickEventEater(this); m_videoWidget->installEventFilter(leventEater); connect(leventEater, &QuickEventEater::addEffect, this, &Monitor::slotAddEffect); m_qmlManager = new QmlManager(m_glMonitor); connect(m_qmlManager, &QmlManager::effectChanged, this, &Monitor::effectChanged); connect(m_qmlManager, &QmlManager::effectPointsChanged, this, &Monitor::effectPointsChanged); auto *monitorEventEater = new QuickMonitorEventEater(this); m_videoWidget->installEventFilter(monitorEventEater); connect(monitorEventEater, &QuickMonitorEventEater::doKeyPressEvent, this, &Monitor::doKeyPressEvent); glayout->addWidget(m_videoWidget, 0, 0); m_verticalScroll = new QScrollBar(Qt::Vertical); glayout->addWidget(m_verticalScroll, 0, 1); m_verticalScroll->hide(); m_horizontalScroll = new QScrollBar(Qt::Horizontal); glayout->addWidget(m_horizontalScroll, 1, 0); m_horizontalScroll->hide(); connect(m_horizontalScroll, &QAbstractSlider::valueChanged, this, &Monitor::setOffsetX); connect(m_verticalScroll, &QAbstractSlider::valueChanged, this, &Monitor::setOffsetY); connect(m_glMonitor, &GLWidget::frameDisplayed, this, &Monitor::onFrameDisplayed, Qt::DirectConnection); connect(m_glMonitor, &GLWidget::mouseSeek, this, &Monitor::slotMouseSeek); connect(m_glMonitor, &GLWidget::monitorPlay, this, &Monitor::slotPlay); connect(m_glMonitor, &GLWidget::startDrag, this, &Monitor::slotStartDrag); connect(m_glMonitor, &GLWidget::switchFullScreen, this, &Monitor::slotSwitchFullScreen); connect(m_glMonitor, &GLWidget::zoomChanged, this, &Monitor::setZoom); connect(m_glMonitor, SIGNAL(lockMonitor(bool)), this, SLOT(slotLockMonitor(bool)), Qt::DirectConnection); connect(m_glMonitor, &GLWidget::showContextMenu, this, &Monitor::slotShowMenu); connect(m_glMonitor, &GLWidget::gpuNotSupported, this, &Monitor::gpuError); m_glWidget->setMinimumSize(QSize(320, 180)); layout->addWidget(m_glWidget, 10); layout->addStretch(); // Tool bar buttons m_toolbar = new QToolBar(this); int size = style()->pixelMetric(QStyle::PM_SmallIconSize); QSize iconSize(size, size); m_toolbar->setIconSize(iconSize); m_scalingLabel = new QLabel(this); m_scalingLabel->setToolTip(i18n("Preview resolution - lower resolution means faster preview")); m_scalingLabel->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); QFontMetricsF fm(m_scalingLabel->font()); int labelWidth = (int) fm.boundingRect(QRect(0, 0, fm.averageCharWidth()*10, fm.lineSpacing() * 2), Qt::AlignCenter, QStringLiteral("000p")).width(); QMargins labelMrg = m_scalingLabel->contentsMargins(); m_scalingLabel->setFixedWidth(labelWidth + labelMrg.left() + labelMrg.right()); m_scalingLabel->setAlignment(Qt::AlignCenter); connect(m_scalingLabel, &QLabel::linkActivated, [&]() { switch (KdenliveSettings::previewScaling()) { case 2: KdenliveSettings::setPreviewScaling(4); break; case 4: KdenliveSettings::setPreviewScaling(8); break; case 8: KdenliveSettings::setPreviewScaling(16); break; case 16: KdenliveSettings::setPreviewScaling(0); break; default: KdenliveSettings::setPreviewScaling(2); } m_monitorManager->scalingChanged(); m_monitorManager->updatePreviewScaling(); }); connect(manager, &MonitorManager::updatePreviewScaling, [this]() { m_glMonitor->updateScaling(); switch (KdenliveSettings::previewScaling()) { case 2: m_scalingLabel->setText(QString("%1").arg(i18n("720p"))); break; case 4: m_scalingLabel->setText(QString("%1").arg(i18n("540p"))); break; case 8: m_scalingLabel->setText(QString("%1").arg(i18n("360p"))); break; case 16: m_scalingLabel->setText(QString("%1").arg(i18n("270p"))); break; default: m_scalingLabel->setText(QString("%1").arg(i18n("1:1"))); break; } refreshMonitorIfActive(); }); m_toolbar->addWidget(m_scalingLabel); m_speedLabel = new QLabel(this); m_speedLabel->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); KColorScheme scheme(palette().currentColorGroup(), KColorScheme::Button); QColor bg = scheme.background(KColorScheme::LinkBackground).color(); bg = scheme.background(KColorScheme::PositiveBackground).color(); m_speedLabel->setStyleSheet(QString("padding-left: %4; padding-right: %4;background-color: rgb(%1,%2,%3);").arg(bg.red()).arg(bg.green()).arg(bg.blue()).arg(m_speedLabel->sizeHint().height()/4)); m_toolbar->addWidget(m_speedLabel); m_speedLabel->setFixedWidth(0); if (id == Kdenlive::ClipMonitor) { // Add options for recording m_recManager = new RecManager(this); connect(m_recManager, &RecManager::warningMessage, this, &Monitor::warningMessage); connect(m_recManager, &RecManager::addClipToProject, this, &Monitor::addClipToProject); m_toolbar->addAction(manager->getAction(QStringLiteral("insert_project_tree"))); m_toolbar->setToolTip(i18n("Insert Zone to Project Bin")); m_toolbar->addSeparator(); } else if (id == Kdenlive::ProjectMonitor) { connect(m_glMonitor, &GLWidget::paused, m_monitorManager, &MonitorManager::cleanMixer); } if (id != Kdenlive::DvdMonitor) { QAction *markIn = new QAction(QIcon::fromTheme(QStringLiteral("zone-in")), i18n("Set Zone In"), this); QAction *markOut = new QAction(QIcon::fromTheme(QStringLiteral("zone-out")), i18n("Set Zone Out"), this); m_toolbar->addAction(markIn); m_toolbar->addAction(markOut); connect(markIn, &QAction::triggered, [&, manager]() { m_monitorManager->activateMonitor(m_id); manager->getAction(QStringLiteral("mark_in"))->trigger(); }); connect(markOut, &QAction::triggered, [&, manager]() { m_monitorManager->activateMonitor(m_id); manager->getAction(QStringLiteral("mark_out"))->trigger(); }); } // Per monitor rewind action QAction *rewind = new QAction(QIcon::fromTheme(QStringLiteral("media-seek-backward")), i18n("Rewind"), this); m_toolbar->addAction(rewind); connect(rewind, &QAction::triggered, this, &Monitor::slotRewind); auto *playButton = new QToolButton(m_toolbar); m_playMenu = new QMenu(i18n("Play..."), this); connect(m_playMenu, &QMenu::aboutToShow, this, &Monitor::slotActivateMonitor); QAction *originalPlayAction = static_cast(manager->getAction(QStringLiteral("monitor_play"))); m_playAction = new KDualAction(i18n("Play"), i18n("Pause"), this); m_playAction->setInactiveIcon(QIcon::fromTheme(QStringLiteral("media-playback-start"))); m_playAction->setActiveIcon(QIcon::fromTheme(QStringLiteral("media-playback-pause"))); QString strippedTooltip = m_playAction->toolTip().remove(QRegExp(QStringLiteral("\\s\\(.*\\)"))); // append shortcut if it exists for action if (originalPlayAction->shortcut() == QKeySequence(0)) { m_playAction->setToolTip(strippedTooltip); } else { m_playAction->setToolTip(strippedTooltip + QStringLiteral(" (") + originalPlayAction->shortcut().toString() + QLatin1Char(')')); } m_playMenu->addAction(m_playAction); connect(m_playAction, &QAction::triggered, this, &Monitor::slotSwitchPlay); playButton->setMenu(m_playMenu); playButton->setPopupMode(QToolButton::MenuButtonPopup); m_toolbar->addWidget(playButton); // Per monitor forward action QAction *forward = new QAction(QIcon::fromTheme(QStringLiteral("media-seek-forward")), i18n("Forward"), this); m_toolbar->addAction(forward); connect(forward, &QAction::triggered, [this]() { Monitor::slotForward(); }); playButton->setDefaultAction(m_playAction); m_configMenu = new QMenu(i18n("Misc..."), this); if (id != Kdenlive::DvdMonitor) { if (id == Kdenlive::ClipMonitor) { m_markerMenu = new QMenu(i18n("Go to marker..."), this); } else { m_markerMenu = new QMenu(i18n("Go to guide..."), this); } m_markerMenu->setEnabled(false); m_configMenu->addMenu(m_markerMenu); connect(m_markerMenu, &QMenu::triggered, this, &Monitor::slotGoToMarker); m_forceSize = new KSelectAction(QIcon::fromTheme(QStringLiteral("transform-scale")), i18n("Force Monitor Size"), this); QAction *fullAction = m_forceSize->addAction(QIcon(), i18n("Force 100%")); fullAction->setData(100); QAction *halfAction = m_forceSize->addAction(QIcon(), i18n("Force 50%")); halfAction->setData(50); QAction *freeAction = m_forceSize->addAction(QIcon(), i18n("Free Resize")); freeAction->setData(0); m_configMenu->addAction(m_forceSize); m_forceSize->setCurrentAction(freeAction); connect(m_forceSize, static_cast(&KSelectAction::triggered), this, &Monitor::slotForceSize); } // Create Volume slider popup m_audioSlider = new QSlider(Qt::Vertical); m_audioSlider->setRange(0, 100); m_audioSlider->setValue(KdenliveSettings::volume()); connect(m_audioSlider, &QSlider::valueChanged, this, &Monitor::slotSetVolume); auto *widgetslider = new QWidgetAction(this); widgetslider->setText(i18n("Audio volume")); widgetslider->setDefaultWidget(m_audioSlider); auto *menu = new QMenu(i18n("Volume"), this); menu->setIcon(QIcon::fromTheme(QStringLiteral("audio-volume-medium"))); menu->addAction(widgetslider); m_configMenu->addMenu(menu); /*QIcon icon; if (KdenliveSettings::volume() == 0) { icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted")); } else { icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); } m_audioButton->setIcon(icon);*/ setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); setLayout(layout); setMinimumHeight(200); connect(this, &Monitor::scopesClear, m_glMonitor, &GLWidget::releaseAnalyse, Qt::DirectConnection); connect(m_glMonitor, &GLWidget::analyseFrame, this, &Monitor::frameUpdated); if (id != Kdenlive::ClipMonitor) { // TODO: reimplement // connect(render, &Render::durationChanged, this, &Monitor::durationChanged); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::saveZone, this, &Monitor::updateTimelineClipZone); + connect(m_glMonitor->getControllerProxy(), &MonitorProxy::saveZoneWithUndo, this, &Monitor::zoneUpdatedWithUndo); } else { connect(m_glMonitor->getControllerProxy(), &MonitorProxy::saveZone, this, &Monitor::updateClipZone); } connect(m_glMonitor->getControllerProxy(), &MonitorProxy::triggerAction, pCore.get(), &Core::triggerAction); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekNextKeyframe, this, &Monitor::seekToNextKeyframe); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekPreviousKeyframe, this, &Monitor::seekToPreviousKeyframe); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::addRemoveKeyframe, this, &Monitor::addRemoveKeyframe); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::seekToKeyframe, this, &Monitor::slotSeekToKeyFrame); m_sceneVisibilityAction = new QAction(QIcon::fromTheme(QStringLiteral("transform-crop")), i18n("Show/Hide edit mode"), this); m_sceneVisibilityAction->setCheckable(true); m_sceneVisibilityAction->setChecked(KdenliveSettings::showOnMonitorScene()); connect(m_sceneVisibilityAction, &QAction::triggered, this, &Monitor::slotEnableEffectScene); m_toolbar->addAction(m_sceneVisibilityAction); m_toolbar->addSeparator(); m_timePos = new TimecodeDisplay(pCore->timecode(), this); m_toolbar->addWidget(m_timePos); auto *configButton = new QToolButton(m_toolbar); configButton->setIcon(QIcon::fromTheme(QStringLiteral("kdenlive-menu"))); configButton->setToolTip(i18n("Options")); configButton->setMenu(m_configMenu); configButton->setPopupMode(QToolButton::InstantPopup); m_toolbar->addWidget(configButton); m_toolbar->addSeparator(); QMargins mrg = m_toolbar->contentsMargins(); m_audioMeterWidget = new MonitorAudioLevel(m_toolbar->height() - mrg.top() - mrg.bottom(), this); m_toolbar->addWidget(m_audioMeterWidget); if (!m_audioMeterWidget->isValid) { KdenliveSettings::setMonitoraudio(0x01); m_audioMeterWidget->setVisibility(false); } else { m_audioMeterWidget->setVisibility((KdenliveSettings::monitoraudio() & m_id) != 0); } connect(m_timePos, SIGNAL(timeCodeEditingFinished()), this, SLOT(slotSeek())); layout->addWidget(m_toolbar); if (m_recManager) { layout->addWidget(m_recManager->toolbar()); } // Load monitor overlay qml loadQmlScene(MonitorSceneDefault); // Monitor dropped fps timer m_droppedTimer.setInterval(1000); m_droppedTimer.setSingleShot(false); connect(&m_droppedTimer, &QTimer::timeout, this, &Monitor::checkDrops); // Info message widget m_infoMessage = new KMessageWidget(this); layout->addWidget(m_infoMessage); m_infoMessage->hide(); } Monitor::~Monitor() { delete m_audioMeterWidget; delete m_glMonitor; delete m_videoWidget; delete m_glWidget; delete m_timePos; } void Monitor::setOffsetX(int x) { m_glMonitor->setOffsetX(x, m_horizontalScroll->maximum()); } void Monitor::setOffsetY(int y) { m_glMonitor->setOffsetY(y, m_verticalScroll->maximum()); } void Monitor::slotGetCurrentImage(bool request) { m_glMonitor->sendFrameForAnalysis = request; m_monitorManager->activateMonitor(m_id); refreshMonitorIfActive(); if (request) { // Update analysis state QTimer::singleShot(500, m_monitorManager, &MonitorManager::checkScopes); } else { m_glMonitor->releaseAnalyse(); } } void Monitor::slotAddEffect(const QStringList &effect) { if (m_id == Kdenlive::ClipMonitor) { if (m_controller) { emit addMasterEffect(m_controller->AbstractProjectItem::clipId(), effect); } } else { emit addEffect(effect); } } void Monitor::refreshIcons() { QList allMenus = this->findChildren(); for (int i = 0; i < allMenus.count(); i++) { QAction *m = allMenus.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setIcon(newIcon); } QList allButtons = this->findChildren(); for (int i = 0; i < allButtons.count(); i++) { KDualAction *m = allButtons.at(i); QIcon ic = m->activeIcon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setActiveIcon(newIcon); ic = m->inactiveIcon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } newIcon = QIcon::fromTheme(ic.name()); m->setInactiveIcon(newIcon); } } QAction *Monitor::recAction() { if (m_recManager) { return m_recManager->recAction(); } return nullptr; } void Monitor::slotLockMonitor(bool lock) { m_monitorManager->lockMonitor(m_id, lock); } void Monitor::setupMenu(QMenu *goMenu, QMenu *overlayMenu, QAction *playZone, QAction *loopZone, QMenu *markerMenu, QAction *loopClip) { delete m_contextMenu; m_contextMenu = new QMenu(this); m_contextMenu->addMenu(m_playMenu); if (goMenu) { m_contextMenu->addMenu(goMenu); } if (markerMenu) { m_contextMenu->addMenu(markerMenu); QList list = markerMenu->actions(); for (int i = 0; i < list.count(); ++i) { if (list.at(i)->objectName() == QLatin1String("edit_marker")) { m_editMarker = list.at(i); break; } } } m_playMenu->addAction(playZone); m_playMenu->addAction(loopZone); if (loopClip) { m_loopClipAction = loopClip; m_playMenu->addAction(loopClip); } // TODO: add save zone to timeline monitor when fixed m_contextMenu->addMenu(m_markerMenu); if (m_id == Kdenlive::ClipMonitor) { m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("document-save")), i18n("Save zone"), this, SLOT(slotSaveZone())); QAction *extractZone = m_configMenu->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Extract Zone"), this, SLOT(slotExtractCurrentZone())); m_contextMenu->addAction(extractZone); } m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("extract_frame"))); m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("extract_frame_to_project"))); if (m_id == Kdenlive::ProjectMonitor) { m_contextMenu->addAction(m_monitorManager->getAction(QStringLiteral("monitor_multitrack"))); } else if (m_id == Kdenlive::ClipMonitor) { QAction *setThumbFrame = m_contextMenu->addAction(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Set current image as thumbnail"), this, SLOT(slotSetThumbFrame())); m_configMenu->addAction(setThumbFrame); } if (overlayMenu) { m_contextMenu->addMenu(overlayMenu); } QAction *switchAudioMonitor = m_configMenu->addAction(i18n("Show Audio Levels"), this, SLOT(slotSwitchAudioMonitor())); switchAudioMonitor->setCheckable(true); switchAudioMonitor->setChecked((KdenliveSettings::monitoraudio() & m_id) != 0); // For some reason, the frame in QAbstracSpinBox (base class of TimeCodeDisplay) needs to be displayed once, then hidden // or it will never appear (supposed to appear on hover). m_timePos->setFrame(false); } void Monitor::slotGoToMarker(QAction *action) { int pos = action->data().toInt(); slotSeek(pos); } void Monitor::slotForceSize(QAction *a) { int resizeType = a->data().toInt(); int profileWidth = 320; int profileHeight = 200; if (resizeType > 0) { // calculate size QRect r = QApplication::primaryScreen()->geometry(); profileHeight = m_glMonitor->profileSize().height() * resizeType / 100; profileWidth = pCore->getCurrentProfile()->dar() * profileHeight; if (profileWidth > r.width() * 0.8 || profileHeight > r.height() * 0.7) { // reset action to free resize const QList list = m_forceSize->actions(); for (QAction *ac : list) { if (ac->data().toInt() == m_forceSizeFactor) { m_forceSize->setCurrentAction(ac); break; } } warningMessage(i18n("Your screen resolution is not sufficient for this action")); return; } } switch (resizeType) { case 100: case 50: // resize full size setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); m_videoWidget->setMinimumSize(profileWidth, profileHeight); m_videoWidget->setMaximumSize(profileWidth, profileHeight); setMinimumSize(QSize(profileWidth, profileHeight + m_toolbar->height() + m_glMonitor->getControllerProxy()->rulerHeight())); break; default: // Free resize m_videoWidget->setMinimumSize(profileWidth, profileHeight); m_videoWidget->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX); setMinimumSize(QSize(profileWidth, profileHeight + m_toolbar->height() + m_glMonitor->getControllerProxy()->rulerHeight())); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); break; } m_forceSizeFactor = resizeType; updateGeometry(); } void Monitor::updateMarkers() { if (m_controller) { m_markerMenu->clear(); QList markers = m_controller->getMarkerModel()->getAllMarkers(); if (!markers.isEmpty()) { for (int i = 0; i < markers.count(); ++i) { int pos = (int)markers.at(i).time().frames(pCore->getCurrentFps()); QString position = pCore->timecode().getTimecode(markers.at(i).time()) + QLatin1Char(' ') + markers.at(i).comment(); QAction *go = m_markerMenu->addAction(position); go->setData(pos); } } m_markerMenu->setEnabled(!m_markerMenu->isEmpty()); } } void Monitor::setGuides(const QMap &guides) { // TODO: load guides model m_markerMenu->clear(); QMapIterator i(guides); QList guidesList; while (i.hasNext()) { i.next(); CommentedTime timeGuide(GenTime(i.key()), i.value()); guidesList << timeGuide; int pos = (int)timeGuide.time().frames(pCore->getCurrentFps()); QString position = pCore->timecode().getTimecode(timeGuide.time()) + QLatin1Char(' ') + timeGuide.comment(); QAction *go = m_markerMenu->addAction(position); go->setData(pos); } // m_ruler->setMarkers(guidesList); m_markerMenu->setEnabled(!m_markerMenu->isEmpty()); checkOverlay(); } void Monitor::slotSeekToPreviousSnap() { if (m_controller) { m_glMonitor->getControllerProxy()->setPosition(getSnapForPos(true).frames(pCore->getCurrentFps())); } } void Monitor::slotSeekToNextSnap() { if (m_controller) { m_glMonitor->getControllerProxy()->setPosition(getSnapForPos(false).frames(pCore->getCurrentFps())); } } int Monitor::position() { return m_glMonitor->getControllerProxy()->getPosition(); } GenTime Monitor::getSnapForPos(bool previous) { int frame = previous ? m_snaps->getPreviousPoint(m_glMonitor->getCurrentPos()) : m_snaps->getNextPoint(m_glMonitor->getCurrentPos()); return {frame, pCore->getCurrentFps()}; } void Monitor::slotLoadClipZone(const QPoint &zone) { m_glMonitor->getControllerProxy()->setZone(zone.x(), zone.y()); checkOverlay(); } void Monitor::slotSetZoneStart() { m_glMonitor->getControllerProxy()->setZoneIn(m_glMonitor->getCurrentPos()); if (m_controller) { m_controller->setZone(m_glMonitor->getControllerProxy()->zone()); } else { // timeline emit timelineZoneChanged(); } checkOverlay(); } void Monitor::slotSetZoneEnd() { m_glMonitor->getControllerProxy()->setZoneOut(m_glMonitor->getCurrentPos() + 1); if (m_controller) { m_controller->setZone(m_glMonitor->getControllerProxy()->zone()); } checkOverlay(); } // virtual void Monitor::mousePressEvent(QMouseEvent *event) { m_monitorManager->activateMonitor(m_id); if ((event->button() & Qt::RightButton) == 0u) { if (m_glWidget->geometry().contains(event->pos())) { m_DragStartPosition = event->pos(); event->accept(); } } else if (m_contextMenu) { slotActivateMonitor(); m_contextMenu->popup(event->globalPos()); event->accept(); } QWidget::mousePressEvent(event); } void Monitor::slotShowMenu(const QPoint pos) { slotActivateMonitor(); if (m_contextMenu) { if (m_markerMenu) { // Fill guide menu m_markerMenu->clear(); std::shared_ptr model; if (m_id == Kdenlive::ClipMonitor && m_controller) { model = m_controller->getMarkerModel(); } else if (m_id == Kdenlive::ProjectMonitor && pCore->currentDoc()) { model = pCore->currentDoc()->getGuideModel(); } if (model) { QList markersList = model->getAllMarkers(); for (CommentedTime mkr : markersList) { QAction *a = m_markerMenu->addAction(mkr.comment()); a->setData(mkr.time().frames(pCore->getCurrentFps())); } } m_markerMenu->setEnabled(!m_markerMenu->isEmpty()); } m_contextMenu->popup(pos); } } void Monitor::resizeEvent(QResizeEvent *event) { Q_UNUSED(event) if (m_glMonitor->zoom() > 0.0f) { float horizontal = float(m_horizontalScroll->value()) / float(m_horizontalScroll->maximum()); float vertical = float(m_verticalScroll->value()) / float(m_verticalScroll->maximum()); adjustScrollBars(horizontal, vertical); } else { m_horizontalScroll->hide(); m_verticalScroll->hide(); } } void Monitor::adjustScrollBars(float horizontal, float vertical) { if (m_glMonitor->zoom() > 1.0f) { m_horizontalScroll->setPageStep(m_glWidget->width()); m_horizontalScroll->setMaximum((int)((float)m_glMonitor->profileSize().width() * m_glMonitor->zoom()) - m_horizontalScroll->pageStep()); m_horizontalScroll->setValue(qRound(horizontal * float(m_horizontalScroll->maximum()))); emit m_horizontalScroll->valueChanged(m_horizontalScroll->value()); m_horizontalScroll->show(); } else { int max = (int)((float)m_glMonitor->profileSize().width() * m_glMonitor->zoom()) - m_glWidget->width(); emit m_horizontalScroll->valueChanged(qRound(0.5 * max)); m_horizontalScroll->hide(); } if (m_glMonitor->zoom() > 1.0f) { m_verticalScroll->setPageStep(m_glWidget->height()); m_verticalScroll->setMaximum((int)((float)m_glMonitor->profileSize().height() * m_glMonitor->zoom()) - m_verticalScroll->pageStep()); m_verticalScroll->setValue((int)((float)m_verticalScroll->maximum() * vertical)); emit m_verticalScroll->valueChanged(m_verticalScroll->value()); m_verticalScroll->show(); } else { int max = (int)((float)m_glMonitor->profileSize().height() * m_glMonitor->zoom()) - m_glWidget->height(); emit m_verticalScroll->valueChanged(qRound(0.5 * max)); m_verticalScroll->hide(); } } void Monitor::setZoom() { if (qFuzzyCompare(m_glMonitor->zoom(), 1.0f)) { m_horizontalScroll->hide(); m_verticalScroll->hide(); m_glMonitor->setOffsetX(m_horizontalScroll->value(), m_horizontalScroll->maximum()); m_glMonitor->setOffsetY(m_verticalScroll->value(), m_verticalScroll->maximum()); } else { adjustScrollBars(0.5f, 0.5f); } } void Monitor::slotSwitchFullScreen(bool minimizeOnly) { // TODO: disable screensaver? pause(); if (!m_glWidget->isFullScreen() && !minimizeOnly) { // Move monitor widget to the second screen (one screen for Kdenlive, the other one for the Monitor widget) if (qApp->screens().count() > 1) { for (auto screen : qApp->screens()) { QRect screenRect = screen->availableGeometry(); if (!screenRect.contains(pCore->window()->geometry().center())) { m_glWidget->setParent(nullptr); m_glWidget->move(this->parentWidget()->mapFromGlobal(screenRect.center())); m_glWidget->setGeometry(screenRect); break; } } } else { m_glWidget->setParent(nullptr); } m_glWidget->showFullScreen(); m_videoWidget->setFocus(); } else { m_glWidget->showNormal(); auto *lay = (QVBoxLayout *)layout(); lay->insertWidget(0, m_glWidget, 10); } } // virtual void Monitor::mouseReleaseEvent(QMouseEvent *event) { if (m_dragStarted) { event->ignore(); return; } if (event->button() != Qt::RightButton) { if (m_glMonitor->geometry().contains(event->pos())) { if (isActive()) { slotPlay(); } else { slotActivateMonitor(); } } // else event->ignore(); //QWidget::mouseReleaseEvent(event); } m_dragStarted = false; event->accept(); QWidget::mouseReleaseEvent(event); } void Monitor::slotStartDrag() { if (m_id == Kdenlive::ProjectMonitor || m_controller == nullptr) { // dragging is only allowed for clip monitor return; } auto *drag = new QDrag(this); auto *mimeData = new QMimeData; // Get drag state QQuickItem *root = m_glMonitor->rootObject(); int dragType = 0; if (root) { dragType = root->property("dragType").toInt(); root->setProperty("dragType", 0); } QByteArray prodData; QPoint p = m_glMonitor->getControllerProxy()->zone(); if (p.x() == -1 || p.y() == -1) { prodData = m_controller->AbstractProjectItem::clipId().toUtf8(); } else { QStringList list; list.append(m_controller->AbstractProjectItem::clipId()); list.append(QString::number(p.x())); list.append(QString::number(p.y())); prodData.append(list.join(QLatin1Char('/')).toUtf8()); } switch (dragType) { case 1: // Audio only drag prodData.prepend('A'); break; case 2: // Audio only drag prodData.prepend('V'); break; default: break; } mimeData->setData(QStringLiteral("kdenlive/producerslist"), prodData); drag->setMimeData(mimeData); /*QPixmap pix = m_currentClip->thumbnail(); drag->setPixmap(pix); drag->setHotSpot(QPoint(0, 50));*/ drag->exec(Qt::MoveAction); } // virtual void Monitor::mouseMoveEvent(QMouseEvent *event) { if (m_dragStarted || m_controller == nullptr) { return; } if ((event->pos() - m_DragStartPosition).manhattanLength() < QApplication::startDragDistance()) { return; } { auto *drag = new QDrag(this); auto *mimeData = new QMimeData; m_dragStarted = true; QStringList list; list.append(m_controller->AbstractProjectItem::clipId()); QPoint p = m_glMonitor->getControllerProxy()->zone(); list.append(QString::number(p.x())); list.append(QString::number(p.y())); QByteArray clipData; clipData.append(list.join(QLatin1Char(';')).toUtf8()); mimeData->setData(QStringLiteral("kdenlive/clip"), clipData); drag->setMimeData(mimeData); drag->exec(Qt::MoveAction); } event->accept(); } /*void Monitor::dragMoveEvent(QDragMoveEvent * event) { event->setDropAction(Qt::IgnoreAction); event->setDropAction(Qt::MoveAction); if (event->mimeData()->hasText()) { event->acceptProposedAction(); } } Qt::DropActions Monitor::supportedDropActions() const { // returns what actions are supported when dropping return Qt::MoveAction; }*/ QStringList Monitor::mimeTypes() const { QStringList qstrList; // list of accepted MIME types for drop qstrList.append(QStringLiteral("kdenlive/clip")); return qstrList; } // virtual void Monitor::wheelEvent(QWheelEvent *event) { slotMouseSeek(event->delta(), event->modifiers()); event->accept(); } void Monitor::mouseDoubleClickEvent(QMouseEvent *event) { slotSwitchFullScreen(); event->accept(); } void Monitor::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { slotSwitchFullScreen(); event->accept(); return; } if (m_glWidget->isFullScreen()) { event->ignore(); emit passKeyPress(event); return; } QWidget::keyPressEvent(event); } void Monitor::slotMouseSeek(int eventDelta, uint modifiers) { if ((modifiers & Qt::ControlModifier) != 0u) { int delta = qRound(pCore->getCurrentFps()); if (eventDelta > 0) { delta = 0 - delta; } m_glMonitor->getControllerProxy()->setPosition(m_glMonitor->getCurrentPos() - delta); } else if ((modifiers & Qt::AltModifier) != 0u) { if (eventDelta >= 0) { emit seekToPreviousSnap(); } else { emit seekToNextSnap(); } } else { if (eventDelta >= 0) { slotRewindOneFrame(); } else { slotForwardOneFrame(); } } } void Monitor::slotSetThumbFrame() { if (m_controller == nullptr) { return; } m_controller->setProducerProperty(QStringLiteral("kdenlive:thumbnailFrame"), m_glMonitor->getCurrentPos()); emit refreshClipThumbnail(m_controller->AbstractProjectItem::clipId()); } void Monitor::slotExtractCurrentZone() { if (m_controller == nullptr) { return; } GenTime inPoint(getZoneStart(), pCore->getCurrentFps()); GenTime outPoint(getZoneEnd(), pCore->getCurrentFps()); pCore->jobManager()->startJob({m_controller->clipId()}, -1, QString(), inPoint, outPoint); } std::shared_ptr Monitor::currentController() const { return m_controller; } void Monitor::slotExtractCurrentFrame(QString frameName, bool addToProject) { if (QFileInfo(frameName).fileName().isEmpty()) { // convenience: when extracting an image to be added to the project, // suggest a suitable image file name. In the project monitor, this // suggestion bases on the project file name; in the clip monitor, // the suggestion bases on the clip file name currently shown. // Finally, the frame number is added to this suggestion, prefixed // with "-f", so we get something like clip-f#.png. QString suggestedImageName = QFileInfo(currentController() ? currentController()->clipName() : pCore->currentDoc()->url().isValid() ? pCore->currentDoc()->url().fileName() : i18n("untitled")) .completeBaseName() + QStringLiteral("-f") + QString::number(m_glMonitor->getCurrentPos()).rightJustified(6, QLatin1Char('0')) + QStringLiteral(".png"); frameName = QFileInfo(frameName, suggestedImageName).fileName(); } QString framesFolder = KRecentDirs::dir(QStringLiteral(":KdenliveFramesFolder")); if (framesFolder.isEmpty()) { framesFolder = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); } QScopedPointer dlg(new QDialog(this)); QScopedPointer fileWidget(new KFileWidget(QUrl::fromLocalFile(framesFolder), dlg.data())); dlg->setWindowTitle(addToProject ? i18n("Save Image") : i18n("Save Image to Project")); auto *layout = new QVBoxLayout; layout->addWidget(fileWidget.data()); QCheckBox *b = nullptr; if (m_id == Kdenlive::ClipMonitor) { b = new QCheckBox(i18n("Export image using source resolution"), dlg.data()); b->setChecked(KdenliveSettings::exportframe_usingsourceres()); fileWidget->setCustomWidget(b); } fileWidget->setConfirmOverwrite(true); fileWidget->okButton()->show(); fileWidget->cancelButton()->show(); QObject::connect(fileWidget->okButton(), &QPushButton::clicked, fileWidget.data(), &KFileWidget::slotOk); QObject::connect(fileWidget.data(), &KFileWidget::accepted, fileWidget.data(), &KFileWidget::accept); QObject::connect(fileWidget.data(), &KFileWidget::accepted, dlg.data(), &QDialog::accept); QObject::connect(fileWidget->cancelButton(), &QPushButton::clicked, dlg.data(), &QDialog::reject); dlg->setLayout(layout); fileWidget->setMimeFilter(QStringList() << QStringLiteral("image/png")); fileWidget->setMode(KFile::File | KFile::LocalOnly); fileWidget->setOperationMode(KFileWidget::Saving); QUrl relativeUrl; relativeUrl.setPath(frameName); fileWidget->setSelectedUrl(relativeUrl); KSharedConfig::Ptr conf = KSharedConfig::openConfig(); QWindow *handle = dlg->windowHandle(); if ((handle != nullptr) && conf->hasGroup("FileDialogSize")) { KWindowConfig::restoreWindowSize(handle, conf->group("FileDialogSize")); dlg->resize(handle->size()); } if (dlg->exec() == QDialog::Accepted) { QString selectedFile = fileWidget->selectedFile(); if (!selectedFile.isEmpty()) { // Create Qimage with frame QImage frame; // check if we are using a proxy if ((m_controller != nullptr) && !m_controller->getProducerProperty(QStringLiteral("kdenlive:proxy")).isEmpty() && m_controller->getProducerProperty(QStringLiteral("kdenlive:proxy")) != QLatin1String("-")) { // using proxy, use original clip url to get frame frame = m_glMonitor->getControllerProxy()->extractFrame(m_glMonitor->getCurrentPos(), m_controller->getProducerProperty(QStringLiteral("kdenlive:originalurl")), -1, -1, b != nullptr ? b->isChecked() : false); } else { frame = m_glMonitor->getControllerProxy()->extractFrame(m_glMonitor->getCurrentPos(), QString(), -1, -1, b != nullptr ? b->isChecked() : false); } frame.save(selectedFile); if (b != nullptr) { KdenliveSettings::setExportframe_usingsourceres(b->isChecked()); } KRecentDirs::add(QStringLiteral(":KdenliveFramesFolder"), QUrl::fromLocalFile(selectedFile).adjusted(QUrl::RemoveFilename).toLocalFile()); if (addToProject) { QString folderInfo = pCore->bin()->getCurrentFolder(); pCore->bin()->droppedUrls(QList {QUrl::fromLocalFile(selectedFile)}, folderInfo); } } } } void Monitor::setTimePos(const QString &pos) { m_timePos->setValue(pos); slotSeek(); } void Monitor::slotSeek() { slotSeek(m_timePos->getValue()); } void Monitor::slotSeek(int pos) { slotActivateMonitor(); m_glMonitor->getControllerProxy()->setPosition(pos); m_monitorManager->cleanMixer(); } void Monitor::checkOverlay(int pos) { if (m_qmlManager->sceneType() != MonitorSceneDefault) { // we are not in main view, ignore return; } QString overlayText; if (pos == -1) { pos = m_timePos->getValue(); } std::shared_ptr model(nullptr); if (m_id == Kdenlive::ClipMonitor) { if (m_controller) { model = m_controller->getMarkerModel(); } } else if (m_id == Kdenlive::ProjectMonitor && pCore->currentDoc()) { model = pCore->currentDoc()->getGuideModel(); } if (model) { bool found = false; CommentedTime marker = model->getMarker(GenTime(pos, pCore->getCurrentFps()), &found); if (found) { overlayText = marker.comment(); } } m_glMonitor->getControllerProxy()->setMarkerComment(overlayText); } int Monitor::getZoneStart() { return m_glMonitor->getControllerProxy()->zoneIn(); } int Monitor::getZoneEnd() { return m_glMonitor->getControllerProxy()->zoneOut(); } void Monitor::slotZoneStart() { slotActivateMonitor(); m_glMonitor->getControllerProxy()->setPosition(m_glMonitor->getControllerProxy()->zoneIn()); } void Monitor::slotZoneEnd() { slotActivateMonitor(); m_glMonitor->getControllerProxy()->setPosition(m_glMonitor->getControllerProxy()->zoneOut() - 1); } void Monitor::slotRewind(double speed) { slotActivateMonitor(); if (qFuzzyIsNull(speed)) { double currentspeed = m_glMonitor->playSpeed(); if (currentspeed > -1) { m_glMonitor->purgeCache(); speed = -1; resetSpeedInfo(); } else { m_speedIndex++; if (m_speedIndex > 4) { m_speedIndex = 0; } speed = -MonitorManager::speedArray[m_speedIndex]; m_speedLabel->setFixedWidth(QWIDGETSIZE_MAX); m_speedLabel->setText(QString("x%1").arg(speed)); } } m_playAction->setActive(true); m_glMonitor->switchPlay(true, speed); } void Monitor::slotForward(double speed, bool allowNormalPlay) { slotActivateMonitor(); if (qFuzzyIsNull(speed)) { double currentspeed = m_glMonitor->playSpeed(); if (currentspeed < 1) { if (allowNormalPlay) { m_glMonitor->purgeCache(); resetSpeedInfo(); m_playAction->setActive(true); m_glMonitor->switchPlay(true, 1); return; } else { m_speedIndex = 0; } } else { m_speedIndex++; } if (m_speedIndex > 4) { m_speedIndex = 0; } speed = MonitorManager::speedArray[m_speedIndex]; m_speedLabel->setFixedWidth(QWIDGETSIZE_MAX); m_speedLabel->setText(QString("x%1").arg(speed)); } m_playAction->setActive(true); m_glMonitor->switchPlay(true, speed); } void Monitor::slotRewindOneFrame(int diff) { slotActivateMonitor(); m_glMonitor->getControllerProxy()->setPosition(qMax(0, m_glMonitor->getCurrentPos() - diff)); } void Monitor::slotForwardOneFrame(int diff) { slotActivateMonitor(); if (m_id == Kdenlive::ClipMonitor) { m_glMonitor->getControllerProxy()->setPosition(qMin(m_glMonitor->duration() - 1, m_glMonitor->getCurrentPos() + diff)); } else { m_glMonitor->getControllerProxy()->setPosition(m_glMonitor->getCurrentPos() + diff); } } void Monitor::adjustRulerSize(int length, const std::shared_ptr &markerModel) { if (m_controller != nullptr) { m_glMonitor->setRulerInfo(length); } else { m_glMonitor->setRulerInfo(length, markerModel); } m_timePos->setRange(0, length); if (markerModel) { connect(markerModel.get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); connect(markerModel.get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); connect(markerModel.get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); } } void Monitor::stop() { m_playAction->setActive(false); m_glMonitor->stop(); } void Monitor::mute(bool mute, bool updateIconOnly) { // TODO: we should set the "audio_off" property to 1 to mute the consumer instead of changing volume QIcon icon; if (mute || KdenliveSettings::volume() == 0) { icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted")); } else { icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); } if (!updateIconOnly) { m_glMonitor->setVolume(mute ? 0 : (double)KdenliveSettings::volume() / 100.0); } } void Monitor::start() { if (!isVisible() || !isActive()) { return; } m_glMonitor->startConsumer(); } void Monitor::slotRefreshMonitor(bool visible) { if (visible) { if (slotActivateMonitor()) { start(); } } } void Monitor::forceMonitorRefresh() { slotActivateMonitor(); m_glMonitor->refresh(); } void Monitor::refreshMonitorIfActive(bool directUpdate) { if (isActive()) { if (directUpdate) { m_glMonitor->refresh(); } else { m_glMonitor->requestRefresh(); } } } void Monitor::pause() { if (!m_playAction->isActive()) { return; } slotActivateMonitor(); m_glMonitor->switchPlay(false); m_playAction->setActive(false); resetSpeedInfo(); } void Monitor::switchPlay(bool play) { m_playAction->setActive(play); m_glMonitor->switchPlay(play); resetSpeedInfo(); } void Monitor::slotSwitchPlay() { slotActivateMonitor(); m_glMonitor->switchPlay(m_playAction->isActive()); bool showDropped; if (m_id == Kdenlive::ClipMonitor) { showDropped = KdenliveSettings::displayClipMonitorInfo() & 0x20; } else { showDropped = KdenliveSettings::displayProjectMonitorInfo() & 0x20; } if (showDropped) { m_glMonitor->resetDrops(); m_droppedTimer.start(); } else { m_droppedTimer.stop(); } resetSpeedInfo(); } void Monitor::slotPlay() { m_playAction->trigger(); } void Monitor::resetPlayOrLoopZone(const QString &binId) { if (activeClipId() == binId) { m_glMonitor->resetZoneMode(); } } void Monitor::slotPlayZone() { slotActivateMonitor(); bool ok = m_glMonitor->playZone(); if (ok) { m_playAction->setActive(true); } } void Monitor::slotLoopZone() { slotActivateMonitor(); bool ok = m_glMonitor->playZone(true); if (ok) { m_playAction->setActive(true); } } void Monitor::slotLoopClip() { slotActivateMonitor(); bool ok = m_glMonitor->loopClip(); if (ok) { m_playAction->setActive(true); } } void Monitor::updateClipProducer(const std::shared_ptr &prod) { if (m_glMonitor->setProducer(prod, isActive(), -1)) { prod->set_speed(1.0); } } void Monitor::updateClipProducer(const QString &playlist) { Q_UNUSED(playlist) // TODO // Mlt::Producer *prod = new Mlt::Producer(*m_glMonitor->profile(), playlist.toUtf8().constData()); // m_glMonitor->setProducer(prod, isActive(), render->seekFramePosition()); m_glMonitor->switchPlay(true); } void Monitor::slotOpenClip(const std::shared_ptr &controller, int in, int out) { if (m_controller) { m_glMonitor->resetZoneMode(); disconnect(m_controller.get(), &ProjectClip::audioThumbReady, this, &Monitor::prepareAudioThumb); disconnect(m_controller->getMarkerModel().get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); disconnect(m_controller->getMarkerModel().get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); disconnect(m_controller->getMarkerModel().get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); } m_controller = controller; m_snaps.reset(new SnapModel()); m_glMonitor->getControllerProxy()->resetZone(); if (controller) { connect(m_controller.get(), &ProjectClip::audioThumbReady, this, &Monitor::prepareAudioThumb); connect(m_controller->getMarkerModel().get(), SIGNAL(dataChanged(const QModelIndex &, const QModelIndex &, const QVector &)), this, SLOT(checkOverlay())); connect(m_controller->getMarkerModel().get(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); connect(m_controller->getMarkerModel().get(), SIGNAL(rowsRemoved(const QModelIndex &, int, int)), this, SLOT(checkOverlay())); if (m_recManager->toolbar()->isVisible()) { // we are in record mode, don't display clip return; } m_timePos->setRange(0, (int)m_controller->frameDuration() - 1); m_glMonitor->setRulerInfo((int)m_controller->frameDuration() - 1, controller->getMarkerModel()); loadQmlScene(MonitorSceneDefault); updateMarkers(); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::addSnap, this, &Monitor::addSnapPoint, Qt::DirectConnection); connect(m_glMonitor->getControllerProxy(), &MonitorProxy::removeSnap, this, &Monitor::removeSnapPoint, Qt::DirectConnection); if (out == -1) { m_glMonitor->getControllerProxy()->setZone(m_controller->zone(), false); } else { m_glMonitor->getControllerProxy()->setZone(in, out, false); } m_snaps->addPoint((int)m_controller->frameDuration() - 1); // Loading new clip / zone, stop if playing if (m_playAction->isActive()) { m_playAction->setActive(false); } m_audioMeterWidget->audioChannels = controller->audioInfo() ? controller->audioInfo()->channels() : 0; if (!m_controller->hasVideo() || KdenliveSettings::displayClipMonitorInfo() & 0x10) { m_glMonitor->getControllerProxy()->setAudioThumb(m_audioMeterWidget->audioChannels == 0 ? QUrl() : ThumbnailCache::get()->getAudioThumbPath(m_controller->clipId())); } m_controller->getMarkerModel()->registerSnapModel(m_snaps); m_glMonitor->getControllerProxy()->setClipProperties(controller->clipType(), controller->hasAudioAndVideo(), controller->clipName()); m_glMonitor->setProducer(m_controller->originalProducer(), isActive(), in); // hasEffects = controller->hasEffects(); } else { loadQmlScene(MonitorSceneDefault); m_glMonitor->setProducer(nullptr, isActive()); m_glMonitor->getControllerProxy()->setAudioThumb(); m_audioMeterWidget->audioChannels = 0; m_glMonitor->getControllerProxy()->setClipProperties(ClipType::Unknown, false, QString()); } if (slotActivateMonitor()) { start(); } checkOverlay(); } const QString Monitor::activeClipId() { if (m_controller) { return m_controller->clipId(); } return QString(); } void Monitor::slotOpenDvdFile(const QString &file) { // TODO Q_UNUSED(file) m_glMonitor->initializeGL(); // render->loadUrl(file); } void Monitor::slotSaveZone() { // TODO? or deprecate // render->saveZone(pCore->currentDoc()->projectDataFolder(), m_ruler->zone()); } void Monitor::setCustomProfile(const QString &profile, const Timecode &tc) { // TODO or deprecate Q_UNUSED(profile) m_timePos->updateTimeCode(tc); if (/* DISABLES CODE */ (true)) { return; } slotActivateMonitor(); // render->prepareProfileReset(tc.fps()); // TODO: this is a temporary profile for DVD preview, it should not alter project profile // pCore->setCurrentProfile(profile); m_glMonitor->reloadProfile(); } void Monitor::resetProfile() { m_timePos->updateTimeCode(pCore->timecode()); m_glMonitor->reloadProfile(); m_glMonitor->rootObject()->setProperty("framesize", QRect(0, 0, m_glMonitor->profileSize().width(), m_glMonitor->profileSize().height())); double fps = pCore->getCurrentFps(); // Update drop frame info m_qmlManager->setProperty(QStringLiteral("dropped"), false); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(fps, 'g', 2)); } void Monitor::resetConsumer(bool fullReset) { m_glMonitor->resetConsumer(fullReset); } const QString Monitor::sceneList(const QString &root, const QString &fullPath) { return m_glMonitor->sceneList(root, fullPath); } void Monitor::updateClipZone() { if (m_controller == nullptr) { return; } m_controller->setZone(m_glMonitor->getControllerProxy()->zone()); } void Monitor::updateTimelineClipZone() { emit zoneUpdated(m_glMonitor->getControllerProxy()->zone()); } void Monitor::switchDropFrames(bool drop) { m_glMonitor->setDropFrames(drop); } void Monitor::switchMonitorInfo(int code) { int currentOverlay; if (m_id == Kdenlive::ClipMonitor) { currentOverlay = KdenliveSettings::displayClipMonitorInfo(); currentOverlay ^= code; KdenliveSettings::setDisplayClipMonitorInfo(currentOverlay); } else { currentOverlay = KdenliveSettings::displayProjectMonitorInfo(); currentOverlay ^= code; KdenliveSettings::setDisplayProjectMonitorInfo(currentOverlay); } updateQmlDisplay(currentOverlay); } void Monitor::updateMonitorGamma() { if (isActive()) { stop(); m_glMonitor->updateGamma(); start(); } else { m_glMonitor->updateGamma(); } } void Monitor::slotEditMarker() { if (m_editMarker) { m_editMarker->trigger(); } } void Monitor::updateTimecodeFormat() { m_timePos->slotUpdateTimeCodeFormat(); m_glMonitor->rootObject()->setProperty("timecode", m_timePos->displayText()); } QPoint Monitor::getZoneInfo() const { if (m_controller == nullptr) { return {}; } return m_controller->zone(); } void Monitor::slotEnableEffectScene(bool enable) { KdenliveSettings::setShowOnMonitorScene(enable); MonitorSceneType sceneType = enable ? m_lastMonitorSceneType : MonitorSceneDefault; slotShowEffectScene(sceneType, true); if (enable) { emit updateScene(); } } void Monitor::slotShowEffectScene(MonitorSceneType sceneType, bool temporary) { if (sceneType == MonitorSceneNone) { // We just want to revert to normal scene if (m_qmlManager->sceneType() == MonitorSceneSplit || m_qmlManager->sceneType() == MonitorSceneDefault) { // Ok, nothing to do return; } sceneType = MonitorSceneDefault; } if (!temporary) { m_lastMonitorSceneType = sceneType; } loadQmlScene(sceneType); } void Monitor::slotSeekToKeyFrame() { if (m_qmlManager->sceneType() == MonitorSceneGeometry) { // Adjust splitter pos int kfr = m_glMonitor->rootObject()->property("requestedKeyFrame").toInt(); emit seekToKeyframe(kfr); } } void Monitor::setUpEffectGeometry(const QRect &r, const QVariantList &list, const QVariantList &types) { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return; } if (!list.isEmpty()) { root->setProperty("centerPointsTypes", types); root->setProperty("centerPoints", list); } if (!r.isEmpty()) { root->setProperty("framesize", r); } } void Monitor::setEffectSceneProperty(const QString &name, const QVariant &value) { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return; } root->setProperty(name.toUtf8().constData(), value); } QRect Monitor::effectRect() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return {}; } return root->property("framesize").toRect(); } QVariantList Monitor::effectPolygon() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return QVariantList(); } return root->property("centerPoints").toList(); } QVariantList Monitor::effectRoto() const { QQuickItem *root = m_glMonitor->rootObject(); if (!root) { return QVariantList(); } QVariantList points = root->property("centerPoints").toList(); QVariantList controlPoints = root->property("centerPointsTypes").toList(); // rotoscoping effect needs a list of QVariantList mix; mix.reserve(points.count() * 3); for (int i = 0; i < points.count(); i++) { mix << controlPoints.at(2 * i); mix << points.at(i); mix << controlPoints.at(2 * i + 1); } return mix; } void Monitor::setEffectKeyframe(bool enable) { QQuickItem *root = m_glMonitor->rootObject(); if (root) { root->setProperty("iskeyframe", enable); } } bool Monitor::effectSceneDisplayed(MonitorSceneType effectType) { return m_qmlManager->sceneType() == effectType; } void Monitor::slotSetVolume(int volume) { KdenliveSettings::setVolume(volume); QIcon icon; double renderVolume = m_glMonitor->volume(); m_glMonitor->setVolume((double)volume / 100.0); if (renderVolume > 0 && volume > 0) { return; } /*if (volume == 0) { icon = QIcon::fromTheme(QStringLiteral("audio-volume-muted")); } else { icon = QIcon::fromTheme(QStringLiteral("audio-volume-medium")); }*/ } void Monitor::sendFrameForAnalysis(bool analyse) { m_glMonitor->sendFrameForAnalysis = analyse; } void Monitor::updateAudioForAnalysis() { m_glMonitor->updateAudioForAnalysis(); } void Monitor::onFrameDisplayed(const SharedFrame &frame) { if (!m_glMonitor->checkFrameNumber(frame.get_position(), m_offset, m_playAction->isActive())) { m_playAction->setActive(false); } m_monitorManager->frameDisplayed(frame); } void Monitor::checkDrops() { int dropped = m_glMonitor->droppedFrames(); if (dropped == 0) { // No dropped frames since last check m_qmlManager->setProperty(QStringLiteral("dropped"), false); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(pCore->getCurrentFps(), 'g', 2)); } else { m_glMonitor->resetDrops(); dropped = pCore->getCurrentFps() - dropped; m_qmlManager->setProperty(QStringLiteral("dropped"), true); m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(dropped, 'g', 2)); } } void Monitor::reloadProducer(const QString &id) { if (!m_controller) { return; } if (m_controller->AbstractProjectItem::clipId() == id) { slotOpenClip(m_controller); } } QString Monitor::getMarkerThumb(GenTime pos) { if (!m_controller) { return QString(); } if (!m_controller->getClipHash().isEmpty()) { bool ok = false; QDir dir = pCore->currentDoc()->getCacheDir(CacheThumbs, &ok); if (ok) { QString url = dir.absoluteFilePath(m_controller->getClipHash() + QLatin1Char('#') + QString::number((int)pos.frames(pCore->getCurrentFps())) + QStringLiteral(".png")); if (QFile::exists(url)) { return url; } } } return QString(); } void Monitor::setPalette(const QPalette &p) { QWidget::setPalette(p); QList allButtons = this->findChildren(); for (int i = 0; i < allButtons.count(); i++) { QToolButton *m = allButtons.at(i); QIcon ic = m->icon(); if (ic.isNull() || ic.name().isEmpty()) { continue; } QIcon newIcon = QIcon::fromTheme(ic.name()); m->setIcon(newIcon); } QQuickItem *root = m_glMonitor->rootObject(); if (root) { QMetaObject::invokeMethod(root, "updatePalette"); } m_audioMeterWidget->refreshPixmap(); } void Monitor::gpuError() { qCWarning(KDENLIVE_LOG) << " + + + + Error initializing Movit GLSL manager"; warningMessage(i18n("Cannot initialize Movit's GLSL manager, please disable Movit"), -1); } void Monitor::warningMessage(const QString &text, int timeout, const QList &actions) { m_infoMessage->setMessageType(KMessageWidget::Warning); m_infoMessage->setText(text); for (QAction *action : actions) { m_infoMessage->addAction(action); } m_infoMessage->setCloseButtonVisible(true); m_infoMessage->animatedShow(); if (timeout > 0) { QTimer::singleShot(timeout, m_infoMessage, &KMessageWidget::animatedHide); } } void Monitor::activateSplit() { loadQmlScene(MonitorSceneSplit); if (isActive()) { m_glMonitor->requestRefresh(); } else if (slotActivateMonitor()) { start(); } } void Monitor::slotSwitchCompare(bool enable) { if (m_id == Kdenlive::ProjectMonitor) { if (enable) { if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Split scene is already active return; } m_splitEffect.reset(new Mlt::Filter(pCore->getCurrentProfile()->profile(), "frei0r.alphagrad")); if ((m_splitEffect != nullptr) && m_splitEffect->is_valid()) { m_splitEffect->set("0", 0.5); // 0 is the Clip left parameter m_splitEffect->set("1", 0); // 1 is gradient width m_splitEffect->set("2", -0.747); // 2 is tilt } else { // frei0r.scal0tilt is not available warningMessage(i18n("The alphagrad filter is required for that feature, please install frei0r and restart Kdenlive")); return; } emit createSplitOverlay(m_splitEffect); return; } // Delete temp track emit removeSplitOverlay(); m_splitEffect.reset(); loadQmlScene(MonitorSceneDefault); if (isActive()) { m_glMonitor->requestRefresh(); } else if (slotActivateMonitor()) { start(); } return; } if (m_controller == nullptr || !m_controller->hasEffects()) { // disable split effect if (m_controller) { pCore->displayMessage(i18n("Clip has no effects"), InformationMessage); } else { pCore->displayMessage(i18n("Select a clip in project bin to compare effect"), InformationMessage); } return; } if (enable) { if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Split scene is already active qDebug() << " . . . .. ALREADY ACTIVE"; return; } buildSplitEffect(m_controller->masterProducer()); } else if (m_splitEffect) { // TODO m_glMonitor->setProducer(m_controller->originalProducer(), isActive(), position()); m_splitEffect.reset(); m_splitProducer.reset(); loadQmlScene(MonitorSceneDefault); } slotActivateMonitor(); } void Monitor::buildSplitEffect(Mlt::Producer *original) { m_splitEffect.reset(new Mlt::Filter(pCore->getCurrentProfile()->profile(), "frei0r.alphagrad")); if ((m_splitEffect != nullptr) && m_splitEffect->is_valid()) { m_splitEffect->set("0", 0.5); // 0 is the Clip left parameter m_splitEffect->set("1", 0); // 1 is gradient width m_splitEffect->set("2", -0.747); // 2 is tilt } else { // frei0r.scal0tilt is not available pCore->displayMessage(i18n("The alphagrad filter is required for that feature, please install frei0r and restart Kdenlive"), ErrorMessage); return; } QString splitTransition = TransitionsRepository::get()->getCompositingTransition(); Mlt::Transition t(pCore->getCurrentProfile()->profile(), splitTransition.toUtf8().constData()); if (!t.is_valid()) { m_splitEffect.reset(); pCore->displayMessage(i18n("The cairoblend transition is required for that feature, please install frei0r and restart Kdenlive"), ErrorMessage); return; } Mlt::Tractor trac(pCore->getCurrentProfile()->profile()); std::shared_ptr clone = ProjectClip::cloneProducer(std::make_shared(original)); // Delete all effects int ct = 0; Mlt::Filter *filter = clone->filter(ct); while (filter != nullptr) { QString ix = QString::fromLatin1(filter->get("kdenlive_id")); if (!ix.isEmpty()) { if (clone->detach(*filter) == 0) { } else { ct++; } } else { ct++; } delete filter; filter = clone->filter(ct); } trac.set_track(*original, 0); trac.set_track(*clone.get(), 1); clone.get()->attach(*m_splitEffect.get()); t.set("always_active", 1); trac.plant_transition(t, 0, 1); delete original; m_splitProducer = std::make_shared(trac.get_producer()); m_glMonitor->setProducer(m_splitProducer, isActive(), position()); m_glMonitor->setRulerInfo((int)m_controller->frameDuration(), m_controller->getMarkerModel()); loadQmlScene(MonitorSceneSplit); } QSize Monitor::profileSize() const { return m_glMonitor->profileSize(); } void Monitor::loadQmlScene(MonitorSceneType type) { if (m_id == Kdenlive::DvdMonitor || type == m_qmlManager->sceneType()) { return; } bool sceneWithEdit = type == MonitorSceneGeometry || type == MonitorSceneCorners || type == MonitorSceneRoto; if ((m_sceneVisibilityAction != nullptr) && !m_sceneVisibilityAction->isChecked() && sceneWithEdit) { // User doesn't want effect scenes pCore->displayMessage(i18n("Enable edit mode in monitor to edit effect"), InformationMessage, 500); type = MonitorSceneDefault; } m_qmlManager->setScene(m_id, type, pCore->getCurrentFrameSize(), pCore->getCurrentDar(), m_glMonitor->displayRect(), m_glMonitor->zoom(), m_timePos->maximum()); QQuickItem *root = m_glMonitor->rootObject(); switch (type) { case MonitorSceneSplit: QObject::connect(root, SIGNAL(qmlMoveSplit()), this, SLOT(slotAdjustEffectCompare()), Qt::UniqueConnection); break; case MonitorSceneGeometry: case MonitorSceneCorners: case MonitorSceneRoto: break; case MonitorSceneRipple: QObject::connect(root, SIGNAL(doAcceptRipple(bool)), this, SIGNAL(acceptRipple(bool)), Qt::UniqueConnection); QObject::connect(root, SIGNAL(switchTrimMode(int)), this, SIGNAL(switchTrimMode(int)), Qt::UniqueConnection); break; case MonitorSceneDefault: QObject::connect(root, SIGNAL(editCurrentMarker()), this, SLOT(slotEditInlineMarker()), Qt::UniqueConnection); m_qmlManager->setProperty(QStringLiteral("timecode"), m_timePos->displayText()); if (m_id == Kdenlive::ClipMonitor) { updateQmlDisplay(KdenliveSettings::displayClipMonitorInfo()); } else if (m_id == Kdenlive::ProjectMonitor) { updateQmlDisplay(KdenliveSettings::displayProjectMonitorInfo()); } break; default: break; } m_qmlManager->setProperty(QStringLiteral("fps"), QString::number(pCore->getCurrentFps(), 'g', 2)); } void Monitor::setQmlProperty(const QString &name, const QVariant &value) { m_qmlManager->setProperty(name, value); } void Monitor::slotAdjustEffectCompare() { double percent = 0.5; if (m_qmlManager->sceneType() == MonitorSceneSplit) { // Adjust splitter pos QQuickItem *root = m_glMonitor->rootObject(); percent = root->property("percentage").toDouble(); // Store real frame percentage for resize events root->setProperty("realpercent", percent); } if (m_splitEffect) { m_splitEffect->set("0", 0.5 - (percent - 0.5) * .666); } m_glMonitor->refresh(); } void Monitor::slotSwitchRec(bool enable) { if (!m_recManager) { return; } if (enable) { m_toolbar->setVisible(false); m_recManager->toolbar()->setVisible(true); } else if (m_recManager->toolbar()->isVisible()) { m_recManager->stop(); m_toolbar->setVisible(true); emit refreshCurrentClip(); } } void Monitor::doKeyPressEvent(QKeyEvent *ev) { keyPressEvent(ev); } void Monitor::slotEditInlineMarker() { QQuickItem *root = m_glMonitor->rootObject(); if (root) { std::shared_ptr model; if (m_controller) { // We are editing a clip marker model = m_controller->getMarkerModel(); } else { model = pCore->currentDoc()->getGuideModel(); } QString newComment = root->property("markerText").toString(); bool found = false; CommentedTime oldMarker = model->getMarker(m_timePos->gentime(), &found); if (!found || newComment == oldMarker.comment()) { // No change return; } oldMarker.setComment(newComment); model->addMarker(oldMarker.time(), oldMarker.comment(), oldMarker.markerType()); } } void Monitor::prepareAudioThumb() { if (m_controller) { m_glMonitor->getControllerProxy()->setAudioThumb(ThumbnailCache::get()->getAudioThumbPath(m_controller->clipId())); } } void Monitor::slotSwitchAudioMonitor() { if (!m_audioMeterWidget->isValid) { KdenliveSettings::setMonitoraudio(0x01); m_audioMeterWidget->setVisibility(false); return; } int currentOverlay = KdenliveSettings::monitoraudio(); currentOverlay ^= m_id; KdenliveSettings::setMonitoraudio(currentOverlay); if ((KdenliveSettings::monitoraudio() & m_id) != 0) { // We want to enable this audio monitor, so make monitor active slotActivateMonitor(); } displayAudioMonitor(isActive()); } void Monitor::displayAudioMonitor(bool isActive) { bool enable = isActive && ((KdenliveSettings::monitoraudio() & m_id) != 0); if (enable) { connect(m_monitorManager, &MonitorManager::frameDisplayed, m_audioMeterWidget, &ScopeWidget::onNewFrame, Qt::UniqueConnection); } else { disconnect(m_monitorManager, &MonitorManager::frameDisplayed, m_audioMeterWidget, &ScopeWidget::onNewFrame); } m_audioMeterWidget->setVisibility((KdenliveSettings::monitoraudio() & m_id) != 0); if (isActive && m_glWidget->isFullScreen()) { // If both monitors are fullscreen, this is necessary to do the switch m_glWidget->showFullScreen(); m_videoWidget->setFocus(); } } void Monitor::updateQmlDisplay(int currentOverlay) { m_glMonitor->rootObject()->setVisible((currentOverlay & 0x01) != 0); m_glMonitor->rootObject()->setProperty("showMarkers", currentOverlay & 0x04); bool showDropped = currentOverlay & 0x20; m_glMonitor->rootObject()->setProperty("showFps", showDropped); m_glMonitor->rootObject()->setProperty("showTimecode", currentOverlay & 0x02); m_glMonitor->rootObject()->setProperty("showAudiothumb", currentOverlay & 0x10); if (showDropped) { if (!m_droppedTimer.isActive() && m_playAction->isActive()) { m_glMonitor->resetDrops(); m_droppedTimer.start(); } } else { m_droppedTimer.stop(); } } void Monitor::clearDisplay() { m_glMonitor->clear(); } void Monitor::panView(QPoint diff) { // Only pan if scrollbars are visible if (m_horizontalScroll->isVisible()) { m_horizontalScroll->setValue(m_horizontalScroll->value() + diff.x()); } if (m_verticalScroll->isVisible()) { m_verticalScroll->setValue(m_verticalScroll->value() + diff.y()); } } void Monitor::processSeek(int pos) { slotActivateMonitor(); pause(); m_glMonitor->requestSeek(pos); m_monitorManager->cleanMixer(); } void Monitor::requestSeek(int pos) { m_glMonitor->getControllerProxy()->setPosition(pos); } void Monitor::setProducer(std::shared_ptr producer, int pos) { m_glMonitor->setProducer(std::move(producer), isActive(), pos); } void Monitor::reconfigure() { m_glMonitor->reconfigure(); } void Monitor::slotSeekPosition(int pos) { emit seekPosition(pos); m_timePos->setValue(pos); checkOverlay(); } void Monitor::slotStart() { slotActivateMonitor(); m_glMonitor->switchPlay(false); m_glMonitor->getControllerProxy()->setPosition(0); resetSpeedInfo(); } void Monitor::slotEnd() { slotActivateMonitor(); m_glMonitor->switchPlay(false); resetSpeedInfo(); if (m_id == Kdenlive::ClipMonitor) { m_glMonitor->getControllerProxy()->setPosition(m_glMonitor->duration() - 1); } else { m_glMonitor->getControllerProxy()->setPosition(pCore->projectDuration() - 1); } } void Monitor::resetSpeedInfo() { m_speedIndex = -1; m_speedLabel->setFixedWidth(0); m_speedLabel->clear(); } void Monitor::addSnapPoint(int pos) { m_snaps->addPoint(pos); } void Monitor::removeSnapPoint(int pos) { m_snaps->removePoint(pos); } void Monitor::slotSetScreen(int screenIndex) { emit screenChanged(screenIndex); } void Monitor::slotZoomIn() { m_glMonitor->slotZoom(true); } void Monitor::slotZoomOut() { m_glMonitor->slotZoom(false); } void Monitor::setConsumerProperty(const QString &name, const QString &value) { m_glMonitor->setConsumerProperty(name, value); } void Monitor::purgeCache() { m_glMonitor->purgeCache(); } void Monitor::updateBgColor() { m_glMonitor->m_bgColor = KdenliveSettings::window_background(); } MonitorProxy *Monitor::getControllerProxy() { return m_glMonitor->getControllerProxy(); } diff --git a/src/monitor/monitor.h b/src/monitor/monitor.h index 4c5905a76..912df5dc9 100644 --- a/src/monitor/monitor.h +++ b/src/monitor/monitor.h @@ -1,367 +1,368 @@ /*************************************************************************** * Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@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) any later version. * * * * 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, write to the * * Free Software Foundation, Inc., * * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * ***************************************************************************/ #ifndef MONITOR_H #define MONITOR_H #include "abstractmonitor.h" #include "bin/model/markerlistmodel.hpp" #include "definitions.h" #include "gentime.h" #include "scopes/sharedframe.h" #include "timecodedisplay.h" #include #include #include #include class SnapModel; class ProjectClip; class MonitorManager; class QSlider; class KDualAction; class KSelectAction; class KMessageWidget; class QScrollBar; class RecManager; class QmlManager; class QLabel; class GLWidget; class MonitorAudioLevel; class MonitorProxy; namespace Mlt { class Profile; class Filter; } // namespace Mlt class QuickEventEater : public QObject { Q_OBJECT public: explicit QuickEventEater(QObject *parent = nullptr); protected: bool eventFilter(QObject *obj, QEvent *event) override; signals: void addEffect(const QStringList &); }; class QuickMonitorEventEater : public QObject { Q_OBJECT public: explicit QuickMonitorEventEater(QWidget *parent); protected: bool eventFilter(QObject *obj, QEvent *event) override; signals: void doKeyPressEvent(QKeyEvent *); }; class Monitor : public AbstractMonitor { Q_OBJECT public: friend class MonitorManager; Monitor(Kdenlive::MonitorId id, MonitorManager *manager, QWidget *parent = nullptr); ~Monitor() override; void resetProfile(); /** @brief Rebuild consumers after a property change */ void resetConsumer(bool fullReset); void setCustomProfile(const QString &profile, const Timecode &tc); void setupMenu(QMenu *goMenu, QMenu *overlayMenu, QAction *playZone, QAction *loopZone, QMenu *markerMenu = nullptr, QAction *loopClip = nullptr); const QString sceneList(const QString &root, const QString &fullPath = QString()); const QString activeClipId(); int position(); void updateTimecodeFormat(); void updateMarkers(); /** @brief Controller for the clip currently displayed (only valid for clip monitor). */ std::shared_ptr currentController() const; /** @brief Add timeline guides to the ruler and context menu */ void setGuides(const QMap &guides); void reloadProducer(const QString &id); /** @brief Reimplemented from QWidget, updates the palette colors. */ void setPalette(const QPalette &p); /** @brief Returns current project's fps. */ double fps() const; /** @brief Get url for the clip's thumbnail */ QString getMarkerThumb(GenTime pos); int getZoneStart(); int getZoneEnd(); void setUpEffectGeometry(const QRect &r, const QVariantList &list = QVariantList(), const QVariantList &types = QVariantList()); /** @brief Set a property on the effect scene */ void setEffectSceneProperty(const QString &name, const QVariant &value); /** @brief Returns effective display size */ QSize profileSize() const; QRect effectRect() const; QVariantList effectPolygon() const; QVariantList effectRoto() const; void setEffectKeyframe(bool enable); void sendFrameForAnalysis(bool analyse); void updateAudioForAnalysis(); void switchMonitorInfo(int code); void switchDropFrames(bool drop); void updateMonitorGamma(); void mute(bool, bool updateIconOnly = false) override; /** @brief Returns the action displaying record toolbar */ QAction *recAction(); void refreshIcons(); /** @brief Send audio thumb data to qml for on monitor display */ void prepareAudioThumb(); void connectAudioSpectrum(bool activate); /** @brief Set a property on the Qml scene **/ void setQmlProperty(const QString &name, const QVariant &value); void displayAudioMonitor(bool isActive); /** @brief Prepare split effect from timeline clip producer **/ void activateSplit(); /** @brief Clear monitor display **/ void clearDisplay(); void setProducer(std::shared_ptr producer, int pos = -1); void reconfigure(); /** @brief Saves current monitor frame to an image file, and add it to project if addToProject is set to true **/ void slotExtractCurrentFrame(QString frameName = QString(), bool addToProject = false); /** @brief Zoom in active monitor */ void slotZoomIn(); /** @brief Zoom out active monitor */ void slotZoomOut(); /** @brief Set a property on the MLT consumer */ void setConsumerProperty(const QString &name, const QString &value); /** @brief Play or Loop zone sets a fake "out" on the producer. It is necessary to reset this before reloading the producer */ void resetPlayOrLoopZone(const QString &binId); /** @brief Returns a pointer to monitor proxy, allowing to manage seek and consumer position */ MonitorProxy *getControllerProxy(); protected: void mousePressEvent(QMouseEvent *event) override; void mouseReleaseEvent(QMouseEvent *event) override; void mouseDoubleClickEvent(QMouseEvent *event) override; void resizeEvent(QResizeEvent *event) override; void keyPressEvent(QKeyEvent *event) override; /** @brief Move to another position on mouse wheel event. * * Moves towards the end of the clip/timeline on mouse wheel down/back, the * opposite on mouse wheel up/forward. * Ctrl + wheel moves by a second, without Ctrl it moves by a single frame. */ void wheelEvent(QWheelEvent *event) override; void mouseMoveEvent(QMouseEvent *event) override; virtual QStringList mimeTypes() const; void updateBgColor(); private: std::shared_ptr m_controller; /** @brief The QQuickView that handles our monitor display (video and qml overlay) **/ GLWidget *m_glMonitor; /** @brief Container for our QQuickView monitor display (QQuickView needs to be embedded) **/ QWidget *m_glWidget; /** @brief Scrollbar for our monitor view, used when zooming the monitor **/ QScrollBar *m_verticalScroll; /** @brief Scrollbar for our monitor view, used when zooming the monitor **/ QScrollBar *m_horizontalScroll; /** @brief Widget holding the window for the QQuickView **/ QWidget *m_videoWidget; /** @brief Manager for qml overlay for the QQuickView **/ QmlManager *m_qmlManager; std::shared_ptr m_snaps; std::shared_ptr m_splitEffect; std::shared_ptr m_splitProducer; int m_length; bool m_dragStarted; RecManager *m_recManager; /** @brief The widget showing current time position **/ TimecodeDisplay *m_timePos; KDualAction *m_playAction; KSelectAction *m_forceSize; /** Has to be available so we can enable and disable it. */ QAction *m_loopClipAction; QAction *m_sceneVisibilityAction; QAction *m_zoomVisibilityAction; QMenu *m_contextMenu; QMenu *m_configMenu; QMenu *m_playMenu; QMenu *m_markerMenu; QPoint m_DragStartPosition; /** true if selected clip is transition, false = selected clip is clip. * Necessary because sometimes we get two signals, e.g. we get a clip and we get selected transition = nullptr. */ bool m_loopClipTransition; GenTime getSnapForPos(bool previous); QToolBar *m_toolbar; QSlider *m_audioSlider; QAction *m_editMarker; KMessageWidget *m_infoMessage; int m_forceSizeFactor; int m_offset; MonitorSceneType m_lastMonitorSceneType; MonitorAudioLevel *m_audioMeterWidget; QTimer m_droppedTimer; double m_displayedFps; QLabel *m_scalingLabel; QLabel *m_speedLabel; int m_speedIndex; void adjustScrollBars(float horizontal, float vertical); void loadQmlScene(MonitorSceneType type); void updateQmlDisplay(int currentOverlay); /** @brief Create temporary Mlt::Tractor holding a clip and it's effectless clone */ void buildSplitEffect(Mlt::Producer *original); /** @brief Reset and hide speed info label */ void resetSpeedInfo(); private slots: void slotSetThumbFrame(); void slotSaveZone(); void slotSeek(); void updateClipZone(); void slotGoToMarker(QAction *action); void slotSetVolume(int volume); void slotEditMarker(); void slotExtractCurrentZone(); void onFrameDisplayed(const SharedFrame &frame); void slotStartDrag(); void setZoom(); void slotEnableEffectScene(bool enable); void slotAdjustEffectCompare(); void slotShowMenu(const QPoint pos); void slotForceSize(QAction *a); void slotSeekToKeyFrame(); /** @brief Display a non blocking error message to user **/ void warningMessage(const QString &text, int timeout = 5000, const QList &actions = QList()); void slotLockMonitor(bool lock); void slotAddEffect(const QStringList &effect); void slotSwitchPlay(); void slotEditInlineMarker(); /** @brief Pass keypress event to mainwindow */ void doKeyPressEvent(QKeyEvent *); /** @brief There was an error initializing Movit */ void gpuError(); void setOffsetX(int x); void setOffsetY(int y); /** @brief Pan monitor view */ void panView(QPoint diff); /** @brief Project monitor zone changed, inform timeline */ void updateTimelineClipZone(); void slotSeekPosition(int); void addSnapPoint(int pos); void removeSnapPoint(int pos); /** @brief Pause monitor and process seek */ void processSeek(int pos); /** @brief Check and display dropped frames */ void checkDrops(); public slots: void slotSetScreen(int screenIndex); void slotOpenDvdFile(const QString &); // void slotSetClipProducer(DocClipBase *clip, QPoint zone = QPoint(), bool forceUpdate = false, int position = -1); void updateClipProducer(const std::shared_ptr &prod); void updateClipProducer(const QString &playlist); void slotOpenClip(const std::shared_ptr &controller, int in = -1, int out = -1); void slotRefreshMonitor(bool visible); void slotSeek(int pos); void stop() override; void start() override; void switchPlay(bool play); void slotPlay() override; void pause(); void slotPlayZone(); void slotLoopZone(); /** @brief Loops the selected item (clip or transition). */ void slotLoopClip(); void slotForward(double speed = 0, bool allowNormalPlay = false) override; void slotRewind(double speed = 0) override; void slotRewindOneFrame(int diff = 1); void slotForwardOneFrame(int diff = 1); void slotStart(); void slotEnd(); void slotSetZoneStart(); void slotSetZoneEnd(); void slotZoneStart(); void slotZoneEnd(); void slotLoadClipZone(const QPoint &zone); void slotSeekToNextSnap(); void slotSeekToPreviousSnap(); void adjustRulerSize(int length, const std::shared_ptr &markerModel = nullptr); void setTimePos(const QString &pos); QPoint getZoneInfo() const; /** @brief Display the on monitor effect scene (to adjust geometry over monitor). */ void slotShowEffectScene(MonitorSceneType sceneType, bool temporary = false); bool effectSceneDisplayed(MonitorSceneType effectType); /** @brief split screen to compare clip with and without effect */ void slotSwitchCompare(bool enable); void slotMouseSeek(int eventDelta, uint modifiers) override; void slotSwitchFullScreen(bool minimizeOnly = false) override; /** @brief Display or hide the record toolbar */ void slotSwitchRec(bool enable); /** @brief Request QImage of current frame */ void slotGetCurrentImage(bool request); /** @brief Enable/disable display of monitor's audio levels widget */ void slotSwitchAudioMonitor(); /** @brief Request seeking */ void requestSeek(int pos); /** @brief Check current position to show relevant infos in qml view (markers, zone in/out, etc). */ void checkOverlay(int pos = -1); void refreshMonitorIfActive(bool directUpdate = false) override; void forceMonitorRefresh(); /** @brief Clear read ahead cache, to ensure up to date audio */ void purgeCache(); signals: void screenChanged(int screenIndex); void seekPosition(int pos); void updateScene(); void durationChanged(int); void refreshClipThumbnail(const QString &); void zoneUpdated(const QPoint &); + void zoneUpdatedWithUndo(const QPoint &, const QPoint &); void timelineZoneChanged(); /** @brief Editing transitions / effects over the monitor requires the renderer to send frames as QImage. * This causes a major slowdown, so we only enable it if required */ void requestFrameForAnalysis(bool); void effectChanged(const QRect &); void effectPointsChanged(const QVariantList &); void addRemoveKeyframe(); void seekToNextKeyframe(); void seekToPreviousKeyframe(); void seekToKeyframe(int); void addClipToProject(const QUrl &); /** @brief Request display of current bin clip. */ void refreshCurrentClip(); void addEffect(const QStringList &); void addMasterEffect(QString, const QStringList &); void passKeyPress(QKeyEvent *); /** @brief Enable / disable project monitor multitrack view (split view with one track in each quarter). */ void multitrackView(bool, bool); void timeCodeUpdated(const QString &); void addMarker(); void deleteMarker(bool deleteGuide = true); void seekToPreviousSnap(); void seekToNextSnap(); void createSplitOverlay(std::shared_ptr); void removeSplitOverlay(); void acceptRipple(bool); void switchTrimMode(int); }; #endif diff --git a/src/monitor/monitorproxy.cpp b/src/monitor/monitorproxy.cpp index 2421cc371..df5e8233c 100644 --- a/src/monitor/monitorproxy.cpp +++ b/src/monitor/monitorproxy.cpp @@ -1,303 +1,313 @@ /*************************************************************************** * Copyright (C) 2018 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 "monitorproxy.h" #include "core.h" #include "doc/kthumb.h" #include "glwidget.h" #include "kdenlivesettings.h" #include "monitormanager.h" #include "profiles/profilemodel.hpp" #include #include #include #include MonitorProxy::MonitorProxy(GLWidget *parent) : QObject(parent) , q(parent) , m_position(0) , m_zoneIn(0) , m_zoneOut(-1) , m_hasAV(false) , m_clipType(0) , m_seekFinished(true) { } int MonitorProxy::getPosition() const { return m_position; } int MonitorProxy::rulerHeight() const { return q->m_rulerHeight; } int MonitorProxy::overlayType() const { return (q->m_id == (int)Kdenlive::ClipMonitor ? KdenliveSettings::clipMonitorOverlayGuides() : KdenliveSettings::projectMonitorOverlayGuides()); } void MonitorProxy::setOverlayType(int ix) { if (q->m_id == (int)Kdenlive::ClipMonitor) { KdenliveSettings::setClipMonitorOverlayGuides(ix); } else { KdenliveSettings::setProjectMonitorOverlayGuides(ix); } } QString MonitorProxy::markerComment() const { return m_markerComment; } bool MonitorProxy::setPosition(int pos) { if (m_position == pos) { return true; } m_position = pos; emit requestSeek(pos); if (m_seekFinished) { m_seekFinished = false; emit seekFinishedChanged(); } emit positionChanged(pos); return false; } void MonitorProxy::positionFromConsumer(int pos, bool playing) { if (playing) { m_position = pos; emit positionChanged(pos); if (!m_seekFinished) { m_seekFinished = true; emit seekFinishedChanged(); } } else { if (!m_seekFinished && m_position == pos) { m_seekFinished = true; emit seekFinishedChanged(); } } } void MonitorProxy::setMarkerComment(const QString &comment) { if (m_markerComment == comment) { return; } m_markerComment = comment; emit markerCommentChanged(); } int MonitorProxy::zoneIn() const { return m_zoneIn; } int MonitorProxy::zoneOut() const { return m_zoneOut; } void MonitorProxy::setZoneIn(int pos) { if (m_zoneIn > 0) { emit removeSnap(m_zoneIn); } m_zoneIn = pos; if (pos > 0) { emit addSnap(pos); } emit zoneChanged(); emit saveZone(); } void MonitorProxy::setZoneOut(int pos) { if (m_zoneOut > 0) { emit removeSnap(m_zoneOut - 1); } m_zoneOut = pos; if (pos > 0) { emit addSnap(m_zoneOut - 1); } emit zoneChanged(); emit saveZone(); } +void MonitorProxy::startZoneMove() +{ + m_undoZone = QPoint(m_zoneIn, m_zoneOut); +} + +void MonitorProxy::endZoneMove() +{ + emit saveZoneWithUndo(m_undoZone, QPoint(m_zoneIn, m_zoneOut)); +} + void MonitorProxy::setZone(int in, int out, bool sendUpdate) { if (m_zoneIn > 0) { emit removeSnap(m_zoneIn); } if (m_zoneOut > 0) { emit removeSnap(m_zoneOut - 1); } m_zoneIn = in; m_zoneOut = out; if (m_zoneIn > 0) { emit addSnap(m_zoneIn); } if (m_zoneOut > 0) { emit addSnap(m_zoneOut - 1); } emit zoneChanged(); if (sendUpdate) { emit saveZone(); } } void MonitorProxy::setZone(QPoint zone, bool sendUpdate) { setZone(zone.x(), zone.y(), sendUpdate); } void MonitorProxy::resetZone() { m_zoneIn = 0; m_zoneOut = -1; } double MonitorProxy::fps() const { return pCore->getCurrentFps(); } QPoint MonitorProxy::zone() const { return {m_zoneIn, m_zoneOut}; } QImage MonitorProxy::extractFrame(int frame_position, const QString &path, int width, int height, bool useSourceProfile) { if (width == -1) { width = pCore->getCurrentProfile()->width(); height = pCore->getCurrentProfile()->height(); } else if (width % 2 == 1) { width++; } if (!path.isEmpty()) { QScopedPointer producer(new Mlt::Producer(pCore->getCurrentProfile()->profile(), path.toUtf8().constData())); if (producer && producer->is_valid()) { QImage img = KThumb::getFrame(producer.data(), frame_position, width, height); return img; } } if ((q->m_producer == nullptr) || !path.isEmpty()) { QImage pix(width, height, QImage::Format_RGB32); pix.fill(Qt::black); return pix; } Mlt::Frame *frame = nullptr; QImage img; if (useSourceProfile) { // Our source clip's resolution is higher than current profile, export at full res QScopedPointer tmpProfile(new Mlt::Profile()); QString service = q->m_producer->get("mlt_service"); QScopedPointer tmpProd(new Mlt::Producer(*tmpProfile, service.toUtf8().constData(), q->m_producer->get("resource"))); tmpProfile->from_producer(*tmpProd); width = tmpProfile->width(); height = tmpProfile->height(); if (tmpProd && tmpProd->is_valid()) { Mlt::Filter scaler(*tmpProfile, "swscale"); Mlt::Filter converter(*tmpProfile, "avcolor_space"); tmpProd->attach(scaler); tmpProd->attach(converter); // TODO: paste effects // Clip(*tmpProd).addEffects(*q->m_producer); double projectFps = pCore->getCurrentFps(); double currentFps = tmpProfile->fps(); if (qFuzzyCompare(projectFps, currentFps)) { tmpProd->seek(q->m_producer->position()); } else { tmpProd->seek(q->m_producer->position() * currentFps / projectFps); } frame = tmpProd->get_frame(); img = KThumb::getFrame(frame, width, height); delete frame; } } else if (KdenliveSettings::gpu_accel()) { QString service = q->m_producer->get("mlt_service"); QScopedPointer tmpProd( new Mlt::Producer(pCore->getCurrentProfile()->profile(), service.toUtf8().constData(), q->m_producer->get("resource"))); Mlt::Filter scaler(pCore->getCurrentProfile()->profile(), "swscale"); Mlt::Filter converter(pCore->getCurrentProfile()->profile(), "avcolor_space"); tmpProd->attach(scaler); tmpProd->attach(converter); tmpProd->seek(q->m_producer->position()); frame = tmpProd->get_frame(); img = KThumb::getFrame(frame, width, height); delete frame; } else { frame = q->m_producer->get_frame(); img = KThumb::getFrame(frame, width, height); delete frame; } return img; } void MonitorProxy::activateClipMonitor(bool isClipMonitor) { pCore->monitorManager()->activateMonitor(isClipMonitor ? Kdenlive::ClipMonitor : Kdenlive::ProjectMonitor); } QString MonitorProxy::toTimecode(int frames) const { return KdenliveSettings::frametimecode() ? QString::number(frames) : q->frameToTime(frames); } void MonitorProxy::setClipProperties(ClipType::ProducerType type, bool hasAV, const QString clipName) { if (hasAV != m_hasAV) { m_hasAV = hasAV; emit clipHasAVChanged(); } if (clipName == m_clipName) { m_clipName.clear(); emit clipNameChanged(); } m_clipName = clipName; emit clipNameChanged(); if (type != m_clipType) { m_clipType = type; emit clipTypeChanged(); } } void MonitorProxy::setAudioThumb(const QUrl thumbPath) { m_audioThumb = thumbPath; emit audioThumbChanged(); } QPoint MonitorProxy::profile() { QSize s = pCore->getCurrentFrameSize(); return QPoint(s.width(), s.height()); } diff --git a/src/monitor/monitorproxy.h b/src/monitor/monitorproxy.h index 2b871ea2d..db10b9025 100644 --- a/src/monitor/monitorproxy.h +++ b/src/monitor/monitorproxy.h @@ -1,127 +1,131 @@ /*************************************************************************** * Copyright (C) 2018 by Jean-Baptiste Mardelle (jb@kdenlive.org) * * 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 . * ***************************************************************************/ /** @brief This class is a wrapper around the monitor / glwidget and handles communication * with the qml overlay through its properties. */ #ifndef MONITORPROXY_H #define MONITORPROXY_H #include "definitions.h" #include #include #include class GLWidget; class MonitorProxy : public QObject { Q_OBJECT // Q_PROPERTY(int consumerPosition READ consumerPosition NOTIFY consumerPositionChanged) Q_PROPERTY(int position MEMBER m_position WRITE setPosition NOTIFY positionChanged) Q_PROPERTY(QPoint profile READ profile NOTIFY profileChanged) Q_PROPERTY(int seekFinished MEMBER m_seekFinished NOTIFY seekFinishedChanged) Q_PROPERTY(int zoneIn READ zoneIn WRITE setZoneIn NOTIFY zoneChanged) Q_PROPERTY(int zoneOut READ zoneOut WRITE setZoneOut NOTIFY zoneChanged) Q_PROPERTY(int rulerHeight READ rulerHeight NOTIFY rulerHeightChanged) Q_PROPERTY(QString markerComment READ markerComment NOTIFY markerCommentChanged) Q_PROPERTY(QUrl audioThumb MEMBER m_audioThumb NOTIFY audioThumbChanged) Q_PROPERTY(int overlayType READ overlayType WRITE setOverlayType NOTIFY overlayTypeChanged) /** @brief: Returns true if current clip in monitor has Audio and Video * */ Q_PROPERTY(bool clipHasAV MEMBER m_hasAV NOTIFY clipHasAVChanged) /** @brief: Contains the name of clip currently displayed in monitor * */ Q_PROPERTY(QString clipName MEMBER m_clipName NOTIFY clipNameChanged) /** @brief: Contains the name of clip currently displayed in monitor * */ Q_PROPERTY(int clipType MEMBER m_clipType NOTIFY clipTypeChanged) public: MonitorProxy(GLWidget *parent); /** brief: Returns true if we are still in a seek operation * */ int rulerHeight() const; int overlayType() const; void setOverlayType(int ix); QString markerComment() const; /** brief: update position and end seeking if we reached the requested seek position. * returns true if the position was unchanged, false otherwise * */ int getPosition() const; Q_INVOKABLE bool setPosition(int pos); void positionFromConsumer(int pos, bool playing); void setMarkerComment(const QString &comment); int zoneIn() const; int zoneOut() const; void setZoneIn(int pos); void setZoneOut(int pos); Q_INVOKABLE void setZone(int in, int out, bool sendUpdate = true); /** brief: Activate clip monitor if param is true, project monitor otherwise * */ Q_INVOKABLE void activateClipMonitor(bool isClipMonitor); void setZone(QPoint zone, bool sendUpdate = true); void resetZone(); QPoint zone() const; QImage extractFrame(int frame_position, const QString &path = QString(), int width = -1, int height = -1, bool useSourceProfile = false); Q_INVOKABLE QString toTimecode(int frames) const; + Q_INVOKABLE void startZoneMove(); + Q_INVOKABLE void endZoneMove(); Q_INVOKABLE double fps() const; QPoint profile(); void setClipProperties(ClipType::ProducerType type, bool hasAV, const QString clipName); void setAudioThumb(const QUrl thumbPath = QUrl()); signals: void positionChanged(int); void seekFinishedChanged(); void requestSeek(int pos); void zoneChanged(); void saveZone(); + void saveZoneWithUndo(const QPoint, const QPoint&); void markerCommentChanged(); void rulerHeightChanged(); void addSnap(int); void removeSnap(int); void triggerAction(const QString &name); void overlayTypeChanged(); void seekNextKeyframe(); void seekPreviousKeyframe(); void addRemoveKeyframe(); void seekToKeyframe(); void clipHasAVChanged(); void clipNameChanged(); void clipTypeChanged(); void audioThumbChanged(); void profileChanged(); private: GLWidget *q; int m_position; int m_zoneIn; int m_zoneOut; bool m_hasAV; QUrl m_audioThumb; QString m_markerComment; QString m_clipName; int m_clipType; bool m_seekFinished; + QPoint m_undoZone; }; #endif diff --git a/src/monitor/view/MonitorRuler.qml b/src/monitor/view/MonitorRuler.qml index 5ffefea05..ac84f8e53 100644 --- a/src/monitor/view/MonitorRuler.qml +++ b/src/monitor/view/MonitorRuler.qml @@ -1,264 +1,276 @@ import QtQuick.Controls 1.4 import QtQuick.Controls.Styles 1.4 import QtQuick.Window 2.2 import Kdenlive.Controls 1.0 import QtQuick 2.11 // Monitor ruler Rectangle { id: ruler color: activePalette.base property bool containsMouse: rulerMouseArea.containsMouse property bool seekingFinished : controller.seekFinished Rectangle { color: activePalette.light width: parent.width height: 1 } onSeekingFinishedChanged : { playhead.opacity = seekingFinished ? 1 : 0.5 } Timer { id: zoneToolTipTimer interval: 3000; running: false; } function forceRepaint() { ruler.color = activePalette.base // Enforce repaint rulerTicks.model = 0 rulerTicks.model = ruler.width / frameSize + 2 playhead.fillColor = activePalette.windowText } function updateRuler() { var projectFps = controller.fps() root.timeScale = width / root.duration if (root.duration < 10 * projectFps) { root.frameSize = projectFps * root.timeScale * 0.2 } else if (duration < 100 * projectFps) { frameSize = projectFps * root.timeScale } else if (duration < 400 * projectFps) { root.frameSize = projectFps * root.timeScale * 2 } else { root.frameSize = projectFps * root.timeScale * 4 while (root.frameSize < 10) { root.frameSize *= 4 } } } // Ruler zone Rectangle { id: zone visible: controller.zoneOut > controller.zoneIn color: activePalette.highlight x: controller.zoneIn * root.timeScale width: (controller.zoneOut - controller.zoneIn) * root.timeScale anchors.bottom: parent.bottom height: ruler.height / 2 opacity: 0.8 onXChanged: zoneToolTipTimer.start() onWidthChanged: zoneToolTipTimer.start() } // frame ticks Repeater { id: rulerTicks model: ruler.width / frameSize + 2 Rectangle { property int realPos: index x: realPos * frameSize anchors.bottom: ruler.bottom height: (realPos % 4)? ((realPos % 2) ? 3 : 7) : 12 width: 1 color: activePalette.windowText opacity: 0.5 } } MouseArea { id: rulerMouseArea anchors.fill: parent hoverEnabled: true onPressed: { if (mouse.buttons === Qt.LeftButton) { var pos = Math.max(mouseX, 0) controller.position = Math.min(pos / root.timeScale, root.duration); } } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { var pos = Math.max(mouseX, 0) root.mouseRulerPos = pos if (pressed) { controller.position = Math.min(pos / root.timeScale, root.duration); } } } } // Zone duration indicator Rectangle { visible: inZoneMarker.visible || zoneToolTipTimer.running width: inLabel.contentWidth + 4 height: inLabel.contentHeight + 2 property int centerPos: zone.x + zone.width / 2 - inLabel.contentWidth / 2 x: centerPos < 0 ? 0 : centerPos > ruler.width - inLabel.contentWidth ? ruler.width - inLabel.contentWidth : centerPos color: activePalette.alternateBase anchors.bottom: ruler.top Label { id: inLabel anchors.fill: parent horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignBottom text: trimInMouseArea.containsMouse || trimInMouseArea.pressed ? controller.toTimecode(controller.zoneIn) + '>' + controller.toTimecode(controller.zoneOut - controller.zoneIn) : trimOutMouseArea.containsMouse || trimOutMouseArea.pressed ? controller.toTimecode(controller.zoneOut - controller.zoneIn) + '<' + controller.toTimecode(controller.zoneOut) : controller.toTimecode(controller.zoneOut - controller.zoneIn) font: fixedFont color: activePalette.text } } // monitor zone Rectangle { id: inZoneMarker x: controller.zoneIn * root.timeScale anchors.bottom: parent.bottom anchors.top: parent.top width: 1 color: activePalette.highlight visible: controller.zoneOut > controller.zoneIn && (rulerMouseArea.containsMouse || trimOutMouseArea.containsMouse || trimOutMouseArea.pressed || trimInMouseArea.containsMouse) } Rectangle { x: controller.zoneOut * root.timeScale anchors.bottom: parent.bottom anchors.top: parent.top width: 1 color: activePalette.highlight visible: inZoneMarker.visible } TimelinePlayhead { id: playhead visible: controller.position > -1 height: ruler.height * 0.5 width: ruler.height * 1 opacity: 1 anchors.top: ruler.top fillColor: activePalette.windowText x: controller.position * root.timeScale - (width / 2) } Rectangle { id: trimIn x: zone.x - root.baseUnit / 3 y: zone.y height: zone.height width: root.baseUnit * .8 color: 'lawngreen' opacity: trimInMouseArea.containsMouse || trimInMouseArea.drag.active ? 0.5 : 0 Drag.active: trimInMouseArea.drag.active Drag.proposedAction: Qt.MoveAction MouseArea { id: trimInMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.SizeHorCursor drag.target: parent drag.axis: Drag.XAxis drag.smoothed: false drag.minimumX: 0 drag.maximumX: ruler.width + onPressed: { + controller.startZoneMove() + } + onReleased: { + controller.endZoneMove() + } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { controller.zoneIn = Math.round(trimIn.x / root.timeScale) } } } } Rectangle { id: trimOut width: root.baseUnit * .8 x: zone.x + zone.width - (width * .7) y: zone.y height: zone.height color: 'darkred' opacity: trimOutMouseArea.containsMouse || trimOutMouseArea.drag.active ? 0.5 : 0 Drag.active: trimOutMouseArea.drag.active Drag.proposedAction: Qt.MoveAction MouseArea { id: trimOutMouseArea anchors.fill: parent hoverEnabled: true cursorShape: Qt.SizeHorCursor drag.target: parent drag.axis: Drag.XAxis drag.smoothed: false drag.minimumX: 0 drag.maximumX: ruler.width - trimOut.width + onPressed: { + controller.startZoneMove() + } + onReleased: { + controller.endZoneMove() + } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { controller.zoneOut = Math.round((trimOut.x + trimOut.width) / root.timeScale) } } } } // markers Repeater { model: markersModel delegate: Item { anchors.fill: parent Rectangle { id: markerBase width: 1 height: parent.height x: (model.frame) * root.timeScale; color: model.color } Rectangle { visible: !rulerMouseArea.pressed && (guideArea.containsMouse || (rulerMouseArea.containsMouse && Math.abs(rulerMouseArea.mouseX - markerBase.x) < 4)) opacity: 0.7 property int guidePos: markerBase.x - mlabel.contentWidth / 2 x: guidePos < 0 ? 0 : (guidePos > (parent.width - mlabel.contentWidth) ? parent.width - mlabel.contentWidth : guidePos) radius: 2 width: mlabel.contentWidth height: mlabel.contentHeight * .8 anchors { bottom: parent.top } color: model.color Text { id: mlabel text: model.comment font.pixelSize: root.baseUnit verticalAlignment: Text.AlignVCenter anchors { fill: parent } color: 'white' } MouseArea { z: 10 id: guideArea anchors.fill: parent acceptedButtons: Qt.LeftButton cursorShape: Qt.PointingHandCursor hoverEnabled: true //onDoubleClicked: timeline.editMarker(clipRoot.binId, model.frame) onClicked: { controller.position = model.frame } } } } } /*Rectangle { id: seekCursor visible: controller.seekPosition > -1 color: activePalette.highlight width: 4 height: ruler.height opacity: 0.5 x: controller.seekPosition * root.timeScale y: 0 }*/ } diff --git a/src/project/projectmanager.cpp b/src/project/projectmanager.cpp index 5f60eae36..703dae261 100644 --- a/src/project/projectmanager.cpp +++ b/src/project/projectmanager.cpp @@ -1,1038 +1,1038 @@ /* Copyright (C) 2014 Till Theato 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 3 of the License, or (at your option) any later version. */ #include "projectmanager.h" #include "bin/bin.h" #include "bin/projectitemmodel.h" #include "core.h" #include "doc/kdenlivedoc.h" #include "jobs/jobmanager.h" #include "kdenlivesettings.h" #include "mainwindow.h" #include "monitor/monitormanager.h" #include "profiles/profilemodel.hpp" #include "project/dialogs/archivewidget.h" #include "project/dialogs/backupwidget.h" #include "project/dialogs/noteswidget.h" #include "project/dialogs/projectsettings.h" #include "utils/thumbnailcache.hpp" #include "xml/xml.hpp" // Temporary for testing #include "bin/model/markerlistmodel.hpp" #include "profiles/profilerepository.hpp" #include "project/notesplugin.h" #include "timeline2/model/builders/meltBuilder.hpp" #include "timeline2/view/timelinecontroller.h" #include "timeline2/view/timelinewidget.h" #include #include #include #include #include #include "kdenlive_debug.h" #include #include #include #include #include #include #include #include #include #include static QString getProjectNameFilters(bool ark=true) { auto filter = i18n("Kdenlive project (*.kdenlive)"); if (ark) { filter.append(";;" + i18n("Archived project (*.tar.gz)")); } return filter; } ProjectManager::ProjectManager(QObject *parent) : QObject(parent) , m_mainTimelineModel(nullptr) { m_fileRevert = KStandardAction::revert(this, SLOT(slotRevert()), pCore->window()->actionCollection()); m_fileRevert->setIcon(QIcon::fromTheme(QStringLiteral("document-revert"))); m_fileRevert->setEnabled(false); QAction *a = KStandardAction::open(this, SLOT(openFile()), pCore->window()->actionCollection()); a->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); a = KStandardAction::saveAs(this, SLOT(saveFileAs()), pCore->window()->actionCollection()); a->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as"))); a = KStandardAction::openNew(this, SLOT(newFile()), pCore->window()->actionCollection()); a->setIcon(QIcon::fromTheme(QStringLiteral("document-new"))); m_recentFilesAction = KStandardAction::openRecent(this, SLOT(openFile(QUrl)), pCore->window()->actionCollection()); QAction *backupAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-undo")), i18n("Open Backup File"), this); pCore->window()->addAction(QStringLiteral("open_backup"), backupAction); connect(backupAction, SIGNAL(triggered(bool)), SLOT(slotOpenBackup())); m_notesPlugin = new NotesPlugin(this); m_autoSaveTimer.setSingleShot(true); connect(&m_autoSaveTimer, &QTimer::timeout, this, &ProjectManager::slotAutoSave); // Ensure the default data folder exist QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation)); dir.mkpath(QStringLiteral(".backup")); dir.mkdir(QStringLiteral("titles")); } ProjectManager::~ProjectManager() = default; void ProjectManager::slotLoadOnOpen() { if (m_startUrl.isValid()) { openFile(); } else if (KdenliveSettings::openlastproject()) { openLastFile(); } else { newFile(false); } if (!m_loadClipsOnOpen.isEmpty() && (m_project != nullptr)) { const QStringList list = m_loadClipsOnOpen.split(QLatin1Char(',')); QList urls; urls.reserve(list.count()); for (const QString &path : list) { // qCDebug(KDENLIVE_LOG) << QDir::current().absoluteFilePath(path); urls << QUrl::fromLocalFile(QDir::current().absoluteFilePath(path)); } pCore->bin()->droppedUrls(urls); } m_loadClipsOnOpen.clear(); } void ProjectManager::init(const QUrl &projectUrl, const QString &clipList) { m_startUrl = projectUrl; m_loadClipsOnOpen = clipList; } void ProjectManager::newFile(bool showProjectSettings) { QString profileName = KdenliveSettings::default_profile(); if (profileName.isEmpty()) { profileName = pCore->getCurrentProfile()->path(); } newFile(profileName, showProjectSettings); } void ProjectManager::newFile(QString profileName, bool showProjectSettings) { QUrl startFile = QUrl::fromLocalFile(KdenliveSettings::defaultprojectfolder() + QStringLiteral("/_untitled.kdenlive")); if (checkForBackupFile(startFile, true)) { return; } m_fileRevert->setEnabled(false); QString projectFolder; QMap documentProperties; QMap documentMetadata; QPoint projectTracks(KdenliveSettings::videotracks(), KdenliveSettings::audiotracks()); pCore->monitorManager()->resetDisplay(); QString documentId = QString::number(QDateTime::currentMSecsSinceEpoch()); documentProperties.insert(QStringLiteral("documentid"), documentId); if (!showProjectSettings) { if (!closeCurrentDocument()) { return; } if (KdenliveSettings::customprojectfolder()) { projectFolder = KdenliveSettings::defaultprojectfolder(); if (!projectFolder.endsWith(QLatin1Char('/'))) { projectFolder.append(QLatin1Char('/')); } documentProperties.insert(QStringLiteral("storagefolder"), projectFolder + documentId); } } else { QPointer w = new ProjectSettings(nullptr, QMap(), QStringList(), projectTracks.x(), projectTracks.y(), KdenliveSettings::defaultprojectfolder(), false, true, pCore->window()); connect(w.data(), &ProjectSettings::refreshProfiles, pCore->window(), &MainWindow::slotRefreshProfiles); if (w->exec() != QDialog::Accepted) { delete w; return; } if (!closeCurrentDocument()) { delete w; return; } if (KdenliveSettings::videothumbnails() != w->enableVideoThumbs()) { pCore->window()->slotSwitchVideoThumbs(); } if (KdenliveSettings::audiothumbnails() != w->enableAudioThumbs()) { pCore->window()->slotSwitchAudioThumbs(); } profileName = w->selectedProfile(); projectFolder = w->storageFolder(); projectTracks = w->tracks(); documentProperties.insert(QStringLiteral("enableproxy"), QString::number((int)w->useProxy())); documentProperties.insert(QStringLiteral("generateproxy"), QString::number((int)w->generateProxy())); documentProperties.insert(QStringLiteral("proxyminsize"), QString::number(w->proxyMinSize())); documentProperties.insert(QStringLiteral("proxyparams"), w->proxyParams()); documentProperties.insert(QStringLiteral("proxyextension"), w->proxyExtension()); documentProperties.insert(QStringLiteral("generateimageproxy"), QString::number((int)w->generateImageProxy())); QString preview = w->selectedPreview(); if (!preview.isEmpty()) { documentProperties.insert(QStringLiteral("previewparameters"), preview.section(QLatin1Char(';'), 0, 0)); documentProperties.insert(QStringLiteral("previewextension"), preview.section(QLatin1Char(';'), 1, 1)); } documentProperties.insert(QStringLiteral("proxyimageminsize"), QString::number(w->proxyImageMinSize())); if (!projectFolder.isEmpty()) { if (!projectFolder.endsWith(QLatin1Char('/'))) { projectFolder.append(QLatin1Char('/')); } documentProperties.insert(QStringLiteral("storagefolder"), projectFolder + documentId); } documentMetadata = w->metadata(); delete w; } bool openBackup; m_notesPlugin->clear(); documentProperties.insert(QStringLiteral("decimalPoint"), QLocale().decimalPoint()); KdenliveDoc *doc = new KdenliveDoc(QUrl(), projectFolder, pCore->window()->m_commandStack, profileName, documentProperties, documentMetadata, projectTracks, &openBackup, pCore->window()); doc->m_autosave = new KAutoSaveFile(startFile, doc); ThumbnailCache::get()->clearCache(); pCore->bin()->setDocument(doc); m_project = doc; pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); updateTimeline(0); pCore->window()->connectDocument(); pCore->mixer()->setModel(m_mainTimelineModel); bool disabled = m_project->getDocumentProperty(QStringLiteral("disabletimelineeffects")) == QLatin1String("1"); QAction *disableEffects = pCore->window()->actionCollection()->action(QStringLiteral("disable_timeline_effects")); if (disableEffects) { if (disabled != disableEffects->isChecked()) { disableEffects->blockSignals(true); disableEffects->setChecked(disabled); disableEffects->blockSignals(false); } } emit docOpened(m_project); m_lastSave.start(); } bool ProjectManager::closeCurrentDocument(bool saveChanges, bool quit) { if ((m_project != nullptr) && m_project->isModified() && saveChanges) { QString message; if (m_project->url().fileName().isEmpty()) { message = i18n("Save changes to document?"); } else { message = i18n("The project \"%1\" has been changed.\nDo you want to save your changes?", m_project->url().fileName()); } switch (KMessageBox::warningYesNoCancel(pCore->window(), message)) { case KMessageBox::Yes: // save document here. If saving fails, return false; if (!saveFile()) { return false; } break; case KMessageBox::Cancel: return false; break; default: break; } } pCore->jobManager()->slotCancelJobs(); disconnect(pCore->window()->getMainTimeline()->controller(), &TimelineController::durationChanged, this, &ProjectManager::adjustProjectDuration); pCore->window()->getMainTimeline()->controller()->clipActions.clear(); pCore->window()->getMainTimeline()->controller()->prepareClose(); if (m_mainTimelineModel) { m_mainTimelineModel->prepareClose(); } if (!quit && !qApp->isSavingSession()) { m_autoSaveTimer.stop(); if (m_project) { pCore->jobManager()->slotCancelJobs(); pCore->bin()->abortOperations(); pCore->monitorManager()->clipMonitor()->slotOpenClip(nullptr); pCore->window()->clearAssetPanel(); delete m_project; m_project = nullptr; } } /* // Make sure to reset locale to system's default QString requestedLocale = QLocale::system().name(); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); if (env.contains(QStringLiteral("LC_NUMERIC"))) { requestedLocale = env.value(QStringLiteral("LC_NUMERIC")); } qDebug()<<"//////////// RESETTING LOCALE TO: "<decimal_point; if (QString::fromUtf8(separator) != QString(newLocale.decimalPoint())) { pCore->displayBinMessage(i18n("There is a locale conflict on your system, project might get corrupt"), KMessageWidget::Warning); } setlocale(LC_NUMERIC, requestedLocale.toUtf8().constData()); #endif QLocale::setDefault(newLocale);*/ return true; } bool ProjectManager::saveFileAs(const QString &outputFileName) { pCore->monitorManager()->pauseActiveMonitor(); // Sync document properties prepareSave(); QString saveFolder = QFileInfo(outputFileName).absolutePath(); QString scene = projectSceneList(saveFolder); if (!m_replacementPattern.isEmpty()) { QMapIterator i(m_replacementPattern); while (i.hasNext()) { i.next(); scene.replace(i.key(), i.value()); } } if (!m_project->saveSceneList(outputFileName, scene)) { return false; } QUrl url = QUrl::fromLocalFile(outputFileName); // Save timeline thumbnails QStringList thumbKeys = pCore->window()->getMainTimeline()->controller()->getThumbKeys(); ThumbnailCache::get()->saveCachedThumbs(thumbKeys); m_project->setUrl(url); // setting up autosave file in ~/.kde/data/stalefiles/kdenlive/ // saved under file name // actual saving by KdenliveDoc::slotAutoSave() called by a timer 3 seconds after the document has been edited // This timer is set by KdenliveDoc::setModified() const QString projectId = QCryptographicHash::hash(url.fileName().toUtf8(), QCryptographicHash::Md5).toHex(); QUrl autosaveUrl = QUrl::fromLocalFile(QFileInfo(outputFileName).absoluteDir().absoluteFilePath(projectId + QStringLiteral(".kdenlive"))); if (m_project->m_autosave == nullptr) { // The temporary file is not opened or created until actually needed. // The file filename does not have to exist for KAutoSaveFile to be constructed (if it exists, it will not be touched). m_project->m_autosave = new KAutoSaveFile(autosaveUrl, m_project); } else { m_project->m_autosave->setManagedFile(autosaveUrl); } pCore->window()->setWindowTitle(m_project->description()); m_project->setModified(false); m_recentFilesAction->addUrl(url); // remember folder for next project opening KRecentDirs::add(QStringLiteral(":KdenliveProjectsFolder"), saveFolder); saveRecentFiles(); m_fileRevert->setEnabled(true); pCore->window()->m_undoView->stack()->setClean(); return true; } void ProjectManager::saveRecentFiles() { KSharedConfigPtr config = KSharedConfig::openConfig(); m_recentFilesAction->saveEntries(KConfigGroup(config, "Recent Files")); config->sync(); } bool ProjectManager::saveFileAs() { QFileDialog fd(pCore->window()); fd.setDirectory(m_project->url().isValid() ? m_project->url().adjusted(QUrl::RemoveFilename).toLocalFile() : KdenliveSettings::defaultprojectfolder()); fd.setNameFilter(getProjectNameFilters(false)); fd.setAcceptMode(QFileDialog::AcceptSave); fd.setFileMode(QFileDialog::AnyFile); fd.setDefaultSuffix(QStringLiteral("kdenlive")); if (fd.exec() != QDialog::Accepted || fd.selectedFiles().isEmpty()) { return false; } QString outputFile = fd.selectedFiles().constFirst(); bool ok = false; QDir cacheDir = m_project->getCacheDir(CacheBase, &ok); if (ok) { QFile file(cacheDir.absoluteFilePath(QString::fromLatin1(QUrl::toPercentEncoding(QStringLiteral(".") + outputFile)))); file.open(QIODevice::ReadWrite | QIODevice::Text); file.close(); } return saveFileAs(outputFile); } bool ProjectManager::saveFile() { if (!m_project) { // Calling saveFile before a project was created, something is wrong qCDebug(KDENLIVE_LOG) << "SaveFile called without project"; return false; } if (m_project->url().isEmpty()) { return saveFileAs(); } bool result = saveFileAs(m_project->url().toLocalFile()); m_project->m_autosave->resize(0); return result; } void ProjectManager::openFile() { if (m_startUrl.isValid()) { openFile(m_startUrl); m_startUrl.clear(); return; } QUrl url = QFileDialog::getOpenFileUrl(pCore->window(), QString(), QUrl::fromLocalFile(KRecentDirs::dir(QStringLiteral(":KdenliveProjectsFolder"))), getProjectNameFilters()); if (!url.isValid()) { return; } KRecentDirs::add(QStringLiteral(":KdenliveProjectsFolder"), url.adjusted(QUrl::RemoveFilename).toLocalFile()); m_recentFilesAction->addUrl(url); saveRecentFiles(); openFile(url); } void ProjectManager::openLastFile() { if (m_recentFilesAction->selectableActionGroup()->actions().isEmpty()) { // No files in history newFile(false); return; } QAction *firstUrlAction = m_recentFilesAction->selectableActionGroup()->actions().last(); if (firstUrlAction) { firstUrlAction->trigger(); } else { newFile(false); } } // fix mantis#3160 separate check from openFile() so we can call it from newFile() // to find autosaved files (in ~/.local/share/stalefiles/kdenlive) and recover it bool ProjectManager::checkForBackupFile(const QUrl &url, bool newFile) { // Check for autosave file that belong to the url we passed in. const QString projectId = QCryptographicHash::hash(url.fileName().toUtf8(), QCryptographicHash::Md5).toHex(); QUrl autosaveUrl = newFile ? url : QUrl::fromLocalFile(QFileInfo(url.path()).absoluteDir().absoluteFilePath(projectId + QStringLiteral(".kdenlive"))); QList staleFiles = KAutoSaveFile::staleFiles(autosaveUrl); KAutoSaveFile *orphanedFile = nullptr; // Check if we can have a lock on one of the file, // meaning it is not handled by any Kdenlive instance if (!staleFiles.isEmpty()) { for (KAutoSaveFile *stale : staleFiles) { if (stale->open(QIODevice::QIODevice::ReadWrite)) { // Found orphaned autosave file orphanedFile = stale; break; } } } if (orphanedFile) { if (KMessageBox::questionYesNo(nullptr, i18n("Auto-saved files exist. Do you want to recover them now?"), i18n("File Recovery"), KGuiItem(i18n("Recover")), KGuiItem(i18n("Do not recover"))) == KMessageBox::Yes) { doOpenFile(url, orphanedFile); return true; } } // remove the stale files for (KAutoSaveFile *stale : staleFiles) { stale->open(QIODevice::ReadWrite); delete stale; } return false; } void ProjectManager::openFile(const QUrl &url) { QMimeDatabase db; // Make sure the url is a Kdenlive project file QMimeType mime = db.mimeTypeForUrl(url); if (mime.inherits(QStringLiteral("application/x-compressed-tar"))) { // Opening a compressed project file, we need to process it // qCDebug(KDENLIVE_LOG)<<"Opening archive, processing"; QPointer ar = new ArchiveWidget(url); if (ar->exec() == QDialog::Accepted) { openFile(QUrl::fromLocalFile(ar->extractedProjectFile())); } else if (m_startUrl.isValid()) { // we tried to open an invalid file from command line, init new project newFile(false); } delete ar; return; } /*if (!url.fileName().endsWith(".kdenlive")) { // This is not a Kdenlive project file, abort loading KMessageBox::sorry(pCore->window(), i18n("File %1 is not a Kdenlive project file", url.toLocalFile())); if (m_startUrl.isValid()) { // we tried to open an invalid file from command line, init new project newFile(false); } return; }*/ if ((m_project != nullptr) && m_project->url() == url) { return; } if (!closeCurrentDocument()) { return; } if (checkForBackupFile(url)) { return; } pCore->displayMessage(i18n("Opening file %1", url.toLocalFile()), OperationCompletedMessage, 100); doOpenFile(url, nullptr); } void ProjectManager::doOpenFile(const QUrl &url, KAutoSaveFile *stale) { Q_ASSERT(m_project == nullptr); m_fileRevert->setEnabled(true); delete m_progressDialog; ThumbnailCache::get()->clearCache(); pCore->monitorManager()->resetDisplay(); pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor); m_progressDialog = new QProgressDialog(pCore->window()); m_progressDialog->setWindowTitle(i18n("Loading project")); m_progressDialog->setCancelButton(nullptr); m_progressDialog->setLabelText(i18n("Loading project")); m_progressDialog->setMaximum(0); m_progressDialog->show(); bool openBackup; m_notesPlugin->clear(); KdenliveDoc *doc = new KdenliveDoc(stale ? QUrl::fromLocalFile(stale->fileName()) : url, QString(), pCore->window()->m_commandStack, KdenliveSettings::default_profile().isEmpty() ? pCore->getCurrentProfile()->path() : KdenliveSettings::default_profile(), QMap(), QMap(), QPoint(KdenliveSettings::videotracks(), KdenliveSettings::audiotracks()), &openBackup, pCore->window()); if (stale == nullptr) { const QString projectId = QCryptographicHash::hash(url.fileName().toUtf8(), QCryptographicHash::Md5).toHex(); QUrl autosaveUrl = QUrl::fromLocalFile(QFileInfo(url.path()).absoluteDir().absoluteFilePath(projectId + QStringLiteral(".kdenlive"))); stale = new KAutoSaveFile(autosaveUrl, doc); doc->m_autosave = stale; } else { doc->m_autosave = stale; stale->setParent(doc); // if loading from an autosave of unnamed file then keep unnamed if (url.fileName().contains(QStringLiteral("_untitled.kdenlive"))) { doc->setUrl(QUrl()); } else { doc->setUrl(url); } doc->setModified(true); stale->setParent(doc); } m_progressDialog->setLabelText(i18n("Loading clips")); m_progressDialog->setMaximum(doc->clipsCount()); // TODO refac delete this pCore->bin()->setDocument(doc); QList rulerActions; rulerActions << pCore->window()->actionCollection()->action(QStringLiteral("set_render_timeline_zone")); rulerActions << pCore->window()->actionCollection()->action(QStringLiteral("unset_render_timeline_zone")); rulerActions << pCore->window()->actionCollection()->action(QStringLiteral("clear_render_timeline_zone")); // Set default target tracks to upper audio / lower video tracks m_project = doc; if (!updateTimeline(m_project->getDocumentProperty(QStringLiteral("position")).toInt())) { delete m_progressDialog; m_progressDialog = nullptr; return; } pCore->window()->connectDocument(); pCore->mixer()->setModel(m_mainTimelineModel); QDateTime documentDate = QFileInfo(m_project->url().toLocalFile()).lastModified(); pCore->window()->getMainTimeline()->controller()->loadPreview(m_project->getDocumentProperty(QStringLiteral("previewchunks")), m_project->getDocumentProperty(QStringLiteral("dirtypreviewchunks")), documentDate, m_project->getDocumentProperty(QStringLiteral("disablepreview")).toInt()); emit docOpened(m_project); pCore->displayMessage(QString(), OperationCompletedMessage, 100); if (openBackup) { slotOpenBackup(url); } m_lastSave.start(); delete m_progressDialog; m_progressDialog = nullptr; } void ProjectManager::slotRevert() { if (m_project->isModified() && KMessageBox::warningContinueCancel(pCore->window(), i18n("This will delete all changes made since you last saved your project. Are you sure you want to continue?"), i18n("Revert to last saved version")) == KMessageBox::Cancel) { return; } QUrl url = m_project->url(); if (closeCurrentDocument(false)) { doOpenFile(url, nullptr); } } KdenliveDoc *ProjectManager::current() { return m_project; } bool ProjectManager::slotOpenBackup(const QUrl &url) { QUrl projectFile; QUrl projectFolder; QString projectId; if (url.isValid()) { // we could not open the project file, guess where the backups are projectFolder = QUrl::fromLocalFile(KdenliveSettings::defaultprojectfolder()); projectFile = url; } else { projectFolder = QUrl::fromLocalFile(m_project->projectTempFolder()); projectFile = m_project->url(); projectId = m_project->getDocumentProperty(QStringLiteral("documentid")); } bool result = false; QPointer dia = new BackupWidget(projectFile, projectFolder, projectId, pCore->window()); if (dia->exec() == QDialog::Accepted) { QString requestedBackup = dia->selectedFile(); m_project->backupLastSavedVersion(projectFile.toLocalFile()); closeCurrentDocument(false); doOpenFile(QUrl::fromLocalFile(requestedBackup), nullptr); if (m_project) { m_project->setUrl(projectFile); m_project->setModified(true); pCore->window()->setWindowTitle(m_project->description()); result = true; } } delete dia; return result; } KRecentFilesAction *ProjectManager::recentFilesAction() { return m_recentFilesAction; } void ProjectManager::slotStartAutoSave() { if (m_lastSave.elapsed() > 300000) { // If the project was not saved in the last 5 minute, force save m_autoSaveTimer.stop(); slotAutoSave(); } else { m_autoSaveTimer.start(3000); // will trigger slotAutoSave() in 3 seconds } } void ProjectManager::slotAutoSave() { prepareSave(); QString saveFolder = m_project->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile(); QString scene = projectSceneList(saveFolder); if (!m_replacementPattern.isEmpty()) { QMapIterator i(m_replacementPattern); while (i.hasNext()) { i.next(); scene.replace(i.key(), i.value()); } } m_project->slotAutoSave(scene); m_lastSave.start(); } QString ProjectManager::projectSceneList(const QString &outputFolder) { // Disable multitrack view and overlay bool isMultiTrack = pCore->monitorManager()->isMultiTrack(); bool hasPreview = pCore->window()->getMainTimeline()->controller()->hasPreviewTrack(); if (isMultiTrack) { pCore->window()->getMainTimeline()->controller()->slotMultitrackView(false, false); } if (hasPreview) { pCore->window()->getMainTimeline()->controller()->updatePreviewConnection(false); } pCore->mixer()->pauseMonitoring(true); QString scene = pCore->monitorManager()->projectMonitor()->sceneList(outputFolder); pCore->mixer()->pauseMonitoring(false); if (isMultiTrack) { pCore->window()->getMainTimeline()->controller()->slotMultitrackView(true, false); } if (hasPreview) { pCore->window()->getMainTimeline()->controller()->updatePreviewConnection(true); } return scene; } void ProjectManager::setDocumentNotes(const QString ¬es) { m_notesPlugin->widget()->setHtml(notes); } QString ProjectManager::documentNotes() const { QString text = m_notesPlugin->widget()->toPlainText().simplified(); if (text.isEmpty()) { return QString(); } return m_notesPlugin->widget()->toHtml(); } void ProjectManager::slotAddProjectNote() { m_notesPlugin->showDock(); m_notesPlugin->widget()->raise(); m_notesPlugin->widget()->setFocus(); m_notesPlugin->widget()->addProjectNote(); } void ProjectManager::prepareSave() { pCore->projectItemModel()->saveDocumentProperties(pCore->window()->getMainTimeline()->controller()->documentProperties(), m_project->metadata(), m_project->getGuideModel()); pCore->projectItemModel()->saveProperty(QStringLiteral("kdenlive:documentnotes"), documentNotes()); pCore->projectItemModel()->saveProperty(QStringLiteral("kdenlive:docproperties.groups"), m_mainTimelineModel->groupsData()); } void ProjectManager::slotResetProfiles() { m_project->resetProfile(); pCore->monitorManager()->updateScopeSource(); } void ProjectManager::slotResetConsumers(bool fullReset) { pCore->monitorManager()->resetConsumers(fullReset); } void ProjectManager::disableBinEffects(bool disable) { if (m_project) { if (disable) { m_project->setDocumentProperty(QStringLiteral("disablebineffects"), QString::number((int)true)); } else { m_project->setDocumentProperty(QStringLiteral("disablebineffects"), QString()); } } pCore->monitorManager()->refreshProjectMonitor(); pCore->monitorManager()->refreshClipMonitor(); } void ProjectManager::slotDisableTimelineEffects(bool disable) { if (disable) { m_project->setDocumentProperty(QStringLiteral("disabletimelineeffects"), QString::number((int)true)); } else { m_project->setDocumentProperty(QStringLiteral("disabletimelineeffects"), QString()); } m_mainTimelineModel->setTimelineEffectsEnabled(!disable); pCore->monitorManager()->refreshProjectMonitor(); } void ProjectManager::slotSwitchTrackLock() { pCore->window()->getMainTimeline()->controller()->switchTrackLock(); } void ProjectManager::slotSwitchTrackActive() { pCore->window()->getMainTimeline()->controller()->switchTrackActive(); } void ProjectManager::slotSwitchAllTrackActive() { pCore->window()->getMainTimeline()->controller()->switchAllTrackActive(); } void ProjectManager::slotMakeAllTrackActive() { pCore->window()->getMainTimeline()->controller()->makeAllTrackActive(); } void ProjectManager::slotSwitchAllTrackLock() { pCore->window()->getMainTimeline()->controller()->switchTrackLock(true); } void ProjectManager::slotSwitchTrackTarget() { pCore->window()->getMainTimeline()->controller()->switchTargetTrack(); } QString ProjectManager::getDefaultProjectFormat() { // On first run, lets use an HD1080p profile with fps related to timezone country. Then, when the first video is added to a project, if it does not match // our profile, propose a new default. QTimeZone zone; zone = QTimeZone::systemTimeZone(); QList ntscCountries; ntscCountries << QLocale::Canada << QLocale::Chile << QLocale::CostaRica << QLocale::Cuba << QLocale::DominicanRepublic << QLocale::Ecuador; ntscCountries << QLocale::Japan << QLocale::Mexico << QLocale::Nicaragua << QLocale::Panama << QLocale::Peru << QLocale::Philippines; ntscCountries << QLocale::PuertoRico << QLocale::SouthKorea << QLocale::Taiwan << QLocale::UnitedStates; bool ntscProject = ntscCountries.contains(zone.country()); if (!ntscProject) { return QStringLiteral("atsc_1080p_25"); } return QStringLiteral("atsc_1080p_2997"); } void ProjectManager::saveZone(const QStringList &info, const QDir &dir) { pCore->bin()->saveZone(info, dir); } void ProjectManager::moveProjectData(const QString &src, const QString &dest) { // Move tmp folder (thumbnails, timeline preview) KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(src), QUrl::fromLocalFile(dest)); connect(copyJob, &KJob::result, this, &ProjectManager::slotMoveFinished); connect(copyJob, SIGNAL(percent(KJob *, ulong)), this, SLOT(slotMoveProgress(KJob *, ulong))); m_project->moveProjectData(src, dest); } void ProjectManager::slotMoveProgress(KJob *, unsigned long progress) { pCore->displayMessage(i18n("Moving project folder"), ProcessingJobMessage, static_cast(progress)); } void ProjectManager::slotMoveFinished(KJob *job) { if (job->error() == 0) { pCore->displayMessage(QString(), OperationCompletedMessage, 100); auto *copyJob = static_cast(job); QString newFolder = copyJob->destUrl().toLocalFile(); // Check if project folder is inside document folder, in which case, paths will be relative QDir projectDir(m_project->url().toString(QUrl::RemoveFilename | QUrl::RemoveScheme)); QDir srcDir(m_project->projectTempFolder()); if (srcDir.absolutePath().startsWith(projectDir.absolutePath())) { m_replacementPattern.insert(QStringLiteral(">proxy/"), QStringLiteral(">") + newFolder + QStringLiteral("/proxy/")); } else { m_replacementPattern.insert(m_project->projectTempFolder() + QStringLiteral("/proxy/"), newFolder + QStringLiteral("/proxy/")); } m_project->setProjectFolder(QUrl::fromLocalFile(newFolder)); saveFile(); m_replacementPattern.clear(); slotRevert(); } else { KMessageBox::sorry(pCore->window(), i18n("Error moving project folder: %1", job->errorText())); } } bool ProjectManager::updateTimeline(int pos, int scrollPos) { Q_UNUSED(scrollPos); pCore->jobManager()->slotCancelJobs(); /*qDebug() << "Loading xml"<getProjectXml().constData(); QFile file("/tmp/data.xml"); if (file.open(QIODevice::ReadWrite)) { QTextStream stream(&file); stream << m_project->getProjectXml() << endl; }*/ pCore->window()->getMainTimeline()->loading = true; pCore->window()->slotSwitchTimelineZone(m_project->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1); QScopedPointer xmlProd(new Mlt::Producer(pCore->getCurrentProfile()->profile(), "xml-string", m_project->getProjectXml().constData())); Mlt::Service s(*xmlProd); Mlt::Tractor tractor(s); if (tractor.count() == 0) { // Wow we have a project file with empty tractor, probably corrupted, propose to open a recovery file KMessageBox::ButtonCode res = KMessageBox::warningContinueCancel(qApp->activeWindow(), i18n("Project file is corrupted (no tracks). Try to find a backup file?")); pCore->window()->getMainTimeline()->loading = false; m_project->setModified(false); if (res == KMessageBox::Continue) { // Try opening backup if (!slotOpenBackup(m_project->url())) { newFile(false); } } else { newFile(false); } return false; } m_mainTimelineModel = TimelineItemModel::construct(pCore->getProjectProfile(), m_project->getGuideModel(), m_project->commandStack()); // Add snap point at projec start m_mainTimelineModel->addSnap(0); pCore->window()->getMainTimeline()->setModel(m_mainTimelineModel, pCore->monitorManager()->projectMonitor()->getControllerProxy()); if (!constructTimelineFromMelt(m_mainTimelineModel, tractor, m_progressDialog)) { //TODO: act on project load failure qDebug()<<"// Project failed to load!!"; } const QString groupsData = m_project->getDocumentProperty(QStringLiteral("groups")); // update track compositing int compositing = pCore->currentDoc()->getDocumentProperty(QStringLiteral("compositing"), QStringLiteral("2")).toInt(); pCore->currentDoc()->updateCompositionMode(compositing); if (compositing < 2) { pCore->window()->getMainTimeline()->controller()->switchCompositing(compositing); } if (!groupsData.isEmpty()) { m_mainTimelineModel->loadGroups(groupsData); } connect(pCore->window()->getMainTimeline()->controller(), &TimelineController::durationChanged, this, &ProjectManager::adjustProjectDuration); pCore->monitorManager()->updatePreviewScaling(); pCore->monitorManager()->projectMonitor()->slotActivateMonitor(); pCore->monitorManager()->projectMonitor()->setProducer(m_mainTimelineModel->producer(), pos); pCore->monitorManager()->projectMonitor()->adjustRulerSize(m_mainTimelineModel->duration() - 1, m_project->getGuideModel()); - pCore->window()->getMainTimeline()->controller()->setZone(m_project->zone()); + pCore->window()->getMainTimeline()->controller()->setZone(m_project->zone(), false); //pCore->window()->getMainTimeline()->controller()->setTargetTracks(m_project->targetTracks()); pCore->window()->getMainTimeline()->controller()->setScrollPos(m_project->getDocumentProperty(QStringLiteral("scrollPos")).toInt()); int activeTrackPosition = m_project->getDocumentProperty(QStringLiteral("activeTrack"), QString::number( - 1)).toInt(); if (activeTrackPosition > -1 && activeTrackPosition < m_mainTimelineModel->getTracksCount()) { pCore->window()->getMainTimeline()->controller()->setActiveTrack(m_mainTimelineModel->getTrackIndexFromPosition(activeTrackPosition)); } m_mainTimelineModel->setUndoStack(m_project->commandStack()); return true; } void ProjectManager::adjustProjectDuration() { pCore->monitorManager()->projectMonitor()->adjustRulerSize(m_mainTimelineModel->duration() - 1, nullptr); } void ProjectManager::activateAsset(const QVariantMap &effectData) { if (effectData.contains(QStringLiteral("kdenlive/effect"))) { pCore->window()->addEffect(effectData.value(QStringLiteral("kdenlive/effect")).toString()); } else { pCore->window()->getMainTimeline()->controller()->addAsset(effectData); } } std::shared_ptr ProjectManager::getGuideModel() { return current()->getGuideModel(); } std::shared_ptr ProjectManager::undoStack() { return current()->commandStack(); } void ProjectManager::saveWithUpdatedProfile(const QString &updatedProfile) { // First backup current project with fps appended QString message; bool saveInTempFile = false; if (m_project && m_project->isModified()) { switch ( KMessageBox::warningYesNoCancel(pCore->window(), i18n("The project \"%1\" has been changed.\nDo you want to save your changes?", m_project->url().fileName().isEmpty() ? i18n("Untitled") : m_project->url().fileName()))) { case KMessageBox::Yes: // save document here. If saving fails, return false; if (!saveFile()) { pCore->displayBinMessage(i18n("Project profile change aborted"), KMessageWidget::Information); return; } break; case KMessageBox::Cancel: pCore->displayBinMessage(i18n("Project profile change aborted"), KMessageWidget::Information); return; break; default: saveInTempFile = true; break; } } if (!m_project) { pCore->displayBinMessage(i18n("Project profile change aborted"), KMessageWidget::Information); return; } QString currentFile = m_project->url().toLocalFile(); // Now update to new profile auto &newProfile = ProfileRepository::get()->getProfile(updatedProfile); QString convertedFile = currentFile.section(QLatin1Char('.'), 0, -2); convertedFile.append(QString("-%1.kdenlive").arg((int)(newProfile->fps() * 100))); QString saveFolder = m_project->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile(); QTemporaryFile tmpFile(saveFolder + "/kdenlive-XXXXXX.mlt"); if (saveInTempFile) { // Save current playlist in tmp file if (!tmpFile.open()) { // Something went wrong pCore->displayBinMessage(i18n("Project profile change aborted"), KMessageWidget::Information); return; } prepareSave(); QString scene = projectSceneList(saveFolder); if (!m_replacementPattern.isEmpty()) { QMapIterator i(m_replacementPattern); while (i.hasNext()) { i.next(); scene.replace(i.key(), i.value()); } } tmpFile.write(scene.toUtf8()); if (tmpFile.error() != QFile::NoError) { tmpFile.close(); return; } tmpFile.close(); currentFile = tmpFile.fileName(); // Don't ask again to save m_project->setModified(false); } QFile f(currentFile); QDomDocument doc; doc.setContent(&f, false); f.close(); QDomElement mltProfile = doc.documentElement().firstChildElement(QStringLiteral("profile")); if (!mltProfile.isNull()) { mltProfile.setAttribute(QStringLiteral("frame_rate_num"), newProfile->frame_rate_num()); mltProfile.setAttribute(QStringLiteral("frame_rate_den"), newProfile->frame_rate_den()); mltProfile.setAttribute(QStringLiteral("display_aspect_num"), newProfile->display_aspect_num()); mltProfile.setAttribute(QStringLiteral("display_aspect_den"), newProfile->display_aspect_den()); mltProfile.setAttribute(QStringLiteral("sample_aspect_num"), newProfile->sample_aspect_num()); mltProfile.setAttribute(QStringLiteral("sample_aspect_den"), newProfile->sample_aspect_den()); mltProfile.setAttribute(QStringLiteral("colorspace"), newProfile->colorspace()); mltProfile.setAttribute(QStringLiteral("progressive"), newProfile->progressive()); mltProfile.setAttribute(QStringLiteral("description"), newProfile->description()); mltProfile.setAttribute(QStringLiteral("width"), newProfile->width()); mltProfile.setAttribute(QStringLiteral("height"), newProfile->height()); } QDomNodeList playlists = doc.documentElement().elementsByTagName(QStringLiteral("playlist")); for (int i = 0; i < playlists.count(); ++i) { QDomElement e = playlists.at(i).toElement(); if (e.attribute(QStringLiteral("id")) == QLatin1String("main_bin")) { Xml::setXmlProperty(e, QStringLiteral("kdenlive:docproperties.profile"), updatedProfile); break; } } QDomNodeList producers = doc.documentElement().elementsByTagName(QStringLiteral("producer")); for (int i = 0; i < producers.count(); ++i) { QDomElement e = producers.at(i).toElement(); int length = Xml::getXmlProperty(e, QStringLiteral("length")).toInt(); if (length > 0) { // calculate updated length Xml::setXmlProperty(e, QStringLiteral("length"), pCore->window()->getMainTimeline()->controller()->framesToClock(length)); } } QFile file(convertedFile); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { return; } QTextStream out(&file); out << doc.toString(); if (file.error() != QFile::NoError) { KMessageBox::error(qApp->activeWindow(), i18n("Cannot write to file %1", convertedFile)); file.close(); return; } file.close(); openFile(QUrl::fromLocalFile(convertedFile)); pCore->displayBinMessage(i18n("Project profile changed"), KMessageWidget::Information); } diff --git a/src/timeline2/model/timelinefunctions.cpp b/src/timeline2/model/timelinefunctions.cpp index fc3b594ec..eee36ed44 100644 --- a/src/timeline2/model/timelinefunctions.cpp +++ b/src/timeline2/model/timelinefunctions.cpp @@ -1,1837 +1,1832 @@ /* Copyright (C) 2017 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 "timelinefunctions.hpp" #include "bin/bin.h" #include "bin/projectclip.h" #include "bin/projectfolder.h" #include "bin/projectitemmodel.h" #include "clipmodel.hpp" #include "compositionmodel.hpp" #include "core.h" #include "doc/kdenlivedoc.h" #include "effects/effectstack/model/effectstackmodel.hpp" #include "groupsmodel.hpp" #include "logger.hpp" #include "timelineitemmodel.hpp" #include "trackmodel.hpp" #include "transitions/transitionsrepository.hpp" #include "mainwindow.h" #include #include #include #include #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #pragma GCC diagnostic ignored "-Wsign-conversion" #pragma GCC diagnostic ignored "-Wfloat-equal" #pragma GCC diagnostic ignored "-Wshadow" #pragma GCC diagnostic ignored "-Wpedantic" #include #pragma GCC diagnostic pop QStringList waitingBinIds; QMap mappedIds; QMap tracksMap; QSemaphore semaphore(1); RTTR_REGISTRATION { using namespace rttr; registration::class_("TimelineFunctions") .method("requestClipCut", select_overload, int, int)>(&TimelineFunctions::requestClipCut))( parameter_names("timeline", "clipId", "position")); } bool TimelineFunctions::cloneClip(const std::shared_ptr &timeline, int clipId, int &newId, PlaylistState::ClipState state, Fun &undo, Fun &redo) { // Special case: slowmotion clips double clipSpeed = timeline->m_allClips[clipId]->getSpeed(); bool warp_pitch = timeline->m_allClips[clipId]->getIntProperty(QStringLiteral("warp_pitch")); bool res = timeline->requestClipCreation(timeline->getClipBinId(clipId), newId, state, clipSpeed, warp_pitch, undo, redo); timeline->m_allClips[newId]->m_endlessResize = timeline->m_allClips[clipId]->m_endlessResize; // copy useful timeline properties timeline->m_allClips[clipId]->passTimelineProperties(timeline->m_allClips[newId]); int duration = timeline->getClipPlaytime(clipId); int init_duration = timeline->getClipPlaytime(newId); if (duration != init_duration) { int in = timeline->m_allClips[clipId]->getIn(); res = res && timeline->requestItemResize(newId, init_duration - in, false, true, undo, redo); res = res && timeline->requestItemResize(newId, duration, true, true, undo, redo); } if (!res) { return false; } std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId); std::shared_ptr destStack = timeline->getClipEffectStackModel(newId); destStack->importEffects(sourceStack, state); return res; } bool TimelineFunctions::requestMultipleClipsInsertion(const std::shared_ptr &timeline, const QStringList &binIds, int trackId, int position, QList &clipIds, bool logUndo, bool refreshView) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; for (const QString &binId : binIds) { int clipId; if (timeline->requestClipInsertion(binId, trackId, position, clipId, logUndo, refreshView, false, undo, redo)) { clipIds.append(clipId); position += timeline->getItemPlaytime(clipId); } else { undo(); clipIds.clear(); return false; } } if (logUndo) { pCore->pushUndo(undo, redo, i18n("Insert Clips")); } return true; } bool TimelineFunctions::processClipCut(const std::shared_ptr &timeline, int clipId, int position, int &newId, Fun &undo, Fun &redo) { int trackId = timeline->getClipTrackId(clipId); int trackDuration = timeline->getTrackById_const(trackId)->trackDuration(); int start = timeline->getClipPosition(clipId); int duration = timeline->getClipPlaytime(clipId); if (start > position || (start + duration) < position) { return false; } PlaylistState::ClipState state = timeline->m_allClips[clipId]->clipState(); bool res = cloneClip(timeline, clipId, newId, state, undo, redo); timeline->m_blockRefresh = true; res = res && timeline->requestItemResize(clipId, position - start, true, true, undo, redo); int newDuration = timeline->getClipPlaytime(clipId); // parse effects std::shared_ptr sourceStack = timeline->getClipEffectStackModel(clipId); sourceStack->cleanFadeEffects(true, undo, redo); std::shared_ptr destStack = timeline->getClipEffectStackModel(newId); destStack->cleanFadeEffects(false, undo, redo); res = res && timeline->requestItemResize(newId, duration - newDuration, false, true, undo, redo); // The next requestclipmove does not check for duration change since we don't invalidate timeline, so check duration change now bool durationChanged = trackDuration != timeline->getTrackById_const(trackId)->trackDuration(); res = res && timeline->requestClipMove(newId, trackId, position, true, true, false, true, undo, redo); if (durationChanged) { // Track length changed, check project duration Fun updateDuration = [timeline]() { timeline->updateDuration(); return true; }; updateDuration(); PUSH_LAMBDA(updateDuration, redo); } timeline->m_blockRefresh = false; return res; } bool TimelineFunctions::requestClipCut(std::shared_ptr timeline, int clipId, int position) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; TRACE_STATIC(timeline, clipId, position); bool result = TimelineFunctions::requestClipCut(timeline, clipId, position, undo, redo); if (result) { pCore->pushUndo(undo, redo, i18n("Cut clip")); } TRACE_RES(result); return result; } bool TimelineFunctions::requestClipCut(const std::shared_ptr &timeline, int clipId, int position, Fun &undo, Fun &redo) { const std::unordered_set clipselect = timeline->getGroupElements(clipId); // Remove locked items std::unordered_set clips; for (int cid : clipselect) { if (!timeline->isClip(cid)) { continue; } int tk = timeline->getClipTrackId(cid); if (tk != -1 && !timeline->getTrackById_const(tk)->isLocked()) { clips.insert(cid); } } // We need to call clearSelection before attempting the split or the group split will be corrupted by the selection group (no undo support) timeline->requestClearSelection(); std::unordered_set topElements; std::transform(clips.begin(), clips.end(), std::inserter(topElements, topElements.begin()), [&](int id) { return timeline->m_groups->getRootId(id); }); int count = 0; QList newIds; QList clipsToCut; for (int cid : clips) { if (!timeline->isClip(cid)) { continue; } int start = timeline->getClipPosition(cid); int duration = timeline->getClipPlaytime(cid); if (start < position && (start + duration) > position) { clipsToCut << cid; } } if (clipsToCut.isEmpty()) { return true; } for (int cid : clipsToCut) { count++; int newId; bool res = processClipCut(timeline, cid, position, newId, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } // splitted elements go temporarily in the same group as original ones. timeline->m_groups->setInGroupOf(newId, cid, undo, redo); newIds << newId; } if (count > 0 && timeline->m_groups->isInGroup(clipId)) { // we now split the group hierarchy. // As a splitting criterion, we compare start point with split position auto criterion = [timeline, position](int cid) { return timeline->getItemPosition(cid) < position; }; bool res = true; for (const int topId : topElements) { qDebug()<<"// CHECKING REGROUP ELEMENT: "<isClip(topId)<isGroup(topId); res = res && timeline->m_groups->split(topId, criterion, undo, redo); } if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } } return count > 0; } int TimelineFunctions::requestSpacerStartOperation(const std::shared_ptr &timeline, int trackId, int position) { std::unordered_set clips = timeline->getItemsInRange(trackId, position, -1); if (!clips.empty()) { timeline->requestSetSelection(clips); return (*clips.cbegin()); } return -1; } bool TimelineFunctions::requestSpacerEndOperation(const std::shared_ptr &timeline, int itemId, int startPosition, int endPosition) { // Move group back to original position int track = timeline->getItemTrackId(itemId); bool isClip = timeline->isClip(itemId); if (isClip) { timeline->requestClipMove(itemId, track, startPosition, true, false, false); } else { timeline->requestCompositionMove(itemId, track, startPosition, false, false); } std::unordered_set clips = timeline->getGroupElements(itemId); // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; //int res = timeline->requestClipsGroup(clips, undo, redo, GroupType::Selection); int res = timeline->m_groups->getRootId(itemId); bool final = false; if (res > -1 || clips.size() == 1) { if (clips.size() > 1) { final = timeline->requestGroupMove(itemId, res, 0, endPosition - startPosition, true, true, undo, redo); } else { // only 1 clip to be moved if (isClip) { final = timeline->requestClipMove(itemId, track, endPosition, true, true, true, true, undo, redo); } else { final = timeline->requestCompositionMove(itemId, track, -1, endPosition, true, true, undo, redo); } } } timeline->requestClearSelection(); if (final) { if (startPosition < endPosition) { pCore->pushUndo(undo, redo, i18n("Insert space")); } else { pCore->pushUndo(undo, redo, i18n("Remove space")); } return true; } return false; } bool TimelineFunctions::breakAffectedGroups(const std::shared_ptr &timeline, QVector tracks, QPoint zone, Fun &undo, Fun &redo) { // Check if we have grouped clips that are on unaffected tracks, and ungroup them bool result = true; std::unordered_set affectedItems; // First find all affected items for (int &trackId : tracks) { std::unordered_set items = timeline->getItemsInRange(trackId, zone.x(), zone.y()); affectedItems.insert(items.begin(), items.end()); } for (int item : affectedItems) { if (timeline->m_groups->isInGroup(item)) { int groupId = timeline->m_groups->getRootId(item); std::unordered_set all_children = timeline->m_groups->getLeaves(groupId); for (int child: all_children) { int childTrackId = timeline->getItemTrackId(child); if (!tracks.contains(childTrackId)) { // This item should not be affected by the operation, ungroup it result = result && timeline->requestClipUngroup(child, undo, redo); } } } } return result; } bool TimelineFunctions::extractZone(const std::shared_ptr &timeline, QVector tracks, QPoint zone, bool liftOnly) { // Start undoable command std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool result = true; result = breakAffectedGroups(timeline, tracks, zone, undo, redo); for (int &trackId : tracks) { if (timeline->getTrackById_const(trackId)->isLocked()) { continue; } result = result && TimelineFunctions::liftZone(timeline, trackId, zone, undo, redo); } if (result && !liftOnly) { result = TimelineFunctions::removeSpace(timeline, -1, zone, undo, redo, tracks); } pCore->pushUndo(undo, redo, liftOnly ? i18n("Lift zone") : i18n("Extract zone")); return result; } bool TimelineFunctions::insertZone(const std::shared_ptr &timeline, QList trackIds, const QString &binId, int insertFrame, QPoint zone, bool overwrite, bool useTargets) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; bool res = TimelineFunctions::insertZone(timeline, trackIds, binId, insertFrame, zone, overwrite, useTargets, undo, redo); if (res) { pCore->pushUndo(undo, redo, overwrite ? i18n("Overwrite zone") : i18n("Insert zone")); } else { pCore->displayMessage(i18n("Could not insert zone"), InformationMessage); undo(); } return res; } bool TimelineFunctions::insertZone(const std::shared_ptr &timeline, QList trackIds, const QString &binId, int insertFrame, QPoint zone, bool overwrite, bool useTargets, Fun &undo, Fun &redo) { // Start undoable command bool result = true; QVector affectedTracks; auto it = timeline->m_allTracks.cbegin(); if (!useTargets && overwrite) { // Timeline drop in overwrite mode for (int target_track : trackIds) { if (!timeline->getTrackById_const(target_track)->isLocked()) { affectedTracks << target_track; } } } else { while (it != timeline->m_allTracks.cend()) { int target_track = (*it)->getId(); if (!useTargets || timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { affectedTracks << target_track; } else if (trackIds.contains(target_track)) { // Track is marked as target but not active, remove it trackIds.removeAll(target_track); } ++it; } } if (affectedTracks.isEmpty()) { pCore->displayMessage(i18n("Please activate a track by clicking on a track's label"), InformationMessage); return false; } result = breakAffectedGroups(timeline, affectedTracks, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); if (overwrite) { // Cut all tracks for (int target_track : affectedTracks) { result = result && TimelineFunctions::liftZone(timeline, target_track, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo); if (!result) { qDebug() << "// LIFTING ZONE FAILED\n"; break; } } } else { // Cut all tracks for (int target_track : affectedTracks) { int startClipId = timeline->getClipByPosition(target_track, insertFrame); if (startClipId > -1) { // There is a clip, cut it result = result && TimelineFunctions::requestClipCut(timeline, startClipId, insertFrame, undo, redo); } } result = result && TimelineFunctions::requestInsertSpace(timeline, QPoint(insertFrame, insertFrame + (zone.y() - zone.x())), undo, redo, affectedTracks); } - bool clipInserted = false; if (result) { if (!trackIds.isEmpty()) { int newId = -1; QString binClipId; if (binId.contains(QLatin1Char('/'))) { binClipId = QString("%1/%2/%3").arg(binId.section(QLatin1Char('/'), 0, 0)).arg(zone.x()).arg(zone.y() - 1); } else { binClipId = QString("%1/%2/%3").arg(binId).arg(zone.x()).arg(zone.y() - 1); } result = timeline->requestClipInsertion(binClipId, trackIds.first(), insertFrame, newId, true, true, useTargets, undo, redo, affectedTracks); - if (result) { - clipInserted = true; - } } } return result; } bool TimelineFunctions::liftZone(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo) { // Check if there is a clip at start point int startClipId = timeline->getClipByPosition(trackId, zone.x()); if (startClipId > -1) { // There is a clip, cut it if (timeline->getClipPosition(startClipId) < zone.x()) { qDebug() << "/// CUTTING AT START: " << zone.x() << ", ID: " << startClipId; TimelineFunctions::requestClipCut(timeline, startClipId, zone.x(), undo, redo); qDebug() << "/// CUTTING AT START DONE"; } } int endClipId = timeline->getClipByPosition(trackId, zone.y()); if (endClipId > -1) { // There is a clip, cut it if (timeline->getClipPosition(endClipId) + timeline->getClipPlaytime(endClipId) > zone.y()) { qDebug() << "/// CUTTING AT END: " << zone.y() << ", ID: " << endClipId; TimelineFunctions::requestClipCut(timeline, endClipId, zone.y(), undo, redo); qDebug() << "/// CUTTING AT END DONE"; } } std::unordered_set clips = timeline->getItemsInRange(trackId, zone.x(), zone.y()); for (const auto &clipId : clips) { timeline->requestItemDeletion(clipId, undo, redo); } return true; } bool TimelineFunctions::removeSpace(const std::shared_ptr &timeline, int trackId, QPoint zone, Fun &undo, Fun &redo, QVector allowedTracks) { Q_UNUSED(trackId) std::unordered_set clips; auto it = timeline->m_allTracks.cbegin(); while (it != timeline->m_allTracks.cend()) { int target_track = (*it)->getId(); if (timeline->getTrackById_const(target_track)->shouldReceiveTimelineOp()) { std::unordered_set subs = timeline->getItemsInRange(target_track, zone.y() - 1, -1, true); clips.insert(subs.begin(), subs.end()); } ++it; } if (clips.size() == 0) { // TODO: inform user no change will be performed return true; } bool result = false; timeline->requestSetSelection(clips); int itemId = *clips.begin(); int targetTrackId = timeline->getItemTrackId(itemId); int targetPos = timeline->getItemPosition(itemId) + zone.x() - zone.y(); if (timeline->m_groups->isInGroup(itemId)) { result = timeline->requestGroupMove(itemId, timeline->m_groups->getRootId(itemId), 0, zone.x() - zone.y(), true, true, undo, redo, true, true, allowedTracks); } else if (timeline->isClip(itemId)) { result = timeline->requestClipMove(itemId, targetTrackId, targetPos, true, true, true, true, undo, redo); } else { result = timeline->requestCompositionMove(itemId, targetTrackId, timeline->m_allCompositions[itemId]->getForcedTrack(), targetPos, true, true, undo, redo); } timeline->requestClearSelection(); if (!result) { undo(); } return result; } bool TimelineFunctions::requestInsertSpace(const std::shared_ptr &timeline, QPoint zone, Fun &undo, Fun &redo, QVector allowedTracks) { timeline->requestClearSelection(); Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; std::unordered_set items; if (allowedTracks.isEmpty()) { // Select clips in all tracks items = timeline->getItemsInRange(-1, zone.x(), -1, true); } else { // Select clips in target and active tracks only for (int target_track : allowedTracks) { std::unordered_set subs = timeline->getItemsInRange(target_track, zone.x(), -1, true); items.insert(subs.begin(), subs.end()); } } if (items.empty()) { return true; } timeline->requestSetSelection(items); bool result = true; int itemId = *(items.begin()); int targetTrackId = timeline->getItemTrackId(itemId); int targetPos = timeline->getItemPosition(itemId) + zone.y() - zone.x(); // TODO the three move functions should be unified in a "requestItemMove" function if (timeline->m_groups->isInGroup(itemId)) { result = result && timeline->requestGroupMove(itemId, timeline->m_groups->getRootId(itemId), 0, zone.y() - zone.x(), true, true, local_undo, local_redo, true, true, allowedTracks); } else if (timeline->isClip(itemId)) { result = result && timeline->requestClipMove(itemId, targetTrackId, targetPos, true, true, true, true, local_undo, local_redo); } else { result = result && timeline->requestCompositionMove(itemId, targetTrackId, timeline->m_allCompositions[itemId]->getForcedTrack(), targetPos, true, true, local_undo, local_redo); } timeline->requestClearSelection(); if (!result) { bool undone = local_undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Cannot move selected group"), ErrorMessage); } UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); return result; } bool TimelineFunctions::requestItemCopy(const std::shared_ptr &timeline, int clipId, int trackId, int position) { Q_ASSERT(timeline->isClip(clipId) || timeline->isComposition(clipId)); Fun undo = []() { return true; }; Fun redo = []() { return true; }; int deltaTrack = timeline->getTrackPosition(trackId) - timeline->getTrackPosition(timeline->getItemTrackId(clipId)); int deltaPos = position - timeline->getItemPosition(clipId); std::unordered_set allIds = timeline->getGroupElements(clipId); std::unordered_map mapping; // keys are ids of the source clips, values are ids of the copied clips bool res = true; for (int id : allIds) { int newId = -1; if (timeline->isClip(id)) { PlaylistState::ClipState state = timeline->m_allClips[id]->clipState(); res = cloneClip(timeline, id, newId, state, undo, redo); res = res && (newId != -1); } int target_position = timeline->getItemPosition(id) + deltaPos; int target_track_position = timeline->getTrackPosition(timeline->getItemTrackId(id)) + deltaTrack; if (target_track_position >= 0 && target_track_position < timeline->getTracksCount()) { auto it = timeline->m_allTracks.cbegin(); std::advance(it, target_track_position); int target_track = (*it)->getId(); if (timeline->isClip(id)) { res = res && timeline->requestClipMove(newId, target_track, target_position, true, true, true, true, undo, redo); } else { const QString &transitionId = timeline->m_allCompositions[id]->getAssetId(); std::unique_ptr transProps(timeline->m_allCompositions[id]->properties()); res = res && timeline->requestCompositionInsertion(transitionId, target_track, -1, target_position, timeline->m_allCompositions[id]->getPlaytime(), std::move(transProps), newId, undo, redo); } } else { res = false; } if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } mapping[id] = newId; } qDebug() << "Successful copy, coping groups..."; res = timeline->m_groups->copyGroups(mapping, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); return false; } return true; } void TimelineFunctions::showClipKeyframes(const std::shared_ptr &timeline, int clipId, bool value) { timeline->m_allClips[clipId]->setShowKeyframes(value); QModelIndex modelIndex = timeline->makeClipIndexFromID(clipId); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole}); } void TimelineFunctions::showCompositionKeyframes(const std::shared_ptr &timeline, int compoId, bool value) { timeline->m_allCompositions[compoId]->setShowKeyframes(value); QModelIndex modelIndex = timeline->makeCompositionIndexFromID(compoId); timeline->dataChanged(modelIndex, modelIndex, {TimelineModel::ShowKeyframesRole}); } bool TimelineFunctions::switchEnableState(const std::shared_ptr &timeline, std::unordered_set selection) { Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool result = false; bool disable = true; for (int clipId : selection) { if (!timeline->isClip(clipId)) { continue; } PlaylistState::ClipState oldState = timeline->getClipPtr(clipId)->clipState(); PlaylistState::ClipState state = PlaylistState::Disabled; disable = true; if (oldState == PlaylistState::Disabled) { state = timeline->getTrackById_const(timeline->getClipTrackId(clipId))->trackType(); disable = false; } result = changeClipState(timeline, clipId, state, undo, redo); if (!result) { break; } } // Update action name since clip will be switched int id = *selection.begin(); Fun local_redo = []() { return true; }; Fun local_undo = []() { return true; }; if (timeline->isClip(id)) { bool disabled = timeline->m_allClips[id]->clipState() == PlaylistState::Disabled; QAction *action = pCore->window()->actionCollection()->action(QStringLiteral("clip_switch")); local_redo = [disabled, action]() { action->setText(disabled ? i18n("Enable clip") : i18n("Disable clip")); return true; }; local_undo = [disabled, action]() { action->setText(disabled ? i18n("Disable clip") : i18n("Enable clip")); return true; }; } if (result) { local_redo(); UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); pCore->pushUndo(undo, redo, disable ? i18n("Disable clip") : i18n("Enable clip")); } return result; } bool TimelineFunctions::changeClipState(const std::shared_ptr &timeline, int clipId, PlaylistState::ClipState status, Fun &undo, Fun &redo) { int track = timeline->getClipTrackId(clipId); int start = -1; bool invalidate = false; if (track > -1) { if (!timeline->getTrackById_const(track)->isAudioTrack()) { invalidate = true; } start = timeline->getItemPosition(clipId); } Fun local_undo = []() { return true; }; Fun local_redo = []() { return true; }; // For the state change to work, we need to unplant/replant the clip bool result = true; if (track > -1) { result = timeline->getTrackById(track)->requestClipDeletion(clipId, true, invalidate, local_undo, local_redo, false, false); } result = timeline->m_allClips[clipId]->setClipState(status, local_undo, local_redo); if (result && track > -1) { result = timeline->getTrackById(track)->requestClipInsertion(clipId, start, true, true, local_undo, local_redo); } UPDATE_UNDO_REDO_NOLOCK(local_redo, local_undo, undo, redo); return result; } bool TimelineFunctions::requestSplitAudio(const std::shared_ptr &timeline, int clipId, int audioTarget) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; const std::unordered_set clips = timeline->getGroupElements(clipId); bool done = false; // Now clear selection so we don't mess with groups timeline->requestClearSelection(false, undo, redo); for (int cid : clips) { if (!timeline->getClipPtr(cid)->canBeAudio() || timeline->getClipPtr(cid)->clipState() == PlaylistState::AudioOnly) { // clip without audio or audio only, skip pCore->displayMessage(i18n("One or more clips do not have audio, or are already audio"), ErrorMessage); return false; } int position = timeline->getClipPosition(cid); int track = timeline->getClipTrackId(cid); QList possibleTracks = audioTarget >= 0 ? QList() << audioTarget : timeline->getLowerTracksId(track, TrackType::AudioTrack); if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort undo(); pCore->displayMessage(i18n("No available audio track for split operation"), ErrorMessage); return false; } int newId; bool res = cloneClip(timeline, cid, newId, PlaylistState::AudioOnly, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Audio split failed"), ErrorMessage); return false; } bool success = false; while (!success && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); success = timeline->requestClipMove(newId, newTrack, position, true, true, false, true, undo, redo); } TimelineFunctions::changeClipState(timeline, cid, PlaylistState::VideoOnly, undo, redo); success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo); if (!success) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Audio split failed"), ErrorMessage); return false; } done = true; } if (done) { timeline->requestSetSelection(clips, undo, redo); pCore->pushUndo(undo, redo, i18n("Split Audio")); } return done; } bool TimelineFunctions::requestSplitVideo(const std::shared_ptr &timeline, int clipId, int videoTarget) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; const std::unordered_set clips = timeline->getGroupElements(clipId); bool done = false; // Now clear selection so we don't mess with groups timeline->requestClearSelection(); for (int cid : clips) { if (!timeline->getClipPtr(cid)->canBeVideo() || timeline->getClipPtr(cid)->clipState() == PlaylistState::VideoOnly) { // clip without audio or audio only, skip continue; } int position = timeline->getClipPosition(cid); QList possibleTracks = QList() << videoTarget; if (possibleTracks.isEmpty()) { // No available audio track for splitting, abort undo(); pCore->displayMessage(i18n("No available video track for split operation"), ErrorMessage); return false; } int newId; bool res = cloneClip(timeline, cid, newId, PlaylistState::VideoOnly, undo, redo); if (!res) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Video split failed"), ErrorMessage); return false; } bool success = false; while (!success && !possibleTracks.isEmpty()) { int newTrack = possibleTracks.takeFirst(); success = timeline->requestClipMove(newId, newTrack, position, true, true, true, true, undo, redo); } TimelineFunctions::changeClipState(timeline, cid, PlaylistState::AudioOnly, undo, redo); success = success && timeline->m_groups->createGroupAtSameLevel(cid, std::unordered_set{newId}, GroupType::AVSplit, undo, redo); if (!success) { bool undone = undo(); Q_ASSERT(undone); pCore->displayMessage(i18n("Video split failed"), ErrorMessage); return false; } done = true; } if (done) { pCore->pushUndo(undo, redo, i18n("Split Video")); } return done; } void TimelineFunctions::setCompositionATrack(const std::shared_ptr &timeline, int cid, int aTrack) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; std::shared_ptr compo = timeline->getCompositionPtr(cid); int previousATrack = compo->getATrack(); int previousAutoTrack = static_cast(compo->getForcedTrack() == -1); bool autoTrack = aTrack < 0; if (autoTrack) { // Automatic track compositing, find lower video track aTrack = timeline->getPreviousVideoTrackPos(compo->getCurrentTrackId()); } int start = timeline->getItemPosition(cid); int end = start + timeline->getItemPlaytime(cid); Fun local_redo = [timeline, cid, aTrack, autoTrack, start, end]() { timeline->unplantComposition(cid); QScopedPointer field(timeline->m_tractor->field()); field->lock(); timeline->getCompositionPtr(cid)->setForceTrack(!autoTrack); timeline->getCompositionPtr(cid)->setATrack(aTrack, aTrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(aTrack - 1)); field->unlock(); timeline->replantCompositions(cid, true); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); return true; }; Fun local_undo = [timeline, cid, previousATrack, previousAutoTrack, start, end]() { timeline->unplantComposition(cid); QScopedPointer field(timeline->m_tractor->field()); field->lock(); timeline->getCompositionPtr(cid)->setForceTrack(previousAutoTrack == 0); timeline->getCompositionPtr(cid)->setATrack(previousATrack, previousATrack <= 0 ? -1 : timeline->getTrackIndexFromPosition(previousATrack - 1)); field->unlock(); timeline->replantCompositions(cid, true); timeline->invalidateZone(start, end); timeline->checkRefresh(start, end); return true; }; if (local_redo()) { PUSH_LAMBDA(local_undo, undo); PUSH_LAMBDA(local_redo, redo); } pCore->pushUndo(undo, redo, i18n("Change Composition Track")); } void TimelineFunctions::enableMultitrackView(const std::shared_ptr &timeline, bool enable, bool refresh) { QList videoTracks; for (const auto &track : timeline->m_iteratorTable) { if (timeline->getTrackById_const(track.first)->isAudioTrack() || timeline->getTrackById_const(track.first)->isHidden()) { continue; } videoTracks << track.first; } if (videoTracks.size() < 2) { pCore->displayMessage(i18n("Cannot enable multitrack view on a single track"), InformationMessage); } // First, dis/enable track compositing QScopedPointer service(timeline->m_tractor->field()); Mlt::Field *field = timeline->m_tractor->field(); field->lock(); while ((service != nullptr) && service->is_valid()) { if (service->type() == transition_type) { Mlt::Transition t((mlt_transition)service->get_service()); QString serviceName = t.get("mlt_service"); int added = t.get_int("internal_added"); if (added == 237 && serviceName != QLatin1String("mix")) { // remove all compositing transitions t.set("disable", enable ? "1" : nullptr); } else if (!enable && added == 200) { field->disconnect_service(t); } } service.reset(service->producer()); } if (enable) { for (int i = 0; i < videoTracks.size(); ++i) { Mlt::Transition transition(*timeline->m_tractor->profile(), "composite"); transition.set("mlt_service", "composite"); transition.set("a_track", 0); transition.set("b_track", timeline->getTrackMltIndex(videoTracks.at(i))); transition.set("distort", 0); transition.set("aligned", 0); // 200 is an arbitrary number so we can easily remove these transition later transition.set("internal_added", 200); QString geometry; switch (i) { case 0: switch (videoTracks.size()) { case 2: geometry = QStringLiteral("0 0 50% 100%"); break; case 3: geometry = QStringLiteral("0 0 33% 100%"); break; case 4: geometry = QStringLiteral("0 0 50% 50%"); break; case 5: case 6: geometry = QStringLiteral("0 0 33% 50%"); break; default: geometry = QStringLiteral("0 0 33% 33%"); break; } break; case 1: switch (videoTracks.size()) { case 2: geometry = QStringLiteral("50% 0 50% 100%"); break; case 3: geometry = QStringLiteral("33% 0 33% 100%"); break; case 4: geometry = QStringLiteral("50% 0 50% 50%"); break; case 5: case 6: geometry = QStringLiteral("33% 0 33% 50%"); break; default: geometry = QStringLiteral("33% 0 33% 33%"); break; } break; case 2: switch (videoTracks.size()) { case 3: geometry = QStringLiteral("66% 0 33% 100%"); break; case 4: geometry = QStringLiteral("0 50% 50% 50%"); break; case 5: case 6: geometry = QStringLiteral("66% 0 33% 50%"); break; default: geometry = QStringLiteral("66% 0 33% 33%"); break; } break; case 3: switch (videoTracks.size()) { case 4: geometry = QStringLiteral("50% 50% 50% 50%"); break; case 5: case 6: geometry = QStringLiteral("0 50% 33% 50%"); break; default: geometry = QStringLiteral("0 33% 33% 33%"); break; } break; case 4: switch (videoTracks.size()) { case 5: case 6: geometry = QStringLiteral("33% 50% 33% 50%"); break; default: geometry = QStringLiteral("33% 33% 33% 33%"); break; } break; case 5: switch (videoTracks.size()) { case 6: geometry = QStringLiteral("66% 50% 33% 50%"); break; default: geometry = QStringLiteral("66% 33% 33% 33%"); break; } break; case 6: geometry = QStringLiteral("0 66% 33% 33%"); break; case 7: geometry = QStringLiteral("33% 66% 33% 33%"); break; default: geometry = QStringLiteral("66% 66% 33% 33%"); break; } // Add transition to track: transition.set("geometry", geometry.toUtf8().constData()); transition.set("always_active", 1); field->plant_transition(transition, 0, timeline->getTrackMltIndex(videoTracks.at(i))); } } field->unlock(); if (refresh) { timeline->requestMonitorRefresh(); } } void TimelineFunctions::saveTimelineSelection(const std::shared_ptr &timeline, const std::unordered_set &selection, const QDir &targetDir) { bool ok; QString name = QInputDialog::getText(qApp->activeWindow(), i18n("Add Clip to Library"), i18n("Enter a name for the clip in Library"), QLineEdit::Normal, QString(), &ok); if (name.isEmpty() || !ok) { return; } if (targetDir.exists(name + QStringLiteral(".mlt"))) { // TODO: warn and ask for overwrite / rename } int offset = -1; int lowerAudioTrack = -1; int lowerVideoTrack = -1; QString fullPath = targetDir.absoluteFilePath(name + QStringLiteral(".mlt")); // Build a copy of selected tracks. QMap sourceTracks; for (int i : selection) { int sourceTrack = timeline->getItemTrackId(i); int clipPos = timeline->getItemPosition(i); if (offset < 0 || clipPos < offset) { offset = clipPos; } int trackPos = timeline->getTrackMltIndex(sourceTrack); if (!sourceTracks.contains(trackPos)) { sourceTracks.insert(trackPos, sourceTrack); } } // Build target timeline Mlt::Tractor newTractor(*timeline->m_tractor->profile()); QScopedPointer field(newTractor.field()); int ix = 0; QString composite = TransitionsRepository::get()->getCompositingTransition(); QMapIterator i(sourceTracks); QList compositions; while (i.hasNext()) { i.next(); QScopedPointer newTrackPlaylist(new Mlt::Playlist(*newTractor.profile())); newTractor.set_track(*newTrackPlaylist, ix); // QScopedPointer trackProducer(newTractor.track(ix)); int trackId = i.value(); sourceTracks.insert(timeline->getTrackMltIndex(trackId), ix); std::shared_ptr track = timeline->getTrackById_const(trackId); bool isAudio = track->isAudioTrack(); if (isAudio) { newTrackPlaylist->set("hide", 1); if (lowerAudioTrack < 0) { lowerAudioTrack = ix; } } else { newTrackPlaylist->set("hide", 2); if (lowerVideoTrack < 0) { lowerVideoTrack = ix; } } for (int itemId : selection) { if (timeline->getItemTrackId(itemId) == trackId) { // Copy clip on the destination track if (timeline->isClip(itemId)) { int clip_position = timeline->m_allClips[itemId]->getPosition(); auto clip_loc = track->getClipIndexAt(clip_position); int target_clip = clip_loc.second; QSharedPointer clip = track->getClipProducer(target_clip); newTrackPlaylist->insert_at(clip_position - offset, clip.data(), 1); } else if (timeline->isComposition(itemId)) { // Composition auto *t = new Mlt::Transition(*timeline->m_allCompositions[itemId].get()); QString id(t->get("kdenlive_id")); QString internal(t->get("internal_added")); if (internal.isEmpty()) { compositions << t; if (id.isEmpty()) { qDebug() << "// Warning, this should not happen, transition without id: " << t->get("id") << " = " << t->get("mlt_service"); t->set("kdenlive_id", t->get("mlt_service")); } } } } } ix++; } // Sort compositions and insert if (!compositions.isEmpty()) { std::sort(compositions.begin(), compositions.end(), [](Mlt::Transition *a, Mlt::Transition *b) { return a->get_b_track() < b->get_b_track(); }); while (!compositions.isEmpty()) { QScopedPointer t(compositions.takeFirst()); if (sourceTracks.contains(t->get_a_track()) && sourceTracks.contains(t->get_b_track())) { Mlt::Transition newComposition(*newTractor.profile(), t->get("mlt_service")); Mlt::Properties sourceProps(t->get_properties()); newComposition.inherit(sourceProps); QString id(t->get("kdenlive_id")); int in = qMax(0, t->get_in() - offset); int out = t->get_out() - offset; newComposition.set_in_and_out(in, out); int a_track = sourceTracks.value(t->get_a_track()); int b_track = sourceTracks.value(t->get_b_track()); field->plant_transition(newComposition, a_track, b_track); } } } // Track compositing i.toFront(); ix = 0; while (i.hasNext()) { i.next(); int trackId = i.value(); std::shared_ptr track = timeline->getTrackById_const(trackId); bool isAudio = track->isAudioTrack(); if ((isAudio && ix > lowerAudioTrack) || (!isAudio && ix > lowerVideoTrack)) { // add track compositing / mix Mlt::Transition t(*newTractor.profile(), isAudio ? "mix" : composite.toUtf8().constData()); if (isAudio) { t.set("sum", 1); } t.set("always_active", 1); t.set("internal_added", 237); t.set_tracks(isAudio ? lowerAudioTrack : lowerVideoTrack, ix); field->plant_transition(t, isAudio ? lowerAudioTrack : lowerVideoTrack, ix); } ix++; } Mlt::Consumer xmlConsumer(*newTractor.profile(), ("xml:" + fullPath).toUtf8().constData()); xmlConsumer.set("terminate_on_pause", 1); xmlConsumer.connect(newTractor); xmlConsumer.run(); } int TimelineFunctions::getTrackOffset(const std::shared_ptr &timeline, int startTrack, int destTrack) { qDebug() << "+++++++\nGET TRACK OFFSET: " << startTrack << " - " << destTrack; int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack); int destTrackMltIndex = timeline->getTrackMltIndex(destTrack); int offset = 0; qDebug() << "+++++++\nGET TRACK MLT: " << masterTrackMltIndex << " - " << destTrackMltIndex; if (masterTrackMltIndex == destTrackMltIndex) { return offset; } int step = masterTrackMltIndex > destTrackMltIndex ? -1 : 1; bool isAudio = timeline->isAudioTrack(startTrack); int track = masterTrackMltIndex; while (track != destTrackMltIndex) { track += step; qDebug() << "+ + +TESTING TRACK: " << track; int trackId = timeline->getTrackIndexFromPosition(track - 1); if (isAudio == timeline->isAudioTrack(trackId)) { offset += step; } } return offset; } int TimelineFunctions::getOffsetTrackId(const std::shared_ptr &timeline, int startTrack, int offset, bool audioOffset) { int masterTrackMltIndex = timeline->getTrackMltIndex(startTrack); bool isAudio = timeline->isAudioTrack(startTrack); if (isAudio != audioOffset) { offset = -offset; } qDebug() << "* ** * MASTER INDEX: " << masterTrackMltIndex << ", OFFSET: " << offset; while (offset != 0) { masterTrackMltIndex += offset > 0 ? 1 : -1; qDebug() << "#### TESTING TRACK: " << masterTrackMltIndex; if (masterTrackMltIndex < 0) { masterTrackMltIndex = 0; break; } if (masterTrackMltIndex > (int)timeline->m_allTracks.size()) { masterTrackMltIndex = (int)timeline->m_allTracks.size(); break; } int trackId = timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1); if (timeline->isAudioTrack(trackId) == isAudio) { offset += offset > 0 ? -1 : 1; } } return timeline->getTrackIndexFromPosition(masterTrackMltIndex - 1); } QPair, QList> TimelineFunctions::getAVTracksIds(const std::shared_ptr &timeline) { QList audioTracks; QList videoTracks; for (const auto &track : timeline->m_allTracks) { if (track->isAudioTrack()) { audioTracks << track->getId(); } else { videoTracks << track->getId(); } } return {audioTracks, videoTracks}; } QString TimelineFunctions::copyClips(const std::shared_ptr &timeline, const std::unordered_set &itemIds) { int clipId = *(itemIds.begin()); // We need to retrieve ALL the involved clips, ie those who are also grouped with the given clips std::unordered_set allIds; for (const auto &itemId : itemIds) { std::unordered_set siblings = timeline->getGroupElements(itemId); allIds.insert(siblings.begin(), siblings.end()); } timeline->requestClearSelection(); // TODO better guess for master track int masterTid = timeline->getItemTrackId(clipId); bool audioCopy = timeline->isAudioTrack(masterTid); int masterTrack = timeline->getTrackPosition(masterTid); QDomDocument copiedItems; int offset = -1; QDomElement container = copiedItems.createElement(QStringLiteral("kdenlive-scene")); copiedItems.appendChild(container); QStringList binIds; for (int id : allIds) { if (offset == -1 || timeline->getItemPosition(id) < offset) { offset = timeline->getItemPosition(id); } if (timeline->isClip(id)) { container.appendChild(timeline->m_allClips[id]->toXml(copiedItems)); const QString bid = timeline->m_allClips[id]->binId(); if (!binIds.contains(bid)) { binIds << bid; } } else if (timeline->isComposition(id)) { container.appendChild(timeline->m_allCompositions[id]->toXml(copiedItems)); } else { Q_ASSERT(false); } } QDomElement container2 = copiedItems.createElement(QStringLiteral("bin")); container.appendChild(container2); for (const QString &id : binIds) { std::shared_ptr clip = pCore->projectItemModel()->getClipByBinID(id); QDomDocument tmp; container2.appendChild(clip->toXml(tmp)); } container.setAttribute(QStringLiteral("offset"), offset); if (audioCopy) { container.setAttribute(QStringLiteral("masterAudioTrack"), masterTrack); int masterMirror = timeline->getMirrorVideoTrackId(masterTid); if (masterMirror == -1) { QPair, QList> projectTracks = TimelineFunctions::getAVTracksIds(timeline); if (!projectTracks.second.isEmpty()) { masterTrack = timeline->getTrackPosition(projectTracks.second.first()); } } else { masterTrack = timeline->getTrackPosition(masterMirror); } } /* masterTrack contains the reference track over which we want to paste. this is a video track, unless audioCopy is defined */ container.setAttribute(QStringLiteral("masterTrack"), masterTrack); container.setAttribute(QStringLiteral("documentid"), pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))); QDomElement grp = copiedItems.createElement(QStringLiteral("groups")); container.appendChild(grp); std::unordered_set groupRoots; std::transform(allIds.begin(), allIds.end(), std::inserter(groupRoots, groupRoots.begin()), [&](int id) { return timeline->m_groups->getRootId(id); }); qDebug() << "==============\n GROUP ROOTS: "; for (int gp : groupRoots) { qDebug() << "GROUP: " << gp; } qDebug() << "\n======="; grp.appendChild(copiedItems.createTextNode(timeline->m_groups->toJson(groupRoots))); qDebug() << " / // / PASTED DOC: \n\n" << copiedItems.toString() << "\n\n------------"; return copiedItems.toString(); } bool TimelineFunctions::pasteClips(const std::shared_ptr &timeline, const QString &pasteString, int trackId, int position) { std::function undo = []() { return true; }; std::function redo = []() { return true; }; if (TimelineFunctions::pasteClips(timeline, pasteString, trackId, position, undo, redo)) { pCore->pushUndo(undo, redo, i18n("Paste clips")); return true; } return false; } bool TimelineFunctions::pasteClips(const std::shared_ptr &timeline, const QString &pasteString, int trackId, int position, Fun &undo, Fun &redo) { timeline->requestClearSelection(); while(!semaphore.tryAcquire(1)) { qApp->processEvents(); } waitingBinIds.clear(); QDomDocument copiedItems; copiedItems.setContent(pasteString); if (copiedItems.documentElement().tagName() == QLatin1String("kdenlive-scene")) { qDebug() << " / / READING CLIPS FROM CLIPBOARD"; } else { semaphore.release(1); return false; } const QString docId = copiedItems.documentElement().attribute(QStringLiteral("documentid")); mappedIds.clear(); // Check available tracks QPair, QList> projectTracks = TimelineFunctions::getAVTracksIds(timeline); int masterSourceTrack = copiedItems.documentElement().attribute(QStringLiteral("masterTrack")).toInt(); QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip")); QDomNodeList compositions = copiedItems.documentElement().elementsByTagName(QStringLiteral("composition")); // find paste tracks // List of all source audio tracks QList audioTracks; // List of all source video tracks QList videoTracks; // List of all audio tracks with their corresponding video mirror std::unordered_map audioMirrors; // List of all source audio tracks that don't have video mirror QList singleAudioTracks; for (int i = 0; i < clips.count(); i++) { QDomElement prod = clips.at(i).toElement(); int trackPos = prod.attribute(QStringLiteral("track")).toInt(); if (trackPos < 0 || trackPos >= projectTracks.first.size() + projectTracks.second.size()) { pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), InformationMessage, 500); semaphore.release(1); return false; } bool audioTrack = prod.hasAttribute(QStringLiteral("audioTrack")); if (audioTrack) { if (!audioTracks.contains(trackPos)) { audioTracks << trackPos; } int videoMirror = prod.attribute(QStringLiteral("mirrorTrack")).toInt(); if (videoMirror == -1) { if (singleAudioTracks.contains(trackPos)) { continue; } singleAudioTracks << trackPos; continue; } audioMirrors[trackPos] = videoMirror; if (videoTracks.contains(videoMirror)) { continue; } videoTracks << videoMirror; } else { if (videoTracks.contains(trackPos)) { continue; } videoTracks << trackPos; } } for (int i = 0; i < compositions.count(); i++) { QDomElement prod = compositions.at(i).toElement(); int trackPos = prod.attribute(QStringLiteral("track")).toInt(); if (!videoTracks.contains(trackPos)) { videoTracks << trackPos; } int atrackPos = prod.attribute(QStringLiteral("a_track")).toInt(); if (atrackPos == 0 || videoTracks.contains(atrackPos)) { continue; } videoTracks << atrackPos; } if (audioTracks.isEmpty() && videoTracks.isEmpty()) { // playlist does not have any tracks, exit semaphore.release(1); return true; } // Now we have a list of all source tracks, check that we have enough target tracks std::sort(videoTracks.begin(), videoTracks.end()); std::sort(audioTracks.begin(), audioTracks.end()); std::sort(singleAudioTracks.begin(), singleAudioTracks.end()); //qDebug()<<"== GOT WANTED TKS\n VIDEO: "< projectTracks.second.size() || requestedAudioTracks > projectTracks.first.size()) { pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), InformationMessage, 500); semaphore.release(1); return false; } // Find destination master track // Check we have enough tracks above/below if (requestedVideoTracks > 0) { qDebug() << "MASTERSTK: " << masterSourceTrack << ", VTKS: " << videoTracks; int tracksBelow = masterSourceTrack - videoTracks.first(); int tracksAbove = videoTracks.last() - masterSourceTrack; qDebug() << "// RQST TKS BELOW: " << tracksBelow << " / ABOVE: " << tracksAbove; qDebug() << "// EXISTING TKS BELOW: " << projectTracks.second.indexOf(trackId) << ", IX: " << trackId; qDebug() << "// EXISTING TKS ABOVE: " << projectTracks.second.size() << " - " << projectTracks.second.indexOf(trackId); if (projectTracks.second.indexOf(trackId) < tracksBelow) { qDebug() << "// UPDATING BELOW TID IX TO: " << tracksBelow; // not enough tracks below, try to paste on upper track trackId = projectTracks.second.at(tracksBelow); } else if ((projectTracks.second.size() - (projectTracks.second.indexOf(trackId) + 1)) < tracksAbove) { // not enough tracks above, try to paste on lower track qDebug() << "// UPDATING ABOVE TID IX TO: " << (projectTracks.second.size() - tracksAbove); trackId = projectTracks.second.at(projectTracks.second.size() - tracksAbove - 1); } } else { // Audio only masterSourceTrack = copiedItems.documentElement().attribute(QStringLiteral("masterAudioTrack")).toInt(); int tracksBelow = masterSourceTrack - audioTracks.first(); int tracksAbove = audioTracks.last() - masterSourceTrack; if (projectTracks.first.indexOf(trackId) < tracksBelow) { qDebug() << "// UPDATING BELOW TID IX TO: " << tracksBelow; // not enough tracks below, try to paste on upper track trackId = projectTracks.first.at(tracksBelow); } else if ((projectTracks.first.size() - (projectTracks.first.indexOf(trackId) + 1)) < tracksAbove) { // not enough tracks above, try to paste on lower track qDebug() << "// UPDATING ABOVE TID IX TO: " << (projectTracks.first.size() - tracksAbove); trackId = projectTracks.first.at(projectTracks.first.size() - tracksAbove - 1); } } tracksMap.clear(); bool audioMaster = false; int masterIx = projectTracks.second.indexOf(trackId); if (masterIx == -1) { masterIx = projectTracks.first.indexOf(trackId); audioMaster = true; } qDebug() << "/// PROJECT VIDEO TKS: " << projectTracks.second << ", MASTER: " << trackId; qDebug() << "/// PASTE VIDEO TKS: " << videoTracks << " / MASTER: " << masterSourceTrack; qDebug() << "/// MASTER PASTE: " << masterIx; for (int tk : videoTracks) { int newPos = masterIx + tk - masterSourceTrack; if (newPos < 0 || newPos >= projectTracks.second.size()) { pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), InformationMessage, 500); semaphore.release(1); return false; } tracksMap.insert(tk, projectTracks.second.at(newPos)); } bool audioOffsetCalculated = false; int audioOffset = 0; for (const auto &mirror : audioMirrors) { int videoIx = tracksMap.value(mirror.second); qDebug()<<"==== INSERTING MIRROR AUDIO:"<getMirrorAudioTrackId(videoIx)<<" (MIRROR FROM: "<getMirrorAudioTrackId(videoIx)); if (!audioOffsetCalculated) { int oldPosition = mirror.first; int currentPosition = timeline->getTrackPosition(tracksMap.value(oldPosition)); audioOffset = currentPosition - oldPosition; audioOffsetCalculated = true; } } if (!audioOffsetCalculated && audioMaster) { audioOffset = masterIx - masterSourceTrack; audioOffsetCalculated = true; } for (int i = 0; i < singleAudioTracks.size(); i++) { int oldPos = singleAudioTracks.at(i); if (tracksMap.contains(oldPos)) { continue; } int offsetId = oldPos + audioOffset; if (offsetId < 0 || offsetId >= projectTracks.first.size()) { pCore->displayMessage(i18n("Not enough tracks to paste clipboard"), InformationMessage, 500); semaphore.release(1); return false; } tracksMap.insert(oldPos, projectTracks.first.at(offsetId)); } std::function callBack = [timeline, copiedItems, position](const QString &binId) { waitingBinIds.removeAll(binId); if (waitingBinIds.isEmpty()) { TimelineFunctions::pasteTimelineClips(timeline, copiedItems, position); } }; bool clipsImported = false; if (docId == pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))) { // Check that the bin clips exists in case we try to paste in a copy of original project QDomNodeList binClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("producer")); QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Pasted clips")); for (int i = 0; i < binClips.count(); ++i) { QDomElement currentProd = binClips.item(i).toElement(); QString clipId = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:id")); QString clipHash = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:file_hash")); if (!pCore->projectItemModel()->validateClip(clipId, clipHash)) { // This clip is different in project and in paste data, create a copy QString updatedId = QString::number(pCore->projectItemModel()->getFreeClipId()); Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), updatedId); mappedIds.insert(clipId, updatedId); if (folderId.isEmpty()) { // Folder does not exist const QString rootId = pCore->projectItemModel()->getRootFolder()->clipId(); folderId = QString::number(pCore->projectItemModel()->getFreeFolderId()); pCore->projectItemModel()->requestAddFolder(folderId, i18n("Pasted clips"), rootId, undo, redo); } waitingBinIds << updatedId; clipsImported = true; pCore->projectItemModel()->requestAddBinClip(updatedId, currentProd, folderId, undo, redo, callBack); } } } if (!docId.isEmpty() && docId != pCore->currentDoc()->getDocumentProperty(QStringLiteral("documentid"))) { // paste from another document, import bin clips QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Pasted clips")); if (folderId.isEmpty()) { // Folder does not exist const QString rootId = pCore->projectItemModel()->getRootFolder()->clipId(); folderId = QString::number(pCore->projectItemModel()->getFreeFolderId()); pCore->projectItemModel()->requestAddFolder(folderId, i18n("Pasted clips"), rootId, undo, redo); } QDomNodeList binClips = copiedItems.documentElement().elementsByTagName(QStringLiteral("producer")); for (int i = 0; i < binClips.count(); ++i) { QDomElement currentProd = binClips.item(i).toElement(); QString clipId = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:id")); QString clipHash = Xml::getXmlProperty(currentProd, QStringLiteral("kdenlive:file_hash")); // Check if we already have a clip with same hash in pasted clips folder QString existingId = pCore->projectItemModel()->validateClipInFolder(folderId, clipHash); if (!existingId.isEmpty()) { mappedIds.insert(clipId, existingId); continue; } if (!pCore->projectItemModel()->isIdFree(clipId)) { QString updatedId = QString::number(pCore->projectItemModel()->getFreeClipId()); Xml::setXmlProperty(currentProd, QStringLiteral("kdenlive:id"), updatedId); mappedIds.insert(clipId, updatedId); clipId = updatedId; } waitingBinIds << clipId; clipsImported = true; bool insert = pCore->projectItemModel()->requestAddBinClip(clipId, currentProd, folderId, undo, redo, callBack); if (!insert) { pCore->displayMessage(i18n("Could not add bin clip"), InformationMessage, 500); undo(); semaphore.release(1); return false; } } } if (!clipsImported) { return TimelineFunctions::pasteTimelineClips(timeline, copiedItems, position); } qDebug()<<"++++++++++++\nWAITIND FOR BIN INSERTION: "< &timeline, QDomDocument copiedItems, int position) { // Wait until all bin clips are inserted QDomNodeList clips = copiedItems.documentElement().elementsByTagName(QStringLiteral("clip")); QDomNodeList compositions = copiedItems.documentElement().elementsByTagName(QStringLiteral("composition")); int offset = copiedItems.documentElement().attribute(QStringLiteral("offset")).toInt(); std::function timeline_undo = []() { return true; }; std::function timeline_redo = []() { return true; }; bool res = true; QLocale locale; std::unordered_map correspondingIds; for (int i = 0; i < clips.count(); i++) { QDomElement prod = clips.at(i).toElement(); QString originalId = prod.attribute(QStringLiteral("binid")); if (mappedIds.contains(originalId)) { // Map id originalId = mappedIds.value(originalId); } int in = prod.attribute(QStringLiteral("in")).toInt(); int out = prod.attribute(QStringLiteral("out")).toInt(); int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt()); int pos = prod.attribute(QStringLiteral("position")).toInt() - offset; double speed = locale.toDouble(prod.attribute(QStringLiteral("speed"))); bool warp_pitch = false; if (!qFuzzyCompare(speed, 1.)) { warp_pitch = prod.attribute(QStringLiteral("warp_pitch")).toInt(); } int newId; bool created = timeline->requestClipCreation(originalId, newId, timeline->getTrackById_const(curTrackId)->trackType(), speed, warp_pitch, timeline_undo, timeline_redo); if (!created) { // Something is broken timeline_undo(); semaphore.release(1); return false; } if (timeline->m_allClips[newId]->m_endlessResize) { out = out - in; in = 0; timeline->m_allClips[newId]->m_producer->set("length", out + 1); } timeline->m_allClips[newId]->setInOut(in, out); int targetId = prod.attribute(QStringLiteral("id")).toInt(); correspondingIds[targetId] = newId; res = res && timeline->getTrackById(curTrackId)->requestClipInsertion(newId, position + pos, true, true, timeline_undo, timeline_redo); // paste effects if (res) { std::shared_ptr destStack = timeline->getClipEffectStackModel(newId); destStack->fromXml(prod.firstChildElement(QStringLiteral("effects")), timeline_undo, timeline_redo); } else { //break; } } // Compositions if (res) { for (int i = 0; res && i < compositions.count(); i++) { QDomElement prod = compositions.at(i).toElement(); QString originalId = prod.attribute(QStringLiteral("composition")); int in = prod.attribute(QStringLiteral("in")).toInt(); int out = prod.attribute(QStringLiteral("out")).toInt(); int curTrackId = tracksMap.value(prod.attribute(QStringLiteral("track")).toInt()); int aTrackId = prod.attribute(QStringLiteral("a_track")).toInt(); if (aTrackId > 0) { aTrackId = timeline->getTrackPosition(tracksMap.value(aTrackId)); } int pos = prod.attribute(QStringLiteral("position")).toInt() - offset; int newId; auto transProps = std::make_unique(); QDomNodeList props = prod.elementsByTagName(QStringLiteral("property")); for (int j = 0; j < props.count(); j++) { transProps->set(props.at(j).toElement().attribute(QStringLiteral("name")).toUtf8().constData(), props.at(j).toElement().text().toUtf8().constData()); } res = res && timeline->requestCompositionInsertion(originalId, curTrackId, aTrackId, position + pos, out - in + 1, std::move(transProps), newId, timeline_undo, timeline_redo); } } if (!res) { timeline_undo(); //pCore->pushUndo(undo, redo, i18n("Paste clips")); pCore->displayMessage(i18n("Could not paste items in timeline"), InformationMessage, 500); semaphore.release(1); return false; } // Rebuild groups const QString groupsData = copiedItems.documentElement().firstChildElement(QStringLiteral("groups")).text(); if (!groupsData.isEmpty()) { timeline->m_groups->fromJsonWithOffset(groupsData, tracksMap, position - offset, timeline_undo, timeline_redo); } // Ensure to clear selection in undo/redo too. Fun unselect = [&]() { qDebug() << "starting undo or redo. Selection " << timeline->m_currentSelection; timeline->requestClearSelection(); qDebug() << "after Selection " << timeline->m_currentSelection; return true; }; PUSH_FRONT_LAMBDA(unselect, timeline_undo); PUSH_FRONT_LAMBDA(unselect, timeline_redo); pCore->pushUndo(timeline_undo, timeline_redo, i18n("Paste clips")); //UPDATE_UNDO_REDO_NOLOCK(timeline_redo, timeline_undo, undo, redo); semaphore.release(1); return true; } bool TimelineFunctions::requestDeleteBlankAt(const std::shared_ptr &timeline, int trackId, int position, bool affectAllTracks) { // find blank duration int spaceDuration = timeline->getTrackById_const(trackId)->getBlankSizeAtPos(position); int cid = requestSpacerStartOperation(timeline, affectAllTracks ? -1 : trackId, position); if (cid == -1) { return false; } int start = timeline->getItemPosition(cid); requestSpacerEndOperation(timeline, cid, start, start - spaceDuration); return true; } QDomDocument TimelineFunctions::extractClip(const std::shared_ptr &timeline, int cid, const QString &binId) { int tid = timeline->getClipTrackId(cid); int pos = timeline->getClipPosition(cid); std::shared_ptr clip = pCore->bin()->getBinClip(binId); const QString url = clip->clipUrl(); QFile f(url); QDomDocument sourceDoc; sourceDoc.setContent(&f, false); f.close(); QDomDocument destDoc; QDomElement container = destDoc.createElement(QStringLiteral("kdenlive-scene")); destDoc.appendChild(container); QDomElement bin = destDoc.createElement(QStringLiteral("bin")); container.appendChild(bin); bool isAudio = timeline->isAudioTrack(tid); - int masterTrack = timeline->getTrackPosition(tid); container.setAttribute(QStringLiteral("offset"), pos); container.setAttribute(QStringLiteral("documentid"), QStringLiteral("000000")); // Process producers QLocale locale; QList processedProducers; QMap producerMap; QMap producerSpeed; QMap producerSpeedResource; QDomNodeList producers = sourceDoc.elementsByTagName(QLatin1String("producer")); for (int i = 0; i < producers.count(); ++i) { QDomElement currentProd = producers.item(i).toElement(); bool ok; int clipId = Xml::getXmlProperty(currentProd, QLatin1String("kdenlive:id")).toInt(&ok); if (!ok) { const QString resource = Xml::getXmlProperty(currentProd, QLatin1String("resource")); qDebug()<<"===== CLIP NOT FOUND: "<m(producerMap); while (m.hasNext()) { m.next(); if (m.value() == clipId) { baseProducerId = m.key(); baseProducerClipId = m.value(); qDebug()<<"=== FOUND PRODUCER FOR ID: "< tracksType; int audioTracks = 0; int videoTracks = 0; QDomNodeList tracks = sourceDoc.elementsByTagName(QLatin1String("track")); for (int i = 0; i < tracks.count(); ++i) { QDomElement currentTrack = tracks.item(i).toElement(); if (currentTrack.attribute(QLatin1String("hide")) == QLatin1String("video")) { // Audio track tracksType.insert(currentTrack.attribute(QLatin1String("producer")), true); audioTracks++; } else { // Video track tracksType.insert(currentTrack.attribute(QLatin1String("producer")), false); videoTracks++; } } int track = 1; if (isAudio) { container.setAttribute(QStringLiteral("masterAudioTrack"), 0); } else { track = audioTracks; container.setAttribute(QStringLiteral("masterTrack"), track); } // Process playlists QDomNodeList playlists = sourceDoc.elementsByTagName(QLatin1String("playlist")); for (int i = 0; i < playlists.count(); ++i) { QDomElement currentPlay = playlists.item(i).toElement(); int position = 0; bool audioTrack = tracksType.value(currentPlay.attribute("id")); if (audioTrack != isAudio) { continue; } QDomNodeList elements = currentPlay.childNodes(); for (int j = 0; j < elements.count(); ++j) { QDomElement currentElement = elements.item(j).toElement(); if (currentElement.tagName() == QLatin1String("blank")) { position += currentElement.attribute(QLatin1String("length")).toInt(); continue; } if (currentElement.tagName() == QLatin1String("entry")) { QDomElement clipElement = destDoc.createElement(QStringLiteral("clip")); container.appendChild(clipElement); int in = currentElement.attribute(QLatin1String("in")).toInt(); int out = currentElement.attribute(QLatin1String("out")).toInt(); const QString originalProducer = currentElement.attribute(QLatin1String("producer")); clipElement.setAttribute(QLatin1String("binid"), producerMap.value(originalProducer)); clipElement.setAttribute(QLatin1String("in"), in); clipElement.setAttribute(QLatin1String("out"), out); clipElement.setAttribute(QLatin1String("position"), position + pos); clipElement.setAttribute(QLatin1String("track"), track); //clipElement.setAttribute(QStringLiteral("state"), (int)m_currentState); clipElement.setAttribute(QStringLiteral("state"), audioTrack ? 2 : 1); if (audioTrack) { clipElement.setAttribute(QLatin1String("audioTrack"), 1); int mirror = audioTrack + videoTracks - track - 1; qDebug()<<"=== GOT MIRROR FOR: "< 0) { // Track compositing, discard continue; } QDomElement composition = destDoc.createElement(QStringLiteral("composition")); container.appendChild(composition); int in = currentCompo.attribute(QLatin1String("in")).toInt(); int out = currentCompo.attribute(QLatin1String("out")).toInt(); const QString compoId = Xml::getXmlProperty(currentCompo, QLatin1String("kdenlive_id")); composition.setAttribute(QLatin1String("position"), in + pos); composition.setAttribute(QLatin1String("in"), in); composition.setAttribute(QLatin1String("out"), out); composition.setAttribute(QLatin1String("composition"), compoId); composition.setAttribute(QLatin1String("a_track"), Xml::getXmlProperty(currentCompo, QLatin1String("a_track")).toInt() - 1); composition.setAttribute(QLatin1String("track"), Xml::getXmlProperty(currentCompo, QLatin1String("b_track")).toInt() - 1); QDomNodeList properties = currentCompo.childNodes(); for (int l = 0; l < properties.count(); ++l) { QDomElement prop = properties.item(l).toElement(); const QString &propName = prop.attribute(QLatin1String("name")); Xml::setXmlProperty(composition, propName, prop.text()); } } } qDebug()<<"=== GOT CONVERTED DOCUMENT\n\n"< * * 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 3 of the License, or * (at your option) any later version. * * 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 . */ import QtQuick 2.11 import QtQuick.Controls 2.4 Rectangle { id: rulerRoot // The standard width for labels. Depends on format used (frame number or full timecode) property int labelSize: fontMetrics.boundingRect(timeline.timecode(36000)).width // The spacing between labels. Depends on labelSize property real labelSpacing: labelSize // The space we want between each ticks in the ruler property real tickSpacing: timeline.scaleFactor property alias rulerZone : zone property int workingPreview : timeline.workingPreview property int labelMod: 1 property bool useTimelineRuler : timeline.useRuler property bool showZoneLabels: false property bool resizeActive: false // Used to decide which mouse cursor we should display function adjustStepSize() { if (timeline.scaleFactor > 19) { // Frame size >= 20 pixels rulerRoot.tickSpacing = timeline.scaleFactor // labelSpacing cannot be smaller than 1 frame rulerRoot.labelSpacing = timeline.scaleFactor > rulerRoot.labelSize * 1.3 ? timeline.scaleFactor : Math.floor(rulerRoot.labelSize/timeline.scaleFactor) * timeline.scaleFactor } else { rulerRoot.tickSpacing = Math.floor(3 * root.fontUnit / timeline.scaleFactor) * timeline.scaleFactor rulerRoot.labelSpacing = (Math.floor(rulerRoot.labelSize/rulerRoot.tickSpacing) + 1) * rulerRoot.tickSpacing } rulerRoot.labelMod = Math.max(1, Math.ceil((rulerRoot.labelSize + root.fontUnit) / rulerRoot.tickSpacing)) //console.log('LABELMOD: ', Math.ceil((rulerRoot.labelSize + root.fontUnit) / rulerRoot.tickSpacing))) } function adjustFormat() { rulerRoot.labelSize = fontMetrics.boundingRect(timeline.timecode(36000)).width adjustStepSize() repaintRuler() } function repaintRuler() { // Enforce repaint tickRepeater.model = 0 tickRepeater.model = scrollView.width / rulerRoot.tickSpacing + 2 } color: root.color clip: true // Timeline preview stuff Repeater { model: timeline.dirtyChunks anchors.fill: parent delegate: Rectangle { x: modelData * timeline.scaleFactor y: 0 width: 25 * timeline.scaleFactor height: parent.height / 4 color: 'darkred' } } Repeater { model: timeline.renderedChunks anchors.fill: parent delegate: Rectangle { x: modelData * timeline.scaleFactor y: 0 width: 25 * timeline.scaleFactor height: parent.height / 4 color: 'darkgreen' } } Rectangle { id: working x: rulerRoot.workingPreview * timeline.scaleFactor y: 0 width: 25 * timeline.scaleFactor height: parent.height / 4 color: 'orange' visible: rulerRoot.workingPreview > -1 } // Ruler marks Repeater { id: tickRepeater model: scrollView.width / rulerRoot.tickSpacing + 2 property int offset: Math.floor(scrollView.contentX /rulerRoot.tickSpacing) Item { property int realPos: (tickRepeater.offset + index) * rulerRoot.tickSpacing / timeline.scaleFactor x: realPos * timeline.scaleFactor height: parent.height property bool showText: (tickRepeater.offset + index)%rulerRoot.labelMod == 0 Rectangle { anchors.bottom: parent.bottom height: parent.showText ? 8 : 4 width: 1 color: activePalette.windowText opacity: 0.5 } Label { visible: parent.showText anchors.top: parent.top anchors.topMargin: 2 text: timeline.timecode(parent.realPos) font: miniFont color: activePalette.windowText } } } // monitor zone Rectangle { id: zone visible: timeline.zoneOut > timeline.zoneIn color: useTimelineRuler ? Qt.rgba(activePalette.highlight.r,activePalette.highlight.g,activePalette.highlight.b,0.5) : Qt.rgba(activePalette.highlight.r,activePalette.highlight.g,activePalette.highlight.b,0.25) x: timeline.zoneIn * timeline.scaleFactor width: (timeline.zoneOut - timeline.zoneIn) * timeline.scaleFactor anchors.bottom: parent.bottom height: parent.height / 3 Rectangle { id: centerDrag anchors.centerIn: parent height: parent.height width: height color: moveMouseArea.containsMouse || moveMouseArea.drag.active ? 'white' : 'transparent' border.color: 'white' border.width: 1.5 opacity: 0.5 Drag.active: moveMouseArea.drag.active Drag.proposedAction: Qt.MoveAction MouseArea { id: moveMouseArea anchors.fill: parent - property double startX hoverEnabled: true drag.target: zone drag.axis: Drag.XAxis drag.smoothed: false + property var startZone onPressed: { - startX = zone.x + startZone = Qt.point(timeline.zoneIn, timeline.zoneOut) } onEntered: { resizeActive = true } onExited: { resizeActive = false } onReleased: { + timeline.updateZone(startZone, Qt.point(timeline.zoneIn, timeline.zoneOut), true) resizeActive = false } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { resizeActive = true var offset = Math.round(zone.x/ timeline.scaleFactor) - timeline.zoneIn if (offset != 0) { var newPos = Math.max(0, controller.suggestSnapPoint(timeline.zoneIn + offset,root.snapping)) timeline.zoneOut += newPos - timeline.zoneIn timeline.zoneIn = newPos } } } } } // Zone frame indicator Rectangle { visible: trimInMouseArea.drag.active || trimInMouseArea.containsMouse width: inLabel.contentWidth height: inLabel.contentHeight anchors.bottom: zone.top color: activePalette.highlight Label { id: inLabel anchors.fill: parent text: timeline.timecode(timeline.zoneIn) font: miniFont color: activePalette.highlightedText } } Rectangle { visible: trimOutMouseArea.drag.active || trimOutMouseArea.containsMouse width: outLabel.contentWidth height: outLabel.contentHeight anchors.bottom: zone.top color: activePalette.highlight x: zone.width - outLabel.contentWidth Label { id: outLabel anchors.fill: parent text: timeline.timecode(timeline.zoneOut) font: miniFont color: activePalette.highlightedText } } Rectangle { id: durationRect anchors.bottom: zone.top visible: (!useTimelineRuler && moveMouseArea.containsMouse) || ((useTimelineRuler || trimInMouseArea.drag.active || trimOutMouseArea.drag.active) && showZoneLabels && parent.width > 3 * width) || (useTimelineRuler && !trimInMouseArea.drag.active && !trimOutMouseArea.drag.active) || moveMouseArea.drag.active anchors.horizontalCenter: parent.horizontalCenter width: durationLabel.contentWidth + 4 height: durationLabel.contentHeight color: activePalette.highlight Label { id: durationLabel anchors.fill: parent horizontalAlignment: Text.AlignHCenter text: timeline.timecode(timeline.zoneOut - timeline.zoneIn) font: miniFont color: activePalette.highlightedText } } Rectangle { id: trimIn anchors.left: parent.left anchors.leftMargin: 0 height: parent.height width: 5 color: 'lawngreen' opacity: 0 Drag.active: trimInMouseArea.drag.active Drag.proposedAction: Qt.MoveAction MouseArea { id: trimInMouseArea anchors.fill: parent hoverEnabled: true drag.target: parent drag.axis: Drag.XAxis drag.smoothed: false + property var startZone onEntered: { resizeActive = true parent.opacity = 1 } onExited: { resizeActive = false parent.opacity = 0 } onPressed: { parent.anchors.left = undefined parent.opacity = 1 + startZone = Qt.point(timeline.zoneIn, timeline.zoneOut) } onReleased: { resizeActive = false parent.anchors.left = zone.left + timeline.updateZone(startZone, Qt.point(timeline.zoneIn, timeline.zoneOut), true) } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { resizeActive = true var newPos = controller.suggestSnapPoint(timeline.zoneIn + Math.round(trimIn.x / timeline.scaleFactor), root.snapping) if (newPos < 0) { newPos = 0 } timeline.zoneIn = timeline.zoneOut > -1 ? Math.min(newPos, timeline.zoneOut - 1) : newPos } } } } Rectangle { id: trimOut anchors.right: parent.right anchors.rightMargin: 0 height: parent.height width: 5 color: 'darkred' opacity: 0 Drag.active: trimOutMouseArea.drag.active Drag.proposedAction: Qt.MoveAction MouseArea { id: trimOutMouseArea anchors.fill: parent hoverEnabled: true drag.target: parent drag.axis: Drag.XAxis drag.smoothed: false + property var startZone onEntered: { resizeActive = true parent.opacity = 1 } onExited: { resizeActive = false parent.opacity = 0 } onPressed: { parent.anchors.right = undefined parent.opacity = 1 + startZone = Qt.point(timeline.zoneIn, timeline.zoneOut) } onReleased: { resizeActive = false parent.anchors.right = zone.right + timeline.updateZone(startZone, Qt.point(timeline.zoneIn, timeline.zoneOut), true) } onPositionChanged: { if (mouse.buttons === Qt.LeftButton) { resizeActive = true timeline.zoneOut = Math.max(controller.suggestSnapPoint(timeline.zoneIn + Math.round((trimOut.x + trimOut.width) / timeline.scaleFactor), root.snapping), timeline.zoneIn + 1) } } } } } } diff --git a/src/timeline2/view/timelinecontroller.cpp b/src/timeline2/view/timelinecontroller.cpp index a2587125e..3e2760373 100644 --- a/src/timeline2/view/timelinecontroller.cpp +++ b/src/timeline2/view/timelinecontroller.cpp @@ -1,3136 +1,3173 @@ /*************************************************************************** * 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(0) , 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, QList audioTargets) { int videoTrack = -1; QList audioTracks; m_hasVideoTarget = hasVideo; m_hasAudioTarget = !audioTargets.isEmpty(); if (m_hasVideoTarget) { videoTrack = m_model->getFirstVideoTrackIndex(); } if (m_hasAudioTarget) { QList tracks; auto it = m_model->m_allTracks.cbegin(); while (it != m_model->m_allTracks.cend()) { if ((*it)->isAudioTrack()) { tracks << (*it)->getId(); } ++it; } int i = 0; while (i < audioTargets.size() && !tracks.isEmpty()) { audioTracks << tracks.takeLast(); i++; } } emit hasAudioTargetChanged(); emit hasVideoTargetChanged(); if (m_videoTargetActive) { setVideoTarget(m_hasVideoTarget && (m_lastVideoTarget > -1) ? m_lastVideoTarget : videoTrack); } if (m_audioTargetActive) { setIntAudioTarget((m_hasAudioTarget && (m_lastAudioTarget.size() == audioTargets.size())) ? m_lastAudioTarget : 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) { int newTid; bool audioRecTrack = d->addRecTrack(); bool addAVTrack = d->addAVTrack(); Fun undo = []() { return true; }; Fun redo = []() { return true; }; bool 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")); } } if (result) { pCore->pushUndo(undo, redo, addAVTrack ? 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 == selectedTrackIx) { setAudioTarget(-1); } if (m_model->m_videoTarget == selectedTrackIx) { setVideoTarget(-1); } if (m_lastAudioTarget.contains(selectedTrackIx)) { m_lastAudioTarget.removeAll(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() { - std::vector snaps = pCore->projectManager()->current()->getGuideModel()->getSnapPoints(); - snaps.push_back(m_zone.x()); - snaps.push_back(m_zone.y() - 1); - int nextSnap = m_model->getNextSnapPos(pCore->getTimelinePosition(), snaps); + 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) { - std::vector snaps = pCore->projectManager()->current()->getGuideModel()->getSnapPoints(); - snaps.push_back(m_zone.x()); - snaps.push_back(m_zone.y() - 1); - setPosition(m_model->getPreviousSnapPos(pCore->getTimelinePosition(), snaps)); + 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 = m_root->property("mainItemId").toInt(); } 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 = m_root->property("mainItemId").toInt(); } 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()); } void TimelineController::addQuickMarker(int cid, int position) { if (cid == -1) { cid = m_root->property("mainItemId").toInt(); } 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 = m_root->property("mainItemId").toInt(); } 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 = m_root->property("mainItemId").toInt(); } 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::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(int track) { if ((track > -1 && !m_model->isTrack(track)) || !m_hasAudioTarget) { return; } m_model->m_audioTarget = track; emit audioTargetChanged(); } void TimelineController::setIntAudioTarget(QList tracks) { if ((!tracks.isEmpty() && !m_model->isTrack(tracks.first())) || !m_hasAudioTarget) { return; } qDebug()<<"/// GOT AUDIO TRACKS: "<m_audioTarget = tracks.isEmpty() ? -1 : tracks.first(); emit audioTargetChanged(); } 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) +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); } - m_zone = zone; - emit zoneChanged(); + updateZone(m_zone, zone, withUndo); +} + +void TimelineController::updateZone(const QPoint oldZone, const QPoint newZone, bool withUndo) +{ + if (!withUndo) { + m_zone = newZone; + emit zoneChanged(); + return; + } + std::function undo = []() { return true; }; + std::function redo = []() { return true; }; + Fun undo_zone = [this, oldZone]() { + m_zone = oldZone; + emit zoneChanged(); + emit zoneMoved(oldZone); + return true; + }; + Fun redo_zone = [this, newZone]() { + m_zone = newZone; + emit zoneChanged(); + emit zoneMoved(newZone); + return true; + }; + redo_zone(); + UPDATE_UNDO_REDO_NOLOCK(redo_zone, undo_zone, undo, redo); + pCore->pushUndo(undo, redo, i18n("Set Zone In")); } 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(); 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) { 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, true); 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); } } 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(); for (int cid : selection) { int start = m_model->getItemPosition(cid); if (seekToEnd) { start += m_model->getItemPlaytime(cid); } setPosition(start); break; } } 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(); } } QPoint TimelineController::getTracksCount() const { QVariant returnedValue; QMetaObject::invokeMethod(m_root, "getTracksCount", Q_RETURN_ARG(QVariant, returnedValue)); QVariantList tracks = returnedValue.toList(); QPoint p(tracks.at(0).toInt(), tracks.at(1).toInt()); return p; } 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; QStringList chunksList = chunks.split(QLatin1Char(','), QString::SkipEmptyParts); QStringList dirtyList = dirty.split(QLatin1Char(','), QString::SkipEmptyParts); 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 == -1 ? -1 : m_model->getTrackPosition(m_model->m_audioTarget); 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 = m_root->property("mainItemId").toInt(); } 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) { clipId = m_root->property("mainItemId").toInt(); } 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 = m_root->property("mainItemId").toInt(); } 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); } int aTrack = -1; int vTrack = -1; std::shared_ptr clip = pCore->bin()->getBinClip(bid); if (out <= in) { out = (int)clip->frameDuration() - 1; } if (dropType == PlaylistState::VideoOnly) { vTrack = tid; } else if (dropType == PlaylistState::AudioOnly) { aTrack = tid; } else { if (m_model->getTrackById_const(tid)->isAudioTrack()) { aTrack = tid; vTrack = clip->hasAudioAndVideo() ? m_model->getMirrorVideoTrackId(aTrack) : -1; } else { vTrack = tid; aTrack = clip->hasAudioAndVideo() ? m_model->getMirrorAudioTrackId(vTrack) : -1; } } QList target_tracks; if (vTrack > -1) { target_tracks << vTrack; } if (aTrack > -1) { target_tracks << aTrack; } std::function 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()) { aTrack = audioTarget(); } 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 = m_root->property("mainItemId").toInt(); } 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 = m_root->property("mainItemId").toInt(); } 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, audioTarget()); } } 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, audioTarget()); } void TimelineController::splitVideo(int clipId) { TimelineFunctions::requestSplitVideo(m_model, clipId, videoTarget()); } void TimelineController::setAudioRef(int clipId) { if (clipId == -1) { clipId = m_root->property("mainItemId").toInt(); } 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 = m_root->property("mainItemId").toInt(); } 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); if (m_model->m_groups->isInGroup(clipId)) { std::unordered_set groupIds = m_model->getGroupElements(clipId); // Check that no item is grouped with our audioRef item // TODO m_model->requestClearSelection(); } const QString otherBinId = getClipBinId(clipId); if (otherBinId == masterBinClipId) { // easy, same clip. int newPos = m_model->getClipPosition(m_audioRef) - m_model->getClipIn(m_audioRef) + m_model->getClipIn(clipId); if (newPos) { bool result = m_model->requestClipMove(clipId, m_model->getClipTrackId(clipId), newPos, true, true, true); if (!result) { pCore->displayMessage(i18n("Cannot move clip to frame %1.", newPos), InformationMessage, 500); } return; } } // Perform audio calculation AudioEnvelope *envelope = new AudioEnvelope(otherBinId, clipId, (size_t)m_model->getClipIn(clipId), (size_t)m_model->getClipPlaytime(clipId), (size_t)m_model->getClipPosition(clipId)); m_audioCorrelator->addChild(envelope); } 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) { setAudioTarget(audioTarget() == m_activeTrack ? -1 : m_activeTrack); } else { setVideoTarget(videoTarget() == m_activeTrack ? -1 : m_activeTrack); } } int TimelineController::audioTarget() const { return m_model->m_audioTarget; } int TimelineController::videoTarget() const { return m_model->m_videoTarget; } bool TimelineController::hasAudioTarget() const { return m_hasAudioTarget; } 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)); } } bool result = true; for (int target : targetIds) { std::shared_ptr destStack = m_model->getClipEffectStackModel(target); result = result && destStack->fromXml(effects, undo, redo); if (!result) { break; } } if (result) { 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(); //getMainSelectedItem(false, true); } 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')) { 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')) { 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 duration = m_model->getClipPlaytime(clipId); int currentTrack = m_model->m_allClips[clipId]->getCurrentTrackId(); bool res = true; if (currentTrack > -1) { res = 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) { int startClipId = m_model->getClipByPosition(trackId, position); if (startClipId > -1) { // There is a clip, cut res = res && TimelineFunctions::requestClipCut(m_model, startClipId, position, undo, redo); } res = res && TimelineFunctions::requestInsertSpace(m_model, QPoint(position, position + duration), undo, redo); } 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); } 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) { QList 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); } 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) { TimelineFunctions::enableMultitrackView(m_model, enable, refresh); } 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() > -1) { m_lastAudioTarget = {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; std::function undo = []() { return true; }; std::function redo = []() { return true; }; 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; int processed = 0; for (int id : items_list) { if (result && m_model->isClip(id)) { std::shared_ptr clip = m_model->getClipPtr(id); if (clip->clipType() == ClipType::Playlist) { int pos = clip->getPosition(); 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); } } 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); processed++; } } } if (result && processed > 0) { pCore->pushUndo(undo, redo, i18n("Expand clip")); } else { undo(); pCore->displayMessage(i18n("Could not expand clip"), InformationMessage, 500); } } diff --git a/src/timeline2/view/timelinecontroller.h b/src/timeline2/view/timelinecontroller.h index 8c83ca574..13e57b0e9 100644 --- a/src/timeline2/view/timelinecontroller.h +++ b/src/timeline2/view/timelinecontroller.h @@ -1,633 +1,637 @@ /*************************************************************************** * 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 . * ***************************************************************************/ #ifndef TIMELINECONTROLLER_H #define TIMELINECONTROLLER_H #include "definitions.h" #include "lib/audio/audioCorrelation.h" #include "timeline2/model/timelineitemmodel.hpp" #include #include class PreviewManager; class QAction; class QQuickItem; // see https://bugreports.qt.io/browse/QTBUG-57714, don't expose a QWidget as a context property class TimelineController : public QObject { Q_OBJECT /* @brief holds a list of currently selected clips (list of clipId's) */ Q_PROPERTY(QList selection READ selection NOTIFY selectionChanged) /* @brief holds the timeline zoom factor */ Q_PROPERTY(double scaleFactor READ scaleFactor WRITE setScaleFactor NOTIFY scaleFactorChanged) /* @brief holds the current project duration */ Q_PROPERTY(int duration READ duration NOTIFY durationChanged) Q_PROPERTY(int fullDuration READ fullDuration NOTIFY durationChanged) Q_PROPERTY(bool audioThumbFormat READ audioThumbFormat NOTIFY audioThumbFormatChanged) Q_PROPERTY(int zoneIn READ zoneIn WRITE setZoneIn NOTIFY zoneChanged) Q_PROPERTY(int zoneOut READ zoneOut WRITE setZoneOut NOTIFY zoneChanged) Q_PROPERTY(bool ripple READ ripple NOTIFY rippleChanged) Q_PROPERTY(bool scrub READ scrub NOTIFY scrubChanged) Q_PROPERTY(bool snap READ snap NOTIFY snapChanged) Q_PROPERTY(bool showThumbnails READ showThumbnails NOTIFY showThumbnailsChanged) Q_PROPERTY(bool showMarkers READ showMarkers NOTIFY showMarkersChanged) Q_PROPERTY(bool showAudioThumbnails READ showAudioThumbnails NOTIFY showAudioThumbnailsChanged) Q_PROPERTY(QVariantList dirtyChunks READ dirtyChunks NOTIFY dirtyChunksChanged) Q_PROPERTY(QVariantList renderedChunks READ renderedChunks NOTIFY renderedChunksChanged) Q_PROPERTY(int workingPreview READ workingPreview NOTIFY workingPreviewChanged) Q_PROPERTY(bool useRuler READ useRuler NOTIFY useRulerChanged) Q_PROPERTY(int activeTrack READ activeTrack WRITE setActiveTrack NOTIFY activeTrackChanged) Q_PROPERTY(int audioTarget READ audioTarget WRITE setAudioTarget NOTIFY audioTargetChanged) Q_PROPERTY(int videoTarget READ videoTarget WRITE setVideoTarget NOTIFY videoTargetChanged) //Q_PROPERTY(int lastAudioTarget MEMBER m_lastAudioTarget NOTIFY lastAudioTargetChanged) Q_PROPERTY(int lastVideoTarget MEMBER m_lastVideoTarget NOTIFY lastVideoTargetChanged) Q_PROPERTY(bool hasAudioTarget READ hasAudioTarget NOTIFY hasAudioTargetChanged) Q_PROPERTY(bool hasVideoTarget READ hasVideoTarget NOTIFY hasVideoTargetChanged) Q_PROPERTY(bool autoScroll READ autoScroll NOTIFY autoScrollChanged) Q_PROPERTY(QColor videoColor READ videoColor NOTIFY colorsChanged) Q_PROPERTY(QColor audioColor READ audioColor NOTIFY colorsChanged) Q_PROPERTY(QColor titleColor READ titleColor NOTIFY colorsChanged) Q_PROPERTY(QColor imageColor READ imageColor NOTIFY colorsChanged) Q_PROPERTY(QColor slideshowColor READ slideshowColor NOTIFY colorsChanged) Q_PROPERTY(QColor targetColor READ targetColor NOTIFY colorsChanged) Q_PROPERTY(QColor targetTextColor READ targetTextColor NOTIFY colorsChanged) Q_PROPERTY(QColor lockedColor READ lockedColor NOTIFY colorsChanged) Q_PROPERTY(QColor selectionColor READ selectionColor NOTIFY colorsChanged) Q_PROPERTY(QColor groupColor READ groupColor NOTIFY colorsChanged) public: TimelineController(QObject *parent); ~TimelineController() override; /** @brief Sets the model that this widgets displays */ void setModel(std::shared_ptr model); std::shared_ptr getModel() const; void setRoot(QQuickItem *root); /** @brief Edit an item's in/out points with a dialog */ Q_INVOKABLE void editItemDuration(int itemId = -1); /** @brief Returns the topmost track containing a selected item (-1 if selection is embty) */ Q_INVOKABLE int selectedTrack() const; /** @brief Select the clip in active track under cursor @param type is the type of the object (clip or composition) @param select: true if the object should be selected and false if it should be deselected @param addToCurrent: if true, the object will be added to the new selection */ void selectCurrentItem(ObjectType type, bool select, bool addToCurrent = false); /** @brief Select all timeline items */ void selectAll(); /* @brief Select all items in one track */ void selectCurrentTrack(); /** @brief Select multiple objects on the timeline @param tracks List of ids of tracks from which to select @param start/endFrame Interval from which to select the items @param addToSelect if true, the old selection is retained */ Q_INVOKABLE void selectItems(const QVariantList &tracks, int startFrame, int endFrame, bool addToSelect); /** @brief request a selection with a list of ids*/ Q_INVOKABLE void selectItems(const QList &ids); /* @brief Returns true is item is selected as well as other items */ Q_INVOKABLE bool isInSelection(int itemId); /* @brief Show/hide audio record controls on a track */ Q_INVOKABLE void switchRecording(int trackId); /* @brief Add recorded file to timeline */ void finishRecording(const QString &recordedFile); /* @brief Open Kdenlive's config diablog on a defined page and tab */ Q_INVOKABLE void showConfig(int page, int tab); /* @brief Returns true if we have at least one active track */ Q_INVOKABLE bool hasActiveTracks() const; /* @brief returns current timeline's zoom factor */ Q_INVOKABLE double scaleFactor() const; /* @brief set current timeline's zoom factor */ void setScaleFactorOnMouse(double scale, bool zoomOnMouse); void setScaleFactor(double scale); /* @brief Returns the project's duration (tractor) */ Q_INVOKABLE int duration() const; Q_INVOKABLE int fullDuration() const; /* @brief Returns the current cursor position (frame currently displayed by MLT) */ /* @brief Returns the seek request position (-1 = no seek pending) */ Q_INVOKABLE int audioTarget() const; Q_INVOKABLE int videoTarget() const; Q_INVOKABLE bool hasAudioTarget() const; Q_INVOKABLE bool hasVideoTarget() const; Q_INVOKABLE bool autoScroll() const; Q_INVOKABLE int activeTrack() const { return m_activeTrack; } Q_INVOKABLE QColor videoColor() const; Q_INVOKABLE QColor audioColor() const; Q_INVOKABLE QColor titleColor() const; Q_INVOKABLE QColor imageColor() const; Q_INVOKABLE QColor slideshowColor() const; Q_INVOKABLE QColor targetColor() const; Q_INVOKABLE QColor targetTextColor() const; Q_INVOKABLE QColor lockedColor() const; Q_INVOKABLE QColor selectionColor() const; Q_INVOKABLE QColor groupColor() const; /* @brief Request a seek operation @param position is the desired new timeline position */ - Q_INVOKABLE int zoneIn() const { return m_zone.x(); } - Q_INVOKABLE int zoneOut() const { return m_zone.y(); } - Q_INVOKABLE void setZoneIn(int inPoint); - Q_INVOKABLE void setZoneOut(int outPoint); - void setZone(const QPoint &zone); + int zoneIn() const { return m_zone.x(); } + int zoneOut() const { return m_zone.y(); } + void setZoneIn(int inPoint); + void setZoneOut(int outPoint); + void setZone(const QPoint &zone, bool withUndo = true); /* @brief Request a seek operation @param position is the desired new timeline position */ Q_INVOKABLE void setPosition(int position); Q_INVOKABLE bool snap(); Q_INVOKABLE bool ripple(); Q_INVOKABLE bool scrub(); Q_INVOKABLE QString timecode(int frames) const; QString framesToClock(int frames) const; Q_INVOKABLE QString simplifiedTC(int frames) const; /* @brief Request inserting a new clip in timeline (dragged from bin or monitor) @param tid is the destination track @param position is the timeline position @param xml is the data describing the dropped clip @param logUndo if set to false, no undo object is stored @return the id of the inserted clip */ Q_INVOKABLE int insertClip(int tid, int position, const QString &xml, bool logUndo, bool refreshView, bool useTargets); /* @brief Request inserting multiple clips into the timeline (dragged from bin or monitor) * @param tid is the destination track * @param position is the timeline position * @param binIds the IDs of the bins being dropped * @param logUndo if set to false, no undo object is stored * @return the ids of the inserted clips */ Q_INVOKABLE QList insertClips(int tid, int position, const QStringList &binIds, bool logUndo, bool refreshView); Q_INVOKABLE void copyItem(); Q_INVOKABLE bool pasteItem(int position = -1, int tid = -1); /* @brief Request inserting a new composition in timeline (dragged from compositions list) @param tid is the destination track @param position is the timeline position @param transitionId is the data describing the dropped composition @param logUndo if set to false, no undo object is stored @return the id of the inserted composition */ Q_INVOKABLE int insertComposition(int tid, int position, const QString &transitionId, bool logUndo); /* @brief Request inserting a new composition in timeline (dragged from compositions list) this function will check if there is a clip at insert point and adjust the composition length accordingly @param tid is the destination track @param position is the timeline position @param transitionId is the data describing the dropped composition @param logUndo if set to false, no undo object is stored @return the id of the inserted composition */ Q_INVOKABLE int insertNewComposition(int tid, int position, const QString &transitionId, bool logUndo); Q_INVOKABLE int insertNewComposition(int tid, int clipId, int offset, const QString &transitionId, bool logUndo); /* @brief Request deletion of the currently selected clips */ Q_INVOKABLE void deleteSelectedClips(); Q_INVOKABLE void triggerAction(const QString &name); /* @brief Returns id of the timeline selcted clip if there is only 1 clip selected * or an AVSplit group. If allowComposition is true, returns composition id if * only 1 is selected, otherwise returns -1. If restrictToCurrentPos is true, it will * only return the id if timeline cursor is inside item */ int getMainSelectedItem(bool restrictToCurrentPos = true, bool allowComposition = false); /* @brief Do we want to display video thumbnails */ bool showThumbnails() const; bool showAudioThumbnails() const; bool showMarkers() const; bool audioThumbFormat() const; /* @brief Do we want to display audio thumbnails */ Q_INVOKABLE bool showWaveforms() const; /* @brief Insert a timeline track */ Q_INVOKABLE void addTrack(int tid); /* @brief Remove a timeline track */ Q_INVOKABLE void deleteTrack(int tid); /* @brief Show / hide audio rec controls in active track */ void switchTrackRecord(int tid = -1); /* @brief Group selected items in timeline */ Q_INVOKABLE void groupSelection(); /* @brief Ungroup selected items in timeline */ Q_INVOKABLE void unGroupSelection(int cid = -1); /* @brief Ask for edit marker dialog */ Q_INVOKABLE void editMarker(int cid = -1, int position = -1); /* @brief Ask for marker add dialog */ Q_INVOKABLE void addMarker(int cid = -1, int position = -1); /* @brief Ask for quick marker add (without dialog) */ Q_INVOKABLE void addQuickMarker(int cid = -1, int position = -1); /* @brief Ask for marker delete */ Q_INVOKABLE void deleteMarker(int cid = -1, int position = -1); /* @brief Ask for all markers delete */ Q_INVOKABLE void deleteAllMarkers(int cid = -1); /* @brief Ask for edit timeline guide dialog */ Q_INVOKABLE void editGuide(int frame = -1); Q_INVOKABLE void moveGuide(int frame, int newFrame); /* @brief Add a timeline guide */ Q_INVOKABLE void switchGuide(int frame = -1, bool deleteOnly = false); /* @brief Request monitor refresh */ Q_INVOKABLE void requestRefresh(); /* @brief Show the asset of the given item in the AssetPanel If the id corresponds to a clip, we show the corresponding effect stack If the id corresponds to a composition, we show its properties */ Q_INVOKABLE void showAsset(int id); Q_INVOKABLE void showTrackAsset(int trackId); /* @brief Adjust height of all similar (audio or video) tracks */ Q_INVOKABLE void adjustAllTrackHeight(int trackId, int height); /** @brief Reset track @trackId height to default track height. Adjusts all tracks if @trackId == -1 */ Q_INVOKABLE void defaultTrackHeight(int trackId); Q_INVOKABLE bool exists(int itemId); Q_INVOKABLE int headerWidth() const; Q_INVOKABLE void setHeaderWidth(int width); /* @brief Seek to next snap point */ void gotoNextSnap(); /* @brief Seek to previous snap point */ void gotoPreviousSnap(); /* @brief Seek to previous guide */ void gotoPreviousGuide(); /* @brief Seek to next guide */ void gotoNextGuide(); /* @brief Set current item's start point to cursor position */ void setInPoint(); /* @brief Set current item's end point to cursor position */ void setOutPoint(); /* @brief Return the project's tractor */ Mlt::Tractor *tractor(); /* @brief Return a track's producer */ Mlt::Producer trackProducer(int tid); /* @brief Get the list of currently selected clip id's */ QList selection() const; /* @brief Add an asset (effect, composition) */ void addAsset(const QVariantMap &data); /* @brief Cuts the clip on current track at timeline position */ Q_INVOKABLE void cutClipUnderCursor(int position = -1, int track = -1); /* @brief Request a spacer operation */ Q_INVOKABLE int requestSpacerStartOperation(int trackId, int position); /* @brief Request a spacer operation */ Q_INVOKABLE bool requestSpacerEndOperation(int clipId, int startPosition, int endPosition); /* @brief Request a Fade in effect for clip */ Q_INVOKABLE void adjustFade(int cid, const QString &effectId, int duration, int initialDuration); Q_INVOKABLE const QString getTrackNameFromMltIndex(int trackPos); /* @brief Request inserting space in a track */ Q_INVOKABLE void insertSpace(int trackId = -1, int frame = -1); Q_INVOKABLE void removeSpace(int trackId = -1, int frame = -1, bool affectAllTracks = false); /* @brief If clip is enabled, disable, otherwise enable */ Q_INVOKABLE void switchEnableState(std::unordered_set selection = {}); Q_INVOKABLE void addCompositionToClip(const QString &assetId, int clipId = -1, int offset = -1); Q_INVOKABLE void addEffectToClip(const QString &assetId, int clipId = -1); Q_INVOKABLE void requestClipCut(int clipId, int position); /** @brief Extract (delete + remove space) current clip */ void extract(int clipId = -1); /** @brief Save current clip cut as bin subclip */ void saveZone(int clipId = -1); Q_INVOKABLE void splitAudio(int clipId); Q_INVOKABLE void splitVideo(int clipId); Q_INVOKABLE void setAudioRef(int clipId = -1); Q_INVOKABLE void alignAudio(int clipId = -1); Q_INVOKABLE void urlDropped(QStringList droppedFile, int frame, int tid); Q_INVOKABLE bool endFakeMove(int clipId, int position, bool updateView, bool logUndo, bool invalidateTimeline); Q_INVOKABLE int getItemMovingTrack(int itemId) const; bool endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool logUndo); bool endFakeGroupMove(int clipId, int groupId, int delta_track, int delta_pos, bool updateView, bool finalMove, Fun &undo, Fun &redo); bool splitAV(); /* @brief Seeks to selected clip start / end */ Q_INVOKABLE void pasteEffects(int targetId = -1); Q_INVOKABLE double fps() const; Q_INVOKABLE void addEffectKeyframe(int cid, int frame, double val); Q_INVOKABLE void removeEffectKeyframe(int cid, int frame); Q_INVOKABLE void updateEffectKeyframe(int cid, int oldFrame, int newFrame, const QVariant &normalizedValue = QVariant()); /** @brief Make current timeline track active/inactive*/ Q_INVOKABLE void switchTrackActive(int trackId = -1); /** @brief Toogle the active/inactive state of all tracks*/ void switchAllTrackActive(); /** @brief Make all tracks active or inactive */ void makeAllTrackActive(); void switchTrackLock(bool applyToAll = false); void switchTargetTrack(); const QString getTrackNameFromIndex(int trackIndex); /* @brief Seeks to selected clip start / end */ void seekCurrentClip(bool seekToEnd = false); /* @brief Seeks to a clip start (or end) based on it's clip id */ void seekToClip(int cid, bool seekToEnd); /* @brief Returns true if timeline cursor is inside the item */ bool positionIsInItem(int id); /* @brief Returns the number of tracks (audioTrakcs, videoTracks) */ QPoint getTracksCount() const; /* @brief Request monitor refresh if item (clip or composition) is under timeline cursor */ void refreshItem(int id); /* @brief Seek timeline to mouse position */ void seekToMouse(); /* @brief Set a property on the active track */ void setActiveTrackProperty(const QString &name, const QString &value); /* @brief Get a property on the active track */ const QVariant getActiveTrackProperty(const QString &name) const; /* @brief Is the active track audio */ bool isActiveTrackAudio() const; /* @brief Returns a list of all luma files used in the project */ QStringList extractCompositionLumas() const; /* @brief Get the frame where mouse is positioned */ int getMousePos(); /* @brief Get the frame where mouse is positioned */ int getMouseTrack(); /* @brief Returns a map of track ids/track names */ QMap getTrackNames(bool videoOnly); /* @brief Returns the transition a track index for a composition (MLT index / Track id) */ QPair getCompositionATrack(int cid) const; void setCompositionATrack(int cid, int aTrack); /* @brief Return true if composition's a_track is automatic (no forced track) */ bool compositionAutoTrack(int cid) const; const QString getClipBinId(int clipId) const; void focusItem(int itemId); /* @brief Create and display a split clip view to compare effect */ bool createSplitOverlay(int clipId, std::shared_ptr filter); /* @brief Delete the split clip view to compare effect */ void removeSplitOverlay(); /* @brief Add current timeline zone to preview rendering */ void addPreviewRange(bool add); /* @brief Clear current timeline zone from preview rendering */ void clearPreviewRange(bool resetZones); void startPreviewRender(); void stopPreviewRender(); QVariantList dirtyChunks() const; QVariantList renderedChunks() const; /* @brief returns the frame currently processed by timeline preview, -1 if none */ int workingPreview() const; /** @brief Return true if we want to use timeline ruler zone for editing */ bool useRuler() const; /* @brief Load timeline preview from saved doc */ void loadPreview(const QString &chunks, const QString &dirty, const QDateTime &documentDate, int enable); /* @brief Return document properties with added settings from timeline */ QMap documentProperties(); /** @brief Change track compsiting mode */ void switchCompositing(int mode); /** @brief Change a clip item's speed in timeline */ Q_INVOKABLE void changeItemSpeed(int clipId, double speed); /** @brief Delete selected zone and fill gap by moving following clips * @param lift if true, the zone will simply be deleted but clips won't be moved */ void extractZone(QPoint zone, bool liftOnly = false); /** @brief Insert clip monitor into timeline * @returns the zone end position or -1 on fail */ Q_INVOKABLE bool insertClipZone(const QString &binId, int tid, int pos); int insertZone(const QString &binId, QPoint zone, bool overwrite); void updateClip(int clipId, const QVector &roles); void showClipKeyframes(int clipId, bool value); void showCompositionKeyframes(int clipId, bool value); /** @brief Adjust all timeline tracks height */ void resetTrackHeight(); /** @brief timeline preview params changed, reset */ void resetPreview(); /** @brief Set target tracks (video, audio) */ void setTargetTracks(bool hasVideo, QList audioTargets); /** @brief Return asset's display name from it's id (effect or composition) */ Q_INVOKABLE const QString getAssetName(const QString &assetId, bool isTransition); /** @brief Set keyboard grabbing on current selection */ Q_INVOKABLE void grabCurrent(); /** @brief Returns keys for all used thumbnails */ QStringList getThumbKeys(); /** @brief Returns true if a drag operation is currently running in timeline */ bool dragOperationRunning(); /** @brief Disconnect some stuff before closing project */ void prepareClose(); /** @brief Check that we don't keep a deleted track id */ void checkTrackDeletion(int selectedTrackIx); /** @brief Return true if an overlay track is used */ bool hasPreviewTrack() const; void updatePreviewConnection(bool enable); /** @brief Display project master effects */ Q_INVOKABLE void showMasterEffects(); /** @brief Return true if an instance of this bin clip is currently undet timeline cursor */ bool refreshIfVisible(int cid); /** @brief Collapse / expand active track */ void collapseActiveTrack(); /** @brief Expand MLT playlist to its contained clips/compositions */ void expandActiveClip(); public slots: void resetView(); Q_INVOKABLE void setAudioTarget(int track); void setIntAudioTarget(QList tracks); Q_INVOKABLE void setVideoTarget(int track); Q_INVOKABLE void setActiveTrack(int track); void addEffectToCurrentClip(const QStringList &effectData); /** @brief Dis / enable timeline preview. */ void disablePreview(bool disable); void invalidateItem(int cid); void invalidateTrack(int tid); void invalidateZone(int in, int out); void checkDuration(); /** @brief Dis / enable multi track view. */ void slotMultitrackView(bool enable, bool refresh = true); /** @brief Save timeline selected clips to target folder. */ void saveTimelineSelection(const QDir &targetDir); /** @brief Restore timeline scroll pos on open. */ void setScrollPos(int pos); + /** @brief change zone info with undo. */ + Q_INVOKABLE void updateZone(const QPoint oldZone, const QPoint newZone, bool withUndo = true); private slots: void updateClipActions(); void updateVideoTarget(); void updateAudioTarget(); public: /** @brief a list of actions that have to be enabled/disabled depending on the timeline selection */ QList clipActions; private: QQuickItem *m_root; KActionCollection *m_actionCollection; std::shared_ptr m_model; bool m_usePreview; int m_audioTarget; int m_videoTarget; int m_activeTrack; int m_audioRef; bool m_hasAudioTarget {false}; bool m_hasVideoTarget {false}; int m_lastVideoTarget {-1}; QList m_lastAudioTarget; bool m_videoTargetActive {true}; bool m_audioTargetActive {true}; QPair m_recordStart; int m_recordTrack; QPoint m_zone; double m_scale; static int m_duration; PreviewManager *m_timelinePreview; QAction *m_disablePreview; std::shared_ptr m_audioCorrelator; QMutex m_metaMutex; bool m_ready; + std::vector m_activeSnaps; + int m_snapStackIndex; void initializePreview(); bool darkBackground() const; int getMenuOrTimelinePos() const; signals: void selected(Mlt::Producer *producer); void selectionChanged(); void frameFormatChanged(); void trackHeightChanged(); void scaleFactorChanged(); void audioThumbFormatChanged(); void durationChanged(); void audioTargetChanged(); void videoTargetChanged(); void hasAudioTargetChanged(); void hasVideoTargetChanged(); void lastAudioTargetChanged(); void autoScrollChanged(); void lastVideoTargetChanged(); void activeTrackChanged(); void colorsChanged(); void showThumbnailsChanged(); void showAudioThumbnailsChanged(); void showMarkersChanged(); void rippleChanged(); void scrubChanged(); void seeked(int position); void zoneChanged(); void zoneMoved(const QPoint &zone); /* @brief Requests that a given parameter model is displayed in the asset panel */ void showTransitionModel(int tid, std::shared_ptr); void showItemEffectStack(const QString &clipName, std::shared_ptr, QSize frameSize, bool showKeyframes); /* @brief notify of chunks change */ void dirtyChunksChanged(); void renderedChunksChanged(); void workingPreviewChanged(); void useRulerChanged(); void updateZoom(double); /* @brief emitted when timeline selection changes, true if a clip is selected */ void timelineClipSelected(bool); /* @brief User enabled / disabled snapping, update timeline behavior */ void snapChanged(); Q_INVOKABLE void ungrabHack(); }; #endif diff --git a/src/timeline2/view/timelinetabs.cpp b/src/timeline2/view/timelinetabs.cpp index 9a7272948..f84ca6b92 100644 --- a/src/timeline2/view/timelinetabs.cpp +++ b/src/timeline2/view/timelinetabs.cpp @@ -1,100 +1,101 @@ /*************************************************************************** * Copyright (C) 2017 by Nicolas Carion * * 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 "timelinetabs.hpp" #include "assets/model/assetparametermodel.hpp" #include "core.h" #include "mainwindow.h" #include "monitor/monitor.h" #include "monitor/monitormanager.h" #include "project/projectmanager.h" #include "timelinecontroller.h" #include "timelinewidget.h" TimelineContainer::TimelineContainer(QWidget *parent) :QWidget(parent) { } QSize TimelineContainer::sizeHint() const { return QSize(800, pCore->window()->height() / 2); } TimelineTabs::TimelineTabs(QWidget *parent) : QTabWidget(parent) , m_mainTimeline(new TimelineWidget(this)) { setTabBarAutoHide(true); setTabsClosable(true); addTab(m_mainTimeline, i18n("Main timeline")); connectTimeline(m_mainTimeline); // Resize to 0 the size of the close button of the main timeline, so that the user cannot close it. if (tabBar()->tabButton(0, QTabBar::RightSide) != nullptr) { tabBar()->tabButton(0, QTabBar::RightSide)->resize(0, 0); } connect(pCore->monitorManager()->projectMonitor(), &Monitor::zoneUpdated, m_mainTimeline, &TimelineWidget::zoneUpdated); + connect(pCore->monitorManager()->projectMonitor(), &Monitor::zoneUpdatedWithUndo, m_mainTimeline, &TimelineWidget::zoneUpdatedWithUndo); connect(m_mainTimeline, &TimelineWidget::zoneMoved, pCore->monitorManager()->projectMonitor(), &Monitor::slotLoadClipZone); connect(pCore->monitorManager()->projectMonitor(), &Monitor::addEffect, m_mainTimeline->controller(), &TimelineController::addEffectToCurrentClip); } TimelineTabs::~TimelineTabs() { // clear source m_mainTimeline->setSource(QUrl()); } TimelineWidget *TimelineTabs::getMainTimeline() const { return m_mainTimeline; } TimelineWidget *TimelineTabs::getCurrentTimeline() const { return static_cast(currentWidget()); } void TimelineTabs::connectTimeline(TimelineWidget *timeline) { connect(timeline, &TimelineWidget::focusProjectMonitor, pCore->monitorManager(), &MonitorManager::focusProjectMonitor); connect(this, &TimelineTabs::audioThumbFormatChanged, timeline->controller(), &TimelineController::audioThumbFormatChanged); connect(this, &TimelineTabs::showThumbnailsChanged, timeline->controller(), &TimelineController::showThumbnailsChanged); connect(this, &TimelineTabs::showAudioThumbnailsChanged, timeline->controller(), &TimelineController::showAudioThumbnailsChanged); connect(this, &TimelineTabs::changeZoom, timeline, &TimelineWidget::slotChangeZoom); connect(this, &TimelineTabs::fitZoom, timeline, &TimelineWidget::slotFitZoom); connect(timeline->controller(), &TimelineController::showTransitionModel, this, &TimelineTabs::showTransitionModel); connect(timeline->controller(), &TimelineController::updateZoom, [&](double value) { emit updateZoom(getCurrentTimeline()->zoomForScale(value)); }); connect(timeline->controller(), &TimelineController::showItemEffectStack, this, &TimelineTabs::showItemEffectStack); } void TimelineTabs::disconnectTimeline(TimelineWidget *timeline) { disconnect(timeline, &TimelineWidget::focusProjectMonitor, pCore->monitorManager(), &MonitorManager::focusProjectMonitor); disconnect(timeline->controller(), &TimelineController::durationChanged, pCore->projectManager(), &ProjectManager::adjustProjectDuration); disconnect(this, &TimelineTabs::audioThumbFormatChanged, timeline->controller(), &TimelineController::audioThumbFormatChanged); disconnect(this, &TimelineTabs::showThumbnailsChanged, timeline->controller(), &TimelineController::showThumbnailsChanged); disconnect(this, &TimelineTabs::showAudioThumbnailsChanged, timeline->controller(), &TimelineController::showAudioThumbnailsChanged); disconnect(this, &TimelineTabs::changeZoom, timeline, &TimelineWidget::slotChangeZoom); disconnect(timeline->controller(), &TimelineController::showTransitionModel, this, &TimelineTabs::showTransitionModel); disconnect(timeline->controller(), &TimelineController::showItemEffectStack, this, &TimelineTabs::showItemEffectStack); delete timeline; } diff --git a/src/timeline2/view/timelinewidget.cpp b/src/timeline2/view/timelinewidget.cpp index 7ab925e86..c9576beb2 100644 --- a/src/timeline2/view/timelinewidget.cpp +++ b/src/timeline2/view/timelinewidget.cpp @@ -1,393 +1,398 @@ /*************************************************************************** * 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 "timelinewidget.h" #include "../model/builders/meltBuilder.hpp" #include "assets/keyframes/model/keyframemodel.hpp" #include "assets/model/assetparametermodel.hpp" #include "capture/mediacapture.h" #include "core.h" #include "doc/docundostack.hpp" #include "doc/kdenlivedoc.h" #include "kdenlivesettings.h" #include "mainwindow.h" #include "profiles/profilemodel.hpp" #include "project/projectmanager.h" #include "monitor/monitorproxy.h" #include "qml/timelineitems.h" #include "qmltypes/thumbnailprovider.h" #include "timelinecontroller.h" #include "utils/clipboardproxy.hpp" #include "effects/effectsrepository.hpp" #include // #include #include #include #include #include #include #include #include #include const int TimelineWidget::comboScale[] = {1, 2, 4, 8, 15, 30, 50, 75, 100, 150, 200, 300, 500, 800, 1000, 1500, 2000, 3000, 6000, 15000, 30000}; TimelineWidget::TimelineWidget(QWidget *parent) : QQuickWidget(parent) { KDeclarative::KDeclarative kdeclarative; kdeclarative.setDeclarativeEngine(engine()); kdeclarative.setupEngine(engine()); kdeclarative.setupContext(); setClearColor(palette().window().color()); registerTimelineItems(); m_sortModel = std::make_unique(this); m_proxy = new TimelineController(this); connect(m_proxy, &TimelineController::zoneMoved, this, &TimelineWidget::zoneMoved); connect(m_proxy, &TimelineController::ungrabHack, this, &TimelineWidget::slotUngrabHack); setResizeMode(QQuickWidget::SizeRootObjectToView); engine()->addImageProvider(QStringLiteral("thumbnail"), new ThumbnailProvider); setVisible(false); setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); setFocusPolicy(Qt::StrongFocus); m_favEffects = new QMenu(i18n("Insert an effect..."), this); m_favCompositions = new QMenu(i18n("Insert a composition..."), this); } TimelineWidget::~TimelineWidget() { delete m_proxy; } void TimelineWidget::updateEffectFavorites() { const QMap effects = sortedItems(KdenliveSettings::favorite_effects(), false); QMapIterator i(effects); m_favEffects->clear(); while (i.hasNext()) { i.next(); QAction *ac = m_favEffects->addAction(i.key()); ac->setData(i.value()); } } void TimelineWidget::updateTransitionFavorites() { const QMap effects = sortedItems(KdenliveSettings::favorite_transitions(), true); QMapIterator i(effects); m_favCompositions->clear(); while (i.hasNext()) { i.next(); QAction *ac = m_favCompositions->addAction(i.key()); ac->setData(i.value()); } } const QMap TimelineWidget::sortedItems(const QStringList &items, bool isTransition) { QMap sortedItems; for (const QString &effect : items) { sortedItems.insert(m_proxy->getAssetName(effect, isTransition), effect); } return sortedItems; } void TimelineWidget::setTimelineMenu(QMenu *clipMenu, QMenu *compositionMenu, QMenu *timelineMenu, QMenu *guideMenu, QMenu *timelineRulerMenu, QAction *editGuideAction, QMenu *headerMenu, QMenu *thumbsMenu) { m_timelineClipMenu = clipMenu; m_timelineCompositionMenu = compositionMenu; m_timelineMenu = timelineMenu; m_timelineRulerMenu = timelineRulerMenu; m_guideMenu = guideMenu; m_headerMenu = headerMenu; m_thumbsMenu = thumbsMenu; m_headerMenu->addMenu(m_thumbsMenu); m_editGuideAcion = editGuideAction; updateEffectFavorites(); updateTransitionFavorites(); connect(m_favEffects, &QMenu::triggered, [&] (QAction *ac) { m_proxy->addEffectToClip(ac->data().toString()); }); connect(m_favCompositions, &QMenu::triggered, [&] (QAction *ac) { m_proxy->addCompositionToClip(ac->data().toString()); }); connect(m_guideMenu, &QMenu::triggered, [&] (QAction *ac) { m_proxy->setPosition(ac->data().toInt()); }); connect(m_headerMenu, &QMenu::triggered, [&] (QAction *ac) { m_proxy->setActiveTrackProperty(QStringLiteral("kdenlive:thumbs_format"), ac->data().toString()); }); // Fix qml focus issue connect(m_headerMenu, &QMenu::aboutToHide, this, &TimelineWidget::slotUngrabHack, Qt::DirectConnection); connect(m_timelineClipMenu, &QMenu::aboutToHide, this, &TimelineWidget::slotUngrabHack, Qt::DirectConnection); connect(m_timelineCompositionMenu, &QMenu::aboutToHide, this, &TimelineWidget::slotUngrabHack, Qt::DirectConnection); connect(m_timelineRulerMenu, &QMenu::aboutToHide, this, &TimelineWidget::slotUngrabHack, Qt::DirectConnection); connect(m_timelineMenu, &QMenu::aboutToHide, this, &TimelineWidget::slotUngrabHack, Qt::DirectConnection); m_timelineClipMenu->addMenu(m_favEffects); m_timelineClipMenu->addMenu(m_favCompositions); m_timelineMenu->addMenu(m_favCompositions); } void TimelineWidget::setModel(const std::shared_ptr &model, MonitorProxy *proxy) { m_sortModel->setSourceModel(model.get()); m_sortModel->setSortRole(TimelineItemModel::SortRole); m_sortModel->sort(0, Qt::DescendingOrder); m_proxy->setModel(model); rootContext()->setContextProperty("multitrack", m_sortModel.get()); rootContext()->setContextProperty("controller", model.get()); rootContext()->setContextProperty("timeline", m_proxy); rootContext()->setContextProperty("proxy", proxy); // Create a unique id for this timeline to prevent thumbnails // leaking from one project to another because of qml's image caching rootContext()->setContextProperty("documentId", QUuid::createUuid()); rootContext()->setContextProperty("audiorec", pCore->getAudioDevice()); rootContext()->setContextProperty("guidesModel", pCore->projectManager()->current()->getGuideModel().get()); rootContext()->setContextProperty("clipboard", new ClipboardProxy(this)); rootContext()->setContextProperty("miniFont", QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); const QStringList effs = sortedItems(KdenliveSettings::favorite_effects(), false).values(); const QStringList trans = sortedItems(KdenliveSettings::favorite_transitions(), true).values(); setSource(QUrl(QStringLiteral("qrc:/qml/timeline.qml"))); connect(rootObject(), SIGNAL(mousePosChanged(int)), pCore->window(), SLOT(slotUpdateMousePosition(int))); connect(rootObject(), SIGNAL(zoomIn(bool)), pCore->window(), SLOT(slotZoomIn(bool))); connect(rootObject(), SIGNAL(zoomOut(bool)), pCore->window(), SLOT(slotZoomOut(bool))); connect(rootObject(), SIGNAL(processingDrag(bool)), pCore->window(), SIGNAL(enableUndo(bool))); connect(m_proxy, &TimelineController::seeked, proxy, &MonitorProxy::setPosition); rootObject()->setProperty("dar", pCore->getCurrentDar()); connect(rootObject(), SIGNAL(showClipMenu(int)), this, SLOT(showClipMenu(int))); connect(rootObject(), SIGNAL(showCompositionMenu()), this, SLOT(showCompositionMenu())); connect(rootObject(), SIGNAL(showTimelineMenu()), this, SLOT(showTimelineMenu())); connect(rootObject(), SIGNAL(showRulerMenu()), this, SLOT(showRulerMenu())); connect(rootObject(), SIGNAL(showHeaderMenu()), this, SLOT(showHeaderMenu())); m_proxy->setRoot(rootObject()); setVisible(true); loading = false; m_proxy->checkDuration(); } void TimelineWidget::mousePressEvent(QMouseEvent *event) { emit focusProjectMonitor(); m_clickPos = event->globalPos(); QQuickWidget::mousePressEvent(event); } void TimelineWidget::showClipMenu(int cid) { // Hide not applicable effects QList effects = m_favEffects->actions(); int tid = model()->getClipTrackId(cid); bool isAudioTrack = false; if (tid > -1) { isAudioTrack = model()->isAudioTrack(tid); } m_favCompositions->setEnabled(!isAudioTrack); for (auto ac : effects) { const QString &id = ac->data().toString(); if (EffectsRepository::get()->isAudioEffect(id) != isAudioTrack) { ac->setVisible(false); } else { ac->setVisible(true); } } m_timelineClipMenu->popup(m_clickPos); } void TimelineWidget::showCompositionMenu() { m_timelineCompositionMenu->popup(m_clickPos); } void TimelineWidget::showHeaderMenu() { bool isAudio = m_proxy->isActiveTrackAudio(); QList menuActions = m_headerMenu->actions(); QAction *showRec = nullptr; for (QAction *ac : menuActions) { if (ac->data().toString() == QLatin1String("show_track_record")) { showRec = ac; break; } } if (!isAudio) { // Video track int currentThumbs = m_proxy->getActiveTrackProperty(QStringLiteral("kdenlive:thumbs_format")).toInt(); QList actions = m_thumbsMenu->actions(); for (QAction *ac : actions) { if (ac->data().toInt() == currentThumbs) { ac->setChecked(true); break; } } m_thumbsMenu->menuAction()->setVisible(true); if (showRec) { showRec->setVisible(false); } } else { // Audio track m_thumbsMenu->menuAction()->setVisible(false); if (showRec) { showRec->setVisible(true); showRec->setChecked(m_proxy->getActiveTrackProperty(QStringLiteral("kdenlive:audio_rec")).toInt() == 1); } } m_headerMenu->popup(m_clickPos); } void TimelineWidget::showRulerMenu() { m_guideMenu->clear(); const QList guides = pCore->projectManager()->current()->getGuideModel()->getAllMarkers(); QAction *ac; m_editGuideAcion->setEnabled(false); double fps = pCore->getCurrentFps(); int currentPos = rootObject()->property("consumerPosition").toInt(); for (auto guide : guides) { ac = new QAction(guide.comment(), this); int frame = guide.time().frames(fps); ac->setData(frame); if (frame == currentPos) { m_editGuideAcion->setEnabled(true); } m_guideMenu->addAction(ac); } m_timelineRulerMenu->popup(m_clickPos); } void TimelineWidget::showTimelineMenu() { m_guideMenu->clear(); const QList guides = pCore->projectManager()->current()->getGuideModel()->getAllMarkers(); QAction *ac; m_editGuideAcion->setEnabled(false); double fps = pCore->getCurrentFps(); int currentPos = rootObject()->property("consumerPosition").toInt(); for (auto guide : guides) { ac = new QAction(guide.comment(), this); int frame = guide.time().frames(fps); ac->setData(frame); if (frame == currentPos) { m_editGuideAcion->setEnabled(true); } m_guideMenu->addAction(ac); } m_timelineMenu->popup(m_clickPos); } void TimelineWidget::slotChangeZoom(int value, bool zoomOnMouse) { double pixelScale = QFontMetrics(font()).maxWidth() * 2; m_proxy->setScaleFactorOnMouse(pixelScale / comboScale[value], zoomOnMouse); } void TimelineWidget::slotFitZoom() { QVariant returnedValue; double prevScale = m_proxy->scaleFactor(); QMetaObject::invokeMethod(rootObject(), "fitZoom", Q_RETURN_ARG(QVariant, returnedValue)); double scale = returnedValue.toDouble(); QMetaObject::invokeMethod(rootObject(), "scrollPos", Q_RETURN_ARG(QVariant, returnedValue)); int scrollPos = returnedValue.toInt(); if (qFuzzyCompare(prevScale, scale)) { scale = m_prevScale; scrollPos = m_scrollPos; } else { m_prevScale = prevScale; m_scrollPos = scrollPos; scrollPos = 0; } m_proxy->setScaleFactorOnMouse(scale, false); // Update zoom slider m_proxy->updateZoom(scale); QMetaObject::invokeMethod(rootObject(), "goToStart", Q_ARG(QVariant, scrollPos)); } Mlt::Tractor *TimelineWidget::tractor() { return m_proxy->tractor(); } TimelineController *TimelineWidget::controller() { return m_proxy; } std::shared_ptr TimelineWidget::model() { return m_proxy->getModel(); } void TimelineWidget::zoneUpdated(const QPoint &zone) { - m_proxy->setZone(zone); + m_proxy->setZone(zone, false); +} + +void TimelineWidget::zoneUpdatedWithUndo(const QPoint &oldZone, const QPoint &newZone) +{ + m_proxy->updateZone(oldZone, newZone); } void TimelineWidget::setTool(ProjectTool tool) { rootObject()->setProperty("activeTool", (int)tool); } QPoint TimelineWidget::getTracksCount() const { return m_proxy->getTracksCount(); } void TimelineWidget::slotUngrabHack() { // Workaround bug: https://bugreports.qt.io/browse/QTBUG-59044 // https://phabricator.kde.org/D5515 if (quickWindow() && quickWindow()->mouseGrabberItem()) { quickWindow()->mouseGrabberItem()->ungrabMouse(); // Reset menu position QTimer::singleShot(200, this, [this]() { rootObject()->setProperty("mainFrame", -1); }); } } int TimelineWidget::zoomForScale(double value) const { int scale = 100.0 / value; int ix = 13; while (comboScale[ix] > scale && ix > 0) { ix--; } return ix; } void TimelineWidget::focusTimeline() { setFocus(); if (rootObject()) { rootObject()->setFocus(true); } } void TimelineWidget::endDrag() { if (rootObject()) { QMetaObject::invokeMethod(rootObject(), "endBinDrag"); } } diff --git a/src/timeline2/view/timelinewidget.h b/src/timeline2/view/timelinewidget.h index f835bd507..f475b9a92 100644 --- a/src/timeline2/view/timelinewidget.h +++ b/src/timeline2/view/timelinewidget.h @@ -1,108 +1,109 @@ /*************************************************************************** * 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 . * ***************************************************************************/ #ifndef TIMELINEWIDGET_H #define TIMELINEWIDGET_H #include "timeline2/model/timelineitemmodel.hpp" #include class ThumbnailProvider; class TimelineController; class QSortFilterProxyModel; class MonitorProxy; class QMenu; class TimelineWidget : public QQuickWidget { Q_OBJECT public: TimelineWidget(QWidget *parent = Q_NULLPTR); ~TimelineWidget() override; /* @brief Sets the model shown by this widget */ void setModel(const std::shared_ptr &model, MonitorProxy *proxy); /* @brief Return the project's tractor */ Mlt::Tractor *tractor(); TimelineController *controller(); std::shared_ptr model(); void setTool(ProjectTool tool); QPoint getTracksCount() const; /* @brief calculate zoom level for a scale */ int zoomForScale(double value) const; /* @brief Give keyboard focus to timeline qml */ void focusTimeline(); /** @brief Initiate timeline clip context menu */ void setTimelineMenu(QMenu *clipMenu, QMenu *compositionMenu, QMenu *timelineMenu, QMenu *timelineRulerMenu, QMenu *guideMenu, QAction *editGuideAction, QMenu *headerMenu, QMenu *thumbsMenu); bool loading; protected: void mousePressEvent(QMouseEvent *event) override; public slots: void slotChangeZoom(int value, bool zoomOnMouse); void slotFitZoom(); void zoneUpdated(const QPoint &zone); + void zoneUpdatedWithUndo(const QPoint &oldZone, const QPoint &newZone); /* @brief Favorite effects have changed, reload model for context menu */ void updateEffectFavorites(); /* @brief Favorite transitions have changed, reload model for context menu */ void updateTransitionFavorites(); /* @brief Bin clip drag ended, make sure we correctly processed the drop */ void endDrag(); private slots: void slotUngrabHack(); void showClipMenu(int cid); void showCompositionMenu(); void showTimelineMenu(); void showRulerMenu(); void showHeaderMenu(); private: TimelineController *m_proxy; QMenu *m_timelineClipMenu; QMenu *m_timelineCompositionMenu; QMenu *m_timelineMenu; QMenu *m_timelineRulerMenu; QMenu *m_guideMenu; QMenu *m_headerMenu; QMenu *m_thumbsMenu; QMenu *m_favEffects; QMenu *m_favCompositions; QAction *m_editGuideAcion; static const int comboScale[]; std::unique_ptr m_sortModel; /* @brief Keep last scale before fit to restore it on second click */ double m_prevScale; /* @brief Keep last scroll position before fit to restore it on second click */ int m_scrollPos; /* @brief Returns an alphabetically sorted list of favorite effects or transitions */ const QMap sortedItems(const QStringList &items, bool isTransition); QPoint m_clickPos; signals: void focusProjectMonitor(); void zoneMoved(const QPoint &zone); }; #endif