projectmanager.cpp 38.6 KB
Newer Older
Till Theato's avatar
Till Theato committed
1
2
3
4
5
6
7
8
9
10
11
/*
Copyright (C) 2014  Till Theato <root@ttill.de>
This file is part of kdenlive. See www.kdenlive.org.

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 3 of the License, or
(at your option) any later version.
*/

#include "projectmanager.h"
12
#include "bin/bin.h"
13
#include "bin/projectitemmodel.h"
Nicolas Carion's avatar
Nicolas Carion committed
14
15
#include "core.h"
#include "doc/kdenlivedoc.h"
16
#include "jobs/jobmanager.h"
Nicolas Carion's avatar
Nicolas Carion committed
17
18
#include "kdenlivesettings.h"
#include "mainwindow.h"
Till Theato's avatar
Till Theato committed
19
#include "monitor/monitormanager.h"
20
#include "profiles/profilemodel.hpp"
Till Theato's avatar
Till Theato committed
21
#include "project/dialogs/archivewidget.h"
22
#include "project/dialogs/backupwidget.h"
23
#include "project/dialogs/noteswidget.h"
Nicolas Carion's avatar
style    
Nicolas Carion committed
24
#include "project/dialogs/projectsettings.h"
25
#include "utils/thumbnailcache.hpp"
26
27
#include "xml/xml.hpp"

28
29
30
// Temporary for testing
#include "bin/model/markerlistmodel.hpp"

31
#include "profiles/profilerepository.hpp"
32
#include "project/notesplugin.h"
33
#include "timeline2/model/builders/meltBuilder.hpp"
34
#include "timeline2/view/timelinecontroller.h"
Nicolas Carion's avatar
linting    
Nicolas Carion committed
35
#include "timeline2/view/timelinewidget.h"
36

Till Theato's avatar
Till Theato committed
37
#include <KActionCollection>
38
#include <KJob>
Till Theato's avatar
Till Theato committed
39
#include <KMessageBox>
Nicolas Carion's avatar
Nicolas Carion committed
40
#include <KRecentDirs>
41
#include <klocalizedstring.h>
42

Nicolas Carion's avatar
Nicolas Carion committed
43
44
45
#include "kdenlive_debug.h"
#include <KConfigGroup>
#include <QAction>
Till Theato's avatar
Till Theato committed
46
#include <QCryptographicHash>
47
#include <QFileDialog>
Nicolas Carion's avatar
Nicolas Carion committed
48
#include <QLocale>
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
49
50
#include <QMimeDatabase>
#include <QMimeType>
Nicolas Carion's avatar
Nicolas Carion committed
51
#include <QProgressDialog>
52
#include <QTimeZone>
Till Theato's avatar
Till Theato committed
53

Nicolas Carion's avatar
Nicolas Carion committed
54
ProjectManager::ProjectManager(QObject *parent)
55
    : QObject(parent)
Nicolas Carion's avatar
Nicolas Carion committed
56

Till Theato's avatar
Till Theato committed
57
58
{
    m_fileRevert = KStandardAction::revert(this, SLOT(slotRevert()), pCore->window()->actionCollection());
59
    m_fileRevert->setIcon(QIcon::fromTheme(QStringLiteral("document-revert")));
Till Theato's avatar
Till Theato committed
60
61
    m_fileRevert->setEnabled(false);

Nicolas Carion's avatar
Nicolas Carion committed
62
    QAction *a = KStandardAction::open(this, SLOT(openFile()), pCore->window()->actionCollection());
63
    a->setIcon(QIcon::fromTheme(QStringLiteral("document-open")));
Nicolas Carion's avatar
Nicolas Carion committed
64
    a = KStandardAction::saveAs(this, SLOT(saveFileAs()), pCore->window()->actionCollection());
65
    a->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as")));
Nicolas Carion's avatar
Nicolas Carion committed
66
    a = KStandardAction::openNew(this, SLOT(newFile()), pCore->window()->actionCollection());
67
    a->setIcon(QIcon::fromTheme(QStringLiteral("document-new")));
68
    m_recentFilesAction = KStandardAction::openRecent(this, SLOT(openFile(QUrl)), pCore->window()->actionCollection());
69

70
    QAction *backupAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-undo")), i18n("Open Backup File"), this);
71
    pCore->window()->addAction(QStringLiteral("open_backup"), backupAction);
72
    connect(backupAction, SIGNAL(triggered(bool)), SLOT(slotOpenBackup()));
73
74

    m_notesPlugin = new NotesPlugin(this);
75
76

    m_autoSaveTimer.setSingleShot(true);
Laurent Montel's avatar
Laurent Montel committed
77
    connect(&m_autoSaveTimer, &QTimer::timeout, this, &ProjectManager::slotAutoSave);
78

79
80
    // Ensure the default data folder exist
    QDir dir(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation));
81
    dir.mkpath(QStringLiteral(".backup"));
82
    dir.mkdir(QStringLiteral("titles"));
Till Theato's avatar
Till Theato committed
83
84
}

Nicolas Carion's avatar
Nicolas Carion committed
85
ProjectManager::~ProjectManager() = default;
Till Theato's avatar
Till Theato committed
86

87
void ProjectManager::slotLoadOnOpen()
Till Theato's avatar
Till Theato committed
88
{
89
90
    if (m_startUrl.isValid()) {
        openFile();
Laurent Montel's avatar
Laurent Montel committed
91
    } else if (KdenliveSettings::openlastproject()) {
92
        openLastFile();
Laurent Montel's avatar
Laurent Montel committed
93
94
    } else {
        newFile(false);
Till Theato's avatar
Till Theato committed
95
96
    }

Nicolas Carion's avatar
Nicolas Carion committed
97
    if (!m_loadClipsOnOpen.isEmpty() && (m_project != nullptr)) {
Laurent Montel's avatar
Laurent Montel committed
98
        const QStringList list = m_loadClipsOnOpen.split(QLatin1Char(','));
Laurent Montel's avatar
Laurent Montel committed
99
        QList<QUrl> urls;
Laurent Montel's avatar
Laurent Montel committed
100
101
        urls.reserve(list.count());
        for (const QString &path : list) {
Nicolas Carion's avatar
Nicolas Carion committed
102
            // qCDebug(KDENLIVE_LOG) << QDir::current().absoluteFilePath(path);
Till Theato's avatar
Till Theato committed
103
104
            urls << QUrl::fromLocalFile(QDir::current().absoluteFilePath(path));
        }
105
        pCore->bin()->droppedUrls(urls);
Till Theato's avatar
Till Theato committed
106
    }
107
108
109
    m_loadClipsOnOpen.clear();
}

