]> git.sesse.net Git - kdenlive/blob - src/projectsettings.cpp
Fix proxy clip having wrong duration and crash when deleting proxies from the project...
[kdenlive] / src / projectsettings.cpp
1 /***************************************************************************
2  *   Copyright (C) 2008 by Jean-Baptiste Mardelle (jb@kdenlive.org)        *
3  *                                                                         *
4  *   This program is free software; you can redistribute it and/or modify  *
5  *   it under the terms of the GNU General Public License as published by  *
6  *   the Free Software Foundation; either version 2 of the License, or     *
7  *   (at your option) any later version.                                   *
8  *                                                                         *
9  *   This program is distributed in the hope that it will be useful,       *
10  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
11  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
12  *   GNU General Public License for more details.                          *
13  *                                                                         *
14  *   You should have received a copy of the GNU General Public License     *
15  *   along with this program; if not, write to the                         *
16  *   Free Software Foundation, Inc.,                                       *
17  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA          *
18  ***************************************************************************/
19
20 #include "projectsettings.h"
21 #include "kdenlivesettings.h"
22 #include "profilesdialog.h"
23 #include "docclipbase.h"
24 #include "titlewidget.h"
25 #include "effectslist.h"
26
27 #include <KStandardDirs>
28 #include <KMessageBox>
29 #include <KDebug>
30 #include <kio/directorysizejob.h>
31 #include <KIO/NetAccess>
32 #include <KTemporaryFile>
33 #include <KFileDialog>
34
35 #include <QDir>
36 #include <kmessagebox.h>
37
38 ProjectSettings::ProjectSettings(ProjectList *projectlist, QStringList lumas, int videotracks, int audiotracks, const QString projectPath, bool readOnlyTracks, bool savedProject, QWidget * parent) :
39     QDialog(parent), m_savedProject(savedProject), m_projectList(projectlist), m_lumas(lumas)
40 {
41     setupUi(this);
42
43     list_search->setTreeWidget(files_list);
44
45     QMap <QString, QString> profilesInfo = ProfilesDialog::getProfilesInfo();
46     QMapIterator<QString, QString> i(profilesInfo);
47     while (i.hasNext()) {
48         i.next();
49         profiles_list->addItem(i.key(), i.value());
50     }
51     project_folder->setMode(KFile::Directory);
52     project_folder->setUrl(KUrl(projectPath));
53     QString currentProf = KdenliveSettings::current_profile();
54
55     for (int i = 0; i < profiles_list->count(); i++) {
56         if (profiles_list->itemData(i).toString() == currentProf) {
57             profiles_list->setCurrentIndex(i);
58             break;
59         }
60     }
61
62     m_buttonOk = buttonBox->button(QDialogButtonBox::Ok);
63     //buttonOk->setEnabled(false);
64     audio_thumbs->setChecked(KdenliveSettings::audiothumbnails());
65     video_thumbs->setChecked(KdenliveSettings::videothumbnails());
66     audio_tracks->setValue(audiotracks);
67     video_tracks->setValue(videotracks);
68     connect(generate_proxy, SIGNAL(toggled(bool)), proxy_minsize, SLOT(setEnabled(bool)));
69     connect(generate_imageproxy, SIGNAL(toggled(bool)), proxy_imageminsize, SLOT(setEnabled(bool)));
70     QString proxyparameters;
71     QString proxyextension;
72     if (projectlist) {
73         enable_proxy->setChecked(projectlist->getDocumentProperty("enableproxy").toInt());
74         generate_proxy->setChecked(projectlist->getDocumentProperty("generateproxy").toInt());
75         proxy_minsize->setValue(projectlist->getDocumentProperty("proxyminsize").toInt());
76         proxyparameters = projectlist->getDocumentProperty("proxyparams");
77         generate_imageproxy->setChecked(projectlist->getDocumentProperty("generateimageproxy").toInt());
78         proxy_imageminsize->setValue(projectlist->getDocumentProperty("proxyimageminsize").toInt());
79         proxyextension = projectlist->getDocumentProperty("proxyextension");
80     }
81     else {
82         enable_proxy->setChecked(KdenliveSettings::enableproxy());
83         generate_proxy->setChecked(KdenliveSettings::generateproxy());
84         proxy_minsize->setValue(KdenliveSettings::proxyminsize());
85         proxyparameters = KdenliveSettings::proxyparams();
86         generate_imageproxy->setChecked(KdenliveSettings::generateimageproxy());
87         proxy_imageminsize->setValue(KdenliveSettings::proxyimageminsize());
88         proxyextension = KdenliveSettings::proxyextension();
89       
90     }
91
92     proxy_minsize->setEnabled(generate_proxy->isChecked());
93     proxy_imageminsize->setEnabled(generate_imageproxy->isChecked());
94
95
96     // load proxy profiles
97     QString profileFile = KStandardDirs::locateLocal("appdata", "encodingprofiles.rc");
98     KConfig conf(profileFile, KConfig::SimpleConfig);
99     KConfigGroup group(&conf, "proxy");
100     QMap <QString, QString> values = group.entryMap();
101     QMapIterator<QString, QString> k(values);
102     int ix = -1;
103     while (k.hasNext()) {
104         k.next();
105         if (!k.key().isEmpty()) {
106             QString params = k.value().section(';', 0, 0);
107             QString extension = k.value().section(';', 1, 1);
108             if (params == proxyparameters && extension == proxyextension) {
109                 // this is the current profile
110                 ix = proxy_profile->count();
111             }
112             proxy_profile->addItem(k.key(), k.value());
113         }
114     }
115     if (ix == -1) {
116         // Current project proxy settings not found
117         ix = proxy_profile->count();
118         proxy_profile->addItem(i18n("Current Settings"), QString(proxyparameters + ';' + proxyextension));
119     }
120     proxy_profile->setCurrentIndex(ix);
121     slotUpdateProxyParams();
122
123     // Proxy GUI stuff
124     proxy_showprofileinfo->setIcon(KIcon("help-about"));
125     connect(proxy_profile, SIGNAL(currentIndexChanged(int)), this, SLOT(slotUpdateProxyParams()));
126     proxyparams->setVisible(false);
127     proxyparams->setMaximumHeight(QFontMetrics(font()).lineSpacing() * 5);
128     connect(proxy_showprofileinfo, SIGNAL(clicked(bool)), proxyparams, SLOT(setVisible(bool)));
129     
130     if (readOnlyTracks) {
131         video_tracks->setEnabled(false);
132         audio_tracks->setEnabled(false);
133     }
134     slotUpdateDisplay();
135     if (m_projectList != NULL) {
136         slotUpdateFiles();
137         connect(clear_cache, SIGNAL(clicked()), this, SLOT(slotClearCache()));
138         connect(delete_unused, SIGNAL(clicked()), this, SLOT(slotDeleteUnused()));
139         connect(delete_proxies, SIGNAL(clicked()), this, SLOT(slotDeleteProxies()));
140     } else tabWidget->widget(1)->setEnabled(false);
141     connect(profiles_list, SIGNAL(currentIndexChanged(int)), this, SLOT(slotUpdateDisplay()));
142     connect(project_folder, SIGNAL(textChanged(const QString &)), this, SLOT(slotUpdateButton(const QString &)));
143     connect(button_export, SIGNAL(clicked()), this, SLOT(slotExportToText()));
144 }
145
146 void ProjectSettings::slotDeleteUnused()
147 {
148     QStringList toDelete;
149     QList <DocClipBase*> list = m_projectList->documentClipList();
150     for (int i = 0; i < list.count(); i++) {
151         DocClipBase *clip = list.at(i);
152         if (clip->numReferences() == 0 && clip->clipType() != SLIDESHOW) {
153             KUrl url = clip->fileURL();
154             if (!url.isEmpty() && !toDelete.contains(url.path())) toDelete << url.path();
155         }
156     }
157
158     // make sure our urls are not used in another clip
159     for (int i = 0; i < list.count(); i++) {
160         DocClipBase *clip = list.at(i);
161         if (clip->numReferences() > 0) {
162             KUrl url = clip->fileURL();
163             if (!url.isEmpty() && toDelete.contains(url.path())) toDelete.removeAll(url.path());
164         }
165     }
166
167     if (toDelete.count() == 0) {
168         // No physical url to delete, we only remove unused clips from project (color clips for example have no physical url)
169         if (KMessageBox::warningContinueCancel(this, i18n("This will remove all unused clips from your project."), i18n("Clean up project")) == KMessageBox::Cancel) return;
170         m_projectList->cleanup();
171         slotUpdateFiles();
172         return;
173     }
174     if (KMessageBox::warningYesNoList(this, i18n("This will remove the following files from your hard drive.\nThis action cannot be undone, only use if you know what you are doing.\nAre you sure you want to continue?"), toDelete, i18n("Delete unused clips")) != KMessageBox::Yes) return;
175     m_projectList->trashUnusedClips();
176     slotUpdateFiles();
177 }
178
179 void ProjectSettings::slotClearCache()
180 {
181     buttonBox->setEnabled(false);
182     KIO::NetAccess::del(KUrl(project_folder->url().path(KUrl::AddTrailingSlash) + "thumbs/"), this);
183     KStandardDirs::makeDir(project_folder->url().path(KUrl::AddTrailingSlash) + "thumbs/");
184     buttonBox->setEnabled(true);
185     slotUpdateFiles(true);
186 }
187
188 void ProjectSettings::slotDeleteProxies()
189 {
190     if (KMessageBox::warningContinueCancel(this, i18n("Deleting proxy clips will disable proxies for this project.")) != KMessageBox::Continue) return;
191     buttonBox->setEnabled(false);
192     enable_proxy->setChecked(false);
193     emit disableProxies();
194     KIO::NetAccess::del(KUrl(project_folder->url().path(KUrl::AddTrailingSlash) + "proxy/"), this);
195     KStandardDirs::makeDir(project_folder->url().path(KUrl::AddTrailingSlash) + "proxy/");
196     buttonBox->setEnabled(true);
197     slotUpdateFiles(true);
198 }
199
200 void ProjectSettings::slotUpdateFiles(bool cacheOnly)
201 {
202     KIO::DirectorySizeJob *job = KIO::directorySize(project_folder->url().path(KUrl::AddTrailingSlash) + "thumbs/");
203     job->exec();
204     thumbs_count->setText(QString::number(job->totalFiles()));
205     thumbs_size->setText(KIO::convertSize(job->totalSize()));
206     job = KIO::directorySize(project_folder->url().path(KUrl::AddTrailingSlash) + "proxy/");
207     job->exec();
208     proxy_count->setText(QString::number(job->totalFiles()));
209     proxy_size->setText(KIO::convertSize(job->totalSize()));
210     delete job;
211     if (cacheOnly) return;
212     int unused = 0;
213     int used = 0;
214     KIO::filesize_t usedSize = 0;
215     KIO::filesize_t unUsedSize = 0;
216     QList <DocClipBase*> list = m_projectList->documentClipList();
217     files_list->clear();
218
219     // List all files that are used in the project. That also means:
220     // images included in slideshow and titles, files in playlist clips
221     // TODO: images used in luma transitions, files used for LADSPA effects?
222
223     // Setup categories
224     QTreeWidgetItem *videos = new QTreeWidgetItem(files_list, QStringList() << i18n("Video clips"));
225     videos->setIcon(0, KIcon("video-x-generic"));
226     videos->setExpanded(true);
227     QTreeWidgetItem *sounds = new QTreeWidgetItem(files_list, QStringList() << i18n("Audio clips"));
228     sounds->setIcon(0, KIcon("audio-x-generic"));
229     sounds->setExpanded(true);
230     QTreeWidgetItem *images = new QTreeWidgetItem(files_list, QStringList() << i18n("Image clips"));
231     images->setIcon(0, KIcon("image-x-generic"));
232     images->setExpanded(true);
233     QTreeWidgetItem *slideshows = new QTreeWidgetItem(files_list, QStringList() << i18n("Slideshow clips"));
234     slideshows->setIcon(0, KIcon("image-x-generic"));
235     slideshows->setExpanded(true);
236     QTreeWidgetItem *texts = new QTreeWidgetItem(files_list, QStringList() << i18n("Text clips"));
237     texts->setIcon(0, KIcon("text-plain"));
238     texts->setExpanded(true);
239     QTreeWidgetItem *others = new QTreeWidgetItem(files_list, QStringList() << i18n("Other clips"));
240     others->setIcon(0, KIcon("unknown"));
241     others->setExpanded(true);
242     int count = 0;
243     QStringList allFonts;
244     foreach(const QString & file, m_lumas) {
245         count++;
246         new QTreeWidgetItem(images, QStringList() << file);
247     }
248
249     for (int i = 0; i < list.count(); i++) {
250         DocClipBase *clip = list.at(i);
251         if (clip->clipType() == SLIDESHOW) {
252             QStringList subfiles = extractSlideshowUrls(clip->fileURL());
253             foreach(const QString & file, subfiles) {
254                 count++;
255                 new QTreeWidgetItem(slideshows, QStringList() << file);
256             }
257         } else if (!clip->fileURL().isEmpty()) {
258             //allFiles.append(clip->fileURL().path());
259             switch (clip->clipType()) {
260             case TEXT:
261                 new QTreeWidgetItem(texts, QStringList() << clip->fileURL().path());
262                 break;
263             case AUDIO:
264                 new QTreeWidgetItem(sounds, QStringList() << clip->fileURL().path());
265                 break;
266             case IMAGE:
267                 new QTreeWidgetItem(images, QStringList() << clip->fileURL().path());
268                 break;
269             case UNKNOWN:
270                 new QTreeWidgetItem(others, QStringList() << clip->fileURL().path());
271                 break;
272             default:
273                 new QTreeWidgetItem(videos, QStringList() << clip->fileURL().path());
274                 break;
275             }
276             count++;
277         }
278         if (clip->clipType() == TEXT) {
279             QStringList imagefiles = TitleWidget::extractImageList(clip->getProperty("xmldata"));
280             QStringList fonts = TitleWidget::extractFontList(clip->getProperty("xmldata"));
281             foreach(const QString & file, imagefiles) {
282                 count++;
283                 new QTreeWidgetItem(images, QStringList() << file);
284             }
285             allFonts << fonts;
286         } else if (clip->clipType() == PLAYLIST) {
287             QStringList files = extractPlaylistUrls(clip->fileURL().path());
288             foreach(const QString & file, files) {
289                 count++;
290                 new QTreeWidgetItem(others, QStringList() << file);
291             }
292         }
293
294         if (clip->numReferences() == 0) {
295             unused++;
296             unUsedSize += clip->fileSize();
297         } else {
298             used++;
299             usedSize += clip->fileSize();
300         }
301     }
302 #if QT_VERSION >= 0x040500
303     allFonts.removeDuplicates();
304 #endif
305     // Hide unused categories
306     for (int i = 0; i < files_list->topLevelItemCount(); i++) {
307         if (files_list->topLevelItem(i)->childCount() == 0) {
308             files_list->topLevelItem(i)->setHidden(true);
309         }
310     }
311     files_count->setText(QString::number(count));
312     fonts_list->addItems(allFonts);
313     if (allFonts.isEmpty()) {
314         fonts_list->setHidden(true);
315         label_fonts->setHidden(true);
316     }
317     used_count->setText(QString::number(used));
318     used_size->setText(KIO::convertSize(usedSize));
319     unused_count->setText(QString::number(unused));
320     unused_size->setText(KIO::convertSize(unUsedSize));
321     delete_unused->setEnabled(unused > 0);
322 }
323
324 void ProjectSettings::accept()
325 {
326     if (!m_savedProject && selectedProfile() != KdenliveSettings::current_profile())
327         if (KMessageBox::warningContinueCancel(this, i18n("Changing the profile of your project cannot be undone.\nIt is recommended to save your project before attempting this operation that might cause some corruption in transitions.\n Are you sure you want to proceed?"), i18n("Confirm profile change")) == KMessageBox::Cancel) return;
328     QDialog::accept();
329 }
330
331 void ProjectSettings::slotUpdateDisplay()
332 {
333     QString currentProfile = profiles_list->itemData(profiles_list->currentIndex()).toString();
334     QMap< QString, QString > values = ProfilesDialog::getSettingsFromFile(currentProfile);
335     p_size->setText(values.value("width") + 'x' + values.value("height"));
336     p_fps->setText(values.value("frame_rate_num") + '/' + values.value("frame_rate_den"));
337     p_aspect->setText(values.value("sample_aspect_num") + '/' + values.value("sample_aspect_den"));
338     p_display->setText(values.value("display_aspect_num") + '/' + values.value("display_aspect_den"));
339     if (values.value("progressive").toInt() == 0) {
340         p_progressive->setText(i18n("Interlaced (%1 fields per second)",
341                                     QString::number((double)2 * values.value("frame_rate_num").toInt() / values.value("frame_rate_den").toInt(), 'f', 2)));
342     } else {
343         p_progressive->setText(i18n("Progressive"));
344     }
345     p_colorspace->setText(ProfilesDialog::getColorspaceDescription(values.value("colorspace").toInt()));
346 }
347
348 void ProjectSettings::slotUpdateButton(const QString &path)
349 {
350     if (path.isEmpty()) m_buttonOk->setEnabled(false);
351     else {
352         m_buttonOk->setEnabled(true);
353         slotUpdateFiles(true);
354     }
355 }
356
357 QString ProjectSettings::selectedProfile() const
358 {
359     return profiles_list->itemData(profiles_list->currentIndex()).toString();
360 }
361
362 KUrl ProjectSettings::selectedFolder() const
363 {
364     return project_folder->url();
365 }
366
367 QPoint ProjectSettings::tracks()
368 {
369     QPoint p;
370     p.setX(video_tracks->value());
371     p.setY(audio_tracks->value());
372     return p;
373 }
374
375 bool ProjectSettings::enableVideoThumbs() const
376 {
377     return video_thumbs->isChecked();
378 }
379
380 bool ProjectSettings::enableAudioThumbs() const
381 {
382     return audio_thumbs->isChecked();
383 }
384
385 bool ProjectSettings::useProxy() const
386 {
387     return enable_proxy->isChecked();
388 }
389
390 bool ProjectSettings::generateProxy() const
391 {
392     return generate_proxy->isChecked();
393 }
394
395 bool ProjectSettings::generateImageProxy() const
396 {
397     return generate_imageproxy->isChecked();
398 }
399
400 int ProjectSettings::proxyMinSize() const
401 {
402     return proxy_minsize->value();
403 }
404
405 int ProjectSettings::proxyImageMinSize() const
406 {
407     return proxy_imageminsize->value();
408 }
409
410 QString ProjectSettings::proxyParams() const
411 {
412     QString params = proxy_profile->itemData(proxy_profile->currentIndex()).toString();
413     return params.section(';', 0, 0);
414 }
415
416 QString ProjectSettings::proxyExtension() const
417 {
418     QString params = proxy_profile->itemData(proxy_profile->currentIndex()).toString();
419     return params.section(';', 1, 1);
420 }
421
422 //static
423 QStringList ProjectSettings::extractPlaylistUrls(QString path)
424 {
425     QStringList urls;
426     QDomDocument doc;
427     QFile file(path);
428     if (!file.open(QIODevice::ReadOnly))
429         return urls;
430     if (!doc.setContent(&file)) {
431         file.close();
432         return urls;
433     }
434     file.close();
435     QString root = doc.documentElement().attribute("root");
436     if (!root.isEmpty() && !root.endsWith('/')) root.append('/');
437     QDomNodeList files = doc.elementsByTagName("producer");
438     for (int i = 0; i < files.count(); i++) {
439         QDomElement e = files.at(i).toElement();
440         QString type = EffectsList::property(e, "mlt_service");
441         if (type != "colour") {
442             QString url = EffectsList::property(e, "resource");
443             if (!url.isEmpty()) {
444                 if (!url.startsWith('/')) url.prepend(root);
445                 if (url.section('.', 0, -2).endsWith("/.all")) {
446                     // slideshow clip, extract image urls
447                     urls << extractSlideshowUrls(KUrl(url));
448                 } else urls << url;
449                 if (url.endsWith(".mlt") || url.endsWith(".kdenlive")) {
450                     //TODO: Do something to avoid infinite loops if 2 files reference themselves...
451                     urls << extractPlaylistUrls(url);
452                 }
453             }
454         }
455     }
456
457     // luma files for transitions
458     files = doc.elementsByTagName("transition");
459     for (int i = 0; i < files.count(); i++) {
460         QDomElement e = files.at(i).toElement();
461         QString url = EffectsList::property(e, "luma");
462         if (!url.isEmpty()) {
463             if (!url.startsWith('/')) url.prepend(root);
464             urls << url;
465         }
466     }
467
468     return urls;
469 }
470
471
472 //static
473 QStringList ProjectSettings::extractSlideshowUrls(KUrl url)
474 {
475     QStringList urls;
476     QString path = url.directory(KUrl::AppendTrailingSlash);
477     QString ext = url.path().section('.', -1);
478     QDir dir(path);
479     if (url.path().contains(".all.")) {
480         // this is a mime slideshow, like *.jpeg
481         QStringList filters;
482         filters << "*." + ext;
483         dir.setNameFilters(filters);
484         QStringList result = dir.entryList(QDir::Files);
485         urls.append(path + filters.at(0) + " (" + i18np("1 image found", "%1 images found", result.count()) + ")");
486     } else {
487         // this is a pattern slideshow, like sequence%4d.jpg
488         QString filter = url.fileName();
489         QString ext = filter.section('.', -1);
490         filter = filter.section('%', 0, -2);
491         QString regexp = "^" + filter + "\\d+\\." + ext + "$";
492         QRegExp rx(regexp);
493         int count = 0;
494         QStringList result = dir.entryList(QDir::Files);
495         foreach(const QString & path, result) {
496             if (rx.exactMatch(path)) count++;
497         }
498         urls.append(url.path() + " (" + i18np("1 image found", "%1 images found", count) + ")");
499     }
500     return urls;
501 }
502
503 void ProjectSettings::slotExportToText()
504 {
505     QString savePath = KFileDialog::getSaveFileName(project_folder->url(), "text/plain", this);
506     if (savePath.isEmpty()) return;
507     QString data;
508     data.append(i18n("Project folder: %1",  project_folder->url().path()) + "\n");
509     data.append(i18n("Project profile: %1",  profiles_list->currentText()) + "\n");
510     data.append(i18n("Total clips: %1 (%2 used in timeline).", files_count->text(), used_count->text()) + "\n\n");
511     for (int i = 0; i < files_list->topLevelItemCount(); i++) {
512         if (files_list->topLevelItem(i)->childCount() > 0) {
513             data.append("\n" + files_list->topLevelItem(i)->text(0) + ":\n\n");
514             for (int j = 0; j < files_list->topLevelItem(i)->childCount(); j++) {
515                 data.append(files_list->topLevelItem(i)->child(j)->text(0) + "\n");
516             }
517         }
518     }
519     KTemporaryFile tmpfile;
520     if (!tmpfile.open()) {
521         kWarning() << "/////  CANNOT CREATE TMP FILE in: " << tmpfile.fileName();
522         return;
523     }
524     QFile xmlf(tmpfile.fileName());
525     xmlf.open(QIODevice::WriteOnly);
526     xmlf.write(data.toUtf8());
527     if (xmlf.error() != QFile::NoError) {
528         xmlf.close();
529         return;
530     }
531     xmlf.close();
532     KIO::NetAccess::upload(tmpfile.fileName(), savePath, 0);
533 }
534
535 void ProjectSettings::slotUpdateProxyParams()
536 {
537     QString params = proxy_profile->itemData(proxy_profile->currentIndex()).toString();
538     proxyparams->setPlainText(params.section(';', 0, 0));
539 }
540
541 #include "projectsettings.moc"
542
543