File indexing completed on 2024-04-28 08:44:22

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