Laurent Montel's avatar
Laurent Montel committed
110
void ProjectManager::init(const QUrl &projectUrl, const QString &clipList)
111
112
113
{
    m_startUrl = projectUrl;
    m_loadClipsOnOpen = clipList;
Till Theato's avatar
Till Theato committed
114
115
}

116
117
118
119
120
121
122
123
124
125
void ProjectManager::newFile(bool showProjectSettings)
{
    QString profileName = KdenliveSettings::default_profile();
    if (profileName.isEmpty()) {
        profileName = pCore->getCurrentProfile()->path();
    }
    newFile(profileName, showProjectSettings);
}

void ProjectManager::newFile(QString profileName, bool showProjectSettings)
Till Theato's avatar
Till Theato committed
126
{
127
    // fix mantis#3160
128
    QUrl startFile = QUrl::fromLocalFile(KdenliveSettings::defaultprojectfolder() + QStringLiteral("/_untitled.kdenlive"));
129
    if (checkForBackupFile(startFile, true)) {
130
131
        return;
    }
Till Theato's avatar
Till Theato committed
132
    m_fileRevert->setEnabled(false);
133
    QString projectFolder;
Laurent Montel's avatar
Laurent Montel committed
134
135
    QMap<QString, QString> documentProperties;
    QMap<QString, QString> documentMetadata;
Till Theato's avatar
Till Theato committed
136
    QPoint projectTracks(KdenliveSettings::videotracks(), KdenliveSettings::audiotracks());
137
    pCore->monitorManager()->resetDisplay();
138
139
    QString documentId = QString::number(QDateTime::currentMSecsSinceEpoch());
    documentProperties.insert(QStringLiteral("documentid"), documentId);
Till Theato's avatar
Till Theato committed
140
    if (!showProjectSettings) {
Till Theato's avatar
Till Theato committed
141
142
        if (!closeCurrentDocument()) {
            return;
Till Theato's avatar
Till Theato committed
143
        }
144
145
        if (KdenliveSettings::customprojectfolder()) {
            projectFolder = KdenliveSettings::defaultprojectfolder();
146
            if (!projectFolder.endsWith(QLatin1Char('/'))) {
Laurent Montel's avatar
Laurent Montel committed
147
                projectFolder.append(QLatin1Char('/'));
148
149
            }
            documentProperties.insert(QStringLiteral("storagefolder"), projectFolder + documentId);
150
        }
Till Theato's avatar
Till Theato committed
151
    } else {
Nicolas Carion's avatar
Nicolas Carion committed
152
153
        QPointer<ProjectSettings> w = new ProjectSettings(nullptr, QMap<QString, QString>(), QStringList(), projectTracks.x(), projectTracks.y(),
                                                          KdenliveSettings::defaultprojectfolder(), false, true, pCore->window());
Laurent Montel's avatar
Laurent Montel committed
154
        connect(w.data(), &ProjectSettings::refreshProfiles, pCore->window(), &MainWindow::slotRefreshProfiles);
Till Theato's avatar
Till Theato committed
155
156
157
158
        if (w->exec() != QDialog::Accepted) {
            delete w;
            return;
        }
Till Theato's avatar
Till Theato committed
159
160
161
        if (!closeCurrentDocument()) {
            delete w;
            return;
Till Theato's avatar
Till Theato committed
162
163
164
165
166
167
168
169
        }
        if (KdenliveSettings::videothumbnails() != w->enableVideoThumbs()) {
            pCore->window()->slotSwitchVideoThumbs();
        }
        if (KdenliveSettings::audiothumbnails() != w->enableAudioThumbs()) {
            pCore->window()->slotSwitchAudioThumbs();
        }
        profileName = w->selectedProfile();
170
        projectFolder = w->storageFolder();
Till Theato's avatar
Till Theato committed
171
        projectTracks = w->tracks();
Nicolas Carion's avatar
Nicolas Carion committed
172
173
        documentProperties.insert(QStringLiteral("enableproxy"), QString::number((int)w->useProxy()));
        documentProperties.insert(QStringLiteral("generateproxy"), QString::number((int)w->generateProxy()));
174
175
176
        documentProperties.insert(QStringLiteral("proxyminsize"), QString::number(w->proxyMinSize()));
        documentProperties.insert(QStringLiteral("proxyparams"), w->proxyParams());
        documentProperties.insert(QStringLiteral("proxyextension"), w->proxyExtension());
Nicolas Carion's avatar
Nicolas Carion committed
177
        documentProperties.insert(QStringLiteral("generateimageproxy"), QString::number((int)w->generateImageProxy()));
178
179
        QString preview = w->selectedPreview();
        if (!preview.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
180
181
            documentProperties.insert(QStringLiteral("previewparameters"), preview.section(QLatin1Char(';'), 0, 0));
            documentProperties.insert(QStringLiteral("previewextension"), preview.section(QLatin1Char(';'), 1, 1));
182
        }
183
        documentProperties.insert(QStringLiteral("proxyimageminsize"), QString::number(w->proxyImageMinSize()));
184
        if (!projectFolder.isEmpty()) {
185
            if (!projectFolder.endsWith(QLatin1Char('/'))) {
Laurent Montel's avatar
Laurent Montel committed
186
                projectFolder.append(QLatin1Char('/'));
187
            }
Nicolas Carion's avatar
Nicolas Carion committed
188
            documentProperties.insert(QStringLiteral("storagefolder"), projectFolder + documentId);
189
        }
Till Theato's avatar
Till Theato committed
190
191
192
193
        documentMetadata = w->metadata();
        delete w;
    }
    bool openBackup;
194
    m_notesPlugin->clear();
195
    documentProperties.insert(QStringLiteral("decimalPoint"), QLocale().decimalPoint());
Nicolas Carion's avatar
Nicolas Carion committed
196
    KdenliveDoc *doc = new KdenliveDoc(QUrl(), projectFolder, pCore->window()->m_commandStack, profileName, documentProperties, documentMetadata, projectTracks,
197
                                       &openBackup, pCore->window());
198
    doc->m_autosave = new KAutoSaveFile(startFile, doc);
199
    pCore->bin()->setDocument(doc);
200
    m_project = doc;
201
202
    pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor);
    updateTimeline(0);
