Commit 8eaa4442 authored by Jean-Baptiste Mardelle's avatar Jean-Baptiste Mardelle
Browse files

Merge branch 'master' of

parents 1ee9dc36 1ca77384
Pipeline #194720 failed with stage
in 55 seconds
......@@ -9,7 +9,7 @@ Currently supported distributions are:
* Ubuntu 20.04 LTS Focal Fossa and derivatives
* Arch Linux
But you should be able to build it on any platform that provides up-to-date versions of the following dependencies: Qt >= 5.7, KF5 >= 5.50, MLT >= 7.4.0.
But you should be able to build it on any platform that provides up-to-date versions of the following dependencies: Qt >= 5.15.2, KF5 >= 5.86, MLT >= 7.4.0.
## Build on Linux
......@@ -117,6 +117,9 @@ make install
# Kdenlive
cd ../../kdenlive
mkdir build && cd build
# Even if you specified a user-writable INSTALL_PREFIX, some Qt plugins like the MLT thumbnailer are
# going be installed in non-user-writable system paths to make them work. If you really do not want
# to give root privileges, you need to set KDE_INSTALL_USE_QT_SYS_PATHS to OFF in the line below.
make -j$JOBS
make install
# 'sudo make install' if INSTALL_PREFIX is not user-writable
# 'sudo make install' if INSTALL_PREFIX is not user-writable or if KDE_INSTALL_USE_QT_SYS_PATHS=ON
Note that `make install` is required for Kdenlive, otherwise the effects will not be installed and cannot be used.
......@@ -19,6 +19,13 @@ class DocumentValidator
DocumentValidator(const QDomDocument &doc, QUrl documentUrl);
bool isProject() const;
/** @brief Check if the document is a valid Kdenlive project
* @param currentVersion The version of the document, with the current
* version defined as DOCUMENTVERSION in kdenlivedoc.cpp.
* @return A QPair with the first value true if the document is valid, and
* the second value the original decimal point string only if upgradeTo100
* changed the decimal point.
QPair<bool, QString> validate(const double currentVersion);
bool isModified() const;
/** @brief Check if the project contains references to Movit stuff (GLSL), and try to convert if wanted. */
This diff is collapsed.
......@@ -16,6 +16,7 @@ SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
#include <QDir>
#include <QList>
#include <QMap>
#include <QObject>
#include <QUuid>
#include <memory>
#include <qdom.h>
......@@ -43,12 +44,45 @@ namespace Mlt {
class Profile;
/** Object returned by KdenliveDoc::Open(), containing a pointer to a KdenliveDoc
* (if successful) and also additional information about whether the doc was
* modified or upgraded, and any error message. If the doc is nullptr, then
* errorMessage() will return an error string that can be shown to the user.
class DocOpenResult {
bool isSuccessful() const { return m_doc != nullptr; }
KdenliveDoc * getDocument() const { return m_doc; }
/** @returns an error message if the doc could not be opened. */
QString getError() const { return m_errorMessage; }
/** @return true if the doc was upgraded from an older version */
bool wasUpgraded() const { return m_upgraded; }
/** @return true if the doc was modified by the validator */
bool wasModified() const { return m_modified; }
void setDocument(KdenliveDoc *doc) { m_doc = doc; }
void setError(const QString &error) { m_errorMessage = error; }
void setUpgraded(bool upgraded) { m_upgraded = upgraded; }
void setModified(bool modified) { m_modified = modified; }
KdenliveDoc *m_doc = nullptr;
QString m_errorMessage = QString();
QString m_notification = QString();
bool m_upgraded = false;
bool m_modified = false;
class KdenliveDoc : public QObject
KdenliveDoc(const QUrl &url, QString projectFolder, QUndoGroup *undoGroup, const QString &profileName, const QMap<QString, QString> &properties,
const QMap<QString, QString> &metadata, const QPair<int, int> &tracks, int audioChannels, bool *openBackup, MainWindow *parent = nullptr);
/** @brief Create a new empty Kdenlive project with the specified profile and requested number of tracks. */
KdenliveDoc(QString projectFolder, QUndoGroup *undoGroup, const QString &profileName, const QMap<QString, QString> &properties,
const QMap<QString, QString> &metadata, const QPair<int, int> &tracks, int audioChannels, MainWindow *parent = nullptr);
/** @brief Open an existing Kdenlive project, returning nothing if the project cannot be opened. */
static DocOpenResult Open(const QUrl &url, const QString &projectFolder, QUndoGroup *undoGroup,
bool recoverCorruption, MainWindow *parent = nullptr);
~KdenliveDoc() override;
friend class LoadJob;
/** @brief Get current document's producer. */
......@@ -75,6 +109,10 @@ public:
/** @brief Defines whether the document needs to be saved. */
bool isModified() const;
/** @brief Adds a "modified" attribute to the document root so that a backup
* will be created the next time the document is saved.
void requestBackup();
/** @brief Returns the project folder, used to store project temporary files. */
QString projectTempFolder() const;
......@@ -150,6 +188,7 @@ public:
QString getAutoProxyProfile();
/** @brief Returns the number of clips in this project (useful to show loading progress) */
int clipsCount() const;
int updateClipsCount();
/** @brief Returns a list of project tags (color / description) */
QMap <int, QStringList> getProjectTags() const;
/** @brief Returns the number of audio channels for this project */
......@@ -163,6 +202,7 @@ public:
* @return Original decimal point, or an empty string if it was “.” already
QString &modifiedDecimalPoint();
void setModifiedDecimalPoint(const QString &decimalPoint) { m_modifiedDecimalPoint = decimalPoint; }
/** @brief Initialize subtitle model */
void initializeSubtitles(const std::shared_ptr<SubtitleModel> m_subtitle);
/** @brief Returns a path for current document's subtitle file. If final is true, this will be the project filename with ".srt" appended. Otherwise a file in /tmp */
......@@ -176,7 +216,12 @@ public:
void processProxyNodes(QDomNodeList producers, const QString &root, const QMap<QString, QString> &proxies);
QUrl m_url;
/** @brief Create a new KdenliveDoc using the provided QDomDocument (an
* existing project file), used by the Open() named constructor. */
KdenliveDoc(const QUrl &url, QDomDocument& newDom, QString projectFolder, QUndoGroup *undoGroup,
MainWindow *parent = nullptr);
/** @brief Set document default properties using hard-coded values and KdenliveSettings. */
void initializeProperties();
QDomDocument m_document;
int m_clipsCount;
/** @brief MLT's root (base path) that is stripped from urls in saved xml */
......@@ -198,6 +243,8 @@ private:
enum DOCSTATUS { CleanProject, ModifiedProject, UpgradedProject };
DOCSTATUS m_documentOpenStatus;
QUrl m_url;
/** @brief The project folder, used to store project files (titles, effects...). */
QString m_projectFolder;
QList<int> m_undoChunks;
......@@ -228,22 +275,22 @@ public slots:
void slotCreateTextTemplateClip(const QString &group, const QString &groupId, QUrl path);
/** @brief Sets the document as modified or up to date.
* If crash recovery is turned on, a timer calls KdenliveDoc::slotAutoSave() \n
* Emits docModified connected to MainWindow::slotUpdateDocumentState \n
* @param mod (optional) true if the document has to be saved */
void setModified(bool mod = true);
QMap<QString, QString> proxyClipsById(const QStringList &ids, bool proxy, const QMap<QString, QString> &proxyPath = QMap<QString, QString>());
void slotProxyCurrentItem(bool doProxy, QList<std::shared_ptr<ProjectClip>> clipList = QList<std::shared_ptr<ProjectClip>>(), bool force = false,
QUndoCommand *masterCommand = nullptr);
/** @brief Saves the current project at the autosave location.
* The autosave files are in ~/.kde/data/stalefiles/kdenlive/ */
void slotAutoSave(const QString &scene);
/** @brief Groups were changed, save to MLT. */
void groupsChanged(const QString &groups);
void switchProfile(ProfileParam* pf, const QString clipName);
void switchProfile(ProfileParam* pf, const QString &clipName);
private slots:
void slotModified();
......@@ -226,11 +226,10 @@ void ProjectManager::newFile(QString profileName, bool showProjectSettings)
documentMetadata = w->metadata();
delete w;
bool openBackup;
KdenliveDoc *doc = new KdenliveDoc(QUrl(), projectFolder, pCore->window()->m_commandStack, profileName, documentProperties, documentMetadata, projectTracks,
audioChannels, &openBackup, pCore->window());
// TODO: KdenliveDoc constructor
KdenliveDoc *doc = new KdenliveDoc(projectFolder, pCore->window()->m_commandStack, profileName, documentProperties, documentMetadata, projectTracks, audioChannels, pCore->window());
doc->m_autosave = new KAutoSaveFile(startFile, doc);
doc->m_sameProjectFolder = sameProjectFolder;
......@@ -598,7 +597,7 @@ void ProjectManager::openFile(const QUrl &url)
doOpenFile(url, nullptr);
void ProjectManager::doOpenFile(const QUrl &url, KAutoSaveFile *stale)
void ProjectManager::doOpenFile(const QUrl &url, KAutoSaveFile *stale, bool isBackup)
Q_ASSERT(m_project == nullptr);
......@@ -616,19 +615,54 @@ void ProjectManager::doOpenFile(const QUrl &url, KAutoSaveFile *stale)
bool openBackup;
int audioChannels = 2;
if (KdenliveSettings::audio_channels() == 1) {
audioChannels = 4;
} else if (KdenliveSettings::audio_channels() == 2) {
audioChannels = 6;
DocOpenResult openResult = KdenliveDoc::Open(stale ? QUrl::fromLocalFile(stale->fileName()) : url,
QString(), pCore->window()->m_commandStack, false, pCore->window());
KdenliveDoc *doc;
if (!openResult.isSuccessful()) {
if (!isBackup) {
int answer = KMessageBox::warningYesNoCancel(
pCore->window(), i18n("Cannot open the project file. Error:\n%1\nDo you want to open a backup file?", openResult.getError()),
i18n("Error opening file"), KGuiItem(i18n("Open Backup")), KGuiItem(i18n("Recover")));
if (answer == KMessageBox::ButtonCode::Yes) { // Open Backup
} else if (answer == KMessageBox::ButtonCode::No) { // Recover
// if file was broken by Kdenlive 0.9.4, we can try recovering it. If successful, continue through rest of this function.
openResult = KdenliveDoc::Open(stale ? QUrl::fromLocalFile(stale->fileName()) : url,
QString(), pCore->window()->m_commandStack, true, pCore->window());
if (openResult.isSuccessful()) {
doc = openResult.getDocument();
} else {
KMessageBox::error(pCore->window(), "Could not recover corrupted file.");
} else {
KMessageBox::detailedSorry(pCore->window(), "Could not open the backup project file.", openResult.getError());
} else {
doc = openResult.getDocument();
// if we could not open the file, and could not recover (or user declined), stop now
if (!openResult.isSuccessful()) {
delete m_progressDialog;
m_progressDialog = nullptr;
if (openResult.wasUpgraded()) {
pCore->displayMessage(i18n("Your project was upgraded, a backup will be created on next save"),
} else if (openResult.wasModified()) {
pCore->displayMessage(i18n("Your project was modified on opening, a backup will be created on next save"),
pCore->displayMessage(QString(), OperationCompletedMessage);
KdenliveDoc *doc = new KdenliveDoc(stale ? QUrl::fromLocalFile(stale->fileName()) : url, QString(), pCore->window()->m_commandStack,
KdenliveSettings::default_profile().isEmpty() ? pCore->getCurrentProfile()->path() : KdenliveSettings::default_profile(),
QMap<QString, QString>(), QMap<QString, QString>(), {KdenliveSettings::videotracks(), KdenliveSettings::audiotracks()},
audioChannels, &openBackup, pCore->window());
if (stale == nullptr) {
const QString projectId = QCryptographicHash::hash(url.fileName().toUtf8(), QCryptographicHash::Md5).toHex();
QUrl autosaveUrl = QUrl::fromLocalFile(QFileInfo(url.path()).absoluteDir().absoluteFilePath(projectId + QStringLiteral(".kdenlive")));
......@@ -673,9 +707,6 @@ void ProjectManager::doOpenFile(const QUrl &url, KAutoSaveFile *stale)
emit docOpened(m_project);
pCore->displayMessage(QString(), OperationCompletedMessage, 100);
if (openBackup) {
delete m_progressDialog;
m_progressDialog = nullptr;
......@@ -710,7 +741,7 @@ bool ProjectManager::slotOpenBackup(const QUrl &url)
projectFolder = QUrl::fromLocalFile(KdenliveSettings::defaultprojectfolder());
projectFile = url;
} else {
projectFolder = QUrl::fromLocalFile(m_project->projectTempFolder());
projectFolder = QUrl::fromLocalFile(m_project ? m_project->projectTempFolder() : QString());
projectFile = m_project->url();
projectId = m_project->getDocumentProperty(QStringLiteral("documentid"));
......@@ -720,7 +751,7 @@ bool ProjectManager::slotOpenBackup(const QUrl &url)
QString requestedBackup = dia->selectedFile();
doOpenFile(QUrl::fromLocalFile(requestedBackup), nullptr);
doOpenFile(QUrl::fromLocalFile(requestedBackup), nullptr, true);
if (m_project) {
if (!m_project->url().isEmpty()) {
// Only update if restore succeeded
......@@ -51,10 +51,10 @@ public:
/** @brief Store command line args for later opening. */
void init(const QUrl &projectUrl, const QString &clipList);
void doOpenFile(const QUrl &url, KAutoSaveFile *stale);
void doOpenFile(const QUrl &url, KAutoSaveFile *stale, bool isBackup = false);
KRecentFilesAction *recentFilesAction();
void prepareSave();
/** @brief Disable all bin effects in current project
/** @brief Disable all bin effects in current project
* @param disable if true, all project bin effects will be disabled
* @param refreshMonitor if false, monitors will not be refreshed
<kdenlivetitle duration="125" LC_NUMERIC="C" width="1920" height="1080" out="125">
<item type="QGraphicsTextItem" z-index="0">
<position x="548" y="376">
<content shadow="0;#64000000;3;3;3" font-underline="0" box-height="197" font-outline-color="0,0,0,255" font="Noto Sans" letter-spacing="0" font-pixel-size="144" font-italic="0" typewriter="0;2;1;0;0" alignment="0" font-weight="75" font-outline="2" box-width="835.516" font-color="95,255,67,255">Hello World</content>
<startviewport rect="0,0,1920,1080"/>
<endviewport rect="0,0,1920,1080"/>
<background color="0,0,0,0"/>
......@@ -236,3 +236,136 @@ TEST_CASE("Save File", "[SF]")
pCore->m_projectManager = nullptr;
TEST_CASE("Non-BMP Unicode", "[NONBMP]")
auto binModel = pCore->projectItemModel();
// A Kdenlive bug (bugzilla bug 435768) caused characters outside the Basic
// Multilingual Plane to be lost when the file is loaded. This string
// contains the onigiri emoji which should survive a save+open round trip.
// If Kdenlive drops the emoji, then the result is just "testtest" which can
// be checked for.
const QString emojiTestString = QString::fromUtf8("test\xF0\x9F\x8D\x99test");
std::shared_ptr<DocUndoStack> undoStack = std::make_shared<DocUndoStack>(nullptr);
std::shared_ptr<MarkerListModel> guideModel = std::make_shared<MarkerListModel>(undoStack);
QTemporaryFile saveFile(QDir::temp().filePath("kdenlive_test_XXXXXX.kdenlive"));
qDebug() << "Choosing temp file with template" << (QDir::temp().filePath("kdenlive_test_XXXXXX.kdenlive"));
qDebug() << "New temporary file:" << saveFile.fileName();
SECTION("Save title with special chars")
// Create document
Mock<KdenliveDoc> docMock;
// When(Method(docMock, getDocumentProperty)).AlwaysDo([](const QString &name, const QString &defaultValue) {
// Q_UNUSED(name) Q_UNUSED(defaultValue)
// qDebug() << "Intercepted call";
// return QStringLiteral("dummyId");
// });
KdenliveDoc &mockedDoc = docMock.get();
// We mock the project class so that the undoStack function returns our undoStack, and our mocked document
Mock<ProjectManager> pmMock;
When(Method(pmMock, undoStack)).AlwaysReturn(undoStack);
When(Method(pmMock, cacheDir)).AlwaysReturn(QDir(QStandardPaths::writableLocation(QStandardPaths::CacheLocation)));
When(Method(pmMock, current)).AlwaysReturn(&mockedDoc);
ProjectManager &mocked = pmMock.get();
pCore->m_projectManager = &mocked;
pCore->m_projectManager->m_project = &mockedDoc;
pCore->m_projectManager->m_project->m_guideModel = guideModel;
// We also mock timeline object to spy few functions and mock others
TimelineItemModel tim(&profile_file, undoStack);
Mock<TimelineItemModel> timMock(tim);
auto timeline = std::shared_ptr<TimelineItemModel>(&timMock.get(), [](...) {});
TimelineItemModel::finishConstruct(timeline, guideModel);
mocked.testSetActiveDocument(&mockedDoc, timeline);
QDir dir = QDir::temp();
std::unordered_map<QString, QString> binIdCorresp;
QStringList expandedFolders;
QDomDocument doc = mockedDoc.createEmptyDocument(2, 2);
QScopedPointer<Mlt::Producer> xmlProd(new Mlt::Producer(profile_file, "xml-string", doc.toString().toUtf8()));
Mlt::Service s(*xmlProd);
Mlt::Tractor tractor(s);
binModel->loadBinPlaylist(&tractor, timeline->tractor(), binIdCorresp, expandedFolders, nullptr);
// create a simple title with the non-BMP test string
auto titleXml = ("<kdenlivetitle duration=\"150\" LC_NUMERIC=\"C\" width=\"1920\" height=\"1080\" out=\"149\">\n <item type=\"QGraphicsTextItem\" z-index=\"0\">\n <position x=\"777\" y=\"482\">\n <transform>1,0,0,0,1,0,0,0,1</transform>\n </position>\n <content shadow=\"0;#64000000;3;3;3\" font-underline=\"0\" box-height=\"138\" font-outline-color=\"0,0,0,255\" font=\"DejaVu Sans\" letter-spacing=\"0\" font-pixel-size=\"120\" font-italic=\"0\" typewriter=\"0;2;1;0;0\" alignment=\"0\" font-weight=\"63\" font-outline=\"3\" box-width=\"573.25\" font-color=\"252,233,79,255\">"+
"</content>\n </item>\n <startviewport rect=\"0,0,1920,1080\"/>\n <endviewport rect=\"0,0,1920,1080\"/>\n <background color=\"0,0,0,0\"/>\n</kdenlivetitle>\n");
QString binId2 = createTextProducer(profile_file, binModel, titleXml, emojiTestString, 150);
TrackModel::construct(timeline, -1, -1, QString(), true);
TrackModel::construct(timeline, -1, -1, QString(), true);
int tid1 = TrackModel::construct(timeline);
/*int tid1b =*/ TrackModel::construct(timeline);
// Setup timeline audio drop info
QMap<int, QString> audioInfo;
audioInfo.insert(1, QStringLiteral("stream1"));
timeline->m_binAudioTargets = audioInfo;
timeline->m_videoTarget = tid1;
// Undo resize
// Undo first insert
// Undo second insert
// open the file and check that it contains emojiTestString
QFile file(saveFile.fileName());
QByteArray contents = file.readAll();
if (contents.contains(emojiTestString.toUtf8())) {
qDebug() << "File contains test string";
} else {
qDebug() << "File does not contain test string:" << contents;
// try opening the file as a Kdenlivedoc and check that the title hasn't
// lost the emoji
// convert qtemporaryfile saveFile to QUrl
QUrl openURL = QUrl::fromLocalFile(saveFile.fileName());
QUndoGroup *undoGroup = new QUndoGroup();
auto openResults = KdenliveDoc::Open(openURL, QDir::temp().path(),
undoGroup, false, nullptr);
REQUIRE(openResults.isSuccessful() == true);
KdenliveDoc *openedDoc = openResults.getDocument();
QDomDocument *newDoc = &openedDoc->m_document;
auto producers = newDoc->elementsByTagName(QStringLiteral("producer"));
QDomElement textTitle;
for (int i = 0; i < producers.size(); i++) {
auto kdenliveid = getProperty(, QStringLiteral("kdenlive:id"));
if (kdenliveid != nullptr && kdenliveid->text() == binId2) {
textTitle =;
auto clipname = getProperty(textTitle, QStringLiteral("kdenlive:clipname"));
REQUIRE(clipname != nullptr);
CHECK(clipname->text() == emojiTestString);
auto xmldata = getProperty(textTitle, QStringLiteral("xmldata"));
REQUIRE(xmldata != nullptr);
pCore->m_projectManager = nullptr;
......@@ -69,17 +69,18 @@ QString createAVProducer(Mlt::Profile &prof, std::shared_ptr<ProjectItemModel> b
return binId;
QString createTextProducer(Mlt::Profile &prof, std::shared_ptr<ProjectItemModel> binModel, int length)
QString createTextProducer(Mlt::Profile &prof, std::shared_ptr<ProjectItemModel> binModel, const QString &xmldata, const QString &clipname, int length)
std::shared_ptr<Mlt::Producer> producer =
std::make_shared<Mlt::Producer>(prof, QFileInfo(sourcesPath + "/dataset/title.kdenlivetitle").absoluteFilePath().toStdString().c_str());
std::make_shared<Mlt::Producer>(prof, "kdenlivetitle");
producer->set("length", length);
producer->set_in_and_out(0, length - 1);
producer->set("kdenlive:duration", length);
producer->set("length", length);
producer->set_string("kdenlive:clipname", clipname.toUtf8());
producer->set_string("xmldata", xmldata.toUtf8());
QString binId = QString::number(binModel->getFreeClipId());
auto binClip = ProjectClip::construct(binId, QIcon(), binModel, producer);
......@@ -87,6 +88,18 @@ QString createTextProducer(Mlt::Profile &prof, std::shared_ptr<ProjectItemModel>
Fun undo = []() { return true; };
Fun redo = []() { return true; };
REQUIRE(binModel->addItem(binClip, binModel->getRootFolder()->clipId(), undo, redo));
REQUIRE(binClip->clipType() == ClipType::TextTemplate);
REQUIRE(binClip->clipType() == ClipType::Text);
return binId;
std::unique_ptr<QDomElement> getProperty(const QDomElement &doc, const QString &name) {
QDomNodeList list = doc.elementsByTagName("property");
for (int i = 0; i < list.count(); i++) {
QDomElement e =;
if (e.attribute("name") == name) {
return std::make_unique<QDomElement>(e);
return nullptr;
......@@ -87,6 +87,8 @@ QString createProducer(Mlt::Profile &prof, std::string color, std::shared_ptr<Pr
QString createProducerWithSound(Mlt::Profile &prof, std::shared_ptr<ProjectItemModel> binModel, int length = 10);
QString createTextProducer(Mlt::Profile &prof, std::shared_ptr<ProjectItemModel> binModel, int length = 10);
QString createTextProducer(Mlt::Profile &prof, std::shared_ptr<ProjectItemModel> binModel, const QString &xmldata, const QString &clipname, int length = 10);
QString createAVProducer(Mlt::Profile &prof, std::shared_ptr<ProjectItemModel> binModel);
std::unique_ptr<QDomElement> getProperty(const QDomElement& element, const QString& name);
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment