kdenlivedoc.cpp 70 KB
Newer Older
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
1
/***************************************************************************
2
 *   Copyright (C) 2007 by Jean-Baptiste Mardelle (jb@kdenlive.org)        *
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
3 4 5 6 7 8
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
9 10 11 12 13 14 15 16 17
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA          *
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
18 19
 ***************************************************************************/

Vincent Pinon's avatar
Vincent Pinon committed
20
#include "kdenlivedoc.h"
Nicolas Carion's avatar
Nicolas Carion committed
21 22
#include "bin/bin.h"
#include "bin/bincommands.h"
23
#include "bin/binplaylist.hpp"
24
#include "bin/clipcreator.hpp"
25
#include "bin/model/markerlistmodel.hpp"
Nicolas Carion's avatar
Nicolas Carion committed
26
#include "bin/projectclip.h"
27
#include "bin/projectitemmodel.h"
Nicolas Carion's avatar
Nicolas Carion committed
28 29
#include "core.h"
#include "dialogs/profilesdialog.h"
Vincent Pinon's avatar
Vincent Pinon committed
30 31
#include "documentchecker.h"
#include "documentvalidator.h"
Nicolas Carion's avatar
Nicolas Carion committed
32
#include "docundostack.hpp"
33
#include "effects/effectsrepository.hpp"
34
#include "jobs/jobmanager.h"
35
#include "kdenlivesettings.h"
36
#include "mainwindow.h"
Nicolas Carion's avatar
Nicolas Carion committed
37 38
#include "mltcontroller/clipcontroller.h"
#include "profiles/profilemodel.hpp"
Nicolas Carion's avatar
linting  
Nicolas Carion committed
39
#include "profiles/profilerepository.hpp"
40
#include "project/projectcommands.h"
Vincent Pinon's avatar
Vincent Pinon committed
41
#include "titler/titlewidget.h"
42
#include "transitions/transitionsrepository.hpp"
43

Nicolas Carion's avatar
Nicolas Carion committed
44
#include <config-kdenlive.h>
45

Nicolas Carion's avatar
Nicolas Carion committed
46 47
#include <KBookmark>
#include <KBookmarkManager>
48
#include <KIO/CopyJob>
49
#include <KIO/FileCopyJob>
50
#include <KIO/JobUiDelegate>
Nicolas Carion's avatar
Nicolas Carion committed
51 52
#include <KMessageBox>
#include <klocalizedstring.h>
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
53

Nicolas Carion's avatar
Nicolas Carion committed
54
#include "kdenlive_debug.h"
55
#include <QCryptographicHash>
Nicolas Carion's avatar
Nicolas Carion committed
56
#include <QDomImplementation>
57
#include <QFile>
58
#include <QFileDialog>
Nicolas Carion's avatar
Nicolas Carion committed
59
#include <QUndoGroup>
60
#include <QUndoStack>
61

62
#include <KJobWidgets/KJobWidgets>
63
#include <QStandardPaths>
Nicolas Carion's avatar
Nicolas Carion committed
64
#include <mlt++/Mlt.h>
65

Vincent Pinon's avatar
Vincent Pinon committed
66 67 68 69
#include <locale>
#ifdef Q_OS_MAC
#include <xlocale.h>
#endif
70

71
const double DOCUMENTVERSION = 0.99;
72

Nicolas Carion's avatar
Nicolas Carion committed
73 74
KdenliveDoc::KdenliveDoc(const QUrl &url, QString projectFolder, QUndoGroup *undoGroup, const QString &profileName, const QMap<QString, QString> &properties,
                         const QMap<QString, QString> &metadata, const QPoint &tracks, bool *openBackup, MainWindow *parent)
75 76 77
    : QObject(parent)
    , m_autosave(nullptr)
    , m_url(url)
78
    , m_clipsCount(0)
Vincent Pinon's avatar
Vincent Pinon committed
79
    , m_commandStack(std::make_shared<DocUndoStack>(undoGroup))
80
    , m_modified(false)
81
    , m_documentOpenStatus(CleanProject)
Nicolas Carion's avatar
Nicolas Carion committed
82
    , m_projectFolder(std::move(projectFolder))
83
{
84
    m_guideModel.reset(new MarkerListModel(m_commandStack, this));
85
    connect(m_guideModel.get(), &MarkerListModel::modelChanged, this, &KdenliveDoc::guidesChanged);
86
    connect(this, SIGNAL(updateCompositionMode(int)), parent, SLOT(slotUpdateCompositeAction(int)));
Alberto Villa's avatar
Alberto Villa committed
87
    bool success = false;
88
    connect(m_commandStack.get(), &QUndoStack::indexChanged, this, &KdenliveDoc::slotModified);
89
    connect(m_commandStack.get(), &DocUndoStack::invalidate, this, &KdenliveDoc::checkPreviewStack, Qt::DirectConnection);
Nicolas Carion's avatar
Nicolas Carion committed
90
    // connect(m_commandStack, SIGNAL(cleanChanged(bool)), this, SLOT(setModified(bool)));
91 92

    // init default document properties
93
    m_documentProperties[QStringLiteral("zoom")] = QLatin1Char('8');
Laurent Montel's avatar
Laurent Montel committed
94 95
    m_documentProperties[QStringLiteral("verticalzoom")] = QLatin1Char('1');
    m_documentProperties[QStringLiteral("zonein")] = QLatin1Char('0');
96
    m_documentProperties[QStringLiteral("zoneout")] = QStringLiteral("-1");
Nicolas Carion's avatar
Nicolas Carion committed
97
    m_documentProperties[QStringLiteral("enableproxy")] = QString::number((int)KdenliveSettings::enableproxy());
98 99
    m_documentProperties[QStringLiteral("proxyparams")] = KdenliveSettings::proxyparams();
    m_documentProperties[QStringLiteral("proxyextension")] = KdenliveSettings::proxyextension();
100
    m_documentProperties[QStringLiteral("previewparameters")] = KdenliveSettings::previewparams();
101
    m_documentProperties[QStringLiteral("previewextension")] = KdenliveSettings::previewextension();
102 103
    m_documentProperties[QStringLiteral("externalproxyparams")] = KdenliveSettings::externalProxyProfile();
    m_documentProperties[QStringLiteral("enableexternalproxy")] = QString::number((int)KdenliveSettings::externalproxy());
Nicolas Carion's avatar
Nicolas Carion committed
104
    m_documentProperties[QStringLiteral("generateproxy")] = QString::number((int)KdenliveSettings::generateproxy());
105
    m_documentProperties[QStringLiteral("proxyminsize")] = QString::number(KdenliveSettings::proxyminsize());
Nicolas Carion's avatar
Nicolas Carion committed
106
    m_documentProperties[QStringLiteral("generateimageproxy")] = QString::number((int)KdenliveSettings::generateimageproxy());
107
    m_documentProperties[QStringLiteral("proxyimageminsize")] = QString::number(KdenliveSettings::proxyimageminsize());
108
    m_documentProperties[QStringLiteral("proxyimagesize")] = QString::number(KdenliveSettings::proxyimagesize());
109 110 111
    m_documentProperties[QStringLiteral("videoTarget")] = QString::number(tracks.y());
    m_documentProperties[QStringLiteral("audioTarget")] = QString::number(tracks.y() - 1);
    m_documentProperties[QStringLiteral("activeTrack")] = QString::number(tracks.y());
112
    m_documentProperties[QStringLiteral("enableTimelineZone")] = QLatin1Char('0');
113 114
    m_documentProperties[QStringLiteral("zonein")] = QLatin1Char('0');
    m_documentProperties[QStringLiteral("zoneout")] = QStringLiteral("75");
115
    m_documentProperties[QStringLiteral("seekOffset")] = QString::number(TimelineModel::seekDuration);
116

117 118 119 120 121 122
    // Load properties
    QMapIterator<QString, QString> i(properties);
    while (i.hasNext()) {
        i.next();
        m_documentProperties[i.key()] = i.value();
    }
123

124 125 126 127 128 129
    // Load metadata
    QMapIterator<QString, QString> j(metadata);
    while (j.hasNext()) {
        j.next();
        m_documentMetadata[j.key()] = j.value();
    }
130 131 132
    /*if (QLocale().decimalPoint() != QLocale::system().decimalPoint()) {
        qDebug()<<"* * ** AARCH DOCUMENT  PROBLEM;";
        exit(1);
133
        setlocale(LC_NUMERIC, "");
134 135 136
        QLocale systemLocale = QLocale::system();
        systemLocale.setNumberOptions(QLocale::OmitGroupSeparator);
        QLocale::setDefault(systemLocale);
137
        // locale conversion might need to be redone
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
138 139 140 141
        ///TODO: how to reset repositories...
        //EffectsRepository::get()->init();
        //TransitionsRepository::get()->init();
        //initEffects::parseEffectFiles(pCore->getMltRepository(), QString::fromLatin1(setlocale(LC_NUMERIC, nullptr)));
142
    }*/
143
    *openBackup = false;
144
    if (url.isValid()) {
145
        QFile file(url.toLocalFile());
146
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
147
            // The file cannot be opened
Nicolas Carion's avatar
Nicolas Carion committed
148 149
            if (KMessageBox::warningContinueCancel(parent, i18n("Cannot open the project file,\nDo you want to open a backup file?"),
                                                   i18n("Error opening file"), KGuiItem(i18n("Open Backup"))) == KMessageBox::Continue) {
150 151
                *openBackup = true;
            }
Nicolas Carion's avatar
Nicolas Carion committed
152
            // KMessageBox::error(parent, KIO::NetAccess::lastErrorString());
Laurent Montel's avatar
Laurent Montel committed
153 154
        } else {
            qCDebug(KDENLIVE_LOG) << " // / processing file open";
155
            QString errorMsg;
156 157
            int line;
            int col;
158
            QDomImplementation::setInvalidDataPolicy(QDomImplementation::DropInvalidChars);
159
            success = m_document.setContent(&file, false, &errorMsg, &line, &col);
160
            file.close();
161

162 163
            if (!success) {
                // It is corrupted
Nicolas Carion's avatar
Nicolas Carion committed
164 165 166
                int answer = KMessageBox::warningYesNoCancel(
                    parent, i18n("Cannot open the project file, error is:\n%1 (line %2, col %3)\nDo you want to open a backup file?", errorMsg, line, col),
                    i18n("Error opening file"), KGuiItem(i18n("Open Backup")), KGuiItem(i18n("Recover")));
167
                if (answer == KMessageBox::Yes) {
168
                    *openBackup = true;
Laurent Montel's avatar
Laurent Montel committed
169
                } else if (answer == KMessageBox::No) {
170 171 172
                    // Try to recover broken file produced by Kdenlive 0.9.4
                    if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
                        int correction = 0;
Laurent Montel's avatar
Laurent Montel committed
173
                        QString playlist = QString::fromUtf8(file.readAll());
174 175 176 177
                        while (!success && correction < 2) {
                            int errorPos = 0;
                            line--;
                            col = col - 2;
178
                            for (int k = 0; k < line && errorPos < playlist.length(); ++k) {
179
                                errorPos = playlist.indexOf(QLatin1Char('\n'), errorPos);
180 181 182
                                errorPos++;
                            }
                            errorPos += col;
Laurent Montel's avatar
Laurent Montel committed
183 184 185
                            if (errorPos >= playlist.length()) {
                                break;
                            }
186 187 188 189 190 191 192 193
                            playlist.remove(errorPos, 1);
                            line = 0;
                            col = 0;
                            success = m_document.setContent(playlist, false, &errorMsg, &line, &col);
                            correction++;
                        }
                        if (!success) {
                            KMessageBox::sorry(parent, i18n("Cannot recover this project file"));
Laurent Montel's avatar
Laurent Montel committed
194
                        } else {
195 196
                            // Document was modified, ask for backup
                            QDomElement mlt = m_document.documentElement();
197
                            mlt.setAttribute(QStringLiteral("modified"), 1);
198 199 200
                        }
                    }
                }
Laurent Montel's avatar
Laurent Montel committed
201 202
            } else {
                qCDebug(KDENLIVE_LOG) << " // / processing file open: validate";
203
                pCore->displayMessage(i18n("Validating"), OperationCompletedMessage, 100);
204
                qApp->processEvents();
205
                DocumentValidator validator(m_document, url);
Alberto Villa's avatar
Alberto Villa committed
206
                success = validator.isProject();
207
                if (!success) {
208
                    // It is not a project file
209
                    pCore->displayMessage(i18n("File %1 is not a Kdenlive project file", m_url.toLocalFile()), OperationCompletedMessage, 100);
Nicolas Carion's avatar
Nicolas Carion committed
210 211 212
                    if (KMessageBox::warningContinueCancel(
                            parent, i18n("File %1 is not a valid project file.\nDo you want to open a backup file?", m_url.toLocalFile()),
                            i18n("Error opening file"), KGuiItem(i18n("Open Backup"))) == KMessageBox::Continue) {
213 214
                        *openBackup = true;
                    }
215
                } else {
216
                    /*
Alberto Villa's avatar
Alberto Villa committed
217 218
                     * Validate the file against the current version (upgrade
                     * and recover it if needed). It is NOT a passive operation
219
                     */
Alberto Villa's avatar
Alberto Villa committed
220 221
                    // TODO: backup the document or alert the user?
                    success = validator.validate(DOCUMENTVERSION);
222 223 224
                    if (success && !KdenliveSettings::gpu_accel()) {
                        success = validator.checkMovit();
                    }
225
                    if (success) { // Let the validator handle error messages
Laurent Montel's avatar
Laurent Montel committed
226
                        qCDebug(KDENLIVE_LOG) << " // / processing file validate ok";
227
                        pCore->displayMessage(i18n("Check missing clips"), InformationMessage, 300);
228
                        qApp->processEvents();
229
                        DocumentChecker d(m_url, m_document);
230
                        success = !d.hasErrorInClips();
231
                        if (success) {
232
                            loadDocumentProperties();
233 234 235 236 237 238
                            if (m_document.documentElement().hasAttribute(QStringLiteral("upgraded"))) {
                                m_documentOpenStatus = UpgradedProject;
                                pCore->displayMessage(i18n("Your project was upgraded, a backup will be created on next save"), ErrorMessage);
                            } else if (m_document.documentElement().hasAttribute(QStringLiteral("modified")) || validator.isModified()) {
                                m_documentOpenStatus = ModifiedProject;
                                pCore->displayMessage(i18n("Your project was modified on opening, a backup will be created on next save"), ErrorMessage);
Laurent Montel's avatar
Laurent Montel committed
239 240
                                setModified(true);
                            }
241
                            pCore->displayMessage(QString(), OperationCompletedMessage);
242 243
                        }
                    }
244
                }
245
            }
246
        }