203
    pCore->window()->connectDocument();
204
205
    bool disabled = m_project->getDocumentProperty(QStringLiteral("disabletimelineeffects")) == QLatin1String("1");
    QAction *disableEffects = pCore->window()->actionCollection()->action(QStringLiteral("disable_timeline_effects"));
206
207
208
209
210
211
212
    if (disableEffects) {
        if (disabled != disableEffects->isChecked()) {
            disableEffects->blockSignals(true);
            disableEffects->setChecked(disabled);
            disableEffects->blockSignals(false);
        }
    }
213
    emit docOpened(m_project);
214
    m_lastSave.start();
Till Theato's avatar
Till Theato committed
215
216
}

Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
217
bool ProjectManager::closeCurrentDocument(bool saveChanges, bool quit)
Till Theato's avatar
Till Theato committed
218
{
Nicolas Carion's avatar
Nicolas Carion committed
219
    if ((m_project != nullptr) && m_project->isModified() && saveChanges) {
Till Theato's avatar
Till Theato committed
220
221
222
223
224
225
226
227
        QString message;
        if (m_project->url().fileName().isEmpty()) {
            message = i18n("Save changes to document?");
        } else {
            message = i18n("The project <b>\"%1\"</b> has been changed.\nDo you want to save your changes?", m_project->url().fileName());
        }

        switch (KMessageBox::warningYesNoCancel(pCore->window(), message)) {
Nicolas Carion's avatar
Nicolas Carion committed
228
        case KMessageBox::Yes:
Till Theato's avatar
Till Theato committed
229
            // save document here. If saving fails, return false;
Nicolas Carion's avatar
Nicolas Carion committed
230
            if (!saveFile()) {
Till Theato's avatar
Till Theato committed
231
232
                return false;
            }
Till Theato's avatar
Till Theato committed
233
            break;
Nicolas Carion's avatar
Nicolas Carion committed
234
        case KMessageBox::Cancel:
Till Theato's avatar
Till Theato committed
235
236
237
238
239
240
            return false;
            break;
        default:
            break;
        }
    }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
241
    pCore->window()->getMainTimeline()->controller()->clipActions.clear();
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
242
    if (!quit && !qApp->isSavingSession()) {
Laurent Montel's avatar
Laurent Montel committed
243
        m_autoSaveTimer.stop();
244
        if (m_project) {
245
            pCore->jobManager()->slotCancelJobs();
246
            pCore->bin()->abortOperations();
Laurent Montel's avatar
Laurent Montel committed
247
            pCore->monitorManager()->clipMonitor()->slotOpenClip(nullptr);
248
            pCore->window()->clearAssetPanel();
249
            delete m_project;
Laurent Montel's avatar
Laurent Montel committed
250
            m_project = nullptr;
251
        }
Laurent Montel's avatar
Laurent Montel committed
252
        pCore->monitorManager()->setDocument(m_project);
253
    }
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
    /*  // Make sure to reset locale to system's default
        QString requestedLocale = QLocale::system().name();
        QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
        if (env.contains(QStringLiteral("LC_NUMERIC"))) {
            requestedLocale = env.value(QStringLiteral("LC_NUMERIC"));
        }
        qDebug()<<"//////////// RESETTING LOCALE TO: "<<requestedLocale;

    #ifdef Q_OS_MAC
        setlocale(LC_NUMERIC_MASK, requestedLocale.toUtf8().constData());
    #elif defined(Q_OS_WIN)
        std::locale::global(std::locale(requestedLocale.toUtf8().constData()));
    #else
        QLocale newLocale(requestedLocale);
        char *separator = localeconv()->decimal_point;
        if (QString::fromUtf8(separator) != QString(newLocale.decimalPoint())) {
            pCore->displayBinMessage(i18n("There is a locale conflict on your system, project might get corrupt"), KMessageWidget::Warning);
        }
        setlocale(LC_NUMERIC, requestedLocale.toUtf8().constData());
    #endif
        QLocale::setDefault(newLocale);*/
Till Theato's avatar
Till Theato committed
275
276
277
278
279
    return true;
}

