kdenlivedoc.cpp 69.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
 ***************************************************************************/

20

Vincent Pinon's avatar
Vincent Pinon committed
21 22 23
#include "kdenlivedoc.h"
#include "documentchecker.h"
#include "documentvalidator.h"
24
#include "mltcontroller/clipcontroller.h"
25
#include "mltcontroller/producerqueue.h"
Vincent Pinon's avatar
Vincent Pinon committed
26
#include <config-kdenlive.h>
27 28
#include "kdenlivesettings.h"
#include "renderer.h"
29
#include "mainwindow.h"
Vincent Pinon's avatar
Vincent Pinon committed
30
#include "project/clipmanager.h"
31
#include "project/projectcommands.h"
32
#include "bin/bincommands.h"
Vincent Pinon's avatar
Vincent Pinon committed
33 34
#include "effectslist/initeffects.h"
#include "dialogs/profilesdialog.h"
Vincent Pinon's avatar
Vincent Pinon committed
35
#include "titler/titlewidget.h"
36 37
#include "project/notesplugin.h"
#include "project/dialogs/noteswidget.h"
38 39 40
#include "core.h"
#include "bin/bin.h"
#include "bin/projectclip.h"
41
#include "utils/KoIconUtils.h"
42
#include "mltcontroller/bincontroller.h"
43
#include "mltcontroller/effectscontroller.h"
44

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

53 54
#include <QCryptographicHash>
#include <QFile>
55 56
#include <QDebug>
#include <QFileDialog>
57
#include <QInputDialog>
58
#include <QDomImplementation>
59 60 61
#include <QUndoGroup>
#include <QTimer>
#include <QUndoStack>
62
#include <QTextEdit>
63

64
#include <mlt++/Mlt.h>
65
#include <KJobWidgets/KJobWidgets>
66
#include <QStandardPaths>
67

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

73 74 75 76 77 78 79 80 81 82 83 84
DocUndoStack::DocUndoStack(QUndoGroup *parent) : QUndoStack(parent)
{
}

//TODO: custom undostack everywhere do that 
void DocUndoStack::push(QUndoCommand *cmd)
{
    if (index() < count()) 
        emit invalidate();
    QUndoStack::push(cmd);
}

85
const double DOCUMENTVERSION = 0.95;
86

87
KdenliveDoc::KdenliveDoc(const QUrl &url, const QUrl &projectFolder, QUndoGroup *undoGroup, const QString &profileName, const QMap <QString, QString>& properties, const QMap <QString, QString>& metadata, const QPoint &tracks, Render *render, NotesPlugin *notes, bool *openBackup, MainWindow *parent) :
88 89 90
    QObject(parent),
    m_autosave(NULL),
    m_url(url),
91 92
    m_width(0),
    m_height(0),
93
    m_render(render),
94
    m_notesWidget(notes->widget()),
95
    m_modified(false),
96
    m_projectFolder(projectFolder)