247
    }
248

Alberto Villa's avatar
Alberto Villa committed
249
    // Something went wrong, or a new file was requested: create a new project
250
    if (!success) {
251
        m_url.clear();
252
        pCore->setCurrentProfile(profileName);
253
        m_document = createEmptyDocument(tracks.x(), tracks.y());
254
        updateProjectProfile(false);
255 256
    } else {
        m_clipsCount = m_document.elementsByTagName(QLatin1String("entry")).size();
257
    }
258

259 260 261 262 263 264 265 266
    if (!m_projectFolder.isEmpty()) {
        // Ask to create the project directory if it does not exist
        QDir folder(m_projectFolder);
        if (!folder.mkpath(QStringLiteral("."))) {
            // Project folder is not writable
            m_projectFolder = m_url.toString(QUrl::RemoveFilename | QUrl::RemoveScheme);
            folder.setPath(m_projectFolder);
            if (folder.exists()) {
Nicolas Carion's avatar
Nicolas Carion committed
267 268 269 270
                KMessageBox::sorry(
                    parent,
                    i18n("The project directory %1, could not be created.\nPlease make sure you have the required permissions.\nDefaulting to system folders",
                         m_projectFolder));
271 272
            } else {
                KMessageBox::information(parent, i18n("Document project folder is invalid, using system default folders"));
273
            }
274
            m_projectFolder.clear();
275 276
        }
    }
