kdenlivedoc.cpp 70.1 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 = 1.00;
72

Nicolas Carion's avatar
Nicolas Carion committed
73
KdenliveDoc::KdenliveDoc(const QUrl &url, QString projectFolder, QUndoGroup *undoGroup, const QString &profileName, const QMap<QString, QString> &properties,
74
                         const QMap<QString, QString> &metadata, const QPair<int, int> &tracks, int audioChannels, 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
112
    m_documentProperties[QStringLiteral("videoTarget")] = QString::number(tracks.second);
    m_documentProperties[QStringLiteral("audioTarget")] = QString::number(tracks.second - 1);
    m_documentProperties[QStringLiteral("activeTrack")] = QString::number(tracks.second);
    m_documentProperties[QStringLiteral("audioChannels")] = QString::number(audioChannels);
113
    m_documentProperties[QStringLiteral("enableTimelineZone")] = QLatin1Char('0');
114
115
    m_documentProperties[QStringLiteral("zonein")] = QLatin1Char('0');
    m_documentProperties[QStringLiteral("zoneout")] = QStringLiteral("75");
116
    m_documentProperties[QStringLiteral("seekOffset")] = QString::number(TimelineModel::seekDuration);
117

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

125
126
127
128
129
130
    // Load metadata
    QMapIterator<QString, QString> j(metadata);
    while (j.hasNext()) {
        j.next();
        m_documentMetadata[j.key()] = j.value();
    }
131
    *openBackup = false;
132
    if (url.isValid()) {
133
        QFile file(url.toLocalFile());
134
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
135
            // The file cannot be opened
Nicolas Carion's avatar
Nicolas Carion committed
136
137
            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) {
138
139
                *openBackup = true;
            }
Nicolas Carion's avatar
Nicolas Carion committed
140
            // KMessageBox::error(parent, KIO::NetAccess::lastErrorString());
Laurent Montel's avatar
Laurent Montel committed
141
142
        } else {
            qCDebug(KDENLIVE_LOG) << " // / processing file open";
143
            QString errorMsg;
144
145
            int line;
            int col;
146
            QDomImplementation::setInvalidDataPolicy(QDomImplementation::DropInvalidChars);
147
            success = m_document.setContent(&file, false, &errorMsg, &line, &col);
148
            file.close();
149

150
151
            if (!success) {
                // It is corrupted
Nicolas Carion's avatar
Nicolas Carion committed
152
153
154
                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")));
155
                if (answer == KMessageBox::Yes) {
156
                    *openBackup = true;
Laurent Montel's avatar
Laurent Montel committed
157
                } else if (answer == KMessageBox::No) {
158
159
160
                    // 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
161
                        QString playlist = QString::fromUtf8(file.readAll());
162
163
164
165
                        while (!success && correction < 2) {
                            int errorPos = 0;
                            line--;
                            col = col - 2;
166
                            for (int k = 0; k < line && errorPos < playlist.length(); ++k) {
167
                                errorPos = playlist.indexOf(QLatin1Char('\n'), errorPos);
168
169
170
                                errorPos++;
                            }
                            errorPos += col;
Laurent Montel's avatar
Laurent Montel committed
171
172
173
                            if (errorPos >= playlist.length()) {
                                break;
                            }
174
175
176
177
178
179
180
181
                            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
182
                        } else {
183
184
                            // Document was modified, ask for backup
                            QDomElement mlt = m_document.documentElement();
185
                            mlt.setAttribute(QStringLiteral("modified"), 1);
186
187
188
                        }
                    }
                }
Laurent Montel's avatar
Laurent Montel committed
189
190
            } else {
                qCDebug(KDENLIVE_LOG) << " // / processing file open: validate";
191
                pCore->displayMessage(i18n("Validating"), OperationCompletedMessage, 100);
192
                qApp->processEvents();
193
                DocumentValidator validator(m_document, url);
Alberto Villa's avatar
Alberto Villa committed
194
                success = validator.isProject();
195
                if (!success) {
196
                    // It is not a project file
197
                    pCore->displayMessage(i18n("File %1 is not a Kdenlive project file", m_url.toLocalFile()), OperationCompletedMessage, 100);
Nicolas Carion's avatar
Nicolas Carion committed
198
199
200
                    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) {
201
202
                        *openBackup = true;
                    }
203
                } else {
204
                    /*
Alberto Villa's avatar
Alberto Villa committed
205
206
                     * Validate the file against the current version (upgrade
                     * and recover it if needed). It is NOT a passive operation
207
                     */
Alberto Villa's avatar
Alberto Villa committed
208
                    // TODO: backup the document or alert the user?
209
210
211
212
213
214
215
216
                    auto validationResult = validator.validate(DOCUMENTVERSION);
                    success = validationResult.first;

                    if (!validationResult.second.isEmpty()) {
                        qDebug() << "DECIMAL POINT has changed to ., was " << validationResult.second;
                        m_modifiedDecimalPoint = validationResult.second;
                    }

217
218
219
                    if (success && !KdenliveSettings::gpu_accel()) {
                        success = validator.checkMovit();
                    }
220
                    if (success) { // Let the validator handle error messages
Laurent Montel's avatar
Laurent Montel committed
221
                        qCDebug(KDENLIVE_LOG) << " // / processing file validate ok";
222
                        pCore->displayMessage(i18n("Check missing clips"), InformationMessage, 300);
223
                        qApp->processEvents();
224
                        DocumentChecker d(m_url, m_document);
225
                        success = !d.hasErrorInClips();
226
                        if (success) {
227
                            loadDocumentProperties();
228
229
230
231
232
233
                            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
234
235
                                setModified(true);
                            }
236
                            pCore->displayMessage(QString(), OperationCompletedMessage);
237
238
                        }
                    }
