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)));
55 QTreeWidgetItem *videos = new QTreeWidgetItem(files_list, QStringList() << i18n("Video clips"));
56 videos->setIcon(0, KIcon("video-x-generic"));
57 videos->setData(0, Qt::UserRole, "videos");
58 videos->setExpanded(false);
59 QTreeWidgetItem *sounds = new QTreeWidgetItem(files_list, QStringList() << i18n("Audio clips"));
60 sounds->setIcon(0, KIcon("audio-x-generic"));
61 sounds->setData(0, Qt::UserRole, "sounds");
62 sounds->setExpanded(false);
63 QTreeWidgetItem *images = new QTreeWidgetItem(files_list, QStringList() << i18n("Image clips"));
64 images->setIcon(0, KIcon("image-x-generic"));
65 images->setData(0, Qt::UserRole, "images");
66 images->setExpanded(false);
67 QTreeWidgetItem *slideshows = new QTreeWidgetItem(files_list, QStringList() << i18n("Slideshow clips"));
68 slideshows->setIcon(0, KIcon("image-x-generic"));
69 slideshows->setData(0, Qt::UserRole, "slideshows");
70 slideshows->setExpanded(false);
71 QTreeWidgetItem *texts = new QTreeWidgetItem(files_list, QStringList() << i18n("Text clips"));
72 texts->setIcon(0, KIcon("text-plain"));
73 texts->setData(0, Qt::UserRole, "texts");
74 texts->setExpanded(false);
75 QTreeWidgetItem *others = new QTreeWidgetItem(files_list, QStringList() << i18n("Other clips"));
76 others->setIcon(0, KIcon("unknown"));
77 others->setData(0, Qt::UserRole, "others");
78 others->setExpanded(false);
79 QTreeWidgetItem *lumas = new QTreeWidgetItem(files_list, QStringList() << i18n("Luma files"));
80 lumas->setIcon(0, KIcon("image-x-generic"));
81 lumas->setData(0, Qt::UserRole, "lumas");
82 lumas->setExpanded(false);
84 QTreeWidgetItem *proxies = new QTreeWidgetItem(files_list, QStringList() << i18n("Proxy clips"));
85 proxies->setIcon(0, KIcon("video-x-generic"));
86 proxies->setData(0, Qt::UserRole, "proxy");
87 proxies->setExpanded(false);
92 QStringList fileNames;
93 generateItems(lumas, luma_list);
95 QStringList slideUrls;
96 QStringList audioUrls;
97 QStringList videoUrls;
98 QStringList imageUrls;
99 QStringList otherUrls;
100 QStringList proxyUrls;
102 for (int i = 0; i < list.count(); i++) {
103 DocClipBase *clip = list.at(i);
104 CLIPTYPE t = clip->clipType();
105 if (t == SLIDESHOW) {
106 KUrl slideUrl = clip->fileURL();
107 //TODO: Slideshow files
108 slideUrls << slideUrl.path();
110 else if (t == IMAGE) imageUrls << clip->fileURL().path();
111 else if (t == TEXT) {
112 QStringList imagefiles = TitleWidget::extractImageList(clip->getProperty("xmldata"));
113 QStringList fonts = TitleWidget::extractFontList(clip->getProperty("xmldata"));
114 imageUrls << imagefiles;
116 } else if (t == PLAYLIST) {
117 QStringList files = ProjectSettings::extractPlaylistUrls(clip->fileURL().path());
120 else if (!clip->fileURL().isEmpty()) {
121 if (t == AUDIO) audioUrls << clip->fileURL().path();
123 videoUrls << clip->fileURL().path();
124 // Check if we have a proxy
125 QString proxy = clip->getProperty("proxy");
126 if (!proxy.isEmpty() && proxy != "-" && QFile::exists(proxy)) proxyUrls << proxy;
131 generateItems(sounds, audioUrls);
132 generateItems(videos, videoUrls);
133 generateItems(images, imageUrls);
134 generateItems(slideshows, slideUrls);
135 generateItems(others, otherUrls);
136 generateItems(proxies, proxyUrls);
138 #if QT_VERSION >= 0x040500
139 allFonts.removeDuplicates();
144 // Hide unused categories, add item count
146 for (int i = 0; i < files_list->topLevelItemCount(); i++) {
147 QTreeWidgetItem *parentItem = files_list->topLevelItem(i);
148 int items = parentItem->childCount();
150 files_list->topLevelItem(i)->setHidden(true);
153 if (parentItem->data(0, Qt::UserRole).toString() == "slideshows")
155 // Special case: slideshows contain several files
156 for (int j = 0; j < items; j++) {
157 total += parentItem->child(j)->data(0, Qt::UserRole + 1).toStringList().count();
161 parentItem->setText(0, files_list->topLevelItem(i)->text(0) + " " + i18np("(%1 item)", "(%1 items)", items));
165 compressed_archive->setText(compressed_archive->text() + " (" + m_name + ".tar.gz)");
166 project_files->setText(i18np("%1 file to archive, requires %2", "%1 files to archive, requires %2", total, KIO::convertSize(m_requestedSize)));
167 buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive"));
168 connect(buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), this, SLOT(slotStartArchiving()));
169 buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
174 ArchiveWidget::~ArchiveWidget()
178 void ArchiveWidget::done ( int r )
180 if (closeAccepted()) QDialog::done(r);
183 void ArchiveWidget::closeEvent ( QCloseEvent * e )
186 if (closeAccepted()) e->accept();
191 bool ArchiveWidget::closeAccepted()
193 if (!archive_url->isEnabled()) {
194 // Archiving in progress, should we stop?
195 if (KMessageBox::warningContinueCancel(this, i18n("Archiving in progress, do you want to stop it?"), i18n("Stop Archiving"), KGuiItem(i18n("Stop Archiving"))) != KMessageBox::Continue) {
198 if (m_copyJob) m_copyJob->kill();
204 void ArchiveWidget::generateItems(QTreeWidgetItem *parentItem, QStringList items)
206 QStringList filesList;
209 bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == "slideshows";
210 foreach(const QString & file, items) {
211 QTreeWidgetItem *item = new QTreeWidgetItem(parentItem, QStringList() << file);
212 fileName = KUrl(file).fileName();
214 // we store each slideshow in a separate subdirectory
215 item->setData(0, Qt::UserRole, ix);
218 QDir dir(slideUrl.directory(KUrl::AppendTrailingSlash));
219 if (slideUrl.fileName().startsWith(".all.")) {
220 // mimetype slideshow (for example *.png)
223 // TODO: improve jpeg image detection with extension like jpeg, requires change in MLT image producers
224 filters << "*." + slideUrl.fileName().section('.', -1);
225 dir.setNameFilters(filters);
226 QFileInfoList resultList = dir.entryInfoList(QDir::Files);
227 QStringList slideImages;
228 for (int i = 0; i < resultList.count(); i++) {
229 m_requestedSize += resultList.at(i).size();
230 slideImages << resultList.at(i).absoluteFilePath();
232 item->setData(0, Qt::UserRole + 1, slideImages);
235 // pattern url (like clip%.3d.png)
236 QStringList result = dir.entryList(QDir::Files);
237 QString filter = slideUrl.fileName();
238 QString ext = filter.section('.', -1);
239 filter = filter.section('%', 0, -2);
240 QString regexp = "^" + filter + "\\d+\\." + ext + "$";
242 QStringList slideImages;
243 QString directory = dir.absolutePath();
244 if (!directory.endsWith('/')) directory.append('/');
245 foreach(const QString & path, result) {
246 if (rx.exactMatch(path)) {
247 m_requestedSize += QFileInfo(directory + path).size();
248 slideImages << directory + path;
251 item->setData(0, Qt::UserRole + 1, slideImages);
254 else if (filesList.contains(fileName)) {
255 // we have 2 files with same name
257 QString newFileName = fileName.section('.', 0, -2) + "_" + QString::number(ix) + "." + fileName.section('.', -1);
258 while (filesList.contains(newFileName)) {
260 newFileName = fileName.section('.', 0, -2) + "_" + QString::number(ix) + "." + fileName.section('.', -1);
262 fileName = newFileName;
263 item->setData(0, Qt::UserRole, fileName);
266 m_requestedSize += QFileInfo(file).size();
267 filesList << fileName;
272 void ArchiveWidget::slotCheckSpace()
274 KDiskFreeSpaceInfo inf = KDiskFreeSpaceInfo::freeSpaceInfo( archive_url->url().path());
275 KIO::filesize_t freeSize = inf.available();;
276 if (freeSize > m_requestedSize) {
278 buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
279 icon_info->setPixmap(KIcon("dialog-ok").pixmap(16, 16));
280 text_info->setText(i18n("Available space on drive: %1", KIO::convertSize(freeSize)));
283 buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
284 icon_info->setPixmap(KIcon("dialog-close").pixmap(16, 16));
285 text_info->setText(i18n("Not enough space on drive, free space: %1", KIO::convertSize(freeSize)));
289 bool ArchiveWidget::slotStartArchiving(bool firstPass)
291 if (firstPass && (m_copyJob || m_archiveThread.isRunning())) {
292 // archiving in progress, abort
293 if (m_copyJob) m_copyJob->kill(KJob::EmitResult);
294 m_abortArchive = true;
297 bool isArchive = compressed_archive->isChecked();
298 if (!firstPass) m_copyJob = NULL;
301 m_abortArchive = false;
302 m_duplicateFiles.clear();
303 m_replacementList.clear();
304 m_foldersList.clear();
306 icon_info->setPixmap(KIcon("system-run").pixmap(16, 16));
307 text_info->setText(i18n("Archiving..."));
309 archive_url->setEnabled(false);
310 compressed_archive->setEnabled(false);
311 m_progressTimer = new QTimer(this);
312 connect(m_progressTimer, SIGNAL(timeout()), this, SLOT(updateProgress()));
313 m_progressTimer->setSingleShot(false);
314 m_progressTimer->setInterval(700);
319 QTreeWidgetItem *parentItem;
320 bool isSlideshow = false;
321 for (int i = 0; i < files_list->topLevelItemCount(); i++) {
322 parentItem = files_list->topLevelItem(i);
323 if (parentItem->childCount() > 0 && !parentItem->isDisabled()) {
324 if (parentItem->data(0, Qt::UserRole).toString() == "slideshows") {
325 KUrl slideFolder(archive_url->url().path(KUrl::AddTrailingSlash) + "slideshows");
326 if (isArchive) m_foldersList.append("slideshows");
327 else KIO::NetAccess::mkdir(slideFolder, this);
330 files_list->setCurrentItem(parentItem);
331 if (!isSlideshow) parentItem->setDisabled(true);
332 destPath = parentItem->data(0, Qt::UserRole).toString() + "/";
333 destUrl = KUrl(archive_url->url().path(KUrl::AddTrailingSlash) + destPath);
334 QTreeWidgetItem *item;
335 for (int j = 0; j < parentItem->childCount(); j++) {
336 item = parentItem->child(j);
337 // Special case: slideshows
339 if (item->isDisabled()) {
342 destPath.append(item->data(0, Qt::UserRole).toString() + "/");
343 destUrl = KUrl(archive_url->url().path(KUrl::AddTrailingSlash) + destPath);
344 QStringList srcFiles = item->data(0, Qt::UserRole + 1).toStringList();
345 for (int k = 0; k < srcFiles.count(); k++) {
346 files << KUrl(srcFiles.at(k));
348 item->setDisabled(true);
349 if (parentItem->indexOfChild(item) == parentItem->childCount() - 1) {
350 // We have processed all slideshows
351 parentItem->setDisabled(true);
355 else if (item->data(0, Qt::UserRole).isNull()) {
356 files << KUrl(item->text(0));
359 // We must rename the destination file, since another file with same name exists
360 //TODO: monitor progress
362 m_filesList.insert(item->text(0), destPath + item->data(0, Qt::UserRole).toString());
364 else m_duplicateFiles.insert(KUrl(item->text(0)), KUrl(destUrl.path(KUrl::AddTrailingSlash) + item->data(0, Qt::UserRole).toString()));
371 if (destPath.isEmpty()) {
372 if (m_duplicateFiles.isEmpty()) return false;
373 QMapIterator<KUrl, KUrl> i(m_duplicateFiles);
376 KUrl startJobSrc = i.key();
377 KUrl startJobDst = i.value();
378 m_duplicateFiles.remove(startJobSrc);
379 KIO::CopyJob *job = KIO::copyAs(startJobSrc, startJobDst, KIO::HideProgressInfo);
380 connect(job, SIGNAL(result(KJob *)), this, SLOT(slotArchivingFinished(KJob *)));
381 connect(job, SIGNAL(processedSize(KJob *, qulonglong)), this, SLOT(slotArchivingProgress(KJob *, qulonglong)));
387 m_foldersList.append(destPath);
388 for (int i = 0; i < files.count(); i++) {
389 m_filesList.insert(files.at(i).path(), destPath + files.at(i).fileName());
391 slotArchivingFinished();
393 else if (files.isEmpty()) {
394 slotStartArchiving(false);
397 KIO::NetAccess::mkdir(destUrl, this);
398 m_copyJob = KIO::copy (files, destUrl, KIO::HideProgressInfo);
399 connect(m_copyJob, SIGNAL(result(KJob *)), this, SLOT(slotArchivingFinished(KJob *)));
400 connect(m_copyJob, SIGNAL(processedSize(KJob *, qulonglong)), this, SLOT(slotArchivingProgress(KJob *, qulonglong)));
403 progressBar->setValue(0);
404 buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Abort"));
409 void ArchiveWidget::slotArchivingFinished(KJob *job)
411 if (job == NULL || job->error() == 0) {
412 if (slotStartArchiving(false)) {
413 // We still have files to archive
416 else if (!compressed_archive->isChecked()) {
417 // Archiving finished
418 progressBar->setValue(100);
419 if (processProjectFile()) {
420 icon_info->setPixmap(KIcon("dialog-ok").pixmap(16, 16));
421 text_info->setText(i18n("Project was successfully archived."));
424 icon_info->setPixmap(KIcon("dialog-close").pixmap(16, 16));
425 text_info->setText(i18n("There was an error processing project file"));
427 } else processProjectFile();
431 icon_info->setPixmap(KIcon("dialog-close").pixmap(16, 16));
432 text_info->setText(i18n("There was an error while copying the files: %1", job->errorString()));
434 if (!compressed_archive->isChecked()) {
435 buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive"));
436 archive_url->setEnabled(true);
437 compressed_archive->setEnabled(true);
438 for (int i = 0; i < files_list->topLevelItemCount(); i++) {
439 files_list->topLevelItem(i)->setDisabled(false);
440 for (int j = 0; j < files_list->topLevelItem(i)->childCount(); j++)
441 files_list->topLevelItem(i)->child(j)->setDisabled(false);
446 void ArchiveWidget::slotArchivingProgress(KJob *, qulonglong size)
448 progressBar->setValue((int) 100 * size / m_requestedSize);
452 bool ArchiveWidget::processProjectFile()
455 QTreeWidgetItem *item;
456 for (int i = 0; i < files_list->topLevelItemCount(); i++) {
457 QTreeWidgetItem *parentItem = files_list->topLevelItem(i);
458 if (parentItem->childCount() > 0) {
459 destUrl = KUrl(archive_url->url().path(KUrl::AddTrailingSlash) + parentItem->data(0, Qt::UserRole).toString());
460 bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == "slideshows";
461 for (int j = 0; j < parentItem->childCount(); j++) {
462 item = parentItem->child(j);
463 KUrl src(item->text(0));
466 dest = KUrl(destUrl.path(KUrl::AddTrailingSlash) + item->data(0, Qt::UserRole).toString() + "/" + src.fileName());
468 else if (item->data(0, Qt::UserRole).isNull()) {
469 dest.addPath(src.fileName());
472 dest.addPath(item->data(0, Qt::UserRole).toString());
474 m_replacementList.insert(src, dest);
479 // process kdenlive producers
480 QDomElement mlt = m_doc.documentElement();
481 QString root = mlt.attribute("root") + "/";
482 mlt.setAttribute("root", archive_url->url().path(KUrl::RemoveTrailingSlash));
483 QDomNodeList prods = mlt.elementsByTagName("kdenlive_producer");
484 for (int i = 0; i < prods.count(); i++) {
485 QDomElement e = prods.item(i).toElement();
486 if (e.isNull()) continue;
487 if (e.hasAttribute("resource")) {
488 KUrl src(e.attribute("resource"));
489 KUrl dest = m_replacementList.value(src);
490 if (!dest.isEmpty()) e.setAttribute("resource", dest.path());
492 if (e.hasAttribute("proxy") && e.attribute("proxy") != "-") {
493 KUrl src(e.attribute("proxy"));
494 KUrl dest = m_replacementList.value(src);
495 if (!dest.isEmpty()) e.setAttribute("proxy", dest.path());
499 // process mlt producers
500 prods = mlt.elementsByTagName("producer");
501 for (int i = 0; i < prods.count(); i++) {
502 QDomElement e = prods.item(i).toElement();
503 if (e.isNull()) continue;
504 QString src = EffectsList::property(e, "resource");
505 if (!src.isEmpty()) {
506 if (!src.startsWith('/')) src.prepend(root);
508 KUrl dest = m_replacementList.value(src);
509 if (!dest.isEmpty()) EffectsList::setProperty(e, "resource", dest.path());
513 // process mlt transitions (for luma files)
514 prods = mlt.elementsByTagName("transition");
516 for (int i = 0; i < prods.count(); i++) {
517 QDomElement e = prods.item(i).toElement();
518 if (e.isNull()) continue;
519 attribute = "resource";
520 QString src = EffectsList::property(e, attribute);
521 if (src.isEmpty()) attribute = "luma";
522 src = EffectsList::property(e, attribute);
523 if (!src.isEmpty()) {
524 if (!src.startsWith('/')) src.prepend(root);
526 KUrl dest = m_replacementList.value(src);
527 if (!dest.isEmpty()) EffectsList::setProperty(e, attribute, dest.path());
531 bool isArchive = compressed_archive->isChecked();
533 m_temp = new KTemporaryFile;
534 if (!m_temp->open()) KMessageBox::error(this, i18n("Cannot create temporary file"));
535 m_temp->write(m_doc.toString().toUtf8());
537 m_archiveThread = QtConcurrent::run(this, &ArchiveWidget::createArchive);
538 m_progressTimer->start();
542 QString path = archive_url->url().path(KUrl::AddTrailingSlash) + m_name + ".kdenlive";
544 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
545 kWarning() << "////// ERROR writing to file: " << path;
546 KMessageBox::error(this, i18n("Cannot write to file %1", path));
550 file.write(m_doc.toString().toUtf8());
551 if (file.error() != QFile::NoError) {
552 KMessageBox::error(this, i18n("Cannot write to file %1", path));
560 void ArchiveWidget::createArchive()
562 QFileInfo dirInfo(archive_url->url().path());
563 QString user = dirInfo.owner();
564 QString group = dirInfo.group();
565 KTar archive(archive_url->url().path(KUrl::AddTrailingSlash) + m_name + ".tar.gz");
566 archive.open( QIODevice::WriteOnly );
567 kDebug()<<"ARCHIVE: "<<archive.device();
569 foreach(const QString &path, m_foldersList) {
570 archive.writeDir(path, user, group);
574 QMapIterator<QString, QString> i(m_filesList);
575 while (i.hasNext()) {
577 archive.addLocalFile(i.key(), i.value());
581 archive.addLocalFile(m_temp->fileName(), m_name + ".kdenlive");
582 bool result = archive.close();
584 emit archivingFinished(result);
587 void ArchiveWidget::slotArchivingFinished(bool result)
590 icon_info->setPixmap(KIcon("dialog-ok").pixmap(16, 16));
591 text_info->setText(i18n("Project was successfully archived."));
594 icon_info->setPixmap(KIcon("dialog-close").pixmap(16, 16));
595 text_info->setText(i18n("There was an error processing project file"));
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);
605 m_progressTimer->stop();
608 void ArchiveWidget::updateProgress()