277
    initCacheDirs();
278

279
    updateProjectFolderPlacesEntry();
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
280 281
}

282 283
KdenliveDoc::~KdenliveDoc()
{
284 285 286 287
    if (m_url.isEmpty()) {
        // Document was never saved, delete cache folder
        QString documentId = QDir::cleanPath(getDocumentProperty(QStringLiteral("documentid")));
        bool ok;
288
        documentId.toLongLong(&ok, 10);
289
        if (ok && !documentId.isEmpty()) {
290
            QDir baseCache = getCacheDir(CacheBase, &ok);
Laurent Montel's avatar
Laurent Montel committed
291
            if (baseCache.dirName() == documentId && baseCache.entryList(QDir::Files).isEmpty()) {
292
                baseCache.removeRecursively();
293 294 295
            }
        }
    }
Nicolas Carion's avatar
Nicolas Carion committed
296
    // qCDebug(KDENLIVE_LOG) << "// DEL CLP MAN";
297
    // Clean up guide model
298
    m_guideModel.reset();
Nicolas Carion's avatar
Nicolas Carion committed
299
    // qCDebug(KDENLIVE_LOG) << "// DEL CLP MAN done";
300
    if (m_autosave) {
Laurent Montel's avatar
Laurent Montel committed
301 302 303
        if (!m_autosave->fileName().isEmpty()) {
            m_autosave->remove();
        }
304 305
        delete m_autosave;
    }
306 307
}

308 309 310 311 312 313
int KdenliveDoc::clipsCount() const
{
    return m_clipsCount;
}


314
const QByteArray KdenliveDoc::getProjectXml()
315
{
316 317 318 319
    const QByteArray result = m_document.toString().toUtf8();
    // We don't need the xml data anymore, throw away
    m_document.clear();
    return result;
320 321
}

322
QDomDocument KdenliveDoc::createEmptyDocument(int videotracks, int audiotracks)
323
{
Laurent Montel's avatar
Laurent Montel committed
324
    QList<TrackInfo> tracks;
325 326 327
    // Tracks are added «backwards», so we need to reverse the track numbering
    // mbt 331: http://www.kdenlive.org/mantis/view.php?id=331
    // Better default names for tracks: Audio 1 etc. instead of blank numbers
Laurent Montel's avatar
Laurent Montel committed
328
    tracks.reserve(audiotracks + videotracks);
329
    for (int i = 0; i < audiotracks; ++i) {
330
        TrackInfo audioTrack;
331
        audioTrack.type = AudioTrack;
332 333 334
        audioTrack.isMute = false;
        audioTrack.isBlind = true;
        audioTrack.isLocked = false;
335
        // audioTrack.trackName = i18n("Audio %1", audiotracks - i);
336
        audioTrack.duration = 0;
337
        tracks.append(audioTrack);
338
    }
339
    for (int i = 0; i < videotracks; ++i) {
340
        TrackInfo videoTrack;
341
        videoTrack.type = VideoTrack;
342 343 344
        videoTrack.isMute = false;
        videoTrack.isBlind = false;
        videoTrack.isLocked = false;
345
        // videoTrack.trackName = i18n("Video %1", i + 1);
346
        videoTrack.duration = 0;
347
        tracks.append(videoTrack);
348
    }
349
    return createEmptyDocument(tracks);
350 351
}

