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 ¬es) 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 }