1 /***************************************************************************
2 * Copyright (C) 2011 by Jean-Baptiste Mardelle (jb@kdenlive.org) *
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. *
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. *
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 ***************************************************************************/
21 #include "archivewidget.h"
22 #include "titlewidget.h"
25 #include <KDiskFreeSpaceInfo>
26 #include <KUrlRequester>
27 #include <KFileDialog>
28 #include <KMessageBox>
30 #include <KIO/NetAccess>
34 #include <QTreeWidget>
35 #include <QtConcurrentRun>
36 #include "projectsettings.h"
39 ArchiveWidget::ArchiveWidget(QString projectName, QDomDocument doc, QList <DocClipBase*> list, QStringList luma_list, QWidget * parent) :
43 m_name(projectName.section('.', 0, -2)),
47 setAttribute(Qt::WA_DeleteOnClose);
49 setWindowTitle(i18n("Archive Project"));
50 archive_url->setUrl(KUrl(QDir::homePath()));
51 connect(archive_url, SIGNAL(textChanged (const QString &)), this, SLOT(slotCheckSpace()));
52 connect(this, SIGNAL(archivingFinished(bool)), this, SLOT(slotArchivingFinished(bool)));
53 connect(this, SIGNAL(archiveProgress(int)), this, SLOT(slotArchivingProgress(int)));
56 QTreeWidgetItem *videos = new QTreeWidgetItem(files_list, QStringList() << i18n("Video clips"));
57 videos->setIcon(0, KIcon("video-x-generic"));
58 videos->setData(0, Qt::UserRole, "videos");
59 videos->setExpanded(false);
60 QTreeWidgetItem *sounds = new QTreeWidgetItem(files_list, QStringList() << i18n("Audio clips"));
61 sounds->setIcon(0, KIcon("audio-x-generic"));
62 sounds->setData(0, Qt::UserRole, "sounds");
63 sounds->setExpanded(false);
64 QTreeWidgetItem *images = new QTreeWidgetItem(files_list, QStringList() << i18n("Image clips"));
65 images->setIcon(0, KIcon("image-x-generic"));
66 images->setData(0, Qt::UserRole, "images");
67 images->setExpanded(false);
68 QTreeWidgetItem *slideshows = new QTreeWidgetItem(files_list, QStringList() << i18n("Slideshow clips"));
69 slideshows->setIcon(0, KIcon("image-x-generic"));
70 slideshows->setData(0, Qt::UserRole, "slideshows");
71 slideshows->setExpanded(false);
72 QTreeWidgetItem *texts = new QTreeWidgetItem(files_list, QStringList() << i18n("Text clips"));
73 texts->setIcon(0, KIcon("text-plain"));
74 texts->setData(0, Qt::UserRole, "texts");
75 texts->setExpanded(false);
76 QTreeWidgetItem *others = new QTreeWidgetItem(files_list, QStringList() << i18n("Other clips"));
77 others->setIcon(0, KIcon("unknown"));
78 others->setData(0, Qt::UserRole, "others");
79 others->setExpanded(false);
80 QTreeWidgetItem *lumas = new QTreeWidgetItem(files_list, QStringList() << i18n("Luma files"));
81 lumas->setIcon(0, KIcon("image-x-generic"));
82 lumas->setData(0, Qt::UserRole, "lumas");
83 lumas->setExpanded(false);
85 QTreeWidgetItem *proxies = new QTreeWidgetItem(files_list, QStringList() << i18n("Proxy clips"));
86 proxies->setIcon(0, KIcon("video-x-generic"));
87 proxies->setData(0, Qt::UserRole, "proxy");
88 proxies->setExpanded(false);
93 QStringList fileNames;
94 generateItems(lumas, luma_list);
96 QStringList slideUrls;
97 QStringList audioUrls;
98 QStringList videoUrls;
99 QStringList imageUrls;
100 QStringList otherUrls;
101 QStringList proxyUrls;
103 for (int i = 0; i < list.count(); i++) {
104 DocClipBase *clip = list.at(i);
105 CLIPTYPE t = clip->clipType();
106 if (t == SLIDESHOW) {
107 KUrl slideUrl = clip->fileURL();
108 //TODO: Slideshow files
109 slideUrls << slideUrl.path();
111 else if (t == IMAGE) imageUrls << clip->fileURL().path();
112 else if (t == TEXT) {
113 QStringList imagefiles = TitleWidget::extractImageList(clip->getProperty("xmldata"));
114 QStringList fonts = TitleWidget::extractFontList(clip->getProperty("xmldata"));
115 imageUrls << imagefiles;
117 } else if (t == PLAYLIST) {
118 QStringList files = ProjectSettings::extractPlaylistUrls(clip->fileURL().path());
121 else if (!clip->fileURL().isEmpty()) {
122 if (t == AUDIO) audioUrls << clip->fileURL().path();
124 videoUrls << clip->fileURL().path();
125 // Check if we have a proxy
126 QString proxy = clip->getProperty("proxy");
127 if (!proxy.isEmpty() && proxy != "-" && QFile::exists(proxy)) proxyUrls << proxy;
132 generateItems(sounds, audioUrls);
133 generateItems(videos, videoUrls);
134 generateItems(images, imageUrls);
135 generateItems(slideshows, slideUrls);
136 generateItems(others, otherUrls);
137 generateItems(proxies, proxyUrls);
139 #if QT_VERSION >= 0x040500
140 allFonts.removeDuplicates();
145 // Hide unused categories, add item count
147 for (int i = 0; i < files_list->topLevelItemCount(); i++) {
148 QTreeWidgetItem *parentItem = files_list->topLevelItem(i);
149 int items = parentItem->childCount();
151 files_list->topLevelItem(i)->setHidden(true);
154 if (parentItem->data(0, Qt::UserRole).toString() == "slideshows")
156 // Special case: slideshows contain several files
157 for (int j = 0; j < items; j++) {
158 total += parentItem->child(j)->data(0, Qt::UserRole + 1).toStringList().count();
162 parentItem->setText(0, files_list->topLevelItem(i)->text(0) + " " + i18np("(%1 item)", "(%1 items)", items));
166 compressed_archive->setText(compressed_archive->text() + " (" + m_name + ".tar.gz)");
167 project_files->setText(i18np("%1 file to archive, requires %2", "%1 files to archive, requires %2", total, KIO::convertSize(m_requestedSize)));
168 buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive"));
169 connect(buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), this, SLOT(slotStartArchiving()));
170 buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
175 ArchiveWidget::~ArchiveWidget()
179 void ArchiveWidget::done ( int r )
181 if (closeAccepted()) QDialog::done(r);
184 void ArchiveWidget::closeEvent ( QCloseEvent * e )
187 if (closeAccepted()) e->accept();
192 bool ArchiveWidget::closeAccepted()
194 if (!archive_url->isEnabled()) {
195 // Archiving in progress, should we stop?
196 if (KMessageBox::warningContinueCancel(this, i18n("Archiving in progress, do you want to stop it?"), i18n("Stop Archiving"), KGuiItem(i18n("Stop Archiving"))) != KMessageBox::Continue) {
199 if (m_copyJob) m_copyJob->kill();
205 void ArchiveWidget::generateItems(QTreeWidgetItem *parentItem, QStringList items)
207 QStringList filesList;
210 bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == "slideshows";
211 foreach(const QString & file, items) {
212 QTreeWidgetItem *item = new QTreeWidgetItem(parentItem, QStringList() << file);
213 fileName = KUrl(file).fileName();
215 // we store each slideshow in a separate subdirectory
216 item->setData(0, Qt::UserRole, ix);
219 QDir dir(slideUrl.directory(KUrl::AppendTrailingSlash));
220 if (slideUrl.fileName().startsWith(".all.")) {
221 // mimetype slideshow (for example *.png)
224 // TODO: improve jpeg image detection with extension like jpeg, requires change in MLT image producers
225 filters << "*." + slideUrl.fileName().section('.', -1);
226 dir.setNameFilters(filters);
227 QFileInfoList resultList = dir.entryInfoList(QDir::Files);
228 QStringList slideImages;
229 for (int i = 0; i < resultList.count(); i++) {
230 m_requestedSize += resultList.at(i).size();
231 slideImages << resultList.at(i).absoluteFilePath();
233 item->setData(0, Qt::UserRole + 1, slideImages);
236 // pattern url (like clip%.3d.png)
237 QStringList result = dir.entryList(QDir::Files);
238 QString filter = slideUrl.fileName();
239 QString ext = filter.section('.', -1);
240 filter = filter.section('%', 0, -2);
241 QString regexp = "^" + filter + "\\d+\\." + ext + "$";
243 QStringList slideImages;
244 QString directory = dir.absolutePath();
245 if (!directory.endsWith('/')) directory.append('/');
246 foreach(const QString & path, result) {
247 if (rx.exactMatch(path)) {
248 m_requestedSize += QFileInfo(directory + path).size();
249 slideImages << directory + path;
252 item->setData(0, Qt::UserRole + 1, slideImages);
255 else if (filesList.contains(fileName)) {
256 // we have 2 files with same name
258 QString newFileName = fileName.section('.', 0, -2) + "_" + QString::number(ix) + "." + fileName.section('.', -1);
259 while (filesList.contains(newFileName)) {
261 newFileName = fileName.section('.', 0, -2) + "_" + QString::number(ix) + "." + fileName.section('.', -1);
263 fileName = newFileName;
264 item->setData(0, Qt::UserRole, fileName);
267 m_requestedSize += QFileInfo(file).size();
268 filesList << fileName;
273 void ArchiveWidget::slotCheckSpace()
275 KDiskFreeSpaceInfo inf = KDiskFreeSpaceInfo::freeSpaceInfo( archive_url->url().path());
276 KIO::filesize_t freeSize = inf.available();;
277 if (freeSize > m_requestedSize) {
279 buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
280 icon_info->setPixmap(KIcon("dialog-ok").pixmap(16, 16));
281 text_info->setText(i18n("Available space on drive: %1", KIO::convertSize(freeSize)));
284 buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
285 icon_info->setPixmap(KIcon("dialog-close").pixmap(16, 16));
286 text_info->setText(i18n("Not enough space on drive, free space: %1", KIO::convertSize(freeSize)));
290 bool ArchiveWidget::slotStartArchiving(bool firstPass)
292 if (firstPass && (m_copyJob || m_archiveThread.isRunning())) {
293 // archiving in progress, abort
294 if (m_copyJob) m_copyJob->kill(KJob::EmitResult);
295 m_abortArchive = true;
298 bool isArchive = compressed_archive->isChecked();
299 if (!firstPass) m_copyJob = NULL;
302 m_abortArchive = false;
303 m_duplicateFiles.clear();
304 m_replacementList.clear();
305 m_foldersList.clear();
307 icon_info->setPixmap(KIcon("system-run").pixmap(16, 16));
308 text_info->setText(i18n("Archiving..."));
310 archive_url->setEnabled(false);
311 compressed_archive->setEnabled(false);
316 QTreeWidgetItem *parentItem;
317 bool isSlideshow = false;
318 for (int i = 0; i < files_list->topLevelItemCount(); i++) {
319 parentItem = files_list->topLevelItem(i);
320 if (parentItem->childCount() > 0 && !parentItem->isDisabled()) {
321 if (parentItem->data(0, Qt::UserRole).toString() == "slideshows") {
322 KUrl slideFolder(archive_url->url().path(KUrl::AddTrailingSlash) + "slideshows");
323 if (isArchive) m_foldersList.append("slideshows");
324 else KIO::NetAccess::mkdir(slideFolder, this);
327 files_list->setCurrentItem(parentItem);
328 if (!isSlideshow) parentItem->setDisabled(true);
329 destPath = parentItem->data(0, Qt::UserRole).toString() + "/";
330 destUrl = KUrl(archive_url->url().path(KUrl::AddTrailingSlash) + destPath);
331 QTreeWidgetItem *item;
332 for (int j = 0; j < parentItem->childCount(); j++) {
333 item = parentItem->child(j);
334 // Special case: slideshows
336 if (item->isDisabled()) {
339 destPath.append(item->data(0, Qt::UserRole).toString() + "/");
340 destUrl = KUrl(archive_url->url().path(KUrl::AddTrailingSlash) + destPath);
341 QStringList srcFiles = item->data(0, Qt::UserRole + 1).toStringList();
342 for (int k = 0; k < srcFiles.count(); k++) {
343 files << KUrl(srcFiles.at(k));
345 item->setDisabled(true);
346 if (parentItem->indexOfChild(item) == parentItem->childCount() - 1) {
347 // We have processed all slideshows
348 parentItem->setDisabled(true);
352 else if (item->data(0, Qt::UserRole).isNull()) {
353 files << KUrl(item->text(0));
356 // We must rename the destination file, since another file with same name exists
357 //TODO: monitor progress
359 m_filesList.insert(item->text(0), destPath + item->data(0, Qt::UserRole).toString());
361 else m_duplicateFiles.insert(KUrl(item->text(0)), KUrl(destUrl.path(KUrl::AddTrailingSlash) + item->data(0, Qt::UserRole).toString()));
368 if (destPath.isEmpty()) {
369 if (m_duplicateFiles.isEmpty()) return false;
370 QMapIterator<KUrl, KUrl> i(m_duplicateFiles);
373 KUrl startJobSrc = i.key();
374 KUrl startJobDst = i.value();
375 m_duplicateFiles.remove(startJobSrc);
376 KIO::CopyJob *job = KIO::copyAs(startJobSrc, startJobDst, KIO::HideProgressInfo);
377 connect(job, SIGNAL(result(KJob *)), this, SLOT(slotArchivingFinished(KJob *)));
378 connect(job, SIGNAL(processedSize(KJob *, qulonglong)), this, SLOT(slotArchivingProgress(KJob *, qulonglong)));
384 m_foldersList.append(destPath);
385 for (int i = 0; i < files.count(); i++) {
386 m_filesList.insert(files.at(i).path(), destPath + files.at(i).fileName());
388 slotArchivingFinished();
390 else if (files.isEmpty()) {
391 slotStartArchiving(false);
394 KIO::NetAccess::mkdir(destUrl, this);
395 m_copyJob = KIO::copy (files, destUrl, KIO::HideProgressInfo);
396 connect(m_copyJob, SIGNAL(result(KJob *)), this, SLOT(slotArchivingFinished(KJob *)));
397 connect(m_copyJob, SIGNAL(processedSize(KJob *, qulonglong)), this, SLOT(slotArchivingProgress(KJob *, qulonglong)));
400 progressBar->setValue(0);
401 buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Abort"));
406 void ArchiveWidget::slotArchivingFinished(KJob *job)
408 if (job == NULL || job->error() == 0) {
409 if (slotStartArchiving(false)) {
410 // We still have files to archive
413 else if (!compressed_archive->isChecked()) {
414 // Archiving finished
415 progressBar->setValue(100);
416 if (processProjectFile()) {
417 icon_info->setPixmap(KIcon("dialog-ok").pixmap(16, 16));
418 text_info->setText(i18n("Project was successfully archived."));
421 icon_info->setPixmap(KIcon("dialog-close").pixmap(16, 16));
422 text_info->setText(i18n("There was an error processing project file"));
424 } else processProjectFile();
428 icon_info->setPixmap(KIcon("dialog-close").pixmap(16, 16));
429 text_info->setText(i18n("There was an error while copying the files: %1", job->errorString()));
431 if (!compressed_archive->isChecked()) {
432 buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive"));
433 archive_url->setEnabled(true);
434 compressed_archive->setEnabled(true);
435 for (int i = 0; i < files_list->topLevelItemCount(); i++) {
436 files_list->topLevelItem(i)->setDisabled(false);
437 for (int j = 0; j < files_list->topLevelItem(i)->childCount(); j++)
438 files_list->topLevelItem(i)->child(j)->setDisabled(false);
443 void ArchiveWidget::slotArchivingProgress(KJob *, qulonglong size)
445 progressBar->setValue((int) 100 * size / m_requestedSize);
449 bool ArchiveWidget::processProjectFile()
452 QTreeWidgetItem *item;
453 for (int i = 0; i < files_list->topLevelItemCount(); i++) {
454 QTreeWidgetItem *parentItem = files_list->topLevelItem(i);
455 if (parentItem->childCount() > 0) {
456 destUrl = KUrl(archive_url->url().path(KUrl::AddTrailingSlash) + parentItem->data(0, Qt::UserRole).toString());
457 bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == "slideshows";
458 for (int j = 0; j < parentItem->childCount(); j++) {
459 item = parentItem->child(j);
460 KUrl src(item->text(0));
463 dest = KUrl(destUrl.path(KUrl::AddTrailingSlash) + item->data(0, Qt::UserRole).toString() + "/" + src.fileName());
465 else if (item->data(0, Qt::UserRole).isNull()) {
466 dest.addPath(src.fileName());
469 dest.addPath(item->data(0, Qt::UserRole).toString());
471 m_replacementList.insert(src, dest);
476 // process kdenlive producers
477 QDomElement mlt = m_doc.documentElement();
478 QString root = mlt.attribute("root") + "/";
479 mlt.setAttribute("root", archive_url->url().path(KUrl::RemoveTrailingSlash));
480 QDomNodeList prods = mlt.elementsByTagName("kdenlive_producer");
481 for (int i = 0; i < prods.count(); i++) {
482 QDomElement e = prods.item(i).toElement();
483 if (e.isNull()) continue;
484 if (e.hasAttribute("resource")) {
485 KUrl src(e.attribute("resource"));
486 KUrl dest = m_replacementList.value(src);
487 if (!dest.isEmpty()) e.setAttribute("resource", dest.path());
489 if (e.hasAttribute("proxy") && e.attribute("proxy") != "-") {
490 KUrl src(e.attribute("proxy"));
491 KUrl dest = m_replacementList.value(src);
492 if (!dest.isEmpty()) e.setAttribute("proxy", dest.path());
496 // process mlt producers
497 prods = mlt.elementsByTagName("producer");
498 for (int i = 0; i < prods.count(); i++) {
499 QDomElement e = prods.item(i).toElement();
500 if (e.isNull()) continue;
501 QString src = EffectsList::property(e, "resource");
502 if (!src.isEmpty()) {
503 if (!src.startsWith('/')) src.prepend(root);
505 KUrl dest = m_replacementList.value(src);
506 if (!dest.isEmpty()) EffectsList::setProperty(e, "resource", dest.path());
510 // process mlt transitions (for luma files)
511 prods = mlt.elementsByTagName("transition");
513 for (int i = 0; i < prods.count(); i++) {
514 QDomElement e = prods.item(i).toElement();
515 if (e.isNull()) continue;
516 attribute = "resource";
517 QString src = EffectsList::property(e, attribute);
518 if (src.isEmpty()) attribute = "luma";
519 src = EffectsList::property(e, attribute);
520 if (!src.isEmpty()) {
521 if (!src.startsWith('/')) src.prepend(root);
523 KUrl dest = m_replacementList.value(src);
524 if (!dest.isEmpty()) EffectsList::setProperty(e, attribute, dest.path());
528 bool isArchive = compressed_archive->isChecked();
530 m_temp = new KTemporaryFile;
531 if (!m_temp->open()) KMessageBox::error(this, i18n("Cannot create temporary file"));
532 m_temp->write(m_doc.toString().toUtf8());
534 m_archiveThread = QtConcurrent::run(this, &ArchiveWidget::createArchive);
538 QString path = archive_url->url().path(KUrl::AddTrailingSlash) + m_name + ".kdenlive";
540 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
541 kWarning() << "////// ERROR writing to file: " << path;
542 KMessageBox::error(this, i18n("Cannot write to file %1", path));
546 file.write(m_doc.toString().toUtf8());
547 if (file.error() != QFile::NoError) {
548 KMessageBox::error(this, i18n("Cannot write to file %1", path));
556 void ArchiveWidget::createArchive()
558 QFileInfo dirInfo(archive_url->url().path());
559 QString user = dirInfo.owner();
560 QString group = dirInfo.group();
561 KTar archive(archive_url->url().path(KUrl::AddTrailingSlash) + m_name + ".tar.gz", "application/x-gzip");
562 archive.open( QIODevice::WriteOnly );
565 foreach(const QString &path, m_foldersList) {
566 archive.writeDir(path, user, group);
571 QMapIterator<QString, QString> i(m_filesList);
572 while (i.hasNext()) {
574 archive.addLocalFile(i.key(), i.value());
575 emit archiveProgress((int) 100 * ix / m_filesList.count());
580 archive.addLocalFile(m_temp->fileName(), m_name + ".kdenlive");
581 bool result = archive.close();
583 emit archivingFinished(result);
586 void ArchiveWidget::slotArchivingFinished(bool result)
589 icon_info->setPixmap(KIcon("dialog-ok").pixmap(16, 16));
590 text_info->setText(i18n("Project was successfully archived."));
593 icon_info->setPixmap(KIcon("dialog-close").pixmap(16, 16));
594 text_info->setText(i18n("There was an error processing project file"));
596 progressBar->setValue(100);
597 buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive"));
598 archive_url->setEnabled(true);
599 compressed_archive->setEnabled(true);
600 for (int i = 0; i < files_list->topLevelItemCount(); i++) {
601 files_list->topLevelItem(i)->setDisabled(false);
602 for (int j = 0; j < files_list->topLevelItem(i)->childCount(); j++)
603 files_list->topLevelItem(i)->child(j)->setDisabled(false);
607 void ArchiveWidget::slotArchivingProgress(int p)
609 progressBar->setValue(p);