239
                }
240
            }
241
        }
242
    }
243

Alberto Villa's avatar
Alberto Villa committed
244
    // Something went wrong, or a new file was requested: create a new project
245
    if (!success) {
246
        m_url.clear();
247
        pCore->setCurrentProfile(profileName);
248
        m_document = createEmptyDocument(tracks.first, tracks.second);
249
        updateProjectProfile(false);
250
251
    } else {
        m_clipsCount = m_document.elementsByTagName(QLatin1String("entry")).size();
252
    }
253

254
255
256
257
258
259
260
261
    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
262
263
264
265
                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));
266
267
            } else {
                KMessageBox::information(parent, i18n("Document project folder is invalid, using system default folders"));
268
            }
269
            m_projectFolder.clear();
270
271
        }
    }
272
    initCacheDirs();
273

274
    updateProjectFolderPlacesEntry();
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
275
276
}

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

303
304
305
306
307
308
int KdenliveDoc::clipsCount() const
{
    return m_clipsCount;
}


309
const QByteArray KdenliveDoc::getAndClearProjectXml()
310
{
311
312
313
314
    const QByteArray result = m_document.toString().toUtf8();
    // We don't need the xml data anymore, throw away
    m_document.clear();
    return result;
315
316
}

317
QDomDocument KdenliveDoc::createEmptyDocument(int videotracks, int audiotracks)
318
{
Laurent Montel's avatar
Laurent Montel committed
319
    QList<TrackInfo> tracks;
320
321
322
    // 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
323
    tracks.reserve(audiotracks + videotracks);
324
    for (int i = 0; i < audiotracks; ++i) {
325
        TrackInfo audioTrack;
326
        audioTrack.type = AudioTrack;
327
328
329
        audioTrack.isMute = false;
        audioTrack.isBlind = true;
        audioTrack.isLocked = false;
330
        // audioTrack.trackName = i18n("Audio %1", audiotracks - i);
331
        audioTrack.duration = 0;
332
        tracks.append(audioTrack);
333
    }
334
    for (int i = 0; i < videotracks; ++i) {
335
        TrackInfo videoTrack;
336
        videoTrack.type = VideoTrack;
337
338
339
        videoTrack.isMute = false;
        videoTrack.isBlind = false;
        videoTrack.isLocked = false;
340
        // videoTrack.trackName = i18n("Video %1", i + 1);
341
        videoTrack.duration = 0;
342
        tracks.append(videoTrack);
343
    }
344
    return createEmptyDocument(tracks);
345
346
}

347
QDomDocument KdenliveDoc::createEmptyDocument(const QList<TrackInfo> &tracks)
348
349
350
{
    // Creating new document
    QDomDocument doc;
351
352
353
354
355
356
357
358
359
360
361
    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());
362
        track.set("kdenlive:timeline_active", 1);
363
        track.set("kdenlive:trackheight", KdenliveSettings::trackheight());
364
        if (tracks.at(i).type == AudioTrack) {
365
            track.set("kdenlive:audio_track", 1);
366
        }
367
368
369
370
371
372
        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
373
            } else {
374
                track.set("hide", 2);
375
            }
376
377
        } else if (tracks.at(i).isBlind) {
            track.set("hide", 1);
Laurent Montel's avatar
Laurent Montel committed
378
        }
379
380
381
382
        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
383
        tractor.insert_track(track, i + 1);
384
    }