352
QDomDocument KdenliveDoc::createEmptyDocument(const QList<TrackInfo> &tracks)
353 354 355
{
    // Creating new document
    QDomDocument doc;
356 357 358 359 360 361 362 363 364 365 366
    Mlt::Profile docProfile;
    Mlt::Consumer xmlConsumer(docProfile, "xml:kdenlive_playlist");
    xmlConsumer.set("no_profile", 1);
    xmlConsumer.set("terminate_on_pause", 1);
    xmlConsumer.set("store", "kdenlive");
    Mlt::Tractor tractor(docProfile);
    Mlt::Producer bk(docProfile, "color:black");
    tractor.insert_track(bk, 0);
    for (int i = 0; i < tracks.count(); ++i) {
        Mlt::Tractor track(docProfile);
        track.set("kdenlive:track_name", tracks.at(i).trackName.toUtf8().constData());
367
        track.set("kdenlive:timeline_active", 1);
368
        track.set("kdenlive:trackheight", KdenliveSettings::trackheight());
369
        if (tracks.at(i).type == AudioTrack) {
370
            track.set("kdenlive:audio_track", 1);
371
        }
372 373 374 375 376 377
        if (tracks.at(i).isLocked) {
            track.set("kdenlive:locked_track", 1);
        }
        if (tracks.at(i).isMute) {
            if (tracks.at(i).isBlind) {
                track.set("hide", 3);
Laurent Montel's avatar
Laurent Montel committed
378
            } else {
379
                track.set("hide", 2);
380
            }
381 382
        } else if (tracks.at(i).isBlind) {
            track.set("hide", 1);
Laurent Montel's avatar
Laurent Montel committed
383
        }
384 385 386 387
        Mlt::Playlist playlist1(docProfile);
        Mlt::Playlist playlist2(docProfile);
        track.insert_track(playlist1, 0);
        track.insert_track(playlist2, 1);
Nicolas Carion's avatar
Nicolas Carion committed
388
        tractor.insert_track(track, i + 1);
389
    }
Nicolas Carion's avatar
Nicolas Carion committed
390
    QScopedPointer<Mlt::Field> field(tractor.field());
391
    QString compositeService = TransitionsRepository::get()->getCompositingTransition();
392
    if (!compositeService.isEmpty()) {
393
        for (int i = 0; i <= tracks.count(); i++) {
394
            if (i > 0 && tracks.at(i - 1).type == AudioTrack) {
395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
                Mlt::Transition tr(docProfile, "mix");
                tr.set("a_track", 0);
                tr.set("b_track", i);
                tr.set("always_active", 1);
                tr.set("sum", 1);
                tr.set("internal_added", 237);
                field->plant_transition(tr, 0, i);
            }
            if (i > 0 && tracks.at(i - 1).type == VideoTrack) {
                Mlt::Transition tr(docProfile, compositeService.toUtf8().constData());
                tr.set("a_track", 0);
                tr.set("b_track", i);
                tr.set("always_active", 1);
                tr.set("internal_added", 237);
                field->plant_transition(tr, 0, i);
            }
411
        }
412
    }
413 414 415 416 417
    Mlt::Producer prod(tractor.get_producer());
    xmlConsumer.connect(prod);
    xmlConsumer.run();
    QString playlist = QString::fromUtf8(xmlConsumer.get("kdenlive_playlist"));
    doc.setContent(playlist);
418
    return doc;
419 420
}

421
bool KdenliveDoc::useProxy() const
422
{
Nicolas Carion's avatar
Nicolas Carion committed
423
    return m_documentProperties.value(QStringLiteral("enableproxy")).toInt() != 0;
424 425
}

426 427 428 429 430
bool KdenliveDoc::useExternalProxy() const
{
    return m_documentProperties.value(QStringLiteral("enableexternalproxy")).toInt() != 0;
}

431 432
bool KdenliveDoc::autoGenerateProxy(int width) const
{
Nicolas Carion's avatar
Nicolas Carion committed
433 434
    return (m_documentProperties.value(QStringLiteral("generateproxy")).toInt() != 0) &&
           width > m_documentProperties.value(QStringLiteral("proxyminsize")).toInt();
435 436 437 438
}

bool KdenliveDoc::autoGenerateImageProxy(int width) const
{
Nicolas Carion's avatar
Nicolas Carion committed
439 440
    return (m_documentProperties.value(QStringLiteral("generateimageproxy")).toInt() != 0) &&
           width > m_documentProperties.value(QStringLiteral("proxyimageminsize")).toInt();
441
}
442

Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
443
void KdenliveDoc::slotAutoSave(const QString &scene)
444
{
445
    if (m_autosave != nullptr) {
446
        if (!m_autosave->isOpen() && !m_autosave->open(QIODevice::ReadWrite)) {
447
            // show error: could not open the autosave file
Laurent Montel's avatar
Laurent Montel committed
448
            qCDebug(KDENLIVE_LOG) << "ERROR; CANNOT CREATE AUTOSAVE FILE";
449 450
            pCore->displayMessage(i18n("Cannot create autosave file %1", m_autosave->fileName()), ErrorMessage);
            return;
451
        }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
452
        if (scene.isEmpty()) {
Nicolas Carion's avatar
Nicolas Carion committed
453
            // Make sure we don't save if scenelist is corrupted
454 455 456
            KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1, scene list is corrupted.", m_autosave->fileName()));
            return;
        }
Laurent Montel's avatar
Laurent Montel committed
457
        m_autosave->resize(0);
458 459 460
        if (m_autosave->write(scene.toUtf8()) < 0) {
            pCore->displayMessage(i18n("Cannot create autosave file %1", m_autosave->fileName()), ErrorMessage);
        };
461
        m_autosave->flush();
462
    }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
463 464
}