97
{
98
    // init m_profile struct
99
    m_commandStack = new DocUndoStack(undoGroup);
100 101 102 103 104 105 106 107 108 109
    m_profile.frame_rate_num = 0;
    m_profile.frame_rate_den = 0;
    m_profile.width = 0;
    m_profile.height = 0;
    m_profile.progressive = 0;
    m_profile.sample_aspect_num = 0;
    m_profile.sample_aspect_den = 0;
    m_profile.display_aspect_num = 0;
    m_profile.display_aspect_den = 0;
    m_profile.colorspace = 0;
110
    m_clipManager = new ClipManager(this);
Laurent Montel's avatar
Laurent Montel committed
111
    connect(m_clipManager, SIGNAL(displayMessage(QString,int)), parent, SLOT(slotGotProgressInfo(QString,int)));
Alberto Villa's avatar
Alberto Villa committed
112
    bool success = false;
113 114
    connect(m_commandStack, SIGNAL(indexChanged(int)), this, SLOT(slotModified()));
    connect(m_commandStack, SIGNAL(invalidate()), this, SLOT(checkPreviewStack()));
115
    connect(m_render, SIGNAL(setDocumentNotes(QString)), this, SLOT(slotSetDocumentNotes(QString)));
116
    connect(pCore->producerQueue(), &ProducerQueue::switchProfile, this, &KdenliveDoc::switchProfile);
117
    //connect(m_commandStack, SIGNAL(cleanChanged(bool)), this, SLOT(setModified(bool)));
118

119 120 121 122 123
    // Init clip modification tracker
    m_modifiedTimer.setInterval(1500);
    connect(&m_fileWatcher, &KDirWatch::dirty, this, &KdenliveDoc::slotClipModified);
    connect(&m_fileWatcher, &KDirWatch::deleted, this, &KdenliveDoc::slotClipMissing);
    connect(&m_modifiedTimer, &QTimer::timeout, this, &KdenliveDoc::slotProcessModifiedClips);
124 125

    // init default document properties
126 127 128 129 130 131 132 133 134 135 136 137
    m_documentProperties[QStringLiteral("zoom")] = '7';
    m_documentProperties[QStringLiteral("verticalzoom")] = '1';
    m_documentProperties[QStringLiteral("zonein")] = '0';
    m_documentProperties[QStringLiteral("zoneout")] = QStringLiteral("100");
    m_documentProperties[QStringLiteral("enableproxy")] = QString::number((int) KdenliveSettings::enableproxy());
    m_documentProperties[QStringLiteral("proxyparams")] = KdenliveSettings::proxyparams();
    m_documentProperties[QStringLiteral("proxyextension")] = KdenliveSettings::proxyextension();
    m_documentProperties[QStringLiteral("generateproxy")] = QString::number((int) KdenliveSettings::generateproxy());
    m_documentProperties[QStringLiteral("proxyminsize")] = QString::number(KdenliveSettings::proxyminsize());
    m_documentProperties[QStringLiteral("generateimageproxy")] = QString::number((int) KdenliveSettings::generateimageproxy());
    m_documentProperties[QStringLiteral("proxyimageminsize")] = QString::number(KdenliveSettings::proxyimageminsize());
    m_documentProperties[QStringLiteral("documentid")] = QString::number(QDateTime::currentMSecsSinceEpoch());
138

139 140 141 142 143 144
    // Load properties
    QMapIterator<QString, QString> i(properties);
    while (i.hasNext()) {
        i.next();
        m_documentProperties[i.key()] = i.value();
    }
145

146 147 148 149 150 151
    // Load metadata
    QMapIterator<QString, QString> j(metadata);
    while (j.hasNext()) {
        j.next();
        m_documentMetadata[j.key()] = j.value();
    }
152

Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
153
    if (QLocale().decimalPoint() != QLocale::system().decimalPoint()) {
154
        setlocale(LC_NUMERIC, "");
155 156 157
        QLocale systemLocale = QLocale::system();
        systemLocale.setNumberOptions(QLocale::OmitGroupSeparator);
        QLocale::setDefault(systemLocale);
158
        // locale conversion might need to be redone
159
        initEffects::parseEffectFiles(pCore->binController()->mltRepository(), setlocale(LC_NUMERIC, NULL));
160
    }
161
    *openBackup = false;
162
    if (url.isValid()) {
163
        QFile file(url.toLocalFile());
164
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
165
            // The file cannot be opened
166
            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) {
167 168 169 170
                *openBackup = true;
            }
            //KMessageBox::error(parent, KIO::NetAccess::lastErrorString());
        }
Alberto Villa's avatar
Alberto Villa committed
171
        else {
172
            qDebug()<<" // / processing file open";
173
            QString errorMsg;
174 175
            int line;
            int col;
176
            QDomImplementation::setInvalidDataPolicy(QDomImplementation::DropInvalidChars);
177
            success = m_document.setContent(&file, false, &errorMsg, &line, &col);
178
            file.close();
179

180 181
            if (!success) {
                // It is corrupted
182
                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")));
183
                if (answer == KMessageBox::Yes) {
184 185 186 187 188 189 190 191 192 193 194
                    *openBackup = true;
                }
                else if (answer == KMessageBox::No) {
                    // Try to recover broken file produced by Kdenlive 0.9.4
                    if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
                        int correction = 0;
                        QString playlist = file.readAll();
                        while (!success && correction < 2) {
                            int errorPos = 0;
                            line--;
                            col = col - 2;
195
                            for (int j = 0; j < line && errorPos < playlist.length(); ++j) {
196
                                errorPos = playlist.indexOf(QStringLiteral("\n"), errorPos);
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
                                errorPos++;
                            }
                            errorPos += col;
                            if (errorPos >= playlist.length()) break;
                            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"));
                        }
                        else {
                            // Document was modified, ask for backup
                            QDomElement mlt = m_document.documentElement();
213
                            mlt.setAttribute(QStringLiteral("modified"), 1);
214 215 216
                        }
                    }
                }
217
            }