bool ProjectManager::saveFileAs(const QString &outputFileName)
{
280
    pCore->monitorManager()->pauseActiveMonitor();
281
282
    // Sync document properties
    prepareSave();
283
284
    QString saveFolder = QFileInfo(outputFileName).absolutePath();
    QString scene = projectSceneList(saveFolder);
285
286
287
288
289
290
291
    if (!m_replacementPattern.isEmpty()) {
        QMapIterator<QString, QString> i(m_replacementPattern);
        while (i.hasNext()) {
            i.next();
            scene.replace(i.key(), i.value());
        }
    }
Nicolas Carion's avatar
Nicolas Carion committed
292
    if (!m_project->saveSceneList(outputFileName, scene)) {
Till Theato's avatar
Till Theato committed
293
294
        return false;
    }
295
    QUrl url = QUrl::fromLocalFile(outputFileName);
Till Theato's avatar
Till Theato committed
296
    // Save timeline thumbnails
297
298
    QStringList thumbKeys = pCore->window()->getMainTimeline()->controller()->getThumbKeys();
    ThumbnailCache::get()->saveCachedThumbs(thumbKeys);
299
    m_project->setUrl(url);
300
    // setting up autosave file in ~/.kde/data/stalefiles/kdenlive/
301
    // saved under file name
302
303
    // actual saving by KdenliveDoc::slotAutoSave() called by a timer 3 seconds after the document has been edited
    // This timer is set by KdenliveDoc::setModified()
304
305
    const QString projectId = QCryptographicHash::hash(url.fileName().toUtf8(), QCryptographicHash::Md5).toHex();
    QUrl autosaveUrl = QUrl::fromLocalFile(QFileInfo(outputFileName).absoluteDir().absoluteFilePath(projectId + QStringLiteral(".kdenlive")));
Laurent Montel's avatar
Laurent Montel committed
306
    if (m_project->m_autosave == nullptr) {
307
308
        // The temporary file is not opened or created until actually needed.
        // The file filename does not have to exist for KAutoSaveFile to be constructed (if it exists, it will not be touched).
309
        m_project->m_autosave = new KAutoSaveFile(autosaveUrl, m_project);
Till Theato's avatar
Till Theato committed
310
    } else {
311
        m_project->m_autosave->setManagedFile(autosaveUrl);
Till Theato's avatar
Till Theato committed
312
313
    }

Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
314
    pCore->window()->setWindowTitle(m_project->description());
Till Theato's avatar
Till Theato committed
315
    m_project->setModified(false);
316
317
    m_recentFilesAction->addUrl(url);
    // remember folder for next project opening
318
    KRecentDirs::add(QStringLiteral(":KdenliveProjectsFolder"), saveFolder);
319
    saveRecentFiles();
Till Theato's avatar
Till Theato committed
320
    m_fileRevert->setEnabled(true);
321
    pCore->window()->m_undoView->stack()->setClean();
Till Theato's avatar
Till Theato committed
322
323
324
325

    return true;
}

326
327
328
329
330
331
332
void ProjectManager::saveRecentFiles()
{
    KSharedConfigPtr config = KSharedConfig::openConfig();
    m_recentFilesAction->saveEntries(KConfigGroup(config, "Recent Files"));
    config->sync();
}

Till Theato's avatar
Till Theato committed
333
334
bool ProjectManager::saveFileAs()
{
335
    QFileDialog fd(pCore->window());
336
    fd.setDirectory(m_project->url().isValid() ? m_project->url().adjusted(QUrl::RemoveFilename).toLocalFile() : KdenliveSettings::defaultprojectfolder());
Laurent Montel's avatar
Laurent Montel committed
337
    fd.setMimeTypeFilters(QStringList() << QStringLiteral("application/x-kdenlive"));
338
    fd.setAcceptMode(QFileDialog::AcceptSave);
339
    fd.setFileMode(QFileDialog::AnyFile);
340
    fd.setDefaultSuffix(QStringLiteral("kdenlive"));
341
    if (fd.exec() != QDialog::Accepted || fd.selectedFiles().isEmpty()) {
342
        return false;
343
    }
Laurent Montel's avatar
Laurent Montel committed
344
    QString outputFile = fd.selectedFiles().constFirst();
Till Theato's avatar
Till Theato committed
345

346
#if KXMLGUI_VERSION_MINOR < 23 && KXMLGUI_VERSION_MAJOR == 5
Laurent Montel's avatar
Laurent Montel committed
347
    // Since Plasma 5.7 (release at same time as KF 5.23,
348
349
    // the file dialog manages the overwrite check
    if (QFile::exists(outputFile)) {
Till Theato's avatar
Till Theato committed
350
351
352
353
        // Show the file dialog again if the user does not want to overwrite the file
        if (KMessageBox::questionYesNo(pCore->window(), i18n("File %1 already exists.\nDo you want to overwrite it?", outputFile)) == KMessageBox::No) {
            return saveFileAs();
        }
354
355
356
    }
#endif

357
358
359
    bool ok = false;
    QDir cacheDir = m_project->getCacheDir(CacheBase, &ok);
    if (ok) {
Laurent Montel's avatar
Laurent Montel committed
360
        QFile file(cacheDir.absoluteFilePath(QString::fromLatin1(QUrl::toPercentEncoding(QStringLiteral(".") + outputFile))));
361
362
363
        file.open(QIODevice::ReadWrite | QIODevice::Text);
        file.close();
    }
Till Theato's avatar
Till Theato committed
364
365
366
367
368
    return saveFileAs(outputFile);
}

bool ProjectManager::saveFile()
{
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
369
370
    if (!m_project) {
        // Calling saveFile before a project was created, something is wrong
Laurent Montel's avatar
Laurent Montel committed
371
        qCDebug(KDENLIVE_LOG) << "SaveFile called without project";
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
372
373
        return false;
    }
Till Theato's avatar
Till Theato committed
374
375
    if (m_project->url().isEmpty()) {
        return saveFileAs();
Nicolas Carion's avatar
Nicolas Carion committed
376
377
378
379
    }
    bool result = saveFileAs(m_project->url().toLocalFile());
    m_project->m_autosave->resize(0);
    return result;
Till Theato's avatar
Till Theato committed
380
381
382
383
}

void ProjectManager::openFile()
{
384
    if (m_startUrl.isValid()) {
Till Theato's avatar
Till Theato committed
385
        openFile(m_startUrl);
386
        m_startUrl.clear();
Till Theato's avatar
Till Theato committed
387
388
        return;
    }
Nicolas Carion's avatar
Nicolas Carion committed
389
390
    QUrl url = QFileDialog::getOpenFileUrl(pCore->window(), QString(), QUrl::fromLocalFile(KRecentDirs::dir(QStringLiteral(":KdenliveProjectsFolder"))),
                                           getMimeType());
391
    if (!url.isValid()) {
Till Theato's avatar
Till Theato committed
392
393
        return;
    }
394
    KRecentDirs::add(QStringLiteral(":KdenliveProjectsFolder"), url.adjusted(QUrl::RemoveFilename).toLocalFile());
395
    m_recentFilesAction->addUrl(url);
396
    saveRecentFiles();
Till Theato's avatar
Till Theato committed
397
398
399
400
401
    openFile(url);
}