465
void KdenliveDoc::setZoom(int horizontal, int vertical)
466
{
467
    m_documentProperties[QStringLiteral("zoom")] = QString::number(horizontal);
468 469 470
    if (vertical > -1) {
        m_documentProperties[QStringLiteral("verticalzoom")] = QString::number(vertical);
    }
471 472
}

473
QPoint KdenliveDoc::zoom() const
474
{
475
    return QPoint(m_documentProperties.value(QStringLiteral("zoom")).toInt(), m_documentProperties.value(QStringLiteral("verticalzoom")).toInt());
476 477
}

478 479
void KdenliveDoc::setZone(int start, int end)
{
480 481
    m_documentProperties[QStringLiteral("zonein")] = QString::number(start);
    m_documentProperties[QStringLiteral("zoneout")] = QString::number(end);
482 483
}

484 485
QPoint KdenliveDoc::zone() const
{
486
    return QPoint(m_documentProperties.value(QStringLiteral("zonein")).toInt(), m_documentProperties.value(QStringLiteral("zoneout")).toInt());
487 488
}

489 490 491 492 493
QPair<int, int> KdenliveDoc::targetTracks() const
{
    return {m_documentProperties.value(QStringLiteral("videoTarget")).toInt(), m_documentProperties.value(QStringLiteral("audioTarget")).toInt()};
}

494
QDomDocument KdenliveDoc::xmlSceneList(const QString &scene)
495
{
496 497
    QDomDocument sceneList;
    sceneList.setContent(scene, true);
498
    QDomElement mlt = sceneList.firstChildElement(QStringLiteral("mlt"));
499
    if (mlt.isNull() || !mlt.hasChildNodes()) {
Nicolas Carion's avatar
Nicolas Carion committed
500
        // scenelist is corrupted
501
        return QDomDocument();
502
    }
503

504
    // Set playlist audio volume to 100%
505 506 507 508 509 510 511
    QDomNodeList tractors = mlt.elementsByTagName(QStringLiteral("tractor"));
    for (int i = 0; i < tractors.count(); ++i) {
        if (tractors.at(i).toElement().hasAttribute(QStringLiteral("global_feed"))) {
            // This is our main tractor
            QDomElement tractor = tractors.at(i).toElement();
            if (Xml::hasXmlProperty(tractor, QLatin1String("meta.volume"))) {
                Xml::setXmlProperty(tractor, QStringLiteral("meta.volume"), QStringLiteral("1"));
512
            }
513
            break;
514 515
        }
    }
516 517 518 519 520 521
    QDomNodeList tracks = mlt.elementsByTagName(QStringLiteral("track"));
    if (tracks.isEmpty()) {
        // Something is very wrong, inform user.
        qDebug()<<" = = = =  = =  CORRUPTED DOC\n"<<scene;
        return QDomDocument();
    }
522

523
    QDomNodeList pls = mlt.elementsByTagName(QStringLiteral("playlist"));
524 525
    QDomElement mainPlaylist;
    for (int i = 0; i < pls.count(); ++i) {
526
        if (pls.at(i).toElement().attribute(QStringLiteral("id")) == BinPlaylist::binPlaylistId) {
527 528 529 530
            mainPlaylist = pls.at(i).toElement();
            break;
        }
    }
531

532
    // check if project contains custom effects to embed them in project file
533
    QDomNodeList effects = mlt.elementsByTagName(QStringLiteral("filter"));
534
    int maxEffects = effects.count();
Nicolas Carion's avatar
Nicolas Carion committed
535
    // qCDebug(KDENLIVE_LOG) << "// FOUD " << maxEffects << " EFFECTS+++++++++++++++++++++";
Laurent Montel's avatar
Laurent Montel committed
536
    QMap<QString, QString> effectIds;
537
    for (int i = 0; i < maxEffects; ++i) {
538 539 540 541
        QDomNode m = effects.at(i);
        QDomNodeList params = m.childNodes();
        QString id;
        QString tag;
542
        for (int j = 0; j < params.count(); ++j) {
543
            QDomElement e = params.item(j).toElement();
544
            if (e.attribute(QStringLiteral("name")) == QLatin1String("kdenlive_id")) {
545 546
                id = e.firstChild().nodeValue();
            }
547
            if (e.attribute(QStringLiteral("name")) == QLatin1String("tag")) {
548 549
                tag = e.firstChild().nodeValue();
            }
Laurent Montel's avatar
Laurent Montel committed
550 551 552
            if (!id.isEmpty() && !tag.isEmpty()) {
                effectIds.insert(id, tag);
            }
553 554
        }
    }
Nicolas Carion's avatar
Nicolas Carion committed
555
    // TODO: find a way to process this before rendering MLT scenelist to xml
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
556
    /*QDomDocument customeffects = initEffects::getUsedCustomEffects(effectIds);
Laurent Montel's avatar
Laurent Montel committed
557
    if (!customeffects.documentElement().childNodes().isEmpty()) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
558 559
        Xml::setXmlProperty(mainPlaylist, QStringLiteral("kdenlive:customeffects"), customeffects.toString());
    }*/
Nicolas Carion's avatar
Nicolas Carion committed
560
    // addedXml.appendChild(sceneList.importNode(customeffects.documentElement(), true));
561

Nicolas Carion's avatar
Nicolas Carion committed
562
    // TODO: move metadata to previous step in saving process
563
    QDomElement docmetadata = sceneList.createElement(QStringLiteral("documentmetadata"));
564 565 566 567 568
    QMapIterator<QString, QString> j(m_documentMetadata);
    while (j.hasNext()) {
        j.next();
        docmetadata.setAttribute(j.key(), j.value());
    }
Nicolas Carion's avatar
Nicolas Carion committed
569
    // addedXml.appendChild(docmetadata);
570

571 572 573
    return sceneList;
}