218
            else {
219
                qDebug()<<" // / processing file open: validate";
220
                parent->slotGotProgressInfo(i18n("Validating"), 0);
221
                qApp->processEvents();
222
                DocumentValidator validator(m_document, url);
Alberto Villa's avatar
Alberto Villa committed
223
                success = validator.isProject();
224
                if (!success) {
225
                    // It is not a project file
226
                    parent->slotGotProgressInfo(i18n("File %1 is not a Kdenlive project file", m_url.path()), 100);
227 228 229
                    if (KMessageBox::warningContinueCancel(parent, i18n("File %1 is not a valid project file.\nDo you want to open a backup file?", m_url.path()), i18n("Error opening file"), KGuiItem(i18n("Open Backup"))) == KMessageBox::Continue) {
                        *openBackup = true;
                    }
230
                } else {
231
                    /*
Alberto Villa's avatar
Alberto Villa committed
232 233
                     * Validate the file against the current version (upgrade
                     * and recover it if needed). It is NOT a passive operation
234
                     */
Alberto Villa's avatar
Alberto Villa committed
235 236
                    // TODO: backup the document or alert the user?
                    success = validator.validate(DOCUMENTVERSION);
237 238 239
                    if (success && !KdenliveSettings::gpu_accel()) {
                        success = validator.checkMovit();
                    }
240
                    if (success) { // Let the validator handle error messages
241
                        qDebug()<<" // / processing file validate ok";
242
                        parent->slotGotProgressInfo(i18n("Check missing clips"), 0);
243
                        qApp->processEvents();
244
                        DocumentChecker d(m_url, m_document);
245
                        success = !d.hasErrorInClips();
246
                        if (success) {
247
                            loadDocumentProperties();
248
                            if (m_document.documentElement().attribute(QStringLiteral("modified")) == QLatin1String("1")) setModified(true);
249
                            if (validator.isModified()) setModified(true);
250 251
                        }
                    }
252
                }
253
            }
254
        }
255
    }
256
    initCacheDirs();
257

Alberto Villa's avatar
Alberto Villa committed
258
    // Something went wrong, or a new file was requested: create a new project
259
    if (!success) {
260
        m_url.clear();
261
        m_profile = ProfilesDialog::getVideoProfile(profileName);
262
        m_document = createEmptyDocument(tracks.x(), tracks.y());
263
        updateProjectProfile(false);
264
    }
265

266
    // Ask to create the project directory if it does not exist
267 268
    QFileInfo checkProjectFolder(m_projectFolder.toString(QUrl::RemoveFilename | QUrl::RemoveScheme));
    if (!QFile::exists(m_projectFolder.path()) && checkProjectFolder.isWritable()) {
269 270 271 272 273 274 275 276 277
        int create = KMessageBox::questionYesNo(parent, i18n("Project directory %1 does not exist. Create it?", m_projectFolder.path()));
        if (create == KMessageBox::Yes) {
            QDir projectDir(m_projectFolder.path());
            bool ok = projectDir.mkpath(m_projectFolder.path());
            if (!ok) {
                KMessageBox::sorry(parent, i18n("The directory %1, could not be created.\nPlease make sure you have the required permissions.", m_projectFolder.path()));
            }
        }
    }
Alberto Villa's avatar
Alberto Villa committed
278 279

    // Make sure the project folder is usable
280
    if (m_projectFolder.isEmpty() || !QFile::exists(m_projectFolder.path())) {
Alberto Villa's avatar
Alberto Villa committed
281
        KMessageBox::information(parent, i18n("Document project folder is invalid, setting it to the default one: %1", KdenliveSettings::defaultprojectfolder()));
282
        m_projectFolder = QUrl::fromLocalFile(KdenliveSettings::defaultprojectfolder());
Alberto Villa's avatar
Alberto Villa committed
283 284 285
    }

    // Make sure that the necessary folders exist
286
    QDir dir(m_projectFolder.path());
287 288
    dir.mkdir(QStringLiteral("titles"));
    dir.mkdir(QStringLiteral(".backup"));
289

290
    updateProjectFolderPlacesEntry();
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
291 292
}

293 294 295 296 297 298
void KdenliveDoc::slotSetDocumentNotes(const QString &notes)
{
    m_notesWidget->setHtml(notes);
}