void ProjectManager::openLastFile()
{
402
    if (m_recentFilesAction->selectableActionGroup()->actions().isEmpty()) {
Till Theato's avatar
Till Theato committed
403
404
405
406
407
        // No files in history
        newFile(false);
        return;
    }

408
    QAction *firstUrlAction = m_recentFilesAction->selectableActionGroup()->actions().last();
Till Theato's avatar
Till Theato committed
409
410
411
412
413
414
415
    if (firstUrlAction) {
        firstUrlAction->trigger();
    } else {
        newFile(false);
    }
}

416
// fix mantis#3160 separate check from openFile() so we can call it from newFile()
417
// to find autosaved files (in ~/.local/share/stalefiles/kdenlive) and recover it
418
bool ProjectManager::checkForBackupFile(const QUrl &url, bool newFile)
419
{
420
    // Check for autosave file that belong to the url we passed in.
421
422
423
    const QString projectId = QCryptographicHash::hash(url.fileName().toUtf8(), QCryptographicHash::Md5).toHex();
    QUrl autosaveUrl = newFile ? url : QUrl::fromLocalFile(QFileInfo(url.path()).absoluteDir().absoluteFilePath(projectId + QStringLiteral(".kdenlive")));
    QList<KAutoSaveFile *> staleFiles = KAutoSaveFile::staleFiles(autosaveUrl);
Laurent Montel's avatar
Laurent Montel committed
424
    KAutoSaveFile *orphanedFile = nullptr;
Laurent Montel's avatar
Laurent Montel committed
425
    // Check if we can have a lock on one of the file,
426
    // meaning it is not handled by any Kdenlive instance
427
    if (!staleFiles.isEmpty()) {
Nicolas Carion's avatar
Nicolas Carion committed
428
        for (KAutoSaveFile *stale : staleFiles) {
429
            if (stale->open(QIODevice::QIODevice::ReadWrite)) {
Laurent Montel's avatar
Laurent Montel committed
430
431
432
                // Found orphaned autosave file
                orphanedFile = stale;
                break;
433
            } else {
Laurent Montel's avatar
Laurent Montel committed
434
435
436
437
                // Another Kdenlive instance is probably handling this autosave file
                staleFiles.removeAll(stale);
                delete stale;
                continue;
438
439
440
441
442
            }
        }
    }

    if (orphanedFile) {
Nicolas Carion's avatar
Nicolas Carion committed
443
        if (KMessageBox::questionYesNo(nullptr, i18n("Auto-saved files exist. Do you want to recover them now?"), i18n("File Recovery"),
Pino Toscano's avatar
Pino Toscano committed
444
                                       KGuiItem(i18n("Recover")), KGuiItem(i18n("Do not recover"))) == KMessageBox::Yes) {
445
            doOpenFile(url, orphanedFile);
446
            return true;
Nicolas Carion's avatar
Nicolas Carion committed
447
448
        }
        // remove the stale files
Nicolas Carion's avatar
Nicolas Carion committed
449
        for (KAutoSaveFile *stale : staleFiles) {
Nicolas Carion's avatar
Nicolas Carion committed
450
451
452
453
            stale->open(QIODevice::ReadWrite);
            delete stale;
        }

454
455
456
457
458
        return false;
    }
    return false;
}

459
void ProjectManager::openFile(const QUrl &url)
Till Theato's avatar
Till Theato committed
460
{
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
461
    QMimeDatabase db;
Till Theato's avatar
Till Theato committed
462
    // Make sure the url is a Kdenlive project file
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
463
    QMimeType mime = db.mimeTypeForUrl(url);
464
    if (mime.inherits(QStringLiteral("application/x-compressed-tar"))) {
Till Theato's avatar
Till Theato committed
465
        // Opening a compressed project file, we need to process it
Nicolas Carion's avatar
Nicolas Carion committed
466
        // qCDebug(KDENLIVE_LOG)<<"Opening archive, processing";
Till Theato's avatar
Till Theato committed
467
468
        QPointer<ArchiveWidget> ar = new ArchiveWidget(url);
        if (ar->exec() == QDialog::Accepted) {
469
            openFile(QUrl::fromLocalFile(ar->extractedProjectFile()));
470
        } else if (m_startUrl.isValid()) {
Till Theato's avatar
Till Theato committed
471
472
473
474
475
476
477
            // we tried to open an invalid file from command line, init new project
            newFile(false);
        }
        delete ar;
        return;
    }

478
    /*if (!url.fileName().endsWith(".kdenlive")) {
Till Theato's avatar
Till Theato committed
479
        // This is not a Kdenlive project file, abort loading
480
        KMessageBox::sorry(pCore->window(), i18n("File %1 is not a Kdenlive project file", url.toLocalFile()));
481
        if (m_startUrl.isValid()) {
Till Theato's avatar
Till Theato committed
482
483
484
485
            // we tried to open an invalid file from command line, init new project
            newFile(false);
        }
        return;
486
    }*/
Till Theato's avatar
Till Theato committed
487

Nicolas Carion's avatar
Nicolas Carion committed
488
    if ((m_project != nullptr) && m_project->url() == url) {
Till Theato's avatar
Till Theato committed
489
490
491
        return;
    }

Till Theato's avatar
Till Theato committed
492
493
    if (!closeCurrentDocument()) {
        return;
Till Theato's avatar
Till Theato committed
494
    }
495
496
    if (checkForBackupFile(url)) {
        return;
Till Theato's avatar
Till Theato committed
497
    }
498
    pCore->window()->slotGotProgressInfo(i18n("Opening file %1", url.toLocalFile()), 100, InformationMessage);
Laurent Montel's avatar
Laurent Montel committed
499
    doOpenFile(url, nullptr);
Till Theato's avatar
Till Theato committed
500
501
}