574
bool KdenliveDoc::saveSceneList(const QString &path, const QString &scene)
575
{
576
    QDomDocument sceneList = xmlSceneList(scene);
577
    if (sceneList.isNull()) {
Nicolas Carion's avatar
Nicolas Carion committed
578
        // Make sure we don't save if scenelist is corrupted
579
        KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1, scene list is corrupted.", path));
580 581
        return false;
    }
582

583
    // Backup current version
584
    backupLastSavedVersion(path);
585 586 587 588 589 590 591 592 593 594 595
    if (m_documentOpenStatus != CleanProject) {
        // create visible backup file and warn user
        QString baseFile = path.section(QStringLiteral(".kdenlive"), 0, 0);
        int ct = 0;
        QString backupFile = baseFile + QStringLiteral("_backup") + QString::number(ct) + QStringLiteral(".kdenlive");
        while (QFile::exists(backupFile)) {
            ct++;
            backupFile = baseFile + QStringLiteral("_backup") + QString::number(ct) + QStringLiteral(".kdenlive");
        }
        QString message;
        if (m_documentOpenStatus == UpgradedProject) {
Nicolas Carion's avatar
Nicolas Carion committed
596 597 598 599
            message =
                i18n("Your project file was upgraded to the latest Kdenlive document version.\nTo make sure you do not lose data, a backup copy called %1 "
                     "was created.",
                     backupFile);
600
        } else {
Pino Toscano's avatar
Pino Toscano committed
601
            message = i18n("Your project file was modified by Kdenlive.\nTo make sure you do not lose data, a backup copy called %1 was created.", backupFile);
602
        }
603

604 605 606 607 608 609
        KIO::FileCopyJob *copyjob = KIO::file_copy(QUrl::fromLocalFile(path), QUrl::fromLocalFile(backupFile));
        if (copyjob->exec()) {
            KMessageBox::information(QApplication::activeWindow(), message);
            m_documentOpenStatus = CleanProject;
        } else {
            KMessageBox::information(
610 611 612
                QApplication::activeWindow(),
                i18n("Your project file was upgraded to the latest Kdenlive document version, but it was not possible to create the backup copy %1.",
                     backupFile));
613 614
        }
    }
615
    QSaveFile file(path);
616
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
Laurent Montel's avatar
Laurent Montel committed
617
        qCWarning(KDENLIVE_LOG) << "//////  ERROR writing to file: " << path;
618
        KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", path));
619
        return false;
620
    }
621 622 623
    const QByteArray sceneData = sceneList.toString().toUtf8();
    file.write(sceneData);
    if (!file.commit()) {
624
        KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", path));
625 626
        return false;
    }
627
    cleanupBackupFiles();
628
    QFileInfo info(path);
629
    QString fileName = QUrl::fromLocalFile(path).fileName().section(QLatin1Char('.'), 0, -2);
Laurent Montel's avatar
Laurent Montel committed
630
    fileName.append(QLatin1Char('-') + m_documentProperties.value(QStringLiteral("documentid")));
631
    fileName.append(info.lastModified().toString(QStringLiteral("-yyyy-MM-dd-hh-mm")));
Laurent Montel's avatar
Laurent Montel committed
632
    fileName.append(QStringLiteral(".kdenlive.png"));
633
    QDir backupFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/.backup"));
634
    emit saveTimelinePreview(backupFolder.absoluteFilePath(fileName));
635
    return true;
636 637
}