299 300
KdenliveDoc::~KdenliveDoc()
{
301 302 303 304 305 306 307 308 309 310 311 312 313
    if (m_url.isEmpty()) {
        // Document was never saved, delete cache folder
        QString documentId = QDir::cleanPath(getDocumentProperty(QStringLiteral("documentid")));
        bool ok;
        documentId.toLong(&ok);
        if (ok && !documentId.isEmpty()) {
            QString kdenliveCacheDir = QStandardPaths::writableLocation(QStandardPaths::CacheLocation);
            QDir cacheDir(kdenliveCacheDir + "/" + documentId);
            if (cacheDir.exists() && cacheDir.dirName() == documentId) {
                cacheDir.removeRecursively();
            }
        }
    }
314
    delete m_commandStack;
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
315
    //qDebug() << "// DEL CLP MAN";
316
    delete m_clipManager;
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
317
    //qDebug() << "// DEL CLP MAN done";
318 319
    if (m_autosave) {
        if (!m_autosave->fileName().isEmpty()) m_autosave->remove();
320 321
        delete m_autosave;
    }
322 323
}

324
int KdenliveDoc::setSceneList()
325
{
326 327
    //m_render->resetProfile(m_profile);
    pCore->bin()->isLoading = true;
328
    pCore->producerQueue()->abortOperations();
329
    if (m_render->setSceneList(m_document.toString(), m_documentProperties.value(QStringLiteral("position")).toInt()) == -1) {
330 331 332
        // INVALID MLT Consumer, something is wrong
        return -1;
    }
333
    pCore->bin()->isLoading = false;
334 335 336 337 338
    
    bool ok = false;
    QDir thumbsFolder = getCacheDir(CacheThumbs, &ok);
    if (ok)
        pCore->binController()->checkThumbnails(thumbsFolder.absolutePath());
339
    m_documentProperties.remove(QStringLiteral("position"));
340
    pCore->monitorManager()->activateMonitor(Kdenlive::ClipMonitor, true);
341
    return 0;
342 343
}

344
QDomDocument KdenliveDoc::createEmptyDocument(int videotracks, int audiotracks)
345
{
346
    QList <TrackInfo> tracks;
347 348 349
    // 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
350
    for (int i = 0; i < audiotracks; ++i) {
351
        TrackInfo audioTrack;
352
        audioTrack.type = AudioTrack;
353 354 355
        audioTrack.isMute = false;
        audioTrack.isBlind = true;
        audioTrack.isLocked = false;
356
        audioTrack.trackName = i18n("Audio %1", audiotracks - i);
357
        audioTrack.duration = 0;
358
        audioTrack.effectsList = EffectsList(true);
359
        tracks.append(audioTrack);
360

361
    }
362
    for (int i = 0; i < videotracks; ++i) {
363
        TrackInfo videoTrack;
364
        videoTrack.type = VideoTrack;
365 366 367
        videoTrack.isMute = false;
        videoTrack.isBlind = false;
        videoTrack.isLocked = false;
368
        videoTrack.trackName = i18n("Video %1", i + 1);
369
        videoTrack.duration = 0;
370
        videoTrack.effectsList = EffectsList(true);
371
        tracks.append(videoTrack);
372
    }
373
    return createEmptyDocument(tracks);
374 375
}

