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