File indexing completed on 2023-11-26 04:48:47

0001 /*
0002 SPDX-FileCopyrightText: 2014 Till Theato <root@ttill.de>
0003 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0004 */
0005 
0006 #include "projectmanager.h"
0007 #include "bin/bin.h"
0008 #include "bin/projectclip.h"
0009 #include "bin/projectitemmodel.h"
0010 #include "core.h"
0011 #include "doc/docundostack.hpp"
0012 #include "doc/kdenlivedoc.h"
0013 #include "jobs/cliploadtask.h"
0014 #include "kdenlivesettings.h"
0015 #include "mainwindow.h"
0016 #include "monitor/monitormanager.h"
0017 #include "profiles/profilemodel.hpp"
0018 #include "project/dialogs/archivewidget.h"
0019 #include "project/dialogs/backupwidget.h"
0020 #include "project/dialogs/noteswidget.h"
0021 #include "project/dialogs/projectsettings.h"
0022 #include "timeline2/model/timelinefunctions.hpp"
0023 #include "utils/qstringutils.h"
0024 #include "utils/thumbnailcache.hpp"
0025 #include "xml/xml.hpp"
0026 #include <audiomixer/mixermanager.hpp>
0027 #include <bin/clipcreator.hpp>
0028 #include <lib/localeHandling.h>
0029 
0030 // Temporary for testing
0031 #include "bin/model/markerlistmodel.hpp"
0032 
0033 #include "profiles/profilerepository.hpp"
0034 #include "project/notesplugin.h"
0035 #include "timeline2/model/builders/meltBuilder.hpp"
0036 #include "timeline2/view/timelinecontroller.h"
0037 #include "timeline2/view/timelinewidget.h"
0038 
0039 #include "utils/KMessageBox_KdenliveCompat.h"
0040 #include <KActionCollection>
0041 #include <KConfigGroup>
0042 #include <KJob>
0043 #include <KJobWidgets>
0044 #include <KLocalizedString>
0045 #include <KMessageBox>
0046 #include <KRecentDirs>
0047 #include <kcoreaddons_version.h>
0048 
0049 #include "kdenlive_debug.h"
0050 #include <QAction>
0051 #include <QCryptographicHash>
0052 #include <QFileDialog>
0053 #include <QJsonArray>
0054 #include <QJsonDocument>
0055 #include <QJsonObject>
0056 #include <QLocale>
0057 #include <QMimeDatabase>
0058 #include <QMimeType>
0059 #include <QProgressDialog>
0060 #include <QSaveFile>
0061 #include <QTimeZone>
0062 
0063 static QString getProjectNameFilters(bool ark = true)
0064 {
0065     QString filter = i18n("Kdenlive Project") + QStringLiteral(" (*.kdenlive)");
0066     if (ark) {
0067         filter.append(";;" + i18n("Archived Project") + QStringLiteral(" (*.tar.gz *.zip)"));
0068     }
0069     return filter;
0070 }
0071 
0072 ProjectManager::ProjectManager(QObject *parent)
0073     : QObject(parent)
0074     , m_activeTimelineModel(nullptr)
0075     , m_notesPlugin(nullptr)
0076 {
0077     // Ensure the default data folder exist
0078     QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
0079     dir.mkpath(QStringLiteral(".backup"));
0080     dir.mkdir(QStringLiteral("titles"));
0081 }
0082 
0083 ProjectManager::~ProjectManager() = default;
0084 
0085 void ProjectManager::slotLoadOnOpen()
0086 {
0087     m_loading = true;
0088     if (m_startUrl.isValid()) {
0089         openFile();
0090     } else if (KdenliveSettings::openlastproject()) {
0091         openLastFile();
0092     } else {
0093         newFile(false);
0094     }
0095     if (!m_loadClipsOnOpen.isEmpty() && (m_project != nullptr)) {
0096         const QStringList list = m_loadClipsOnOpen.split(QLatin1Char(','));
0097         QList<QUrl> urls;
0098         urls.reserve(list.count());
0099         for (const QString &path : list) {
0100             // qCDebug(KDENLIVE_LOG) << QDir::current().absoluteFilePath(path);
0101             urls << QUrl::fromLocalFile(QDir::current().absoluteFilePath(path));
0102         }
0103         pCore->bin()->droppedUrls(urls);
0104     }
0105     m_loadClipsOnOpen.clear();
0106     m_loading = false;
0107     Q_EMIT pCore->closeSplash();
0108     // Release startup crash lock file
0109     QFile lockFile(QDir::temp().absoluteFilePath(QStringLiteral("kdenlivelock")));
0110     lockFile.remove();
0111     // For some reason Qt seems to be doing some stuff that modifies the tabs text after window is shown, so use a timer
0112     QTimer::singleShot(1000, this, []() {
0113         QList<QTabBar *> tabbars = pCore->window()->findChildren<QTabBar *>();
0114         for (QTabBar *tab : qAsConst(tabbars)) {
0115             // Fix tabbar tooltip containing ampersand
0116             for (int i = 0; i < tab->count(); i++) {
0117                 tab->setTabToolTip(i, tab->tabText(i).replace('&', ""));
0118             }
0119         }
0120     });
0121     pCore->window()->checkMaxCacheSize();
0122 }
0123 
0124 void ProjectManager::init(const QUrl &projectUrl, const QString &clipList)
0125 {
0126     m_startUrl = projectUrl;
0127     m_loadClipsOnOpen = clipList;
0128     m_fileRevert = KStandardAction::revert(this, SLOT(slotRevert()), pCore->window()->actionCollection());
0129     m_fileRevert->setIcon(QIcon::fromTheme(QStringLiteral("document-revert")));
0130     m_fileRevert->setEnabled(false);
0131 
0132     QAction *a = KStandardAction::open(this, SLOT(openFile()), pCore->window()->actionCollection());
0133     a->setIcon(QIcon::fromTheme(QStringLiteral("document-open")));
0134     a = KStandardAction::saveAs(this, SLOT(saveFileAs()), pCore->window()->actionCollection());
0135     a->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as")));
0136     a = KStandardAction::openNew(this, SLOT(newFile()), pCore->window()->actionCollection());
0137     a->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
0138     m_recentFilesAction = KStandardAction::openRecent(this, SLOT(openFile(QUrl)), pCore->window()->actionCollection());
0139 
0140     QAction *saveCopyAction = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save Copy…"), this);
0141     pCore->window()->addAction(QStringLiteral("file_save_copy"), saveCopyAction);
0142     connect(saveCopyAction, &QAction::triggered, this, [this] { saveFileAs(true); });
0143 
0144     QAction *backupAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-undo")), i18n("Open Backup File…"), this);
0145     pCore->window()->addAction(QStringLiteral("open_backup"), backupAction);
0146     connect(backupAction, SIGNAL(triggered(bool)), SLOT(slotOpenBackup()));
0147     m_notesPlugin = new NotesPlugin(this);
0148 
0149     m_autoSaveTimer.setSingleShot(true);
0150     connect(&m_autoSaveTimer, &QTimer::timeout, this, &ProjectManager::slotAutoSave);
0151 }
0152 
0153 void ProjectManager::newFile(bool showProjectSettings)
0154 {
0155     QString profileName = KdenliveSettings::default_profile();
0156     if (profileName.isEmpty()) {
0157         profileName = pCore->getCurrentProfile()->path();
0158     }
0159     newFile(profileName, showProjectSettings);
0160 }
0161 
0162 void ProjectManager::newFile(QString profileName, bool showProjectSettings)
0163 {
0164     QUrl startFile = QUrl::fromLocalFile(KdenliveSettings::defaultprojectfolder() + QStringLiteral("/_untitled.kdenlive"));
0165     if (checkForBackupFile(startFile, true)) {
0166         return;
0167     }
0168     m_fileRevert->setEnabled(false);
0169     QString projectFolder;
0170     QMap<QString, QString> documentProperties;
0171     QMap<QString, QString> documentMetadata;
0172     std::pair<int, int> projectTracks{KdenliveSettings::videotracks(), KdenliveSettings::audiotracks()};
0173     int audioChannels = 2;
0174     if (KdenliveSettings::audio_channels() == 1) {
0175         audioChannels = 4;
0176     } else if (KdenliveSettings::audio_channels() == 2) {
0177         audioChannels = 6;
0178     }
0179     pCore->monitorManager()->resetDisplay();
0180     QString documentId = QString::number(QDateTime::currentMSecsSinceEpoch());
0181     documentProperties.insert(QStringLiteral("documentid"), documentId);
0182     bool sameProjectFolder = KdenliveSettings::sameprojectfolder();
0183     if (!showProjectSettings) {
0184         if (!closeCurrentDocument()) {
0185             return;
0186         }
0187         if (KdenliveSettings::customprojectfolder()) {
0188             projectFolder = KdenliveSettings::defaultprojectfolder();
0189             QDir folder(projectFolder);
0190             if (!projectFolder.endsWith(QLatin1Char('/'))) {
0191                 projectFolder.append(QLatin1Char('/'));
0192             }
0193             documentProperties.insert(QStringLiteral("storagefolder"), folder.absoluteFilePath(documentId));
0194         }
0195     } else {
0196         QPointer<ProjectSettings> w = new ProjectSettings(nullptr, QMap<QString, QString>(), QStringList(), projectTracks.first, projectTracks.second,
0197                                                           audioChannels, KdenliveSettings::defaultprojectfolder(), false, true, pCore->window());
0198         connect(w.data(), &ProjectSettings::refreshProfiles, pCore->window(), &MainWindow::slotRefreshProfiles);
0199         if (w->exec() != QDialog::Accepted) {
0200             delete w;
0201             return;
0202         }
0203         if (!closeCurrentDocument()) {
0204             delete w;
0205             return;
0206         }
0207         if (KdenliveSettings::videothumbnails() != w->enableVideoThumbs()) {
0208             pCore->window()->slotSwitchVideoThumbs();
0209         }
0210         if (KdenliveSettings::audiothumbnails() != w->enableAudioThumbs()) {
0211             pCore->window()->slotSwitchAudioThumbs();
0212         }
0213         profileName = w->selectedProfile();
0214         projectFolder = w->storageFolder();
0215         projectTracks = w->tracks();
0216         audioChannels = w->audioChannels();
0217         documentProperties.insert(QStringLiteral("enableproxy"), QString::number(int(w->useProxy())));
0218         documentProperties.insert(QStringLiteral("generateproxy"), QString::number(int(w->generateProxy())));
0219         documentProperties.insert(QStringLiteral("proxyminsize"), QString::number(w->proxyMinSize()));
0220         documentProperties.insert(QStringLiteral("proxyparams"), w->proxyParams());
0221         documentProperties.insert(QStringLiteral("proxyextension"), w->proxyExtension());
0222         documentProperties.insert(QStringLiteral("proxyresize"), QString::number(w->proxyResize()));
0223         documentProperties.insert(QStringLiteral("audioChannels"), QString::number(w->audioChannels()));
0224         documentProperties.insert(QStringLiteral("generateimageproxy"), QString::number(int(w->generateImageProxy())));
0225         QString preview = w->selectedPreview();
0226         if (!preview.isEmpty()) {
0227             documentProperties.insert(QStringLiteral("previewparameters"), preview.section(QLatin1Char(';'), 0, 0));
0228             documentProperties.insert(QStringLiteral("previewextension"), preview.section(QLatin1Char(';'), 1, 1));
0229         }
0230         documentProperties.insert(QStringLiteral("proxyimageminsize"), QString::number(w->proxyImageMinSize()));
0231         if (!projectFolder.isEmpty()) {
0232             if (!projectFolder.endsWith(QLatin1Char('/'))) {
0233                 projectFolder.append(QLatin1Char('/'));
0234             }
0235             documentProperties.insert(QStringLiteral("storagefolder"), projectFolder + documentId);
0236         }
0237         if (w->useExternalProxy()) {
0238             documentProperties.insert(QStringLiteral("enableexternalproxy"), QStringLiteral("1"));
0239             documentProperties.insert(QStringLiteral("externalproxyparams"), w->externalProxyParams());
0240         }
0241         sameProjectFolder = w->docFolderAsStorageFolder();
0242         // Metadata
0243         documentMetadata = w->metadata();
0244         delete w;
0245     }
0246     m_notesPlugin->clear();
0247     KdenliveDoc *doc = new KdenliveDoc(projectFolder, pCore->window()->m_commandStack, profileName, documentProperties, documentMetadata, projectTracks, audioChannels, pCore->window());
0248     doc->m_autosave = new KAutoSaveFile(startFile, doc);
0249     doc->m_sameProjectFolder = sameProjectFolder;
0250     ThumbnailCache::get()->clearCache();
0251     pCore->bin()->setDocument(doc);
0252     m_project = doc;
0253     initSequenceProperties(m_project->uuid(), {KdenliveSettings::audiotracks(), KdenliveSettings::videotracks()});
0254     updateTimeline(true, QString(), QString(), QDateTime(), 0);
0255     pCore->window()->connectDocument();
0256     bool disabled = m_project->getDocumentProperty(QStringLiteral("disabletimelineeffects")) == QLatin1String("1");
0257     QAction *disableEffects = pCore->window()->actionCollection()->action(QStringLiteral("disable_timeline_effects"));
0258     if (disableEffects) {
0259         if (disabled != disableEffects->isChecked()) {
0260             disableEffects->blockSignals(true);
0261             disableEffects->setChecked(disabled);
0262             disableEffects->blockSignals(false);
0263         }
0264     }
0265     activateDocument(m_project->activeUuid);
0266     Q_EMIT docOpened(m_project);
0267     Q_EMIT pCore->gotMissingClipsCount(0, 0);
0268     m_project->loading = false;
0269     m_lastSave.start();
0270     if (pCore->monitorManager()) {
0271         Q_EMIT pCore->monitorManager()->updatePreviewScaling();
0272         pCore->monitorManager()->projectMonitor()->slotActivateMonitor();
0273         pCore->monitorManager()->projectMonitor()->setProducer(m_activeTimelineModel->producer(), 0);
0274         const QUuid uuid = m_project->activeUuid;
0275         pCore->monitorManager()->projectMonitor()->adjustRulerSize(m_activeTimelineModel->duration() - 1, m_project->getFilteredGuideModel(uuid));
0276     }
0277 }
0278 
0279 void ProjectManager::setActiveTimeline(const QUuid &uuid)
0280 {
0281     m_activeTimelineModel = m_project->getTimeline(uuid);
0282     m_project->activeUuid = uuid;
0283 }
0284 
0285 void ProjectManager::activateDocument(const QUuid &uuid)
0286 {
0287     qDebug() << "===== ACTIVATING DOCUMENT: " << uuid << "\n::::::::::::::::::::::";
0288     /*if (m_project && (m_project->uuid() == uuid)) {
0289         auto match = m_timelineModels.find(uuid.toString());
0290         if (match == m_timelineModels.end()) {
0291             qDebug()<<"=== ERROR";
0292             return;
0293         }
0294         m_mainTimelineModel = match->second;
0295         pCore->window()->raiseTimeline(uuid);
0296         qDebug()<<"=== ERROR 2";
0297         return;
0298     }*/
0299     // Q_ASSERT(m_openedDocuments.contains(uuid));
0300     /*m_project = m_openedDocuments.value(uuid);
0301     m_fileRevert->setEnabled(m_project->isModified());
0302     m_notesPlugin->clear();
0303     Q_EMIT docOpened(m_project);*/
0304 
0305     m_activeTimelineModel = m_project->getTimeline(uuid);
0306     m_project->activeUuid = uuid;
0307 
0308     /*pCore->bin()->setDocument(m_project);
0309     pCore->window()->connectDocument();*/
0310     pCore->window()->raiseTimeline(uuid);
0311     pCore->window()->slotSwitchTimelineZone(m_project->getDocumentProperty(QStringLiteral("enableTimelineZone")).toInt() == 1);
0312     pCore->window()->slotSetZoom(m_project->zoom(uuid).x());
0313     // Q_EMIT pCore->monitorManager()->updatePreviewScaling();
0314     // pCore->monitorManager()->projectMonitor()->slotActivateMonitor();
0315 }
0316 
0317 void ProjectManager::testSetActiveDocument(KdenliveDoc *doc, std::shared_ptr<TimelineItemModel> timeline)
0318 {
0319     m_project = doc;
0320     if (timeline == nullptr) {
0321         // New nested document format, build timeline model now
0322         const QUuid uuid = m_project->uuid();
0323         timeline = TimelineItemModel::construct(uuid, m_project->commandStack());
0324         std::shared_ptr<Mlt::Tractor> tc = pCore->projectItemModel()->getExtraTimeline(uuid.toString());
0325         if (!constructTimelineFromTractor(timeline, nullptr, *tc.get(), m_progressDialog, m_project->modifiedDecimalPoint(), QString(), QString())) {
0326             qDebug() << "===== LOADING PROJECT INTERNAL ERROR";
0327         }
0328     }
0329     m_project->addTimeline(doc->uuid(), timeline);
0330     m_activeTimelineModel = timeline;
0331     m_project->activeUuid = doc->uuid();
0332     std::shared_ptr<ProjectClip> mainClip = pCore->projectItemModel()->getClipByBinID(pCore->projectItemModel()->getSequenceId(doc->uuid()));
0333     if (mainClip) {
0334         if (timeline->getGuideModel() == nullptr) {
0335             timeline->setMarkerModel(mainClip->markerModel());
0336         }
0337         m_project->loadSequenceGroupsAndGuides(doc->uuid());
0338     }
0339 }
0340 
0341 std::shared_ptr<TimelineItemModel> ProjectManager::getTimeline()
0342 {
0343     return m_activeTimelineModel;
0344 }
0345 
0346 bool ProjectManager::testSaveFileAs(const QString &outputFileName)
0347 {
0348     QString saveFolder = QFileInfo(outputFileName).absolutePath();
0349     m_project->setDocumentProperty(QStringLiteral("opensequences"), m_project->uuid().toString());
0350     m_project->setDocumentProperty(QStringLiteral("activetimeline"), m_project->uuid().toString());
0351 
0352     QMap<QString, QString> docProperties = m_project->documentProperties(true);
0353     pCore->projectItemModel()->saveDocumentProperties(docProperties, QMap<QString, QString>());
0354     // QString scene = m_activeTimelineModel->sceneList(saveFolder);
0355     int duration = m_activeTimelineModel->duration();
0356     QString scene = pCore->projectItemModel()->sceneList(saveFolder, QString(), QString(), m_activeTimelineModel->tractor(), duration);
0357     QSaveFile file(outputFileName);
0358     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
0359         qDebug() << "//////  ERROR writing to file: " << outputFileName;
0360         return false;
0361     }
0362 
0363     file.write(scene.toUtf8());
0364     if (!file.commit()) {
0365         qDebug() << "Cannot write to file %1";
0366         return false;
0367     }
0368     return true;
0369 }
0370 
0371 bool ProjectManager::closeCurrentDocument(bool saveChanges, bool quit)
0372 {
0373     // Disable autosave
0374     m_autoSaveTimer.stop();
0375     if ((m_project != nullptr) && m_project->isModified() && saveChanges) {
0376         QString message;
0377         if (m_project->url().fileName().isEmpty()) {
0378             message = i18n("Save changes to document?");
0379         } else {
0380             message = i18n("The project <b>\"%1\"</b> has been changed.\nDo you want to save your changes?", m_project->url().fileName());
0381         }
0382 
0383         switch (KMessageBox::warningTwoActionsCancel(pCore->window(), message, {}, KStandardGuiItem::save(), KStandardGuiItem::dontSave())) {
0384         case KMessageBox::PrimaryAction:
0385             // save document here. If saving fails, return false;
0386             if (!saveFile()) {
0387                 return false;
0388             }
0389             break;
0390         case KMessageBox::Cancel:
0391             return false;
0392             break;
0393         default:
0394             break;
0395         }
0396     }
0397     bool guiConstructed = pCore->window() != nullptr;
0398     if (guiConstructed) {
0399         pCore->window()->disableMulticam();
0400         Q_EMIT pCore->window()->clearAssetPanel();
0401         pCore->mixer()->unsetModel();
0402         pCore->monitorManager()->clipMonitor()->slotOpenClip(nullptr);
0403         pCore->monitorManager()->projectMonitor()->setProducer(nullptr);
0404     }
0405     if (m_project) {
0406         m_project->closing = true;
0407         if (guiConstructed && !quit && !qApp->isSavingSession()) {
0408             pCore->bin()->abortOperations();
0409         }
0410         pCore->taskManager.slotCancelJobs(true);
0411         m_project->commandStack()->clear();
0412         pCore->cleanup();
0413         if (guiConstructed) {
0414             const QList<QUuid> uuids = m_project->getTimelinesUuids();
0415             for (auto &uid : uuids) {
0416                 pCore->window()->closeTimelineTab(uid);
0417                 pCore->window()->resetSubtitles(uid);
0418                 m_project->closeTimeline(uid);
0419             }
0420         } else {
0421             // Close all timelines
0422             const QList<QUuid> uuids = m_project->getTimelinesUuids();
0423             for (auto &uid : uuids) {
0424                 m_project->closeTimeline(uid);
0425             }
0426         }
0427     }
0428     // Ensure we don't have stuck references to timelinemodel
0429     // qDebug() << "TIMELINEMODEL COUNTS: " << m_activeTimelineModel.use_count();
0430     // Q_ASSERT(m_activeTimelineModel.use_count() <= 1);
0431     m_activeTimelineModel.reset();
0432     // Release model shared pointers
0433     if (guiConstructed) {
0434         pCore->bin()->cleanDocument();
0435         delete m_project;
0436         m_project = nullptr;
0437     } else {
0438         pCore->projectItemModel()->clean();
0439         m_project = nullptr;
0440     }
0441     mlt_service_cache_set_size(nullptr, "producer_avformat", 0);
0442     ::mlt_pool_purge();
0443     return true;
0444 }
0445 
0446 bool ProjectManager::saveFileAs(const QString &outputFileName, bool saveOverExistingFile, bool saveACopy)
0447 {
0448     pCore->monitorManager()->pauseActiveMonitor();
0449     QString oldProjectFolder =
0450         m_project->url().isEmpty() ? QString() : QFileInfo(m_project->url().toLocalFile()).absolutePath() + QStringLiteral("/cachefiles");
0451     // this was the old project folder in case the "save in project file location" setting was active
0452 
0453     // Sync document properties
0454     if (!saveACopy && outputFileName != m_project->url().toLocalFile()) {
0455         // Project filename changed
0456         pCore->window()->updateProjectPath(outputFileName);
0457     }
0458     prepareSave();
0459     QString saveFolder = QFileInfo(outputFileName).absolutePath();
0460     m_project->updateWorkFilesBeforeSave(outputFileName);
0461     QString scene = projectSceneList(saveFolder);
0462     if (!m_replacementPattern.isEmpty()) {
0463         QMapIterator<QString, QString> i(m_replacementPattern);
0464         while (i.hasNext()) {
0465             i.next();
0466             scene.replace(i.key(), i.value());
0467         }
0468     }
0469     m_project->updateWorkFilesAfterSave();
0470     if (!m_project->saveSceneList(outputFileName, scene, saveOverExistingFile)) {
0471         return false;
0472     }
0473     QUrl url = QUrl::fromLocalFile(outputFileName);
0474     // Save timeline thumbnails
0475     std::unordered_map<QString, std::vector<int>> thumbKeys = pCore->window()->getCurrentTimeline()->controller()->getThumbKeys();
0476     pCore->projectItemModel()->updateCacheThumbnail(thumbKeys);
0477     // Remove duplicates
0478     for (auto p : thumbKeys) {
0479         std::sort(p.second.begin(), p.second.end());
0480         auto last = std::unique(p.second.begin(), p.second.end());
0481         p.second.erase(last, p.second.end());
0482     }
0483     ThumbnailCache::get()->saveCachedThumbs(thumbKeys);
0484     if (!saveACopy) {
0485         m_project->setUrl(url);
0486         // setting up autosave file in ~/.kde/data/stalefiles/kdenlive/
0487         // saved under file name
0488         // actual saving by KdenliveDoc::slotAutoSave() called by a timer 3 seconds after the document has been edited
0489         // This timer is set by KdenliveDoc::setModified()
0490         const QString projectId = QCryptographicHash::hash(url.fileName().toUtf8(), QCryptographicHash::Md5).toHex();
0491         QUrl autosaveUrl = QUrl::fromLocalFile(QFileInfo(outputFileName).absoluteDir().absoluteFilePath(projectId + QStringLiteral(".kdenlive")));
0492         if (m_project->m_autosave == nullptr) {
0493             // The temporary file is not opened or created until actually needed.
0494             // The file filename does not have to exist for KAutoSaveFile to be constructed (if it exists, it will not be touched).
0495             m_project->m_autosave = new KAutoSaveFile(autosaveUrl, m_project);
0496         } else {
0497             m_project->m_autosave->setManagedFile(autosaveUrl);
0498         }
0499 
0500         pCore->window()->setWindowTitle(m_project->description());
0501         m_project->setModified(false);
0502     }
0503 
0504     m_recentFilesAction->addUrl(url);
0505     // remember folder for next project opening
0506     KRecentDirs::add(QStringLiteral(":KdenliveProjectsFolder"), saveFolder);
0507     saveRecentFiles();
0508     if (!saveACopy) {
0509         m_fileRevert->setEnabled(true);
0510         pCore->window()->m_undoView->stack()->setClean();
0511         QString newProjectFolder(saveFolder + QStringLiteral("/cachefiles"));
0512         if (((oldProjectFolder.isEmpty() && m_project->m_sameProjectFolder) || m_project->projectTempFolder() == oldProjectFolder) &&
0513             newProjectFolder != m_project->projectTempFolder()) {
0514             KMessageBox::ButtonCode answer = KMessageBox::warningContinueCancel(
0515                 pCore->window(), i18n("The location of the project file changed. You selected to use the location of the project file to save temporary files. "
0516                                       "This will move all temporary files from <b>%1</b> to <b>%2</b>, the project file will then be reloaded",
0517                                       m_project->projectTempFolder(), newProjectFolder));
0518 
0519             if (answer == KMessageBox::Continue) {
0520                 // Discard running jobs, for example proxy clips since data will be moved
0521                 pCore->taskManager.slotCancelJobs();
0522                 // Proceed with move
0523                 QString documentId = QDir::cleanPath(m_project->getDocumentProperty(QStringLiteral("documentid")));
0524                 bool ok;
0525                 documentId.toLongLong(&ok, 10);
0526                 if (!ok || documentId.isEmpty()) {
0527                     KMessageBox::error(pCore->window(), i18n("Cannot perform operation, invalid document id: %1", documentId));
0528                 } else {
0529                     QDir newDir(newProjectFolder);
0530                     QDir oldDir(m_project->projectTempFolder());
0531                     if (newDir.exists(documentId)) {
0532                         KMessageBox::error(pCore->window(),
0533                                            i18n("Cannot perform operation, target directory already exists: %1", newDir.absoluteFilePath(documentId)));
0534                     } else {
0535                         // Proceed with the move
0536                         moveProjectData(oldDir.absoluteFilePath(documentId), newDir.absolutePath());
0537                     }
0538                 }
0539             }
0540         }
0541     }
0542     return true;
0543 }
0544 
0545 void ProjectManager::saveRecentFiles()
0546 {
0547     KSharedConfigPtr config = KSharedConfig::openConfig();
0548     m_recentFilesAction->saveEntries(KConfigGroup(config, "Recent Files"));
0549     config->sync();
0550 }
0551 
0552 bool ProjectManager::saveFileAs(bool saveACopy)
0553 {
0554     QFileDialog fd(pCore->window());
0555     if (saveACopy) {
0556         fd.setWindowTitle(i18nc("@title:window", "Save Copy"));
0557     }
0558     if (m_project->url().isValid()) {
0559         fd.selectUrl(m_project->url());
0560     } else {
0561         fd.setDirectory(KdenliveSettings::defaultprojectfolder());
0562     }
0563     fd.setNameFilter(getProjectNameFilters(false));
0564     fd.setAcceptMode(QFileDialog::AcceptSave);
0565     fd.setFileMode(QFileDialog::AnyFile);
0566     fd.setDefaultSuffix(QStringLiteral("kdenlive"));
0567     if (fd.exec() != QDialog::Accepted || fd.selectedFiles().isEmpty()) {
0568         return false;
0569     }
0570 
0571     QString outputFile = fd.selectedFiles().constFirst();
0572 
0573     bool ok;
0574     QDir cacheDir = m_project->getCacheDir(CacheBase, &ok);
0575     if (ok) {
0576         QFile file(cacheDir.absoluteFilePath(QString::fromLatin1(QUrl::toPercentEncoding(QStringLiteral(".") + outputFile))));
0577         file.open(QIODevice::ReadWrite | QIODevice::Text);
0578         file.close();
0579     }
0580     return saveFileAs(outputFile, false, saveACopy);
0581 }
0582 
0583 bool ProjectManager::saveFile()
0584 {
0585     if (!m_project) {
0586         // Calling saveFile before a project was created, something is wrong
0587         qCDebug(KDENLIVE_LOG) << "SaveFile called without project";
0588         return false;
0589     }
0590     if (m_project->url().isEmpty()) {
0591         return saveFileAs();
0592     }
0593     bool result = saveFileAs(m_project->url().toLocalFile());
0594     m_project->m_autosave->resize(0);
0595     return result;
0596 }
0597 
0598 void ProjectManager::openFile()
0599 {
0600     if (m_startUrl.isValid()) {
0601         openFile(m_startUrl);
0602         m_startUrl.clear();
0603         return;
0604     }
0605     QUrl url = QFileDialog::getOpenFileUrl(pCore->window(), QString(), QUrl::fromLocalFile(KRecentDirs::dir(QStringLiteral(":KdenliveProjectsFolder"))),
0606                                            getProjectNameFilters());
0607     if (!url.isValid()) {
0608         return;
0609     }
0610     KRecentDirs::add(QStringLiteral(":KdenliveProjectsFolder"), url.adjusted(QUrl::RemoveFilename).toLocalFile());
0611     m_recentFilesAction->addUrl(url);
0612     saveRecentFiles();
0613     openFile(url);
0614 }
0615 
0616 void ProjectManager::openLastFile()
0617 {
0618     if (m_recentFilesAction->selectableActionGroup()->actions().isEmpty()) {
0619         // No files in history
0620         newFile(false);
0621         return;
0622     }
0623 
0624     QAction *firstUrlAction = m_recentFilesAction->selectableActionGroup()->actions().last();
0625     if (firstUrlAction) {
0626         firstUrlAction->trigger();
0627     } else {
0628         newFile(false);
0629     }
0630 }
0631 
0632 // fix mantis#3160 separate check from openFile() so we can call it from newFile()
0633 // to find autosaved files (in ~/.local/share/stalefiles/kdenlive) and recover it
0634 bool ProjectManager::checkForBackupFile(const QUrl &url, bool newFile)
0635 {
0636     // Check for autosave file that belong to the url we passed in.
0637     const QString projectId = QCryptographicHash::hash(url.fileName().toUtf8(), QCryptographicHash::Md5).toHex();
0638     QUrl autosaveUrl = newFile ? url : QUrl::fromLocalFile(QFileInfo(url.path()).absoluteDir().absoluteFilePath(projectId + QStringLiteral(".kdenlive")));
0639     QList<KAutoSaveFile *> staleFiles = KAutoSaveFile::staleFiles(autosaveUrl);
0640     QFileInfo sourceInfo(url.toLocalFile());
0641     QDateTime sourceTime;
0642     if (sourceInfo.exists()) {
0643         sourceTime = QFileInfo(url.toLocalFile()).lastModified();
0644     }
0645     KAutoSaveFile *orphanedFile = nullptr;
0646     // Check if we can have a lock on one of the file,
0647     // meaning it is not handled by any Kdenlive instance
0648     if (!staleFiles.isEmpty()) {
0649         for (KAutoSaveFile *stale : qAsConst(staleFiles)) {
0650             if (stale->open(QIODevice::QIODevice::ReadWrite)) {
0651                 // Found orphaned autosave file
0652                 if (!sourceTime.isValid() || QFileInfo(stale->fileName()).lastModified() > sourceTime) {
0653                     orphanedFile = stale;
0654                     break;
0655                 }
0656             }
0657         }
0658     }
0659 
0660     if (orphanedFile) {
0661         if (KMessageBox::questionTwoActions(nullptr, i18n("Auto-saved file exist. Do you want to recover now?"), i18n("File Recovery"),
0662                                             KGuiItem(i18n("Recover")), KGuiItem(i18n("Do not recover"))) == KMessageBox::PrimaryAction) {
0663             doOpenFile(url, orphanedFile);
0664             return true;
0665         }
0666     }
0667     // remove the stale files
0668     for (KAutoSaveFile *stale : qAsConst(staleFiles)) {
0669         stale->open(QIODevice::ReadWrite);
0670         delete stale;
0671     }
0672     return false;
0673 }
0674 
0675 void ProjectManager::openFile(const QUrl &url)
0676 {
0677     QMimeDatabase db;
0678     // Make sure the url is a Kdenlive project file
0679     QMimeType mime = db.mimeTypeForUrl(url);
0680     if (mime.inherits(QStringLiteral("application/x-compressed-tar")) || mime.inherits(QStringLiteral("application/zip"))) {
0681         // Opening a compressed project file, we need to process it
0682         // qCDebug(KDENLIVE_LOG)<<"Opening archive, processing";
0683         QPointer<ArchiveWidget> ar = new ArchiveWidget(url);
0684         if (ar->exec() == QDialog::Accepted) {
0685             openFile(QUrl::fromLocalFile(ar->extractedProjectFile()));
0686         } else if (m_startUrl.isValid()) {
0687             // we tried to open an invalid file from command line, init new project
0688             newFile(false);
0689         }
0690         delete ar;
0691         return;
0692     }
0693 
0694     /*if (!url.fileName().endsWith(".kdenlive")) {
0695         // This is not a Kdenlive project file, abort loading
0696         KMessageBox::error(pCore->window(), i18n("File %1 is not a Kdenlive project file", url.toLocalFile()));
0697         if (m_startUrl.isValid()) {
0698             // we tried to open an invalid file from command line, init new project
0699             newFile(false);
0700         }
0701         return;
0702     }*/
0703 
0704     if ((m_project != nullptr) && m_project->url() == url) {
0705         return;
0706     }
0707 
0708     if (!closeCurrentDocument()) {
0709         return;
0710     }
0711     if (checkForBackupFile(url)) {
0712         return;
0713     }
0714     pCore->displayMessage(i18n("Opening file %1", url.toLocalFile()), OperationCompletedMessage, 100);
0715     doOpenFile(url, nullptr);
0716 }
0717 
0718 void ProjectManager::abortLoading()
0719 {
0720     KMessageBox::error(pCore->window(), i18n("Could not recover corrupted file."));
0721     delete m_progressDialog;
0722     m_progressDialog = nullptr;
0723     // Don't propose to save corrupted doc
0724     m_project->setModified(false);
0725     // Open default blank document
0726     newFile(false);
0727 }
0728 
0729 void ProjectManager::doOpenFile(const QUrl &url, KAutoSaveFile *stale, bool isBackup)
0730 {
0731     Q_ASSERT(m_project == nullptr);
0732     m_fileRevert->setEnabled(true);
0733 
0734     delete m_progressDialog;
0735     m_progressDialog = nullptr;
0736     ThumbnailCache::get()->clearCache();
0737     pCore->monitorManager()->resetDisplay();
0738     pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor);
0739     if (!m_loading) {
0740         m_progressDialog = new QProgressDialog(pCore->window());
0741         m_progressDialog->setWindowTitle(i18nc("@title:window", "Loading Project"));
0742         m_progressDialog->setCancelButton(nullptr);
0743         m_progressDialog->setLabelText(i18n("Loading project"));
0744         m_progressDialog->setMaximum(0);
0745         m_progressDialog->show();
0746         qApp->processEvents();
0747     }
0748     m_notesPlugin->clear();
0749 
0750     DocOpenResult openResult = KdenliveDoc::Open(stale ? QUrl::fromLocalFile(stale->fileName()) : url,
0751         QString(), pCore->window()->m_commandStack, false, pCore->window());
0752 
0753     KdenliveDoc *doc = nullptr;
0754     if (!openResult.isSuccessful() && !openResult.isAborted()) {
0755         if (!isBackup) {
0756             int answer = KMessageBox::warningTwoActionsCancel(
0757                 pCore->window(), i18n("Cannot open the project file. Error:\n%1\nDo you want to open a backup file?", openResult.getError()),
0758                 i18n("Error opening file"), KGuiItem(i18n("Open Backup")), KGuiItem(i18n("Recover")));
0759             if (answer == KMessageBox::PrimaryAction) { // Open Backup
0760                 slotOpenBackup(url);
0761                 return;
0762             } else if (answer == KMessageBox::SecondaryAction) { // Recover
0763                 // if file was broken by Kdenlive 0.9.4, we can try recovering it. If successful, continue through rest of this function.
0764                 openResult = KdenliveDoc::Open(stale ? QUrl::fromLocalFile(stale->fileName()) : url,
0765                     QString(), pCore->window()->m_commandStack, true, pCore->window());
0766                 if (openResult.isSuccessful()) {
0767                     doc = openResult.getDocument().release();
0768                     doc->requestBackup();
0769                 } else {
0770                     KMessageBox::error(pCore->window(), i18n("Could not recover corrupted file."));
0771                 }
0772             }
0773         } else {
0774             KMessageBox::detailedError(pCore->window(), i18n("Could not open the backup project file."), openResult.getError());
0775         }
0776     } else {
0777         doc = openResult.getDocument().release();
0778     }
0779 
0780     // if we could not open the file, and could not recover (or user declined), stop now
0781     if (!openResult.isSuccessful() || !doc) {
0782         delete m_progressDialog;
0783         m_progressDialog = nullptr;
0784         // Open default blank document
0785         newFile(false);
0786         return;
0787     }
0788 
0789     if (openResult.wasUpgraded()) {
0790         pCore->displayMessage(i18n("Your project was upgraded, a backup will be created on next save"),
0791             ErrorMessage);
0792     } else if (openResult.wasModified()) {
0793         pCore->displayMessage(i18n("Your project was modified on opening, a backup will be created on next save"),
0794             ErrorMessage);
0795     }
0796     pCore->displayMessage(QString(), OperationCompletedMessage);
0797 
0798 
0799     if (stale == nullptr) {
0800         const QString projectId = QCryptographicHash::hash(url.fileName().toUtf8(), QCryptographicHash::Md5).toHex();
0801         QUrl autosaveUrl = QUrl::fromLocalFile(QFileInfo(url.path()).absoluteDir().absoluteFilePath(projectId + QStringLiteral(".kdenlive")));
0802         stale = new KAutoSaveFile(autosaveUrl, doc);
0803         doc->m_autosave = stale;
0804     } else {
0805         doc->m_autosave = stale;
0806         stale->setParent(doc);
0807         // if loading from an autosave of unnamed file, or restore failed then keep unnamed
0808         bool loadingFailed = doc->url().isEmpty();
0809         if (url.fileName().contains(QStringLiteral("_untitled.kdenlive"))) {
0810             doc->setUrl(QUrl());
0811             doc->setModified(true);
0812         } else if (!loadingFailed) {
0813             doc->setUrl(url);
0814         }
0815         doc->setModified(!loadingFailed);
0816         stale->setParent(doc);
0817     }
0818     if (m_progressDialog) {
0819         m_progressDialog->setLabelText(i18n("Loading clips"));
0820         m_progressDialog->setMaximum(doc->clipsCount());
0821     } else {
0822         Q_EMIT pCore->loadingMessageUpdated(QString(), 0, doc->clipsCount());
0823     }
0824 
0825     pCore->bin()->setDocument(doc);
0826 
0827     // Set default target tracks to upper audio / lower video tracks
0828     m_project = doc;
0829     pCore->monitorManager()->projectMonitor()->locked = true;
0830     QDateTime documentDate = QFileInfo(m_project->url().toLocalFile()).lastModified();
0831     if (!updateTimeline(true, m_project->getDocumentProperty(QStringLiteral("previewchunks")),
0832                         m_project->getDocumentProperty(QStringLiteral("dirtypreviewchunks")), documentDate,
0833                         m_project->getDocumentProperty(QStringLiteral("disablepreview")).toInt())) {
0834         KMessageBox::error(pCore->window(), i18n("Could not recover corrupted file."));
0835         delete m_progressDialog;
0836         m_progressDialog = nullptr;
0837         // Don't propose to save corrupted doc
0838         m_project->setModified(false);
0839         // Open default blank document
0840         pCore->monitorManager()->projectMonitor()->locked = false;
0841         newFile(false);
0842         return;
0843     }
0844 
0845     // Re-open active timelines
0846     QStringList openedTimelines = m_project->getDocumentProperty(QStringLiteral("opensequences")).split(QLatin1Char(';'), Qt::SkipEmptyParts);
0847     for (auto &uid : openedTimelines) {
0848         const QUuid uuid(uid);
0849         const QString binId = pCore->projectItemModel()->getSequenceId(uuid);
0850         if (!binId.isEmpty()) {
0851             openTimeline(binId, uuid);
0852         }
0853     }
0854     // Now that sequence clips are fully built, fetch thumbnails
0855     const QStringList sequenceIds = pCore->projectItemModel()->getAllSequenceClips().values();
0856     for (auto &id : sequenceIds) {
0857         ClipLoadTask::start(ObjectId(ObjectType::BinClip, id.toInt(), QUuid()), QDomElement(), true, -1, -1, this);
0858     }
0859     // Raise last active timeline
0860     QUuid activeUuid(m_project->getDocumentProperty(QStringLiteral("activetimeline")));
0861     if (activeUuid.isNull()) {
0862         activeUuid = m_project->uuid();
0863     }
0864     if (!activeUuid.isNull()) {
0865         const QString binId = pCore->projectItemModel()->getSequenceId(activeUuid);
0866         if (binId.isEmpty()) {
0867             if (pCore->projectItemModel()->sequenceCount() == 0) {
0868                 // Something is broken here, abort
0869                 abortLoading();
0870                 return;
0871             }
0872         } else {
0873             openTimeline(binId, activeUuid);
0874         }
0875     }
0876     pCore->window()->connectDocument();
0877     // Now load active sequence in project monitor
0878     pCore->monitorManager()->projectMonitor()->locked = false;
0879     int position = m_project->getSequenceProperty(activeUuid, QStringLiteral("position"), QString::number(0)).toInt();
0880     pCore->monitorManager()->projectMonitor()->setProducer(m_activeTimelineModel->producer(), position);
0881 
0882     Q_EMIT docOpened(m_project);
0883     pCore->displayMessage(QString(), OperationCompletedMessage, 100);
0884     m_lastSave.start();
0885     m_project->loading = false;
0886     if (pCore->monitorManager()) {
0887         Q_EMIT pCore->monitorManager()->updatePreviewScaling();
0888         pCore->monitorManager()->projectMonitor()->slotActivateMonitor();
0889         pCore->monitorManager()->projectMonitor()->setProducer(m_activeTimelineModel->producer(), 0);
0890         const QUuid uuid = m_project->activeUuid;
0891         pCore->monitorManager()->projectMonitor()->adjustRulerSize(m_activeTimelineModel->duration() - 1, m_project->getFilteredGuideModel(uuid));
0892     }
0893     pCore->projectItemModel()->missingClipTimer.start();
0894     delete m_progressDialog;
0895     m_progressDialog = nullptr;
0896 }
0897 
0898 void ProjectManager::slotRevert()
0899 {
0900     if (m_project->isModified() &&
0901         KMessageBox::warningContinueCancel(pCore->window(),
0902                                            i18n("This will delete all changes made since you last saved your project. Are you sure you want to continue?"),
0903                                            i18n("Revert to last saved version")) == KMessageBox::Cancel) {
0904         return;
0905     }
0906     QUrl url = m_project->url();
0907     if (closeCurrentDocument(false)) {
0908         doOpenFile(url, nullptr);
0909     }
0910 }
0911 
0912 KdenliveDoc *ProjectManager::current()
0913 {
0914     return m_project;
0915 }
0916 
0917 bool ProjectManager::slotOpenBackup(const QUrl &url)
0918 {
0919     QUrl projectFile;
0920     QUrl projectFolder;
0921     QString projectId;
0922     if (url.isValid()) {
0923         // we could not open the project file, guess where the backups are
0924         projectFolder = QUrl::fromLocalFile(KdenliveSettings::defaultprojectfolder());
0925         projectFile = url;
0926     } else {
0927         projectFolder = QUrl::fromLocalFile(m_project ? m_project->projectTempFolder() : QString());
0928         projectFile = m_project->url();
0929         projectId = m_project->getDocumentProperty(QStringLiteral("documentid"));
0930     }
0931     bool result = false;
0932     QPointer<BackupWidget> dia = new BackupWidget(projectFile, projectFolder, projectId, pCore->window());
0933     if (dia->exec() == QDialog::Accepted) {
0934         QString requestedBackup = dia->selectedFile();
0935         if (m_project) {
0936             m_project->backupLastSavedVersion(projectFile.toLocalFile());
0937             closeCurrentDocument(false);
0938         }
0939         doOpenFile(QUrl::fromLocalFile(requestedBackup), nullptr, true);
0940         if (m_project) {
0941             if (!m_project->url().isEmpty()) {
0942                 // Only update if restore succeeded
0943                 pCore->window()->slotEditSubtitle();
0944                 m_project->setUrl(projectFile);
0945                 m_project->setModified(true);
0946             }
0947             pCore->window()->setWindowTitle(m_project->description());
0948             result = true;
0949         }
0950     }
0951     delete dia;
0952     return result;
0953 }
0954 
0955 KRecentFilesAction *ProjectManager::recentFilesAction()
0956 {
0957     return m_recentFilesAction;
0958 }
0959 
0960 void ProjectManager::slotStartAutoSave()
0961 {
0962     if (m_lastSave.elapsed() > 300000) {
0963         // If the project was not saved in the last 5 minute, force save
0964         m_autoSaveTimer.stop();
0965         slotAutoSave();
0966     } else {
0967         m_autoSaveTimer.start(3000); // will trigger slotAutoSave() in 3 seconds
0968     }
0969 }
0970 
0971 void ProjectManager::slotAutoSave()
0972 {
0973     prepareSave();
0974     QString saveFolder = m_project->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile();
0975     QString scene = projectSceneList(saveFolder);
0976     if (!m_replacementPattern.isEmpty()) {
0977         QMapIterator<QString, QString> i(m_replacementPattern);
0978         while (i.hasNext()) {
0979             i.next();
0980             scene.replace(i.key(), i.value());
0981         }
0982     }
0983     if (!scene.contains(QLatin1String("<track "))) {
0984         // In some unexplained cases, the MLT playlist is corrupted and all tracks are deleted. Don't save in that case.
0985         pCore->displayMessage(i18n("Project was corrupted, cannot backup. Please close and reopen your project file to recover last backup"), ErrorMessage);
0986         return;
0987     }
0988     m_project->slotAutoSave(scene);
0989     m_lastSave.start();
0990 }
0991 
0992 QString ProjectManager::projectSceneList(const QString &outputFolder, const QString &overlayData)
0993 {
0994     // Disable multitrack view and overlay
0995     bool isMultiTrack = pCore->monitorManager()->isMultiTrack();
0996     bool hasPreview = pCore->window()->getCurrentTimeline()->controller()->hasPreviewTrack();
0997     bool isTrimming = pCore->monitorManager()->isTrimming();
0998     if (isMultiTrack) {
0999         pCore->window()->getCurrentTimeline()->controller()->slotMultitrackView(false, false);
1000     }
1001     if (hasPreview) {
1002         pCore->window()->getCurrentTimeline()->model()->updatePreviewConnection(false);
1003     }
1004     if (isTrimming) {
1005         pCore->window()->getCurrentTimeline()->controller()->requestEndTrimmingMode();
1006     }
1007     pCore->mixer()->pauseMonitoring(true);
1008     // We must save from the primary timeline model
1009     int duration = pCore->window() ? pCore->window()->getCurrentTimeline()->controller()->duration() : m_activeTimelineModel->duration();
1010     QString scene = pCore->projectItemModel()->sceneList(outputFolder, QString(), overlayData, m_activeTimelineModel->tractor(), duration);
1011     pCore->mixer()->pauseMonitoring(false);
1012     if (isMultiTrack) {
1013         pCore->window()->getCurrentTimeline()->controller()->slotMultitrackView(true, false);
1014     }
1015     if (hasPreview) {
1016         pCore->window()->getCurrentTimeline()->model()->updatePreviewConnection(true);
1017     }
1018     if (isTrimming) {
1019         pCore->window()->getCurrentTimeline()->controller()->requestStartTrimmingMode();
1020     }
1021     return scene;
1022 }
1023 
1024 void ProjectManager::setDocumentNotes(const QString &notes)
1025 {
1026     if (m_notesPlugin) {
1027         m_notesPlugin->widget()->setHtml(notes);
1028     }
1029 }
1030 
1031 QString ProjectManager::documentNotes() const
1032 {
1033     QString text = m_notesPlugin->widget()->toPlainText().simplified();
1034     if (text.isEmpty()) {
1035         return QString();
1036     }
1037     return m_notesPlugin->widget()->toHtml();
1038 }
1039 
1040 void ProjectManager::slotAddProjectNote()
1041 {
1042     m_notesPlugin->showDock();
1043     m_notesPlugin->widget()->setFocus();
1044     m_notesPlugin->widget()->addProjectNote();
1045 }
1046 
1047 void ProjectManager::slotAddTextNote(const QString &text)
1048 {
1049     m_notesPlugin->showDock();
1050     m_notesPlugin->widget()->setFocus();
1051     m_notesPlugin->widget()->addTextNote(text);
1052 }
1053 
1054 void ProjectManager::prepareSave()
1055 {
1056     pCore->projectItemModel()->saveDocumentProperties(pCore->window()->getCurrentTimeline()->controller()->documentProperties(), m_project->metadata());
1057     pCore->bin()->saveFolderState();
1058     pCore->projectItemModel()->saveProperty(QStringLiteral("kdenlive:documentnotes"), documentNotes());
1059     pCore->projectItemModel()->saveProperty(QStringLiteral("kdenlive:docproperties.opensequences"), pCore->window()->openedSequences().join(QLatin1Char(';')));
1060     pCore->projectItemModel()->saveProperty(QStringLiteral("kdenlive:docproperties.activetimeline"), m_activeTimelineModel->uuid().toString());
1061 }
1062 
1063 void ProjectManager::slotResetProfiles(bool reloadThumbs)
1064 {
1065     m_project->resetProfile(reloadThumbs);
1066     pCore->monitorManager()->updateScopeSource();
1067 }
1068 
1069 void ProjectManager::slotResetConsumers(bool fullReset)
1070 {
1071     pCore->monitorManager()->resetConsumers(fullReset);
1072 }
1073 
1074 void ProjectManager::disableBinEffects(bool disable, bool refreshMonitor)
1075 {
1076     if (m_project) {
1077         if (disable) {
1078             m_project->setDocumentProperty(QStringLiteral("disablebineffects"), QString::number(1));
1079         } else {
1080             m_project->setDocumentProperty(QStringLiteral("disablebineffects"), QString());
1081         }
1082     }
1083     if (refreshMonitor) {
1084         pCore->monitorManager()->refreshProjectMonitor();
1085         pCore->monitorManager()->refreshClipMonitor();
1086     }
1087 }
1088 
1089 void ProjectManager::slotDisableTimelineEffects(bool disable)
1090 {
1091     if (disable) {
1092         m_project->setDocumentProperty(QStringLiteral("disabletimelineeffects"), QString::number(true));
1093     } else {
1094         m_project->setDocumentProperty(QStringLiteral("disabletimelineeffects"), QString());
1095     }
1096     m_activeTimelineModel->setTimelineEffectsEnabled(!disable);
1097     pCore->monitorManager()->refreshProjectMonitor();
1098 }
1099 
1100 void ProjectManager::slotSwitchTrackDisabled()
1101 {
1102     pCore->window()->getCurrentTimeline()->controller()->switchTrackDisabled();
1103 }
1104 
1105 void ProjectManager::slotSwitchTrackLock()
1106 {
1107     pCore->window()->getCurrentTimeline()->controller()->switchTrackLock();
1108 }
1109 
1110 void ProjectManager::slotSwitchTrackActive()
1111 {
1112     pCore->window()->getCurrentTimeline()->controller()->switchTrackActive();
1113 }
1114 
1115 void ProjectManager::slotSwitchAllTrackActive()
1116 {
1117     pCore->window()->getCurrentTimeline()->controller()->switchAllTrackActive();
1118 }
1119 
1120 void ProjectManager::slotMakeAllTrackActive()
1121 {
1122     pCore->window()->getCurrentTimeline()->controller()->makeAllTrackActive();
1123 }
1124 
1125 void ProjectManager::slotRestoreTargetTracks()
1126 {
1127     pCore->window()->getCurrentTimeline()->controller()->restoreTargetTracks();
1128 }
1129 
1130 void ProjectManager::slotSwitchAllTrackLock()
1131 {
1132     pCore->window()->getCurrentTimeline()->controller()->switchTrackLock(true);
1133 }
1134 
1135 void ProjectManager::slotSwitchTrackTarget()
1136 {
1137     pCore->window()->getCurrentTimeline()->controller()->switchTargetTrack();
1138 }
1139 
1140 QString ProjectManager::getDefaultProjectFormat()
1141 {
1142     // 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
1143     // our profile, propose a new default.
1144     QTimeZone zone;
1145     zone = QTimeZone::systemTimeZone();
1146 
1147     QList<int> ntscCountries;
1148     ntscCountries << QLocale::Canada << QLocale::Chile << QLocale::CostaRica << QLocale::Cuba << QLocale::DominicanRepublic << QLocale::Ecuador;
1149     ntscCountries << QLocale::Japan << QLocale::Mexico << QLocale::Nicaragua << QLocale::Panama << QLocale::Peru << QLocale::Philippines;
1150     ntscCountries << QLocale::PuertoRico << QLocale::SouthKorea << QLocale::Taiwan << QLocale::UnitedStates;
1151     bool ntscProject = ntscCountries.contains(zone.country());
1152     if (!ntscProject) {
1153         return QStringLiteral("atsc_1080p_25");
1154     }
1155     return QStringLiteral("atsc_1080p_2997");
1156 }
1157 
1158 void ProjectManager::saveZone(const QStringList &info, const QDir &dir)
1159 {
1160     pCore->bin()->saveZone(info, dir);
1161 }
1162 
1163 void ProjectManager::moveProjectData(const QString &src, const QString &dest)
1164 {
1165     // Move proxies
1166     bool ok;
1167     const QList<QUrl> proxyUrls = m_project->getProjectData(&ok);
1168     if (!ok) {
1169         // Could not move temporary data, abort
1170         KMessageBox::error(pCore->window(), i18n("Error moving project folder, cannot access cache folder"));
1171         return;
1172     }
1173     Fun copyTmp = [this, src, dest]() {
1174         // Move tmp folder (thumbnails, timeline preview)
1175         KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(src), QUrl::fromLocalFile(dest), KIO::DefaultFlags);
1176         if (copyJob->uiDelegate()) {
1177             KJobWidgets::setWindow(copyJob, pCore->window());
1178         }
1179         connect(copyJob, &KJob::percentChanged, this, &ProjectManager::slotMoveProgress);
1180         connect(copyJob, &KJob::result, this, &ProjectManager::slotMoveFinished);
1181         return true;
1182     };
1183     if (!proxyUrls.isEmpty()) {
1184         QDir proxyDir(dest + QStringLiteral("/proxy/"));
1185         if (proxyDir.mkpath(QStringLiteral("."))) {
1186             KIO::CopyJob *job = KIO::move(proxyUrls, QUrl::fromLocalFile(proxyDir.absolutePath()));
1187             connect(job, &KJob::percentChanged, this, &ProjectManager::slotMoveProgress);
1188             connect(job, &KJob::result, this, [this, copyTmp](KJob *job) {
1189                 if (job->error() == 0) {
1190                     copyTmp();
1191                 } else {
1192                     KMessageBox::error(pCore->window(), i18n("Error moving project folder: %1", job->errorText()));
1193                 }
1194             });
1195             if (job->uiDelegate()) {
1196                 KJobWidgets::setWindow(job, pCore->window());
1197             }
1198         }
1199     } else {
1200         copyTmp();
1201     }
1202 }
1203 
1204 void ProjectManager::slotMoveProgress(KJob *, unsigned long progress)
1205 {
1206     pCore->displayMessage(i18n("Moving project folder"), ProcessingJobMessage, static_cast<int>(progress));
1207 }
1208 
1209 void ProjectManager::slotMoveFinished(KJob *job)
1210 {
1211     if (job->error() == 0) {
1212         pCore->displayMessage(QString(), OperationCompletedMessage, 100);
1213         auto *copyJob = static_cast<KIO::CopyJob *>(job);
1214         QString newFolder = copyJob->destUrl().toLocalFile();
1215         // Check if project folder is inside document folder, in which case, paths will be relative
1216         QDir projectDir(m_project->url().toString(QUrl::RemoveFilename | QUrl::RemoveScheme));
1217         QDir srcDir(m_project->projectTempFolder());
1218         if (srcDir.absolutePath().startsWith(projectDir.absolutePath())) {
1219             m_replacementPattern.insert(QStringLiteral(">proxy/"), QStringLiteral(">") + newFolder + QStringLiteral("/proxy/"));
1220         } else {
1221             m_replacementPattern.insert(m_project->projectTempFolder() + QStringLiteral("/proxy/"), newFolder + QStringLiteral("/proxy/"));
1222         }
1223         m_project->setProjectFolder(QUrl::fromLocalFile(newFolder));
1224         saveFile();
1225         m_replacementPattern.clear();
1226         slotRevert();
1227     } else {
1228         KMessageBox::error(pCore->window(), i18n("Error moving project folder: %1", job->errorText()));
1229     }
1230 }
1231 
1232 void ProjectManager::requestBackup(const QString &errorMessage)
1233 {
1234     KMessageBox::ButtonCode res = KMessageBox::warningContinueCancel(qApp->activeWindow(), errorMessage);
1235     pCore->window()->getCurrentTimeline()->loading = false;
1236     m_project->setModified(false);
1237     if (res == KMessageBox::Continue) {
1238         // Try opening backup
1239         if (!slotOpenBackup(m_project->url())) {
1240             newFile(false);
1241         }
1242     } else {
1243         newFile(false);
1244     }
1245 }
1246 
1247 bool ProjectManager::updateTimeline(bool createNewTab, const QString &chunks, const QString &dirty, const QDateTime &documentDate, bool enablePreview)
1248 {
1249     pCore->taskManager.slotCancelJobs();
1250     const QUuid uuid = m_project->uuid();
1251     std::unique_ptr<Mlt::Producer> xmlProd(
1252         new Mlt::Producer(pCore->getProjectProfile().get_profile(), "xml-string", m_project->getAndClearProjectXml().constData()));
1253     Mlt::Service s(*xmlProd.get());
1254     Mlt::Tractor tractor(s);
1255     if (xmlProd->property_exists("kdenlive:projectTractor")) {
1256         // This is the new multi-timeline document format
1257         m_project->cleanupTimelinePreview(documentDate);
1258         pCore->projectItemModel()->buildPlaylist(uuid);
1259         // Load bin playlist
1260         return loadProjectBin(tractor, m_progressDialog);
1261     }
1262     if (tractor.count() == 0) {
1263         // Wow we have a project file with empty tractor, probably corrupted, propose to open a recovery file
1264         requestBackup(i18n("Project file is corrupted (no tracks). Try to find a backup file?"));
1265         return false;
1266     }
1267     std::shared_ptr<TimelineItemModel> timelineModel = TimelineItemModel::construct(uuid, m_project->commandStack());
1268 
1269     if (m_project->hasDocumentProperty(QStringLiteral("groups"))) {
1270         // This is a pre-nesting project file, move all timeline properties to the timelineModel's tractor
1271         QStringList importedProperties({QStringLiteral("groups"), QStringLiteral("guides"), QStringLiteral("zonein"), QStringLiteral("zoneout"),
1272                                         QStringLiteral("audioTarget"), QStringLiteral("videoTarget"), QStringLiteral("activeTrack"), QStringLiteral("position"),
1273                                         QStringLiteral("scrollPos"), QStringLiteral("disablepreview"), QStringLiteral("previewchunks"),
1274                                         QStringLiteral("dirtypreviewchunks")});
1275         m_project->importSequenceProperties(uuid, importedProperties);
1276     } else {
1277         qDebug() << ":::: NOT FOUND DOCUMENT GUIDES !!!!!!!!!!!\n!!!!!!!!!!!!!!!!!!!!!";
1278     }
1279     m_project->addTimeline(uuid, timelineModel);
1280     TimelineWidget *documentTimeline = nullptr;
1281 
1282     m_project->cleanupTimelinePreview(documentDate);
1283     if (pCore->window()) {
1284         if (!createNewTab) {
1285             documentTimeline = pCore->window()->getCurrentTimeline();
1286             documentTimeline->setModel(timelineModel, pCore->monitorManager()->projectMonitor()->getControllerProxy());
1287         } else {
1288             // Create a new timeline tab
1289             documentTimeline = pCore->window()->openTimeline(uuid, i18n("Sequence 1"), timelineModel);
1290         }
1291     }
1292     pCore->projectItemModel()->buildPlaylist(uuid);
1293     if (m_activeTimelineModel == nullptr) {
1294         m_activeTimelineModel = timelineModel;
1295         m_project->activeUuid = timelineModel->uuid();
1296     }
1297     if (!constructTimelineFromTractor(timelineModel, pCore->projectItemModel(), tractor, m_progressDialog, m_project->modifiedDecimalPoint(), chunks, dirty,
1298                                       enablePreview)) {
1299         // TODO: act on project load failure
1300         qDebug() << "// Project failed to load!!";
1301         requestBackup(i18n("Project file is corrupted - failed to load tracks. Try to find a backup file?"));
1302         return false;
1303     }
1304     // Free memory used by original playlist
1305     xmlProd->clear();
1306     xmlProd.reset(nullptr);
1307     // Build primary timeline sequence
1308     Fun undo = []() { return true; };
1309     Fun redo = []() { return true; };
1310     // Create the timelines folder to store timeline clips
1311     QString folderId = pCore->projectItemModel()->getFolderIdByName(i18n("Sequences"));
1312     if (folderId.isEmpty()) {
1313         pCore->projectItemModel()->requestAddFolder(folderId, i18n("Sequences"), QStringLiteral("-1"), undo, redo);
1314     }
1315     QString mainId;
1316     QPair<int, int> tracks = timelineModel->getAVtracksCount();
1317     std::shared_ptr<Mlt::Producer> prod = std::make_shared<Mlt::Producer>(timelineModel->tractor()->cut());
1318     prod->parent().set("id", uuid.toString().toUtf8().constData());
1319     prod->set("kdenlive:uuid", uuid.toString().toUtf8().constData());
1320     prod->set("kdenlive:clipname", i18n("Sequence 1").toUtf8().constData());
1321     prod->set("kdenlive:producer_type", ClipType::Timeline);
1322     prod->set("kdenlive:sequenceproperties.tracksCount", tracks.first + tracks.second);
1323     prod->parent().set("kdenlive:uuid", uuid.toString().toUtf8().constData());
1324     prod->parent().set("kdenlive:clipname", i18n("Sequence 1").toUtf8().constData());
1325     prod->parent().set("kdenlive:sequenceproperties.hasAudio", tracks.first > 0 ? 1 : 0);
1326     prod->parent().set("kdenlive:sequenceproperties.hasVideo", tracks.second > 0 ? 1 : 0);
1327     if (m_project->hasSequenceProperty(uuid, QStringLiteral("activeTrack"))) {
1328         int activeTrack = m_project->getSequenceProperty(uuid, QStringLiteral("activeTrack")).toInt();
1329         prod->parent().set("kdenlive:sequenceproperties.activeTrack", activeTrack);
1330     }
1331     prod->parent().set("kdenlive:sequenceproperties.tracksCount", tracks.first + tracks.second);
1332     prod->parent().set("kdenlive:sequenceproperties.documentuuid", m_project->uuid().toString().toUtf8().constData());
1333     if (tractor.property_exists("kdenlive:duration")) {
1334         const QString duration(tractor.get("kdenlive:duration"));
1335         const QString maxduration(tractor.get("kdenlive:maxduration"));
1336         prod->parent().set("kdenlive:duration", duration.toLatin1().constData());
1337         prod->parent().set("kdenlive:maxduration", maxduration.toLatin1().constData());
1338     } else {
1339         // Fetch duration from actual tractor
1340         int projectDuration = timelineModel->duration();
1341         if (pCore->window()) {
1342             documentTimeline->controller()->checkDuration();
1343         }
1344         prod->parent().set("kdenlive:duration", timelineModel->tractor()->frames_to_time(projectDuration + 1));
1345         prod->parent().set("kdenlive:maxduration", projectDuration + 1);
1346         prod->parent().set("length", projectDuration + 1);
1347         prod->parent().set("out", projectDuration);
1348     }
1349     prod->parent().set("kdenlive:producer_type", ClipType::Timeline);
1350     // QString retain = QStringLiteral("xml_retain %1").arg(uuid.toString());
1351     // pCore->projectItemModel()->projectTractor()->set(retain.toUtf8().constData(), timelineModel->tractor()->get_service(), 0);
1352     pCore->projectItemModel()->requestAddBinClip(mainId, prod, folderId, undo, redo);
1353     pCore->projectItemModel()->setSequencesFolder(folderId.toInt());
1354     if (pCore->window()) {
1355         pCore->bin()->registerSequence(uuid, mainId);
1356         QObject::connect(timelineModel.get(), &TimelineModel::durationUpdated, this, &ProjectManager::updateSequenceDuration);
1357     }
1358     std::shared_ptr<ProjectClip> mainClip = pCore->projectItemModel()->getClipByBinID(mainId);
1359     timelineModel->setMarkerModel(mainClip->markerModel());
1360     m_project->loadSequenceGroupsAndGuides(uuid);
1361     if (documentTimeline) {
1362         documentTimeline->loadMarkerModel();
1363     }
1364     timelineModel->setUndoStack(m_project->commandStack());
1365 
1366     // Reset locale to C to ensure numbers are serialised correctly
1367     LocaleHandling::resetLocale();
1368     return true;
1369 }
1370 
1371 void ProjectManager::updateSequenceDuration(const QUuid &uuid)
1372 {
1373     const QString binId = pCore->projectItemModel()->getSequenceId(uuid);
1374     std::shared_ptr<ProjectClip> mainClip = pCore->projectItemModel()->getClipByBinID(binId);
1375     std::shared_ptr<TimelineItemModel> model = m_project->getTimeline(uuid);
1376     qDebug() << "::: UPDATING MAIN TIMELINE DURATION: " << model->duration();
1377     if (mainClip && model) {
1378         QMap<QString, QString> properties;
1379         properties.insert(QStringLiteral("kdenlive:duration"), QString(model->tractor()->frames_to_time(model->duration())));
1380         properties.insert(QStringLiteral("kdenlive:maxduration"), QString::number(model->duration()));
1381         properties.insert(QStringLiteral("length"), QString::number(model->duration()));
1382         properties.insert(QStringLiteral("out"), QString::number(model->duration() - 1));
1383         mainClip->setProperties(properties, true);
1384     } else {
1385         qDebug() << ":::: MAIN CLIP PRODUCER NOT FOUND!!!";
1386     }
1387 }
1388 
1389 void ProjectManager::adjustProjectDuration(int duration)
1390 {
1391     pCore->monitorManager()->projectMonitor()->adjustRulerSize(duration - 1, nullptr);
1392 }
1393 
1394 void ProjectManager::activateAsset(const QVariantMap &effectData)
1395 {
1396     if (effectData.contains(QStringLiteral("kdenlive/effect"))) {
1397         pCore->window()->addEffect(effectData.value(QStringLiteral("kdenlive/effect")).toString());
1398     } else {
1399         pCore->window()->getCurrentTimeline()->controller()->addAsset(effectData);
1400     }
1401 }
1402 
1403 std::shared_ptr<MarkerListModel> ProjectManager::getGuideModel()
1404 {
1405     return current()->getGuideModel(pCore->currentTimelineId());
1406 }
1407 
1408 std::shared_ptr<DocUndoStack> ProjectManager::undoStack()
1409 {
1410     return current()->commandStack();
1411 }
1412 
1413 const QDir ProjectManager::cacheDir(bool audio, bool *ok) const
1414 {
1415     if (m_project == nullptr) {
1416         *ok = false;
1417         return QDir();
1418     }
1419     return m_project->getCacheDir(audio ? CacheAudio : CacheThumbs, ok);
1420 }
1421 
1422 void ProjectManager::saveWithUpdatedProfile(const QString &updatedProfile)
1423 {
1424     // First backup current project with fps appended
1425     bool saveInTempFile = false;
1426     if (m_project && m_project->isModified()) {
1427         switch (KMessageBox::warningTwoActionsCancel(pCore->window(),
1428                                                      i18n("The project <b>\"%1\"</b> has been changed.\nDo you want to save your changes?",
1429                                                           m_project->url().fileName().isEmpty() ? i18n("Untitled") : m_project->url().fileName()),
1430                                                      {}, KStandardGuiItem::save(), KStandardGuiItem::dontSave())) {
1431         case KMessageBox::PrimaryAction:
1432             // save document here. If saving fails, return false;
1433             if (!saveFile()) {
1434                 pCore->displayBinMessage(i18n("Project profile change aborted"), KMessageWidget::Information);
1435                 return;
1436             }
1437             break;
1438         case KMessageBox::Cancel:
1439             pCore->displayBinMessage(i18n("Project profile change aborted"), KMessageWidget::Information);
1440             return;
1441             break;
1442         default:
1443             saveInTempFile = true;
1444             break;
1445         }
1446     }
1447 
1448     if (!m_project) {
1449         pCore->displayBinMessage(i18n("Project profile change aborted"), KMessageWidget::Information);
1450         return;
1451     }
1452     QString currentFile = m_project->url().toLocalFile();
1453 
1454     // Now update to new profile
1455     auto &newProfile = ProfileRepository::get()->getProfile(updatedProfile);
1456     QString convertedFile = QStringUtils::appendToFilename(currentFile, QString("-%1").arg(int(newProfile->fps() * 100)));
1457     QString saveFolder = m_project->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile();
1458     QTemporaryFile tmpFile(saveFolder + "/kdenlive-XXXXXX.mlt");
1459     if (saveInTempFile) {
1460         // Save current playlist in tmp file
1461         if (!tmpFile.open()) {
1462             // Something went wrong
1463             pCore->displayBinMessage(i18n("Project profile change aborted"), KMessageWidget::Information);
1464             return;
1465         }
1466         prepareSave();
1467         QString scene = projectSceneList(saveFolder);
1468         if (!m_replacementPattern.isEmpty()) {
1469             QMapIterator<QString, QString> i(m_replacementPattern);
1470             while (i.hasNext()) {
1471                 i.next();
1472                 scene.replace(i.key(), i.value());
1473             }
1474         }
1475         tmpFile.write(scene.toUtf8());
1476         if (tmpFile.error() != QFile::NoError) {
1477             tmpFile.close();
1478             return;
1479         }
1480         tmpFile.close();
1481         currentFile = tmpFile.fileName();
1482         // Don't ask again to save
1483         m_project->setModified(false);
1484     }
1485 
1486     QDomDocument doc;
1487     if (!Xml::docContentFromFile(doc, currentFile, false)) {
1488         KMessageBox::error(qApp->activeWindow(), i18n("Cannot read file %1", currentFile));
1489         return;
1490     }
1491 
1492     QDomElement mltProfile = doc.documentElement().firstChildElement(QStringLiteral("profile"));
1493     if (!mltProfile.isNull()) {
1494         mltProfile.setAttribute(QStringLiteral("frame_rate_num"), newProfile->frame_rate_num());
1495         mltProfile.setAttribute(QStringLiteral("frame_rate_den"), newProfile->frame_rate_den());
1496         mltProfile.setAttribute(QStringLiteral("display_aspect_num"), newProfile->display_aspect_num());
1497         mltProfile.setAttribute(QStringLiteral("display_aspect_den"), newProfile->display_aspect_den());
1498         mltProfile.setAttribute(QStringLiteral("sample_aspect_num"), newProfile->sample_aspect_num());
1499         mltProfile.setAttribute(QStringLiteral("sample_aspect_den"), newProfile->sample_aspect_den());
1500         mltProfile.setAttribute(QStringLiteral("colorspace"), newProfile->colorspace());
1501         mltProfile.setAttribute(QStringLiteral("progressive"), newProfile->progressive());
1502         mltProfile.setAttribute(QStringLiteral("description"), newProfile->description());
1503         mltProfile.setAttribute(QStringLiteral("width"), newProfile->width());
1504         mltProfile.setAttribute(QStringLiteral("height"), newProfile->height());
1505     }
1506     QDomNodeList playlists = doc.documentElement().elementsByTagName(QStringLiteral("playlist"));
1507     double fpsRatio = newProfile->fps() / pCore->getCurrentFps();
1508     for (int i = 0; i < playlists.count(); ++i) {
1509         QDomElement e = playlists.at(i).toElement();
1510         if (e.attribute(QStringLiteral("id")) == QLatin1String("main_bin")) {
1511             Xml::setXmlProperty(e, QStringLiteral("kdenlive:docproperties.profile"), updatedProfile);
1512             // Update guides
1513             const QString &guidesData = Xml::getXmlProperty(e, QStringLiteral("kdenlive:docproperties.guides"));
1514             if (!guidesData.isEmpty()) {
1515                 // Update guides position
1516                 auto json = QJsonDocument::fromJson(guidesData.toUtf8());
1517 
1518                 QJsonArray updatedList;
1519                 if (json.isArray()) {
1520                     auto list = json.array();
1521                     for (const auto &entry : qAsConst(list)) {
1522                         if (!entry.isObject()) {
1523                             qDebug() << "Warning : Skipping invalid marker data";
1524                             continue;
1525                         }
1526                         auto entryObj = entry.toObject();
1527                         if (!entryObj.contains(QLatin1String("pos"))) {
1528                             qDebug() << "Warning : Skipping invalid marker data (does not contain position)";
1529                             continue;
1530                         }
1531                         int pos = qRound(double(entryObj[QLatin1String("pos")].toInt()) * fpsRatio);
1532                         QJsonObject currentMarker;
1533                         currentMarker.insert(QLatin1String("pos"), QJsonValue(pos));
1534                         currentMarker.insert(QLatin1String("comment"), entryObj[QLatin1String("comment")]);
1535                         currentMarker.insert(QLatin1String("type"), entryObj[QLatin1String("type")]);
1536                         updatedList.push_back(currentMarker);
1537                     }
1538                     QJsonDocument updatedJSon(updatedList);
1539                     Xml::setXmlProperty(e, QStringLiteral("kdenlive:docproperties.guides"), QString::fromUtf8(updatedJSon.toJson()));
1540                 }
1541             }
1542             break;
1543         }
1544     }
1545     QDomNodeList producers = doc.documentElement().elementsByTagName(QStringLiteral("producer"));
1546     for (int i = 0; i < producers.count(); ++i) {
1547         QDomElement e = producers.at(i).toElement();
1548         bool ok;
1549         if (Xml::getXmlProperty(e, QStringLiteral("mlt_service")) == QLatin1String("qimage") && Xml::hasXmlProperty(e, QStringLiteral("ttl"))) {
1550             // Slideshow, duration is frame based, should be calculated again
1551             Xml::setXmlProperty(e, QStringLiteral("length"), QStringLiteral("0"));
1552             Xml::removeXmlProperty(e, QStringLiteral("kdenlive:duration"));
1553             e.setAttribute(QStringLiteral("out"), -1);
1554             continue;
1555         }
1556         int length = Xml::getXmlProperty(e, QStringLiteral("length")).toInt(&ok);
1557         if (ok && length > 0) {
1558             // calculate updated length
1559             Xml::setXmlProperty(e, QStringLiteral("length"), pCore->window()->getCurrentTimeline()->controller()->framesToClock(length));
1560         }
1561     }
1562     QDomNodeList chains = doc.documentElement().elementsByTagName(QStringLiteral("chain"));
1563     for (int i = 0; i < chains.count(); ++i) {
1564         QDomElement e = chains.at(i).toElement();
1565         bool ok;
1566         if (Xml::getXmlProperty(e, QStringLiteral("mlt_service")) == QLatin1String("qimage") && Xml::hasXmlProperty(e, QStringLiteral("ttl"))) {
1567             // Slideshow, duration is frame based, should be calculated again
1568             Xml::setXmlProperty(e, QStringLiteral("length"), QStringLiteral("0"));
1569             Xml::removeXmlProperty(e, QStringLiteral("kdenlive:duration"));
1570             e.setAttribute(QStringLiteral("out"), -1);
1571             continue;
1572         }
1573         int length = Xml::getXmlProperty(e, QStringLiteral("length")).toInt(&ok);
1574         if (ok && length > 0) {
1575             // calculate updated length
1576             Xml::setXmlProperty(e, QStringLiteral("length"), pCore->window()->getCurrentTimeline()->controller()->framesToClock(length));
1577         }
1578     }
1579     if (QFile::exists(convertedFile)) {
1580         if (KMessageBox::warningTwoActions(qApp->activeWindow(), i18n("Output file %1 already exists.\nDo you want to overwrite it?", convertedFile), {},
1581                                            KStandardGuiItem::overwrite(), KStandardGuiItem::cancel()) != KMessageBox::PrimaryAction) {
1582             return;
1583         }
1584     }
1585     QFile file(convertedFile);
1586     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
1587         return;
1588     }
1589     QTextStream out(&file);
1590 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
1591     out.setCodec("UTF-8");
1592 #endif
1593     out << doc.toString();
1594     if (file.error() != QFile::NoError) {
1595         KMessageBox::error(qApp->activeWindow(), i18n("Cannot write to file %1", convertedFile));
1596         file.close();
1597         return;
1598     }
1599     file.close();
1600     // Copy subtitle file if any
1601     if (QFile::exists(currentFile + QStringLiteral(".srt"))) {
1602         QFile(currentFile + QStringLiteral(".srt")).copy(convertedFile + QStringLiteral(".srt"));
1603     }
1604     openFile(QUrl::fromLocalFile(convertedFile));
1605     pCore->displayBinMessage(i18n("Project profile changed"), KMessageWidget::Information);
1606 }
1607 
1608 QPair<int, int> ProjectManager::avTracksCount()
1609 {
1610     return pCore->window()->getCurrentTimeline()->controller()->getAvTracksCount();
1611 }
1612 
1613 void ProjectManager::addAudioTracks(int tracksCount)
1614 {
1615     pCore->window()->getCurrentTimeline()->controller()->addTracks(0, tracksCount);
1616 }
1617 
1618 void ProjectManager::initSequenceProperties(const QUuid &uuid, std::pair<int, int> tracks)
1619 {
1620     // Initialize default timeline properties
1621     m_project->setSequenceProperty(uuid, QStringLiteral("documentuuid"), m_project->uuid().toString());
1622     m_project->setSequenceProperty(uuid, QStringLiteral("zoom"), 8);
1623     m_project->setSequenceProperty(uuid, QStringLiteral("verticalzoom"), 1);
1624     m_project->setSequenceProperty(uuid, QStringLiteral("zonein"), 0);
1625     m_project->setSequenceProperty(uuid, QStringLiteral("zoneout"), 75);
1626     m_project->setSequenceProperty(uuid, QStringLiteral("tracks"), tracks.first + tracks.second);
1627     m_project->setSequenceProperty(uuid, QStringLiteral("hasAudio"), tracks.first > 0 ? 1 : 0);
1628     m_project->setSequenceProperty(uuid, QStringLiteral("hasVideo"), tracks.second > 0 ? 1 : 0);
1629     const int activeTrack = tracks.second > 0 ? tracks.first : tracks.first - 1;
1630     m_project->setSequenceProperty(uuid, QStringLiteral("activeTrack"), activeTrack);
1631 }
1632 
1633 bool ProjectManager::openTimeline(const QString &id, const QUuid &uuid, int position, bool duplicate, std::shared_ptr<TimelineItemModel> existingModel)
1634 {
1635     if (position > -1) {
1636         m_project->setSequenceProperty(uuid, QStringLiteral("position"), position);
1637     }
1638     if (pCore->window() && pCore->window()->raiseTimeline(uuid)) {
1639         return false;
1640     }
1641 
1642     // Disable autosave while creating timelines
1643     m_autoSaveTimer.stop();
1644     std::shared_ptr<ProjectClip> clip = pCore->projectItemModel()->getClipByBinID(id);
1645     std::unique_ptr<Mlt::Producer> xmlProd = nullptr;
1646     // Check if a tractor for this playlist already exists in the main timeline
1647     std::shared_ptr<Mlt::Tractor> tc = pCore->projectItemModel()->getExtraTimeline(uuid.toString());
1648     bool internalLoad = false;
1649     if (tc != nullptr && tc->is_valid()) {
1650         internalLoad = true;
1651         if (duplicate) {
1652             pCore->projectItemModel()->setExtraTimelineSaved(uuid.toString());
1653         }
1654     } else {
1655         xmlProd.reset(new Mlt::Producer(clip->originalProducer().get()));
1656         if (xmlProd == nullptr || !xmlProd->is_valid()) {
1657             qDebug() << "::: LOADING EXTRA TIMELINE ERROR\n\nXXXXXXXXXXXXXXXXXXXXXXX";
1658             pCore->displayBinMessage(i18n("Cannot create a timeline from this clip:\n%1", clip->url()), KMessageWidget::Information);
1659             m_autoSaveTimer.start();
1660             return false;
1661         }
1662     }
1663 
1664     // Build timeline
1665     if (existingModel) {
1666         existingModel->m_closing = false;
1667     }
1668     std::shared_ptr<TimelineItemModel> timelineModel = existingModel != nullptr ? existingModel : TimelineItemModel::construct(uuid, m_project->commandStack());
1669     m_project->addTimeline(uuid, timelineModel);
1670     TimelineWidget *timeline = nullptr;
1671     if (internalLoad) {
1672         qDebug() << "QQQQQQQQQQQQQQQQQQQQ\nINTERNAL SEQUENCE LOAD\n\nQQQQQQQQQQQQQQQQQQQQQQ";
1673         qDebug() << "============= LOADING INTERNAL PLAYLIST: " << uuid;
1674         const QString chunks = m_project->getSequenceProperty(uuid, QStringLiteral("previewchunks"));
1675         const QString dirty = m_project->getSequenceProperty(uuid, QStringLiteral("dirtypreviewchunks"));
1676         if (existingModel == nullptr &&
1677             !constructTimelineFromTractor(timelineModel, nullptr, *tc.get(), m_progressDialog, m_project->modifiedDecimalPoint(), chunks, dirty)) {
1678             qDebug() << "===== LOADING PROJECT INTERNAL ERROR";
1679         }
1680         std::shared_ptr<Mlt::Producer> prod = std::make_shared<Mlt::Producer>(timelineModel->tractor());
1681 
1682         // Load stored sequence properties
1683         Mlt::Properties playlistProps(tc->get_properties());
1684         Mlt::Properties sequenceProperties;
1685         sequenceProperties.pass_values(playlistProps, "kdenlive:sequenceproperties.");
1686         for (int i = 0; i < sequenceProperties.count(); i++) {
1687             m_project->setSequenceProperty(uuid, qstrdup(sequenceProperties.get_name(i)), qstrdup(sequenceProperties.get(i)));
1688         }
1689         prod->set("kdenlive:duration", prod->frames_to_time(timelineModel->duration()));
1690         prod->set("kdenlive:maxduration", timelineModel->duration());
1691         prod->set("length", timelineModel->duration());
1692         prod->set("out", timelineModel->duration() - 1);
1693         prod->set("kdenlive:clipname", clip->clipName().toUtf8().constData());
1694         prod->set("kdenlive:description", clip->description().toUtf8().constData());
1695         prod->set("kdenlive:uuid", uuid.toString().toUtf8().constData());
1696         prod->set("kdenlive:producer_type", ClipType::Timeline);
1697 
1698         prod->parent().set("kdenlive:duration", prod->frames_to_time(timelineModel->duration()));
1699         prod->parent().set("kdenlive:maxduration", timelineModel->duration());
1700         prod->parent().set("length", timelineModel->duration());
1701         prod->parent().set("out", timelineModel->duration() - 1);
1702         prod->parent().set("kdenlive:clipname", clip->clipName().toUtf8().constData());
1703         prod->parent().set("kdenlive:description", clip->description().toUtf8().constData());
1704         prod->parent().set("kdenlive:uuid", uuid.toString().toUtf8().constData());
1705         prod->parent().set("kdenlive:producer_type", ClipType::Timeline);
1706         QObject::connect(timelineModel.get(), &TimelineModel::durationUpdated, this, &ProjectManager::updateSequenceDuration);
1707         timelineModel->setMarkerModel(clip->markerModel());
1708         m_project->loadSequenceGroupsAndGuides(uuid);
1709         clip->setProducer(prod, false, false);
1710         if (!duplicate) {
1711             clip->reloadTimeline();
1712         }
1713         if (pCore->bin()) {
1714             pCore->bin()->registerSequence(uuid, id);
1715         }
1716     } else {
1717         qDebug() << "GOT XML SERV: " << xmlProd->type() << " = " << xmlProd->parent().type();
1718         // Mlt::Service s(xmlProd->producer()->get_service());
1719         std::unique_ptr<Mlt::Tractor> tractor;
1720         if (xmlProd->type() == mlt_service_tractor_type) {
1721             tractor.reset(new Mlt::Tractor(*xmlProd.get()));
1722         } else if (xmlProd->type() == mlt_service_producer_type) {
1723             tractor.reset(new Mlt::Tractor((mlt_tractor)xmlProd->get_producer()));
1724             tractor->set("id", uuid.toString().toUtf8().constData());
1725         }
1726         // Load sequence properties from the xml producer
1727         Mlt::Properties playlistProps(xmlProd->get_properties());
1728         Mlt::Properties sequenceProperties;
1729         sequenceProperties.pass_values(playlistProps, "kdenlive:sequenceproperties.");
1730         for (int i = 0; i < sequenceProperties.count(); i++) {
1731             m_project->setSequenceProperty(uuid, qstrdup(sequenceProperties.get_name(i)), qstrdup(sequenceProperties.get(i)));
1732         }
1733 
1734         const QUuid sourceDocUuid(m_project->getSequenceProperty(uuid, QStringLiteral("documentuuid")));
1735         if (sourceDocUuid == m_project->uuid()) {
1736             qDebug() << "WWWWWWWWWWWWWWWWW\n\n\nIMPORTING FRMO SAME PROJECT\n\nWWWWWWWWWWWWWWW";
1737         } else {
1738             qDebug() << "WWWWWWWWWWWWWWWWW\n\nImporting a sequence from another project: " << sourceDocUuid << " = " << m_project->uuid()
1739                      << "\n\nWWWWWWWWWWWWWWW";
1740             pCore->displayMessage(i18n("Importing a sequence clip, this is currently in experimental state"), ErrorMessage);
1741         }
1742         const QString chunks = m_project->getSequenceProperty(uuid, QStringLiteral("previewchunks"));
1743         const QString dirty = m_project->getSequenceProperty(uuid, QStringLiteral("dirtypreviewchunks"));
1744         if (!constructTimelineFromTractor(timelineModel, sourceDocUuid == m_project->uuid() ? nullptr : pCore->projectItemModel(), *tractor.get(),
1745                                           m_progressDialog, m_project->modifiedDecimalPoint(), chunks, dirty)) {
1746             // if (!constructTimelineFromMelt(timelineModel, *tractor.get(), m_progressDialog, m_project->modifiedDecimalPoint(), chunks, dirty)) {
1747             //  TODO: act on project load failure
1748             qDebug() << "// Project failed to load!!";
1749             m_autoSaveTimer.start();
1750             return false;
1751         }
1752         qDebug() << "::: SEQUENCE LOADED WITH TRACKS: " << timelineModel->tractor()->count() << "\nZZZZZZZZZZZZ";
1753         std::shared_ptr<Mlt::Producer> prod = std::make_shared<Mlt::Producer>(timelineModel->tractor());
1754         prod->set("kdenlive:duration", timelineModel->tractor()->frames_to_time(timelineModel->duration()));
1755         prod->set("kdenlive:maxduration", timelineModel->duration());
1756         prod->set("length", timelineModel->duration());
1757         prod->set("kdenlive:producer_type", ClipType::Timeline);
1758         prod->set("out", timelineModel->duration() - 1);
1759         prod->set("kdenlive:clipname", clip->clipName().toUtf8().constData());
1760         prod->set("kdenlive:description", clip->description().toUtf8().constData());
1761         prod->set("kdenlive:uuid", uuid.toString().toUtf8().constData());
1762 
1763         prod->parent().set("kdenlive:duration", prod->frames_to_time(timelineModel->duration()));
1764         prod->parent().set("kdenlive:maxduration", timelineModel->duration());
1765         prod->parent().set("length", timelineModel->duration());
1766         prod->parent().set("out", timelineModel->duration() - 1);
1767         prod->parent().set("kdenlive:clipname", clip->clipName().toUtf8().constData());
1768         prod->parent().set("kdenlive:description", clip->description().toUtf8().constData());
1769         prod->parent().set("kdenlive:uuid", uuid.toString().toUtf8().constData());
1770         prod->parent().set("kdenlive:producer_type", ClipType::Timeline);
1771         timelineModel->setMarkerModel(clip->markerModel());
1772         if (pCore->bin()) {
1773             pCore->bin()->registerSequence(uuid, id);
1774             pCore->bin()->updateSequenceClip(uuid, timelineModel->duration(), -1);
1775         }
1776         updateSequenceProducer(uuid, prod);
1777         clip->setProducer(prod, false, false);
1778         m_project->loadSequenceGroupsAndGuides(uuid);
1779     }
1780     if (pCore->window()) {
1781         // Create tab widget
1782         timeline = pCore->window()->openTimeline(uuid, clip->clipName(), timelineModel);
1783     }
1784 
1785     int activeTrackPosition = m_project->getSequenceProperty(uuid, QStringLiteral("activeTrack"), QString::number(-1)).toInt();
1786     if (timeline == nullptr) {
1787         // We are in testing mode
1788         return true;
1789     }
1790     if (activeTrackPosition == -2) {
1791         // Subtitle model track always has ID == -2
1792         timeline->controller()->setActiveTrack(-2);
1793     } else if (activeTrackPosition > -1 && activeTrackPosition < timeline->model()->getTracksCount()) {
1794         // otherwise, convert the position to a track ID
1795         timeline->controller()->setActiveTrack(timeline->model()->getTrackIndexFromPosition(activeTrackPosition));
1796     } else {
1797         qWarning() << "[BUG] \"activeTrack\" property is" << activeTrackPosition << "but track count is only" << timeline->model()->getTracksCount();
1798         // set it to some valid track instead
1799         timeline->controller()->setActiveTrack(timeline->model()->getTrackIndexFromPosition(0));
1800     }
1801     /*if (m_renderWidget) {
1802         slotCheckRenderStatus();
1803         m_renderWidget->setGuides(m_project->getGuideModel());
1804         m_renderWidget->updateDocumentPath();
1805         m_renderWidget->setRenderProfile(m_project->getRenderProperties());
1806         m_renderWidget->updateMetadataToolTip();
1807     }*/
1808     pCore->window()->raiseTimeline(timeline->getUuid());
1809     pCore->bin()->updateTargets();
1810     m_autoSaveTimer.start();
1811     return true;
1812 }
1813 
1814 void ProjectManager::setTimelinePropery(QUuid uuid, const QString &prop, const QString &val)
1815 {
1816     std::shared_ptr<TimelineItemModel> model = m_project->getTimeline(uuid);
1817     if (model) {
1818         model->tractor()->set(prop.toUtf8().constData(), val.toUtf8().constData());
1819     }
1820 }
1821 
1822 int ProjectManager::getTimelinesCount() const
1823 {
1824     return pCore->projectItemModel()->sequenceCount();
1825 }
1826 
1827 void ProjectManager::syncTimeline(const QUuid &uuid, bool refresh)
1828 {
1829     std::shared_ptr<TimelineItemModel> model = m_project->getTimeline(uuid);
1830     if (model) {
1831         std::shared_ptr<Mlt::Producer> prod = std::make_shared<Mlt::Producer>(model->tractor());
1832         int position = -1;
1833         if (model == m_activeTimelineModel) {
1834             position = pCore->getMonitorPosition();
1835             if (pCore->window()) {
1836                 pCore->window()->getCurrentTimeline()->controller()->saveSequenceProperties();
1837             }
1838         }
1839         if (refresh) {
1840             // Store sequence properties for later re-use
1841             Mlt::Properties sequenceProps;
1842             sequenceProps.pass_values(*model->tractor(), "kdenlive:sequenceproperties.");
1843             pCore->currentDoc()->loadSequenceProperties(uuid, sequenceProps);
1844         }
1845         updateSequenceProducer(uuid, prod);
1846         if (pCore->bin()) {
1847             pCore->bin()->updateSequenceClip(uuid, model->duration(), position);
1848         }
1849     }
1850 }
1851 
1852 bool ProjectManager::closeTimeline(const QUuid &uuid, bool onDeletion, bool clearUndo)
1853 {
1854     std::shared_ptr<TimelineItemModel> model = m_project->getTimeline(uuid);
1855     if (model == nullptr) {
1856         qDebug() << "=== ERROR CANNOT FIND TIMELINE TO CLOSE: " << uuid << "\n\nHHHHHHHHHHHH";
1857         return false;
1858     }
1859     pCore->projectItemModel()->removeReferencedClips(uuid);
1860     pCore->projectItemModel()->setExtraTimelineSaved(uuid.toString());
1861     if (onDeletion) {
1862         // triggered when deleting bin clip, also close timeline tab
1863         pCore->window()->closeTimelineTab(uuid);
1864     } else {
1865         if (!m_project->closing && !onDeletion) {
1866             if (m_project->isModified()) {
1867                 syncTimeline(uuid);
1868             }
1869         }
1870     }
1871     m_project->closeTimeline(uuid);
1872     // The undo stack keeps references to guides model and will crash on undo if not cleared
1873     if (clearUndo) {
1874         qDebug() << ":::::::::::::: WARNING CLEARING NUDO STACK\n\n:::::::::::::::::";
1875         undoStack()->clear();
1876     }
1877     if (!m_project->closing) {
1878         m_project->setModified(true);
1879     }
1880     return true;
1881 }
1882 
1883 void ProjectManager::seekTimeline(const QString &frameAndTrack)
1884 {
1885     int frame;
1886     if (frameAndTrack.contains(QLatin1Char('!'))) {
1887         QUuid uuid(frameAndTrack.section(QLatin1Char('!'), 0, 0));
1888         const QString binId = pCore->projectItemModel()->getSequenceId(uuid);
1889         openTimeline(binId, uuid);
1890         frame = frameAndTrack.section(QLatin1Char('!'), 1).section(QLatin1Char('?'), 0, 0).toInt();
1891     } else {
1892         frame = frameAndTrack.section(QLatin1Char('?'), 0, 0).toInt();
1893     }
1894     if (frameAndTrack.contains(QLatin1Char('?'))) {
1895         // Track and timecode info
1896         int track = frameAndTrack.section(QLatin1Char('?'), 1, 1).toInt();
1897         // Track uses MLT index, so remove 1 to discard black background track
1898         if (track > 0) {
1899             track--;
1900         }
1901         pCore->window()->getCurrentTimeline()->controller()->activateTrackAndSelect(track, true);
1902     } else {
1903         frame = frameAndTrack.toInt();
1904     }
1905     pCore->monitorManager()->projectMonitor()->requestSeek(frame);
1906 }
1907 
1908 void ProjectManager::slotCreateSequenceFromSelection()
1909 {
1910     std::function<bool(void)> undo = []() { return true; };
1911     std::function<bool(void)> redo = []() { return true; };
1912     int aTracks = -1;
1913     int vTracks = -1;
1914     std::pair<int, QString> copiedData = pCore->window()->getCurrentTimeline()->controller()->getCopyItemData();
1915     if (copiedData.first == -1) {
1916         pCore->displayMessage(i18n("Select a clip to create sequence"), InformationMessage);
1917         return;
1918     }
1919     const QUuid sourceSequence = pCore->window()->getCurrentTimeline()->getUuid();
1920     std::pair<int, int> vPosition = pCore->window()->getCurrentTimeline()->controller()->selectionPosition(&aTracks, &vTracks);
1921     pCore->window()->getCurrentTimeline()->model()->requestItemDeletion(copiedData.first, undo, redo, true);
1922     const QString newSequenceId = pCore->bin()->buildSequenceClipWithUndo(undo, redo, aTracks, vTracks);
1923     if (newSequenceId.isEmpty()) {
1924         // Action canceled
1925         undo();
1926         return;
1927     }
1928     const QUuid destSequence = pCore->window()->getCurrentTimeline()->getUuid();
1929     int trackId = pCore->window()->getCurrentTimeline()->controller()->activeTrack();
1930     Fun local_redo1 = [this, destSequence, copiedData, trackId]() {
1931         pCore->window()->raiseTimeline(destSequence);
1932         return true;
1933     };
1934     local_redo1();
1935     bool result = TimelineFunctions::pasteClipsWithUndo(m_activeTimelineModel, copiedData.second, trackId, 0, undo, redo);
1936     if (!result) {
1937         undo();
1938         return;
1939     }
1940     PUSH_LAMBDA(local_redo1, redo);
1941     Fun local_redo = [this, sourceSequence]() {
1942         pCore->window()->raiseTimeline(sourceSequence);
1943         return true;
1944     };
1945     local_redo();
1946     PUSH_LAMBDA(local_redo, redo);
1947     int newId;
1948     result = m_activeTimelineModel->requestClipInsertion(newSequenceId, vPosition.second, vPosition.first, newId, false, true, false, undo, redo, {});
1949     if (!result) {
1950         undo();
1951         return;
1952     }
1953     pCore->pushUndo(undo, redo, i18n("Create Sequence Clip"));
1954 }
1955 
1956 void ProjectManager::updateSequenceProducer(const QUuid &uuid, std::shared_ptr<Mlt::Producer> prod)
1957 {
1958     // On timeline close, update the stored sequence producer
1959     std::shared_ptr<Mlt::Tractor> trac(new Mlt::Tractor(prod->parent()));
1960     qDebug() << "====== STORING SEQUENCE " << uuid << " WITH TKS: " << trac->count();
1961     pCore->projectItemModel()->storeSequence(uuid.toString(), trac);
1962 }
1963 
1964 void ProjectManager::replaceTimelineInstances(const QString &sourceId, const QString &replacementId, bool replaceAudio, bool replaceVideo)
1965 {
1966     std::shared_ptr<ProjectClip> currentItem = pCore->projectItemModel()->getClipByBinID(sourceId);
1967     std::shared_ptr<ProjectClip> replacementItem = pCore->projectItemModel()->getClipByBinID(replacementId);
1968     if (!currentItem || !replacementItem || !m_activeTimelineModel) {
1969         qDebug() << " SOURCE CLI : " << sourceId << " NOT FOUND!!!";
1970         return;
1971     }
1972     int maxDuration = replacementItem->frameDuration();
1973     QList<int> instances = currentItem->timelineInstances();
1974     m_activeTimelineModel->processTimelineReplacement(instances, sourceId, replacementId, maxDuration, replaceAudio, replaceVideo);
1975 }