376
QDomDocument KdenliveDoc::createEmptyDocument(const QList <TrackInfo> &tracks)
377 378 379
{
    // Creating new document
    QDomDocument doc;
380 381
    QDomElement mlt = doc.createElement(QStringLiteral("mlt"));
    mlt.setAttribute(QStringLiteral("LC_NUMERIC"), QLatin1String(""));
382
    doc.appendChild(mlt);
383

384 385 386
    QDomElement blk = doc.createElement(QStringLiteral("producer"));
    blk.setAttribute(QStringLiteral("in"), 0);
    blk.setAttribute(QStringLiteral("out"), 500);
387 388
    blk.setAttribute(QStringLiteral("aspect_ratio"), 1);
    blk.setAttribute(QStringLiteral("set.test_audio"), 0);
389
    blk.setAttribute(QStringLiteral("id"), QStringLiteral("black"));
390

391 392 393
    QDomElement property = doc.createElement(QStringLiteral("property"));
    property.setAttribute(QStringLiteral("name"), QStringLiteral("mlt_type"));
    QDomText value = doc.createTextNode(QStringLiteral("producer"));
394 395 396
    property.appendChild(value);
    blk.appendChild(property);

397 398
    property = doc.createElement(QStringLiteral("property"));
    property.setAttribute(QStringLiteral("name"), QStringLiteral("aspect_ratio"));
399
    value = doc.createTextNode(QString::number(0));
400 401 402
    property.appendChild(value);
    blk.appendChild(property);

403 404
    property = doc.createElement(QStringLiteral("property"));
    property.setAttribute(QStringLiteral("name"), QStringLiteral("length"));
405 406 407 408
    value = doc.createTextNode(QString::number(15000));
    property.appendChild(value);
    blk.appendChild(property);

409 410 411
    property = doc.createElement(QStringLiteral("property"));
    property.setAttribute(QStringLiteral("name"), QStringLiteral("eof"));
    value = doc.createTextNode(QStringLiteral("pause"));
412 413 414
    property.appendChild(value);
    blk.appendChild(property);

415 416 417
    property = doc.createElement(QStringLiteral("property"));
    property.setAttribute(QStringLiteral("name"), QStringLiteral("resource"));
    value = doc.createTextNode(QStringLiteral("black"));
418 419 420
    property.appendChild(value);
    blk.appendChild(property);

421 422 423
    property = doc.createElement(QStringLiteral("property"));
    property.setAttribute(QStringLiteral("name"), QStringLiteral("mlt_service"));
    value = doc.createTextNode(QStringLiteral("colour"));
424 425 426 427 428 429
    property.appendChild(value);
    blk.appendChild(property);

    mlt.appendChild(blk);


430 431 432
    QDomElement tractor = doc.createElement(QStringLiteral("tractor"));
    tractor.setAttribute(QStringLiteral("id"), QStringLiteral("maintractor"));
    tractor.setAttribute(QStringLiteral("global_feed"), 1);
433
    //QDomElement multitrack = doc.createElement("multitrack");
434 435
    QDomElement playlist = doc.createElement(QStringLiteral("playlist"));
    playlist.setAttribute(QStringLiteral("id"), QStringLiteral("black_track"));
436
    mlt.appendChild(playlist);
437

438 439 440 441
    QDomElement blank0 = doc.createElement(QStringLiteral("entry"));
    blank0.setAttribute(QStringLiteral("in"), QStringLiteral("0"));
    blank0.setAttribute(QStringLiteral("out"), QStringLiteral("1"));
    blank0.setAttribute(QStringLiteral("producer"), QStringLiteral("black"));
442
    playlist.appendChild(blank0);
443 444

    // create playlists
445
    int total = tracks.count();
446 447
    // The lower video track will recieve composite transitions
    int lowerVideoTrack = -1;
448
    for (int i = 0; i < total; ++i) {
449
        QDomElement playlist = doc.createElement(QStringLiteral("playlist"));
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
450 451
        playlist.setAttribute(QStringLiteral("id"), "playlist" + QString::number(i+1));
        playlist.setAttribute(QStringLiteral("kdenlive:track_name"), tracks.at(i).trackName);
452
        if (tracks.at(i).type == AudioTrack) {
453
            playlist.setAttribute(QStringLiteral("kdenlive:audio_track"), 1);
454 455 456
        } else if (lowerVideoTrack == -1) {
            // Register first video track
            lowerVideoTrack = i + 1;
457
        }
458
        mlt.appendChild(playlist);
459
    }
460

461 462
    QDomElement track0 = doc.createElement(QStringLiteral("track"));
    track0.setAttribute(QStringLiteral("producer"), QStringLiteral("black_track"));
463 464
    tractor.appendChild(track0);

465
    // create audio and video tracks
466
    for (int i = 0; i < total; ++i) {
467
        QDomElement track = doc.createElement(QStringLiteral("track"));
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
468
        track.setAttribute(QStringLiteral("producer"), "playlist" + QString::number(i+1));
469
        if (tracks.at(i).type == AudioTrack) {
470
            track.setAttribute(QStringLiteral("hide"), QStringLiteral("video"));
471 472
        } else if (tracks.at(i).isBlind) {
            if (tracks.at(i).isMute) {
473
                track.setAttribute(QStringLiteral("hide"), QStringLiteral("all"));
474
            }
475
            else track.setAttribute(QStringLiteral("hide"), QStringLiteral("video"));
476
        }
477
        else if (tracks.at(i).isMute)
478
            track.setAttribute(QStringLiteral("hide"), QStringLiteral("audio"));
479 480 481
        tractor.appendChild(track);
    }

482
    // Transitions
483
    for (int i = 0; i < total; i++) {
484 485 486 487 488 489 490 491 492 493 494 495
        if (i > 0) {
            QDomElement transition = doc.createElement(QStringLiteral("transition"));
            transition.setAttribute(QStringLiteral("always_active"), QStringLiteral("1"));

            QDomElement property = doc.createElement(QStringLiteral("property"));
            property.setAttribute(QStringLiteral("name"), QStringLiteral("mlt_service"));
            value = doc.createTextNode(QStringLiteral("mix"));
            property.appendChild(value);
            transition.appendChild(property);

            property = doc.createElement(QStringLiteral("property"));
            property.setAttribute(QStringLiteral("name"), QStringLiteral("a_track"));
496
            QDomText value = doc.createTextNode(QStringLiteral("0"));
497 498 499 500 501
            property.appendChild(value);
            transition.appendChild(property);

            property = doc.createElement(QStringLiteral("property"));
            property.setAttribute(QStringLiteral("name"), QStringLiteral("b_track"));
502
            value = doc.createTextNode(QString::number(i));
503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
            property.appendChild(value);
            transition.appendChild(property);

            property = doc.createElement(QStringLiteral("property"));
            property.setAttribute(QStringLiteral("name"), QStringLiteral("combine"));
            value = doc.createTextNode(QStringLiteral("1"));
            property.appendChild(value);
            transition.appendChild(property);

            property = doc.createElement(QStringLiteral("property"));
            property.setAttribute(QStringLiteral("name"), QStringLiteral("internal_added"));
            value = doc.createTextNode(QStringLiteral("237"));
            property.appendChild(value);
            transition.appendChild(property);

            tractor.appendChild(transition);
        }
520
        if (i >= lowerVideoTrack && tracks.at(i).type == VideoTrack) {
521
            // Only add composite transition if both tracks are video
522
            QDomElement transition = doc.createElement(QStringLiteral("transition"));
523 524
            property = doc.createElement(QStringLiteral("property"));
            property.setAttribute(QStringLiteral("name"), QStringLiteral("mlt_service"));
525 526 527
            property.appendChild(doc.createTextNode(KdenliveSettings::gpu_accel() ? "movit.overlay" : "frei0r.cairoblend"));
            transition.appendChild(property);

528 529
            property = doc.createElement(QStringLiteral("property"));
            property.setAttribute(QStringLiteral("name"), QStringLiteral("a_track"));
530
            property.appendChild(doc.createTextNode(QString::number(lowerVideoTrack)));
531 532
            transition.appendChild(property);

533 534
            property = doc.createElement(QStringLiteral("property"));
            property.setAttribute(QStringLiteral("name"), QStringLiteral("b_track"));
535
            property.appendChild(doc.createTextNode(QString::number(i+1)));
536 537
            transition.appendChild(property);

538 539 540
            property = doc.createElement(QStringLiteral("property"));
            property.setAttribute(QStringLiteral("name"), QStringLiteral("internal_added"));
            property.appendChild(doc.createTextNode(QStringLiteral("237")));
541 542 543 544
            transition.appendChild(property);

            tractor.appendChild(transition);
        }
545
    }
546
    mlt.appendChild(tractor);
547
    return doc;
548 549
}

