File indexing completed on 2024-04-14 04:46:57

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