502
void ProjectManager::doOpenFile(const QUrl &url, KAutoSaveFile *stale)
Till Theato's avatar
Till Theato committed
503
{
Laurent Montel's avatar
Laurent Montel committed
504
    Q_ASSERT(m_project == nullptr);
Till Theato's avatar
Till Theato committed
505
506
    m_fileRevert->setEnabled(true);

507
    delete m_progressDialog;
508
    pCore->monitorManager()->resetDisplay();
509
    pCore->monitorManager()->activateMonitor(Kdenlive::ProjectMonitor);
510
511
    m_progressDialog = new QProgressDialog(pCore->window());
    m_progressDialog->setWindowTitle(i18n("Loading project"));
Laurent Montel's avatar
Laurent Montel committed
512
    m_progressDialog->setCancelButton(nullptr);
513
    m_progressDialog->setLabelText(i18n("Loading project"));
514
515
    m_progressDialog->setMaximum(0);
    m_progressDialog->show();
Till Theato's avatar
Till Theato committed
516
    bool openBackup;
517
    m_notesPlugin->clear();
Nicolas Carion's avatar
linting    
Nicolas Carion committed
518
519
520
521
    KdenliveDoc *doc = new KdenliveDoc(stale ? QUrl::fromLocalFile(stale->fileName()) : url, QString(), pCore->window()->m_commandStack,
                                       KdenliveSettings::default_profile().isEmpty() ? pCore->getCurrentProfile()->path() : KdenliveSettings::default_profile(),
                                       QMap<QString, QString>(), QMap<QString, QString>(),
                                       QPoint(KdenliveSettings::videotracks(), KdenliveSettings::audiotracks()), &openBackup, pCore->window());
Laurent Montel's avatar
Laurent Montel committed
522
    if (stale == nullptr) {
523
524
525
        const QString projectId = QCryptographicHash::hash(url.fileName().toUtf8(), QCryptographicHash::Md5).toHex();
        QUrl autosaveUrl = QUrl::fromLocalFile(QFileInfo(url.path()).absoluteDir().absoluteFilePath(projectId + QStringLiteral(".kdenlive")));
        stale = new KAutoSaveFile(autosaveUrl, doc);
Till Theato's avatar
Till Theato committed
526
527
528
        doc->m_autosave = stale;
    } else {
        doc->m_autosave = stale;
529
        stale->setParent(doc);
530
        // if loading from an autosave of unnamed file then keep unnamed
Laurent Montel's avatar
Laurent Montel committed
531
        if (url.fileName().contains(QStringLiteral("_untitled.kdenlive"))) {
532
            doc->setUrl(QUrl());
Laurent Montel's avatar
Laurent Montel committed
533
        } else {
534
            doc->setUrl(url);
Laurent Montel's avatar
Laurent Montel committed
535
        }
Till Theato's avatar
Till Theato committed
536
537
538
        doc->setModified(true);
        stale->setParent(doc);
    }
539
    m_progressDialog->setLabelText(i18n("Loading clips"));
540
541

    // TODO refac delete this
542
    pCore->bin()->setDocument(doc);
Laurent Montel's avatar
Laurent Montel committed
543
    QList<QAction *> rulerActions;
544
545
    rulerActions << pCore->window()->actionCollection()->action(QStringLiteral("set_render_timeline_zone"));
    rulerActions << pCore->window()->actionCollection()->action(QStringLiteral("unset_render_timeline_zone"));
546
    rulerActions << pCore->window()->actionCollection()->action(QStringLiteral("clear_render_timeline_zone"));
547
548

    // Set default target tracks to upper audio / lower video tracks
549
    m_project = doc;
550

551
    updateTimeline(m_project->getDocumentProperty(QStringLiteral("position")).toInt());
552
    pCore->window()->connectDocument();
553
    QDateTime documentDate = QFileInfo(m_project->url().toLocalFile()).lastModified();
554
555
556
    pCore->window()->getMainTimeline()->controller()->loadPreview(m_project->getDocumentProperty(QStringLiteral("previewchunks")),
                                                                  m_project->getDocumentProperty(QStringLiteral("dirtypreviewchunks")), documentDate,
                                                                  m_project->getDocumentProperty(QStringLiteral("disablepreview")).toInt());
557

558
    emit docOpened(m_project);
559
    pCore->window()->slotGotProgressInfo(QString(), 100);
Till Theato's avatar
Till Theato committed
560
    if (openBackup) {
561
        slotOpenBackup(url);
Till Theato's avatar
Till Theato committed
562
    }
563
    m_lastSave.start();
564
    delete m_progressDialog;
Laurent Montel's avatar
Laurent Montel committed
565
    m_progressDialog = nullptr;
Till Theato's avatar
Till Theato committed
566
567
568
569
}

void ProjectManager::slotRevert()
{
Nicolas Carion's avatar
Nicolas Carion committed
570
571
572
573
    if (m_project->isModified() &&
        KMessageBox::warningContinueCancel(pCore->window(),
                                           i18n("This will delete all changes made since you last saved your project. Are you sure you want to continue?"),
                                           i18n("Revert to last saved version")) == KMessageBox::Cancel) {
Till Theato's avatar
Till Theato committed
574
575
        return;
    }
576
    QUrl url = m_project->url();
Laurent Montel's avatar
Laurent Montel committed
577
    if (closeCurrentDocument(false)) {
Laurent Montel's avatar
Laurent Montel committed
578
        doOpenFile(url, nullptr);
Laurent Montel's avatar
Laurent Montel committed
579
    }
Till Theato's avatar
Till Theato committed
580
581
582
583
}