550
bool KdenliveDoc::useProxy() const
551
{
552
    return m_documentProperties.value(QStringLiteral("enableproxy")).toInt();
553 554
}

555 556 557 558 559 560 561 562 563
bool KdenliveDoc::autoGenerateProxy(int width) const
{
    return m_documentProperties.value(QStringLiteral("generateproxy")).toInt() && width > m_documentProperties.value(QStringLiteral("proxyminsize")).toInt();
}

bool KdenliveDoc::autoGenerateImageProxy(int width) const
{
    return m_documentProperties.value(QStringLiteral("generateimageproxy")).toInt() && width > m_documentProperties.value(QStringLiteral("proxyimageminsize")).toInt();
}
564

565
void KdenliveDoc::slotAutoSave()
566
{
567 568
    if (m_render && m_autosave) {
        if (!m_autosave->isOpen() && !m_autosave->open(QIODevice::ReadWrite)) {
569
            // show error: could not open the autosave file
570
            qDebug() << "ERROR; CANNOT CREATE AUTOSAVE FILE";
571
        }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
572
        //qDebug() << "// AUTOSAVE FILE: " << m_autosave->fileName();
573
        QDomDocument sceneList = xmlSceneList(m_render->sceneList());
574 575 576 577 578 579 580 581
        if (sceneList.isNull()) {
            //Make sure we don't save if scenelist is corrupted
            KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1, scene list is corrupted.", m_autosave->fileName()));
            return;
        }
        m_autosave->resize(0); 
        m_autosave->write(sceneList.toString().toUtf8());
        m_autosave->flush();
582
    }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
583 584
}