Nicolas Carion's avatar
Nicolas Carion committed
385
    QScopedPointer<Mlt::Field> field(tractor.field());
386
    QString compositeService = TransitionsRepository::get()->getCompositingTransition();
387
    if (!compositeService.isEmpty()) {
388
        for (int i = 0; i <= tracks.count(); i++) {
389
            if (i > 0 && tracks.at(i - 1).type == AudioTrack) {
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
                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);
            }
406
        }
407
    }
408
409
410
411
412
    Mlt::Producer prod(tractor.get_producer());
    xmlConsumer.connect(prod);
    xmlConsumer.run();
    QString playlist = QString::fromUtf8(xmlConsumer.get("kdenlive_playlist"));
    doc.setContent(playlist);
413
    return doc;
414
415
}

416
bool KdenliveDoc::useProxy() const
417
{
Nicolas Carion's avatar
Nicolas Carion committed
418
    return m_documentProperties.value(QStringLiteral("enableproxy")).toInt() != 0;
419
420
}

421
422
423
424
425
bool KdenliveDoc::useExternalProxy() const
{
    return m_documentProperties.value(QStringLiteral("enableexternalproxy")).toInt() != 0;
}

426
427
bool KdenliveDoc::autoGenerateProxy(int width) const
{
Nicolas Carion's avatar
Nicolas Carion committed
428
429
    return (m_documentProperties.value(QStringLiteral("generateproxy")).toInt() != 0) &&
           width > m_documentProperties.value(QStringLiteral("proxyminsize")).toInt();
430
431
432
433
}

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

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

460
void KdenliveDoc::setZoom(int horizontal, int vertical)
461
{
462
    m_documentProperties[QStringLiteral("zoom")] = QString::number(horizontal);
463
464
465
    if (vertical > -1) {
        m_documentProperties[QStringLiteral("verticalzoom")] = QString::number(vertical);
    }
466
467
}

468
QPoint KdenliveDoc::zoom() const
469
{
470
    return QPoint(m_documentProperties.value(QStringLiteral("zoom")).toInt(), m_documentProperties.value(QStringLiteral("verticalzoom")).toInt());
471
472
}

473
474
void KdenliveDoc::setZone(int start, int end)
{
475
476
    m_documentProperties[QStringLiteral("zonein")] = QString::number(start);
    m_documentProperties[QStringLiteral("zoneout")] = QString::number(end);
477
478
}

479
480
QPoint KdenliveDoc::zone() const
{
481
    return QPoint(m_documentProperties.value(QStringLiteral("zonein")).toInt(), m_documentProperties.value(QStringLiteral("zoneout")).toInt());
482
483
}

484
485
486
487
488
QPair<int, int> KdenliveDoc::targetTracks() const
{
    return {m_documentProperties.value(QStringLiteral("videoTarget")).toInt(), m_documentProperties.value(QStringLiteral("audioTarget")).toInt()};
}

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

499
    // Set playlist audio volume to 100%
500
501
502
503
504
505
506
    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"));
507
            }
508
            break;
509
510
        }
    }
511
512
513
514
515
516
    QDomNodeList tracks = mlt.elementsByTagName(QStringLiteral("track"));
    if (tracks.isEmpty()) {
        // Something is very wrong, inform user.
        qDebug()<<" = = = =  = =  CORRUPTED DOC\n"<<scene;
        return QDomDocument();
    }
517

518
    QDomNodeList pls = mlt.elementsByTagName(QStringLiteral("playlist"));
519
520
    QDomElement mainPlaylist;
    for (int i = 0; i < pls.count(); ++i) {
521
        if (pls.at(i).toElement().attribute(QStringLiteral("id")) == BinPlaylist::binPlaylistId) {
522
523
524
525
            mainPlaylist = pls.at(i).toElement();
            break;
        }
    }
526

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

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

566
567
568
    return sceneList;
}

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

578
    // Backup current version
579
    backupLastSavedVersion(path);
580
581
582
583
584
585
586
587
588
589
590
    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
591
592
593
594
            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);
595
        } else {
Pino Toscano's avatar
Pino Toscano committed
596
            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);
597
        }
598

599
600
601
602
603
604
        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(
605
606
607
                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));
608
609
        }
    }
610
    QSaveFile file(path);
611
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
Laurent Montel's avatar
Laurent Montel committed
612
        qCWarning(KDENLIVE_LOG) << "//////  ERROR writing to file: " << path;
613
        KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", path));
614
        return false;
615
    }
Simon Eugster's avatar
Simon Eugster committed
616

617
    const QByteArray sceneData = sceneList.toString().toUtf8();
Simon Eugster's avatar
Simon Eugster committed
618

