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