585
void KdenliveDoc::setZoom(int horizontal, int vertical)
586
{
587 588
    m_documentProperties[QStringLiteral("zoom")] = QString::number(horizontal);
    m_documentProperties[QStringLiteral("verticalzoom")] = QString::number(vertical);
589 590
}

591
QPoint KdenliveDoc::zoom() const
592
{
593
    return QPoint(m_documentProperties.value(QStringLiteral("zoom")).toInt(), m_documentProperties.value(QStringLiteral("verticalzoom")).toInt());
594 595
}

596 597
void KdenliveDoc::setZone(int start, int end)
{
598 599
    m_documentProperties[QStringLiteral("zonein")] = QString::number(start);
    m_documentProperties[QStringLiteral("zoneout")] = QString::number(end);
600 601
}

602 603
QPoint KdenliveDoc::zone() const
{
604
    return QPoint(m_documentProperties.value(QStringLiteral("zonein")).toInt(), m_documentProperties.value(QStringLiteral("zoneout")).toInt());
605 606
}

607
QDomDocument KdenliveDoc::xmlSceneList(const QString &scene)
608
{
609 610
    QDomDocument sceneList;
    sceneList.setContent(scene, true);
611
    QDomElement mlt = sceneList.firstChildElement(QStringLiteral("mlt"));
612
    if (mlt.isNull() || !mlt.hasChildNodes()) {
613 614
        //scenelist is corrupted
        return sceneList;
615
    }
616

617
    // Set playlist audio volume to 100%
618
    QDomElement tractor = mlt.firstChildElement(QStringLiteral("tractor"));
619
    if (!tractor.isNull()) {
620
        QDomNodeList props = tractor.elementsByTagName(QStringLiteral("property"));
621
        for (int i = 0; i < props.count(); ++i) {
622 623
            if (props.at(i).toElement().attribute(QStringLiteral("name")) == QLatin1String("meta.volume")) {
                props.at(i).firstChild().setNodeValue(QStringLiteral("1"));
624 625 626 627
                break;
            }
        }
    }
628
    QDomNodeList pls = mlt.elementsByTagName(QStringLiteral("playlist"));
629 630
    QDomElement mainPlaylist;
    for (int i = 0; i < pls.count(); ++i) {
631
        if (pls.at(i).toElement().attribute(QStringLiteral("id")) == pCore->binController()->binPlaylistId()) {
632 633 634 635
            mainPlaylist = pls.at(i).toElement();
            break;
        }
    }
636

637
    // check if project contains custom effects to embed them in project file
638
    QDomNodeList effects = mlt.elementsByTagName(QStringLiteral("filter"));
639
    int maxEffects = effects.count();
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
640
    //qDebug() << "// FOUD " << maxEffects << " EFFECTS+++++++++++++++++++++";
641
    QMap <QString, QString> effectIds;
642
    for (int i = 0; i < maxEffects; ++i) {
643 644 645 646
        QDomNode m = effects.at(i);
        QDomNodeList params = m.childNodes();
        QString id;
        QString tag;
647
        for (int j = 0; j < params.count(); ++j) {
648
            QDomElement e = params.item(j).toElement();
649
            if (e.attribute(QStringLiteral("name")) == QLatin1String("kdenlive_id")) {
650 651
                id = e.firstChild().nodeValue();
            }
652
            if (e.attribute(QStringLiteral("name")) == QLatin1String("tag")) {
653 654 655 656 657
                tag = e.firstChild().nodeValue();
            }
            if (!id.isEmpty() && !tag.isEmpty()) effectIds.insert(id, tag);
        }
    }
658
    //TODO: find a way to process this before rendering MLT scenelist to xml
659
    QDomDocument customeffects = initEffects::getUsedCustomEffects(effectIds);
660 661 662
    if (customeffects.documentElement().childNodes().count() > 0) {
	EffectsList::setProperty(mainPlaylist, QStringLiteral("kdenlive:customeffects"), customeffects.toString());
    }
663
    //addedXml.appendChild(sceneList.importNode(customeffects.documentElement(), true));
664

665
    //TODO: move metadata to previous step in saving process
666
    QDomElement docmetadata = sceneList.createElement(QStringLiteral("documentmetadata"));
667 668 669 670 671
    QMapIterator<QString, QString> j(m_documentMetadata);
    while (j.hasNext()) {
        j.next();
        docmetadata.setAttribute(j.key(), j.value());
    }
672
    //addedXml.appendChild(docmetadata);
673

674 675 676
    return sceneList;
}