619
620
    file.write(sceneData);
    if (!file.commit()) {
621
        KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", path));
622
623
        return false;
    }
624
    cleanupBackupFiles();
625
    QFileInfo info(path);
626
    QString fileName = QUrl::fromLocalFile(path).fileName().section(QLatin1Char('.'), 0, -2);
Laurent Montel's avatar
Laurent Montel committed
627
    fileName.append(QLatin1Char('-') + m_documentProperties.value(QStringLiteral("documentid")));
628
    fileName.append(info.lastModified().toString(QStringLiteral("-yyyy-MM-dd-hh-mm")));
Laurent Montel's avatar
Laurent Montel committed
629
    fileName.append(QStringLiteral(".kdenlive.png"));
630
    QDir backupFolder(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/.backup"));
631
    emit saveTimelinePreview(backupFolder.absoluteFilePath(fileName));
632
    return true;
633
634
}

635
QString KdenliveDoc::projectTempFolder() const
636
{
637
638
639
    if (m_projectFolder.isEmpty()) {
        return QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
    }
640
641
642
    return m_projectFolder;
}

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

Laurent Montel's avatar
Laurent Montel committed
654
void KdenliveDoc::setProjectFolder(const QUrl &url)
655
{
Laurent Montel's avatar
Laurent Montel committed
656
657
658
    if (url == QUrl::fromLocalFile(m_projectFolder)) {
        return;
    }
659
    setModified(true);
660
661
662
    QDir dir(url.toLocalFile());
    if (!dir.exists()) {
        dir.mkpath(dir.absolutePath());
663
    }
664
    dir.mkdir(QStringLiteral("titles"));
Nicolas Carion's avatar
Nicolas Carion committed
665
666
    /*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);*/
667
    m_projectFolder = url.toLocalFile();
668
669

    updateProjectFolderPlacesEntry();
670
671
}

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

676
    QList<QUrl> cacheUrls;
677
678
679
680
681
    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) {
682
            // the image for title clip must be moved
683
            QUrl oldUrl = QUrl::fromLocalFile(projClip->clipUrl());
684
            if (!oldUrl.isEmpty()) {
Jean-Baptiste Mardelle's avatar
cleanup    
Jean-Baptiste Mardelle committed
685
                QUrl newUrl = QUrl::fromLocalFile(dest + QStringLiteral("/titles/") + oldUrl.fileName());
686
                KIO::Job *job = KIO::copy(oldUrl, newUrl);
Laurent Montel's avatar
Laurent Montel committed
687
                if (job->exec()) {
688
                    projClip->setProducerProperty(QStringLiteral("resource"), newUrl.toLocalFile());
Laurent Montel's avatar
Laurent Montel committed
689
                }
690
691
            }
            continue;
692
        }
693
        QString proxy = projClip->getProducerProperty(QStringLiteral("kdenlive:proxy"));
694
695
696
697
698
        if (proxy.length() > 2 && QFile::exists(proxy)) {
            QUrl pUrl = QUrl::fromLocalFile(proxy);
            if (!cacheUrls.contains(pUrl)) {
                cacheUrls << pUrl;
            }
699
        }
700
701
    }
    if (!cacheUrls.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
702
        QDir proxyDir(dest + QStringLiteral("/proxy/"));
703
704
705
        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
706
            if (static_cast<int>(job->exec()) > 0) {
707
708
                KMessageBox::sorry(QApplication::activeWindow(), i18n("Moving proxy clips failed: %1", job->errorText()));
            }
709
        }
710
711
712
    }
}

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

718
719
720
721
722
Render *KdenliveDoc::renderer()
{
    return nullptr;
}

723
std::shared_ptr<DocUndoStack> KdenliveDoc::commandStack()
724
{
725
    return m_commandStack;
726
727
}

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

733
734
Timecode KdenliveDoc::timecode() const
{
735
    return m_timecode;
736
737
}

738
739
int KdenliveDoc::width() const
{
740
    return pCore->getCurrentProfile()->width();
741
742
}

743
744
int KdenliveDoc::height() const
{
745
    return pCore->getCurrentProfile()->height();
746
747
}

748
QUrl KdenliveDoc::url() const
749
{
750
    return m_url;
751
752
}

753
void KdenliveDoc::setUrl(const QUrl &url)
754
{
755
    m_url = url;
756
757
}

758
void KdenliveDoc::slotModified()
759
{
Nicolas Carion's avatar
Nicolas Carion committed
760
    setModified(!m_commandStack->isClean());
761
762
}

763
764
void KdenliveDoc::setModified(bool mod)
{