QString ProjectManager::getMimeType(bool open)
{
584
    QString mimetype = i18n("Kdenlive project (*.kdenlive)");
Laurent Montel's avatar
Laurent Montel committed
585
    if (open) {
Laurent Montel's avatar
Laurent Montel committed
586
        mimetype.append(QStringLiteral(";;") + i18n("Archived project (*.tar.gz)"));
Laurent Montel's avatar
Laurent Montel committed
587
    }
Till Theato's avatar
Till Theato committed
588
589
590
    return mimetype;
}

Laurent Montel's avatar
Laurent Montel committed
591
KdenliveDoc *ProjectManager::current()
Till Theato's avatar
Till Theato committed
592
593
594
595
{
    return m_project;
}

Laurent Montel's avatar
Laurent Montel committed
596
void ProjectManager::slotOpenBackup(const QUrl &url)
597
{
598
599
    QUrl projectFile;
    QUrl projectFolder;
600
    QString projectId;
601
    if (url.isValid()) {
602
        // we could not open the project file, guess where the backups are
603
        projectFolder = QUrl::fromLocalFile(KdenliveSettings::defaultprojectfolder());
604
605
        projectFile = url;
    } else {
606
        projectFolder = QUrl::fromLocalFile(m_project->projectTempFolder());
607
        projectFile = m_project->url();
608
        projectId = m_project->getDocumentProperty(QStringLiteral("documentid"));
609
610
611
612
613
    }

    QPointer<BackupWidget> dia = new BackupWidget(projectFile, projectFolder, projectId, pCore->window());
    if (dia->exec() == QDialog::Accepted) {
        QString requestedBackup = dia->selectedFile();
614
        m_project->backupLastSavedVersion(projectFile.toLocalFile());
615
        closeCurrentDocument(false);
Laurent Montel's avatar
Laurent Montel committed
616
        doOpenFile(QUrl::fromLocalFile(requestedBackup), nullptr);
Vincent Pinon's avatar
Vincent Pinon committed
617
618
619
620
621
        if (m_project) {
            m_project->setUrl(projectFile);
            m_project->setModified(true);
            pCore->window()->setWindowTitle(m_project->description());
        }
622
623
624
625
    }
    delete dia;
}

Laurent Montel's avatar
Laurent Montel committed
626
KRecentFilesAction *ProjectManager::recentFilesAction()
627
628
629
630
{
    return m_recentFilesAction;
}

631
632
void ProjectManager::slotStartAutoSave()
{
633
634
635
636
    if (m_lastSave.elapsed() > 300000) {
        // If the project was not saved in the last 5 minute, force save
        m_autoSaveTimer.stop();
        slotAutoSave();
Laurent Montel's avatar
Laurent Montel committed
637
    } else {
638
639
        m_autoSaveTimer.start(3000); // will trigger slotAutoSave() in 3 seconds
    }
640
641
642
643
}

void ProjectManager::slotAutoSave()
{
644
    prepareSave();
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
645
646
647
648
649
650
651
652
    QString saveFolder = m_project->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).toLocalFile();
    QString scene = projectSceneList(saveFolder);
    if (!m_replacementPattern.isEmpty()) {
        QMapIterator<QString, QString> i(m_replacementPattern);
        while (i.hasNext()) {
            i.next();
            scene.replace(i.key(), i.value());
        }
653
    }
Jean-Baptiste Mardelle's avatar
Jean-Baptiste Mardelle committed
654
    m_project->slotAutoSave(scene);
655
    m_lastSave.start();
656
657
}

Laurent Montel's avatar
Laurent Montel committed
658
QString ProjectManager::projectSceneList(const QString &outputFolder)
659
{
Nicolas Carion's avatar
Nicolas Carion committed
660
    // TODO: re-implement overlay and all
661
    // TODO refac: repair this
662
    return pCore->monitorManager()->projectMonitor()->sceneList(outputFolder);
663
    /*bool multitrackEnabled = m_trackView->multitrackView;
664
665
666
667
    if (multitrackEnabled) {
        // Multitrack view was enabled, disable for auto save
        m_trackView->slotMultitrackView(false);
    }
668
    m_trackView->connectOverlayTrack(false);
669
    QString scene = pCore->monitorManager()->projectMonitor()->sceneList(outputFolder);
670
    m_trackView->connectOverlayTrack(true);
671
672
673
674
675
    if (multitrackEnabled) {
        // Multitrack view was enabled, re-enable for auto save
        m_trackView->slotMultitrackView(true);
    }
    return scene;
676
    */
677
678
}

679
680
681
682
683
684
685
686
687
688
689
690
691
692
void ProjectManager::setDocumentNotes(const QString &notes)
{
    m_notesPlugin->widget()->setHtml(notes);
}

QString ProjectManager::documentNotes() const
{
    QString text = m_notesPlugin->widget()->toPlainText().simplified();
    if (text.isEmpty()) {
        return QString();
    }
    return m_notesPlugin->widget()->toHtml();
}

693
694
695
696
697
698
699
void ProjectManager::slotAddProjectNote()
{
    m_notesPlugin->widget()->raise();
    m_notesPlugin->widget()->setFocus();
    m_notesPlugin->widget()->addProjectNote();
}

700
701
void ProjectManager::prepareSave()
{
702
    pCore->projectItemModel()->saveDocumentProperties(pCore->window()->getMainTimeline()->controller()->documentProperties(), m_project->metadata(),
Nicolas Carion's avatar
Nicolas Carion committed
703
                                                      m_project->getGuideModel());
704
705
    pCore->projectItemModel()->saveProperty(QStringLiteral("kdenlive:documentnotes"), documentNotes());
    pCore->projectItemModel()->saveProperty(QStringLiteral("kdenlive:docproperties.groups"), m_mainTimelineModel->groupsData());
706
707
}

708
709
710
void ProjectManager::slotResetProfiles()
{
    m_project->resetProfile();
711
    pCore->monitorManager()->resetProfiles(m_project->timecode());
712
713
    pCore->monitorManager()->updateScopeSource();
}
714

715
716
717
718
719
void ProjectManager::slotResetConsumers(bool fullReset)
{
    pCore->monitorManager()->resetConsumers(fullReset);
}