638
QString KdenliveDoc::projectTempFolder() const
639
{
640 641 642
    if (m_projectFolder.isEmpty()) {
        return QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
    }
643 644 645
    return m_projectFolder;
}

646 647 648
QString KdenliveDoc::projectDataFolder() const
{
    if (m_projectFolder.isEmpty()) {
649 650 651
        if (KdenliveSettings::customprojectfolder()) {
            return KdenliveSettings::defaultprojectfolder();
        }
652
        return QStandardPaths::writableLocation(QStandardPaths::MoviesLocation);
653 654 655 656
    }
    return m_projectFolder;
}

Laurent Montel's avatar
Laurent Montel committed
657
void KdenliveDoc::setProjectFolder(const QUrl &url)
658
{
Laurent Montel's avatar
Laurent Montel committed
659 660 661
    if (url == QUrl::fromLocalFile(m_projectFolder)) {
        return;
    }
662
    setModified(true);
663 664 665
    QDir dir(url.toLocalFile());
    if (!dir.exists()) {
        dir.mkpath(dir.absolutePath());
666
    }
667
    dir.mkdir(QStringLiteral("titles"));
Nicolas Carion's avatar
Nicolas Carion committed
668 669
    /*if (KMessageBox::questionYesNo(QApplication::activeWindow(), i18n("You have changed the project folder. Do you want to copy the cached data from %1 to the
     * new folder %2?", m_projectFolder, url.path())) == KMessageBox::Yes) moveProjectData(url);*/
670
    m_projectFolder = url.toLocalFile();
671 672

    updateProjectFolderPlacesEntry();
673 674
}

Nicolas Carion's avatar
Nicolas Carion committed
675
void KdenliveDoc::moveProjectData(const QString & /*src*/, const QString &dest)
676
{
Jean-Baptiste Mardelle's avatar
cleanup  
Jean-Baptiste Mardelle committed
677
    // Move proxies
678

679
    QList<QUrl> cacheUrls;
680 681 682 683 684
    auto binClips = pCore->projectItemModel()->getAllClipIds();
    // First step: all clips referenced by the bin model exist and are inserted
    for (const auto &binClip : binClips) {
        auto projClip = pCore->projectItemModel()->getClipByBinID(binClip);
        if (projClip->clipType() == ClipType::Text) {
685
            // the image for title clip must be moved
686
            QUrl oldUrl = QUrl::fromLocalFile(projClip->clipUrl());
687
            if (!oldUrl.isEmpty()) {
Jean-Baptiste Mardelle's avatar
cleanup  
Jean-Baptiste Mardelle committed
688
                QUrl newUrl = QUrl::fromLocalFile(dest + QStringLiteral("/titles/") + oldUrl.fileName());
689
                KIO::Job *job = KIO::copy(oldUrl, newUrl);
Laurent Montel's avatar
Laurent Montel committed
690
                if (job->exec()) {
691
                    projClip->setProducerProperty(QStringLiteral("resource"), newUrl.toLocalFile());
Laurent Montel's avatar
Laurent Montel committed
692
                }
693 694
            }
            continue;
695
        }
696
        QString proxy = projClip->getProducerProperty(QStringLiteral("kdenlive:proxy"));
697 698 699 700 701
        if (proxy.length() > 2 && QFile::exists(proxy)) {
            QUrl pUrl = QUrl::fromLocalFile(proxy);
            if (!cacheUrls.contains(pUrl)) {
                cacheUrls << pUrl;
            }
702
        }
703 704
    }
    if (!cacheUrls.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
705
        QDir proxyDir(dest + QStringLiteral("/proxy/"));
706 707 708
        if (proxyDir.mkpath(QStringLiteral("."))) {
            KIO::CopyJob *job = KIO::move(cacheUrls, QUrl::fromLocalFile(proxyDir.absolutePath()));
            KJobWidgets::setWindow(job, QApplication::activeWindow());
Nicolas Carion's avatar
Nicolas Carion committed
709
            if (static_cast<int>(job->exec()) > 0) {
710 711
                KMessageBox::sorry(QApplication::activeWindow(), i18n("Moving proxy clips failed: %1", job->errorText()));
            }
712
        }
713 714 715
    }
}

716 717
bool KdenliveDoc::profileChanged(const QString &profile) const
{
718
    return pCore->getCurrentProfile() != ProfileRepository::get()->getProfile(profile);
719 720
}

721 722 723 724 725
Render *KdenliveDoc::renderer()
{
    return nullptr;
}

726
std::shared_ptr<DocUndoStack> KdenliveDoc::commandStack()
727
{
728
    return m_commandStack;
729 730
}

Laurent Montel's avatar
Laurent Montel committed
731
int KdenliveDoc::getFramePos(const QString &duration)
732
{
733
    return m_timecode.getFrameCount(duration);
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
734 735
}

736 737
Timecode KdenliveDoc::timecode() const
{
738
    return m_timecode;
739 740
}

741 742
int KdenliveDoc::width() const
{
743
    return pCore->getCurrentProfile()->width();
744 745
}

746