677 678 679 680 681 682
QString KdenliveDoc::documentNotes() const
{
    QString text = m_notesWidget->toPlainText().simplified();
    if (text.isEmpty()) return QString();
    return m_notesWidget->toHtml();
}
683

684
bool KdenliveDoc::saveSceneList(const QString &path, const QString &scene)
685
{
686
    QDomDocument sceneList = xmlSceneList(scene);
687 688
    if (sceneList.isNull()) {
        //Make sure we don't save if scenelist is corrupted
689
        KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1, scene list is corrupted.", path));
690 691
        return false;
    }
692

693
    // Backup current version
694
    backupLastSavedVersion(path);
695
    QFile file(path);
696

697
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
698
        qWarning() << "//////  ERROR writing to file: " << path;
699
        KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", path));
700
        return false;
701
    }
702

703
    file.write(sceneList.toString().toUtf8());
704
    if (file.error() != QFile::NoError) {
705
        KMessageBox::error(QApplication::activeWindow(), i18n("Cannot write to file %1", path));
706 707 708
        file.close();
        return false;
    }
709
    file.close();
710 711 712
    cleanupBackupFiles();
    QFileInfo info(file);
    QString fileName = QUrl::fromLocalFile(path).fileName().section('.', 0, -2);
713 714
    fileName.append('-' + m_documentProperties.value(QStringLiteral("documentid")));
    fileName.append(info.lastModified().toString(QStringLiteral("-yyyy-MM-dd-hh-mm")));
715 716 717
    fileName.append(".kdenlive.png");
    QDir backupFolder(m_projectFolder.path() + "/.backup");
    emit saveTimelinePreview(backupFolder.absoluteFilePath(fileName));
718
    return true;
719 720
}

721 722
ClipManager *KdenliveDoc::clipManager()
{
723
    return m_clipManager;
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
724 725
}

726 727 728 729 730
QString KdenliveDoc::groupsXml() const
{
    return m_clipManager->groupsXml();
}

731
QUrl KdenliveDoc::projectFolder() const
732
{
733
    //if (m_projectFolder.isEmpty()) return QUrl(QStandardPaths::writableLocation(QStandardPaths::DataLocation) + "//projects/");
734 735 736
    return m_projectFolder;
}

737
void KdenliveDoc::setProjectFolder(QUrl url)
738
{
739
    if (url == m_projectFolder) return;
740
    setModified(true);
741 742 743
    QDir dir(url.toLocalFile());
    if (!dir.exists()) {
        dir.mkpath(dir.absolutePath());
744
    }
745 746
    dir.mkdir(QStringLiteral("titles"));
    dir.mkdir(QStringLiteral(".backup"));
747
    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.path(), url.path())) == KMessageBox::Yes) moveProjectData(url);
748
    m_projectFolder = url;
749 750

    updateProjectFolderPlacesEntry();
751 752
}

753
void KdenliveDoc::moveProjectData(const QUrl &url)
754
{
755
    QList <ClipController*> list = pCore->binController()->getControllerList();
756
    QList<QUrl> cacheUrls;
757
    for (int i = 0; i < list.count(); ++i) {
758
        ClipController *clip = list.at(i);
759
        if (clip->clipType() == Text) {
760
            // the image for title clip must be moved
761
            QUrl oldUrl = clip->clipUrl();
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
762
            QUrl newUrl = QUrl::fromLocalFile(url.toLocalFile() + QDir::separator() + "titles/" + oldUrl.fileName());
763
            KIO::Job *job = KIO::copy(oldUrl, newUrl);
764
            if (job->exec()) clip->setProperty(QStringLiteral("resource"), newUrl.path());
765
        }
766
        /*
767
        QString hash = clip->getClipHash();
768
        QUrl oldVideoThumbUrl = QUrl::fromLocalFile(m_projectFolder.path() + QDir::separator() + "thumbs/" + hash + ".png");
769
        if (QFile::exists(oldVideoThumbUrl.path())) {