720
721
void ProjectManager::slotExpandClip()
{
722
723
    // TODO refac
    // m_trackView->projectView()->expandActiveClip();
724
725
}

726
void ProjectManager::disableBinEffects(bool disable)
727
{
728
729
    if (m_project) {
        if (disable) {
Nicolas Carion's avatar
Nicolas Carion committed
730
            m_project->setDocumentProperty(QStringLiteral("disablebineffects"), QString::number((int)true));
731
        } else {
732
            m_project->setDocumentProperty(QStringLiteral("disablebineffects"), QString());
733
        }
734
    }
735
736
    pCore->monitorManager()->refreshProjectMonitor();
    pCore->monitorManager()->refreshClipMonitor();
737
738
}

739
740
741
void ProjectManager::slotDisableTimelineEffects(bool disable)
{
    if (disable) {
Nicolas Carion's avatar
Nicolas Carion committed
742
        m_project->setDocumentProperty(QStringLiteral("disabletimelineeffects"), QString::number((int)true));
743
    } else {
744
        m_project->setDocumentProperty(QStringLiteral("disabletimelineeffects"), QString());
745
    }
746
747
    m_mainTimelineModel->setTimelineEffectsEnabled(!disable);
    pCore->monitorManager()->refreshProjectMonitor();
748
}
749

750
751
void ProjectManager::slotSwitchTrackLock()
{
752
    pCore->window()->getMainTimeline()->controller()->switchTrackLock();
753
754
}

755
756
757
758
759
void ProjectManager::slotSwitchTrackActive()
{
    pCore->window()->getMainTimeline()->controller()->switchTrackActive();
}

760
761
void ProjectManager::slotSwitchAllTrackLock()
{
762
    pCore->window()->getMainTimeline()->controller()->switchTrackLock(true);
763
764
}

765
766
void ProjectManager::slotSwitchTrackTarget()
{
767
    pCore->window()->getMainTimeline()->controller()->switchTargetTrack();
768
769
}

770
771
QString ProjectManager::getDefaultProjectFormat()
{
Nicolas Carion's avatar
Nicolas Carion committed
772
773
    // On first run, lets use an HD1080p profile with fps related to timezone country. Then, when the first video is added to a project, if it does not match
    // our profile, propose a new default.
774
775
776
    QTimeZone zone;
    zone = QTimeZone::systemTimeZone();

Laurent Montel's avatar
Laurent Montel committed
777
    QList<int> ntscCountries;
778
    ntscCountries << QLocale::Canada << QLocale::Chile << QLocale::CostaRica << QLocale::Cuba << QLocale::DominicanRepublic << QLocale::Ecuador;
Nicolas Carion's avatar
Nicolas Carion committed
779
780
    ntscCountries << QLocale::Japan << QLocale::Mexico << QLocale::Nicaragua << QLocale::Panama << QLocale::Peru << QLocale::Philippines;
    ntscCountries << QLocale::PuertoRico << QLocale::SouthKorea << QLocale::Taiwan << QLocale::UnitedStates;
781
    bool ntscProject = ntscCountries.contains(zone.country());
Laurent Montel's avatar
Laurent Montel committed
782
783
784
    if (!ntscProject) {
        return QStringLiteral("atsc_1080p_25");
    }
785
786
787
    return QStringLiteral("atsc_1080p_2997");
}

Laurent Montel's avatar
Laurent Montel committed
788
void ProjectManager::saveZone(const QStringList &info, const QDir &dir)
789
790
791
{
    pCore->bin()->saveZone(info, dir);
}
792

793
794
795
void ProjectManager::moveProjectData(const QString &src, const QString &dest)
{
    // Move tmp folder (thumbnails, timeline preview)
Laurent Montel's avatar
Laurent Montel committed
796
    KIO::CopyJob *copyJob = KIO::move(QUrl::fromLocalFile(src), QUrl::fromLocalFile(dest));
Laurent Montel's avatar
Laurent Montel committed
797
    connect(copyJob, &KJob::result, this, &ProjectManager::slotMoveFinished);
Laurent Montel's avatar
Laurent Montel committed
798
    connect(copyJob, SIGNAL(percent(KJob *, ulong)), this, SLOT(slotMoveProgress(KJob *, ulong)));
799
800
801
    m_project->moveProjectData(src, dest);
}

802
803
void ProjectManager::slotMoveProgress(KJob *, unsigned long progress)
{
804
    pCore->window()->slotGotProgressInfo(i18n("Moving project folder"), static_cast<int>(progress), ProcessingJobMessage);
805
806
807
808
809
810
}

void ProjectManager::slotMoveFinished(KJob *job)
{
    if (job->error() == 0) {
        pCore->window()->slotGotProgressInfo(QString(), 100, InformationMessage);
Nicolas Carion's avatar
Nicolas Carion committed
811
        auto *copyJob = static_cast<KIO::CopyJob *>(job);
812
        QString newFolder = copyJob->destUrl().toLocalFile();
813
814
        // Check if project folder is inside document folder, in which case, paths will be relative
        QDir projectDir(m_project->url().toString(QUrl::RemoveFilename | QUrl::RemoveScheme));
815
        QDir srcDir(m_project->projectTempFolder());
816
817
818
        if (srcDir.absolutePath().startsWith(projectDir.absolutePath())) {
            m_replacementPattern.insert(QStringLiteral(">proxy/"), QStringLiteral(">") + newFolder + QStringLiteral("/proxy/"));
        } else {
819
            m_replacementPattern.insert(m_project->projectTempFolder() + QStringLiteral("/proxy/"), newFolder + QStringLiteral("/proxy/"));
820
821
822
823
824
825
826
827
828
        }
        m_project->setProjectFolder(QUrl::fromLocalFile(newFolder));
        saveFile();
        m_replacementPattern.clear();
        slotRevert();
    } else {
        KMessageBox::sorry(pCore->window(), i18n("Error moving project folder: %1", job->errorText()));
    }
}
829