]> git.sesse.net Git - kdenlive/blob - src/archivewidget.cpp
Fix archive job never finishing when no external clips to archive
[kdenlive] / src / archivewidget.cpp
1 /***************************************************************************
2  *   Copyright (C) 2011 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
21 #include "archivewidget.h"
22 #include "titlewidget.h"
23
24 #include <KLocale>
25 #include <KDiskFreeSpaceInfo>
26 #include <KUrlRequester>
27 #include <KFileDialog>
28 #include <KMessageBox>
29 #include <KGuiItem>
30 #include <KIO/NetAccess>
31 #include <KTar>
32 #include <KDebug>
33 #include <KApplication>
34 #include <kio/directorysizejob.h>
35 #if KDE_IS_VERSION(4,7,0)
36 #include <KMessageWidget>
37 #endif
38
39 #include <QTreeWidget>
40 #include <QtConcurrentRun>
41 #include "projectsettings.h"
42
43
44 ArchiveWidget::ArchiveWidget(QString projectName, QDomDocument doc, QList <DocClipBase*> list, QStringList luma_list, QWidget * parent) :
45         QDialog(parent),
46         m_requestedSize(0),
47         m_copyJob(NULL),
48         m_name(projectName.section('.', 0, -2)),
49         m_doc(doc),
50         m_abortArchive(false),
51         m_extractMode(false),
52         m_extractArchive(NULL),
53         m_missingClips(0)
54 {
55     setAttribute(Qt::WA_DeleteOnClose);
56     setupUi(this);
57     setWindowTitle(i18n("Archive Project"));
58     archive_url->setUrl(KUrl(QDir::homePath()));
59     connect(archive_url, SIGNAL(textChanged (const QString &)), this, SLOT(slotCheckSpace()));
60     connect(this, SIGNAL(archivingFinished(bool)), this, SLOT(slotArchivingFinished(bool)));
61     connect(this, SIGNAL(archiveProgress(int)), this, SLOT(slotArchivingProgress(int)));
62     connect(proxy_only, SIGNAL(stateChanged(int)), this, SLOT(slotProxyOnly(int)));
63
64     // Setup categories
65     QTreeWidgetItem *videos = new QTreeWidgetItem(files_list, QStringList() << i18n("Video clips"));
66     videos->setIcon(0, KIcon("video-x-generic"));
67     videos->setData(0, Qt::UserRole, "videos");
68     videos->setExpanded(false);
69     QTreeWidgetItem *sounds = new QTreeWidgetItem(files_list, QStringList() << i18n("Audio clips"));
70     sounds->setIcon(0, KIcon("audio-x-generic"));
71     sounds->setData(0, Qt::UserRole, "sounds");
72     sounds->setExpanded(false);
73     QTreeWidgetItem *images = new QTreeWidgetItem(files_list, QStringList() << i18n("Image clips"));
74     images->setIcon(0, KIcon("image-x-generic"));
75     images->setData(0, Qt::UserRole, "images");
76     images->setExpanded(false);
77     QTreeWidgetItem *slideshows = new QTreeWidgetItem(files_list, QStringList() << i18n("Slideshow clips"));
78     slideshows->setIcon(0, KIcon("image-x-generic"));
79     slideshows->setData(0, Qt::UserRole, "slideshows");
80     slideshows->setExpanded(false);
81     QTreeWidgetItem *texts = new QTreeWidgetItem(files_list, QStringList() << i18n("Text clips"));
82     texts->setIcon(0, KIcon("text-plain"));
83     texts->setData(0, Qt::UserRole, "texts");
84     texts->setExpanded(false);
85     QTreeWidgetItem *playlists = new QTreeWidgetItem(files_list, QStringList() << i18n("Playlist clips"));
86     playlists->setIcon(0, KIcon("video-mlt-playlist"));
87     playlists->setData(0, Qt::UserRole, "playlist");
88     playlists->setExpanded(false);
89     QTreeWidgetItem *others = new QTreeWidgetItem(files_list, QStringList() << i18n("Other clips"));
90     others->setIcon(0, KIcon("unknown"));
91     others->setData(0, Qt::UserRole, "others");
92     others->setExpanded(false);
93     QTreeWidgetItem *lumas = new QTreeWidgetItem(files_list, QStringList() << i18n("Luma files"));
94     lumas->setIcon(0, KIcon("image-x-generic"));
95     lumas->setData(0, Qt::UserRole, "lumas");
96     lumas->setExpanded(false);
97     
98     QTreeWidgetItem *proxies = new QTreeWidgetItem(files_list, QStringList() << i18n("Proxy clips"));
99     proxies->setIcon(0, KIcon("video-x-generic"));
100     proxies->setData(0, Qt::UserRole, "proxy");
101     proxies->setExpanded(false);
102     
103     // process all files
104     QStringList allFonts;
105     KUrl::List fileUrls;
106     QStringList fileNames;
107     QStringList extraImageUrls;
108     QStringList otherUrls;
109     generateItems(lumas, luma_list);
110
111     QMap <QString, QString> slideUrls;
112     QMap <QString, QString> audioUrls;
113     QMap <QString, QString>videoUrls;
114     QMap <QString, QString>imageUrls;
115     QMap <QString, QString>playlistUrls;
116     QMap <QString, QString>proxyUrls;
117
118     for (int i = 0; i < list.count(); i++) {
119         DocClipBase *clip = list.at(i);
120         CLIPTYPE t = clip->clipType();
121         QString id = clip->getId();
122         if (t == SLIDESHOW) {
123             KUrl slideUrl = clip->fileURL();
124             //TODO: Slideshow files
125             slideUrls.insert(id, slideUrl.path());
126         }
127         else if (t == IMAGE) imageUrls.insert(id, clip->fileURL().path());
128         else if (t == TEXT) {
129             QStringList imagefiles = TitleWidget::extractImageList(clip->getProperty("xmldata"));
130             QStringList fonts = TitleWidget::extractFontList(clip->getProperty("xmldata"));
131             extraImageUrls << imagefiles;
132             allFonts << fonts;
133         } else if (t == PLAYLIST) {
134             playlistUrls.insert(id, clip->fileURL().path());
135             QStringList files = ProjectSettings::extractPlaylistUrls(clip->fileURL().path());
136             otherUrls << files;
137         }
138         else if (!clip->fileURL().isEmpty()) {
139             if (t == AUDIO) audioUrls.insert(id, clip->fileURL().path());
140             else {
141                 videoUrls.insert(id, clip->fileURL().path());
142                 // Check if we have a proxy
143                 QString proxy = clip->getProperty("proxy");
144                 if (!proxy.isEmpty() && proxy != "-" && QFile::exists(proxy)) proxyUrls.insert(id, proxy);
145             }
146         }
147     }
148
149     generateItems(images, extraImageUrls);
150     generateItems(sounds, audioUrls);
151     generateItems(videos, videoUrls);
152     generateItems(images, imageUrls);
153     generateItems(slideshows, slideUrls);
154     generateItems(playlists, playlistUrls);
155     generateItems(others, otherUrls);
156     generateItems(proxies, proxyUrls);
157     
158     allFonts.removeDuplicates();
159
160 #if KDE_IS_VERSION(4,7,0)
161         m_infoMessage = new KMessageWidget(this);
162         QVBoxLayout *s =  static_cast <QVBoxLayout*> (layout());
163         s->insertWidget(5, m_infoMessage);
164         m_infoMessage->setCloseButtonVisible(false);
165         m_infoMessage->setWordWrap(true);
166         m_infoMessage->hide();
167 #endif
168         
169         // missing clips, warn user
170     if (m_missingClips > 0) {
171         QString infoText = i18np("You have %1 missing clip in your project.", "You have %1 missing clips in your project.", m_missingClips);
172 #if KDE_IS_VERSION(4,7,0)
173         m_infoMessage->setMessageType(KMessageWidget::Warning);
174         m_infoMessage->setText(infoText);
175         m_infoMessage->animatedShow();
176 #else
177         KMessageBox::sorry(this, infoText);
178 #endif
179     }
180
181     //TODO: fonts
182
183     // Hide unused categories, add item count
184     int total = 0;
185     for (int i = 0; i < files_list->topLevelItemCount(); i++) {
186         QTreeWidgetItem *parentItem = files_list->topLevelItem(i);
187         int items = parentItem->childCount();
188         if (items == 0) {
189             files_list->topLevelItem(i)->setHidden(true);
190         }
191         else {
192             if (parentItem->data(0, Qt::UserRole).toString() == "slideshows")
193             {
194                 // Special case: slideshows contain several files
195                 for (int j = 0; j < items; j++) {
196                     total += parentItem->child(j)->data(0, Qt::UserRole + 1).toStringList().count();
197                 }
198             }
199             else total += items;
200             parentItem->setText(0, files_list->topLevelItem(i)->text(0) + " " + i18np("(%1 item)", "(%1 items)", items));
201         }
202     }
203     if (m_name.isEmpty()) m_name = i18n("Untitled");
204     compressed_archive->setText(compressed_archive->text() + " (" + m_name + ".tar.gz)");
205     project_files->setText(i18np("%1 file to archive, requires %2", "%1 files to archive, requires %2", total, KIO::convertSize(m_requestedSize)));
206     buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive"));
207     connect(buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), this, SLOT(slotStartArchiving()));
208     buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
209
210     slotCheckSpace();
211 }
212
213 // Constructor for extract widget
214 ArchiveWidget::ArchiveWidget(const KUrl &url, QWidget * parent):
215     QDialog(parent),
216     m_extractMode(true),
217     m_extractUrl(url)
218 {
219     //setAttribute(Qt::WA_DeleteOnClose);
220
221     setupUi(this);
222     connect(this, SIGNAL(extractingFinished()), this, SLOT(slotExtractingFinished()));
223     connect(this, SIGNAL(showMessage(const QString &, const QString &)), this, SLOT(slotDisplayMessage(const QString &, const QString &)));
224     
225     compressed_archive->setHidden(true);
226     proxy_only->setHidden(true);
227     project_files->setHidden(true);
228     files_list->setHidden(true);
229     label->setText(i18n("Extract to"));
230     setWindowTitle(i18n("Open Archived Project"));
231     archive_url->setUrl(KUrl(QDir::homePath()));
232     buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Extract"));
233     connect(buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), this, SLOT(slotStartExtracting()));
234     buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
235     adjustSize();
236     m_archiveThread = QtConcurrent::run(this, &ArchiveWidget::openArchiveForExtraction);
237 }
238
239
240 ArchiveWidget::~ArchiveWidget()
241 {
242     if (m_extractArchive) delete m_extractArchive;
243 }
244
245 void ArchiveWidget::slotDisplayMessage(const QString &icon, const QString &text)
246 {    
247     icon_info->setPixmap(KIcon(icon).pixmap(16, 16));
248     text_info->setText(text);
249 }
250
251 void ArchiveWidget::slotJobResult(bool success, const QString &text)
252 {
253 #if KDE_IS_VERSION(4,7,0)
254     m_infoMessage->setMessageType(success ? KMessageWidget::Positive : KMessageWidget::Warning);
255     m_infoMessage->setText(text);
256     m_infoMessage->animatedShow();
257 #else
258     if (success) icon_info->setPixmap(KIcon("dialog-ok").pixmap(16, 16));
259     else icon_info->setPixmap(KIcon("dialog-close").pixmap(16, 16));
260     text_info->setText(text);
261 #endif
262 }
263
264 void ArchiveWidget::openArchiveForExtraction()
265 {
266     emit showMessage("system-run", i18n("Opening archive..."));
267     m_extractArchive = new KTar(m_extractUrl.path());
268     if (!m_extractArchive->isOpen() && !m_extractArchive->open( QIODevice::ReadOnly )) {
269         emit showMessage("dialog-close", i18n("Cannot open archive file:\n %1", m_extractUrl.path()));
270         groupBox->setEnabled(false);
271         return;
272     }
273
274     // Check that it is a kdenlive project archive
275     bool isProjectArchive = false;
276     QStringList files = m_extractArchive->directory()->entries();
277     for (int i = 0; i < files.count(); i++) {
278         if (files.at(i).endsWith(".kdenlive")) {
279             m_projectName = files.at(i);
280             isProjectArchive = true;
281             break;
282         }
283     }
284
285     if (!isProjectArchive) {
286         emit showMessage("dialog-close", i18n("File %1\n is not an archived Kdenlive project", m_extractUrl.path()));
287         groupBox->setEnabled(false);
288         return;
289     }
290     buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
291     emit showMessage("dialog-ok", i18n("Ready"));
292 }
293
294 void ArchiveWidget::done ( int r )
295 {
296     if (closeAccepted()) QDialog::done(r);
297 }
298
299 void ArchiveWidget::closeEvent ( QCloseEvent * e )
300 {
301
302     if (closeAccepted()) e->accept();
303     else e->ignore();
304 }
305
306
307 bool ArchiveWidget::closeAccepted()
308 {
309     if (!m_extractMode && !archive_url->isEnabled()) {
310         // Archiving in progress, should we stop?
311         if (KMessageBox::warningContinueCancel(this, i18n("Archiving in progress, do you want to stop it?"), i18n("Stop Archiving"), KGuiItem(i18n("Stop Archiving"))) != KMessageBox::Continue) {
312             return false;
313         }
314         if (m_copyJob) m_copyJob->kill();
315     }
316     return true;
317 }
318
319
320 void ArchiveWidget::generateItems(QTreeWidgetItem *parentItem, QStringList items)
321 {
322     QStringList filesList;
323     QString fileName;
324     int ix = 0;
325     bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == "slideshows";
326     foreach(const QString & file, items) {
327         QTreeWidgetItem *item = new QTreeWidgetItem(parentItem, QStringList() << file);
328         fileName = KUrl(file).fileName();
329         if (isSlideshow) {
330             // we store each slideshow in a separate subdirectory
331             item->setData(0, Qt::UserRole, ix);
332             ix++;
333             KUrl slideUrl(file);
334             QDir dir(slideUrl.directory(KUrl::AppendTrailingSlash));
335             if (slideUrl.fileName().startsWith(".all.")) {
336                 // mimetype slideshow (for example *.png)
337                     QStringList filters;
338                     QString extension;
339                     // TODO: improve jpeg image detection with extension like jpeg, requires change in MLT image producers
340                     filters << "*." + slideUrl.fileName().section('.', -1);
341                     dir.setNameFilters(filters);
342                     QFileInfoList resultList = dir.entryInfoList(QDir::Files);
343                     QStringList slideImages;
344                     qint64 totalSize = 0;
345                     for (int i = 0; i < resultList.count(); i++) {
346                         totalSize += resultList.at(i).size();
347                         slideImages << resultList.at(i).absoluteFilePath();
348                     }
349                     item->setData(0, Qt::UserRole + 1, slideImages);
350                     item->setData(0, Qt::UserRole + 3, totalSize);
351                     m_requestedSize += totalSize;
352             }
353             else {
354                 // pattern url (like clip%.3d.png)
355                 QStringList result = dir.entryList(QDir::Files);
356                 QString filter = slideUrl.fileName();
357                 QString ext = filter.section('.', -1);
358                 filter = filter.section('%', 0, -2);
359                 QString regexp = "^" + filter + "\\d+\\." + ext + "$";
360                 QRegExp rx(regexp);
361                 QStringList slideImages;
362                 QString directory = dir.absolutePath();
363                 if (!directory.endsWith('/')) directory.append('/');
364                 qint64 totalSize = 0;
365                 foreach(const QString & path, result) {
366                     if (rx.exactMatch(path)) {
367                         totalSize += QFileInfo(directory + path).size();
368                         slideImages <<  directory + path;
369                     }
370                 }
371                 item->setData(0, Qt::UserRole + 1, slideImages);
372                 item->setData(0, Qt::UserRole + 3, totalSize);
373                 m_requestedSize += totalSize;
374             }                    
375         }
376         else if (filesList.contains(fileName)) {
377             // we have 2 files with same name
378             int ix = 0;
379             QString newFileName = fileName.section('.', 0, -2) + "_" + QString::number(ix) + "." + fileName.section('.', -1);
380             while (filesList.contains(newFileName)) {
381                 ix ++;
382                 newFileName = fileName.section('.', 0, -2) + "_" + QString::number(ix) + "." + fileName.section('.', -1);
383             }
384             fileName = newFileName;
385             item->setData(0, Qt::UserRole, fileName);
386         }
387         if (!isSlideshow) {
388             qint64 fileSize = QFileInfo(file).size();
389             if (fileSize <= 0) {
390                 item->setIcon(0, KIcon("edit-delete"));
391                 m_missingClips++;
392             }
393             else {
394                 m_requestedSize += fileSize;
395                 item->setData(0, Qt::UserRole + 3, fileSize);
396             }
397             filesList << fileName;
398         }
399     }
400 }
401
402 void ArchiveWidget::generateItems(QTreeWidgetItem *parentItem, QMap <QString, QString> items)
403 {
404     QStringList filesList;
405     QString fileName;
406     int ix = 0;
407     bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == "slideshows";
408     QMap<QString, QString>::const_iterator it = items.constBegin();
409     while (it != items.constEnd()) {
410         QString file = it.value();
411         QTreeWidgetItem *item = new QTreeWidgetItem(parentItem, QStringList() << file);
412         // Store the clip's id
413         item->setData(0, Qt::UserRole + 2, it.key());
414         fileName = KUrl(file).fileName();
415         if (isSlideshow) {
416             // we store each slideshow in a separate subdirectory
417             item->setData(0, Qt::UserRole, ix);
418             ix++;
419             KUrl slideUrl(file);
420             QDir dir(slideUrl.directory(KUrl::AppendTrailingSlash));
421             if (slideUrl.fileName().startsWith(".all.")) {
422                 // mimetype slideshow (for example *.png)
423                     QStringList filters;
424                     QString extension;
425                     // TODO: improve jpeg image detection with extension like jpeg, requires change in MLT image producers
426                     filters << "*." + slideUrl.fileName().section('.', -1);
427                     dir.setNameFilters(filters);
428                     QFileInfoList resultList = dir.entryInfoList(QDir::Files);
429                     QStringList slideImages;
430                     qint64 totalSize = 0;
431                     for (int i = 0; i < resultList.count(); i++) {
432                         totalSize += resultList.at(i).size();
433                         slideImages << resultList.at(i).absoluteFilePath();
434                     }
435                     item->setData(0, Qt::UserRole + 1, slideImages);
436                     item->setData(0, Qt::UserRole + 3, totalSize);
437                     m_requestedSize += totalSize;
438             }
439             else {
440                 // pattern url (like clip%.3d.png)
441                 QStringList result = dir.entryList(QDir::Files);
442                 QString filter = slideUrl.fileName();
443                 QString ext = filter.section('.', -1);
444                 filter = filter.section('%', 0, -2);
445                 QString regexp = "^" + filter + "\\d+\\." + ext + "$";
446                 QRegExp rx(regexp);
447                 QStringList slideImages;
448                 qint64 totalSize = 0;
449                 QString directory = dir.absolutePath();
450                 if (!directory.endsWith('/')) directory.append('/');
451                 foreach(const QString & path, result) {
452                     if (rx.exactMatch(path)) {
453                         totalSize += QFileInfo(directory + path).size();
454                         slideImages <<  directory + path;
455                     }
456                 }
457                 item->setData(0, Qt::UserRole + 1, slideImages);
458                 item->setData(0, Qt::UserRole + 3, totalSize);
459                 m_requestedSize += totalSize;
460             }                    
461         }
462         else if (filesList.contains(fileName)) {
463             // we have 2 files with same name
464             int ix = 0;
465             QString newFileName = fileName.section('.', 0, -2) + "_" + QString::number(ix) + "." + fileName.section('.', -1);
466             while (filesList.contains(newFileName)) {
467                 ix ++;
468                 newFileName = fileName.section('.', 0, -2) + "_" + QString::number(ix) + "." + fileName.section('.', -1);
469             }
470             fileName = newFileName;
471             item->setData(0, Qt::UserRole, fileName);
472         }
473         if (!isSlideshow) {
474             qint64 fileSize = QFileInfo(file).size();
475             if (fileSize <= 0) {
476                 item->setIcon(0, KIcon("edit-delete"));
477                 m_missingClips++;
478             }
479             else {
480                 m_requestedSize += fileSize;
481                 item->setData(0, Qt::UserRole + 3, fileSize);
482             }
483             filesList << fileName;
484         }
485         ++it;
486     }
487 }
488
489 void ArchiveWidget::slotCheckSpace()
490 {
491     KDiskFreeSpaceInfo inf = KDiskFreeSpaceInfo::freeSpaceInfo( archive_url->url().path());
492     KIO::filesize_t freeSize = inf.available();
493     if (freeSize > m_requestedSize) {
494         // everything is ok
495         buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
496         slotDisplayMessage("dialog-ok", i18n("Available space on drive: %1", KIO::convertSize(freeSize)));
497     }
498     else {
499         buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
500         slotDisplayMessage("dialog-close", i18n("Not enough space on drive, free space: %1", KIO::convertSize(freeSize)));
501     }
502 }
503
504 bool ArchiveWidget::slotStartArchiving(bool firstPass)
505 {
506     if (firstPass && (m_copyJob || m_archiveThread.isRunning())) {
507         // archiving in progress, abort
508         if (m_copyJob) m_copyJob->kill(KJob::EmitResult);
509         m_abortArchive = true;
510         return true;
511     }
512     bool isArchive = compressed_archive->isChecked();
513     if (!firstPass) m_copyJob = NULL;
514     else {
515         //starting archiving
516         m_abortArchive = false;
517         m_duplicateFiles.clear();
518         m_replacementList.clear();
519         m_foldersList.clear();
520         m_filesList.clear();
521         slotDisplayMessage("system-run", i18n("Archiving..."));
522         repaint();
523         archive_url->setEnabled(false);
524         proxy_only->setEnabled(false);
525         compressed_archive->setEnabled(false);
526     }
527     KUrl::List files;
528     KUrl destUrl;
529     QString destPath;
530     QTreeWidgetItem *parentItem;
531     bool isSlideshow = false;
532     int items = 0;
533     
534     // We parse all files going into one folder, then start the copy job
535     
536     for (int i = 0; i < files_list->topLevelItemCount(); i++) {
537         parentItem = files_list->topLevelItem(i);
538         if (parentItem->isDisabled()) {
539             parentItem->setExpanded(false);
540             continue;
541         }
542         if (parentItem->childCount() > 0) {
543             if (parentItem->data(0, Qt::UserRole).toString() == "slideshows") {
544                 KUrl slideFolder(archive_url->url().path(KUrl::AddTrailingSlash) + "slideshows");
545                 if (isArchive) m_foldersList.append("slideshows");
546                 else KIO::NetAccess::mkdir(slideFolder, this);
547                 isSlideshow = true;
548             }
549             else isSlideshow = false;
550             files_list->setCurrentItem(parentItem);
551             parentItem->setExpanded(true);
552             destPath = parentItem->data(0, Qt::UserRole).toString() + "/";
553             destUrl = KUrl(archive_url->url().path(KUrl::AddTrailingSlash) + destPath);
554             QTreeWidgetItem *item;
555             for (int j = 0; j < parentItem->childCount(); j++) {
556                 item = parentItem->child(j);
557                 if (item->isDisabled()) continue;
558                 // Special case: slideshows
559                 items++;
560                 if (isSlideshow) {
561                     destPath += item->data(0, Qt::UserRole).toString() + "/";
562                     destUrl = KUrl(archive_url->url().path(KUrl::AddTrailingSlash) + destPath);
563                     QStringList srcFiles = item->data(0, Qt::UserRole + 1).toStringList();
564                     for (int k = 0; k < srcFiles.count(); k++) {
565                         files << KUrl(srcFiles.at(k));
566                     }
567                     item->setDisabled(true);
568                     if (parentItem->indexOfChild(item) == parentItem->childCount() - 1) {
569                         // We have processed all slideshows
570                         parentItem->setDisabled(true);
571                     }
572                     break;
573                 }
574                 else if (item->data(0, Qt::UserRole).isNull()) {
575                     files << KUrl(item->text(0));
576                 }
577                 else {
578                     // We must rename the destination file, since another file with same name exists
579                     //TODO: monitor progress
580                     if (isArchive) {
581                         m_filesList.insert(item->text(0), destPath + item->data(0, Qt::UserRole).toString());
582                     }
583                     else m_duplicateFiles.insert(KUrl(item->text(0)), KUrl(destUrl.path(KUrl::AddTrailingSlash) + item->data(0, Qt::UserRole).toString()));
584                 }
585             }
586             if (!isSlideshow) parentItem->setDisabled(true);
587             break;
588         }
589     }
590
591     if (items == 0) {
592         // No clips to archive
593         if (isArchive) slotArchivingFinished(NULL, true);
594         return true;
595     }
596     
597     if (destPath.isEmpty()) {
598         if (m_duplicateFiles.isEmpty()) return false;        
599         QMapIterator<KUrl, KUrl> i(m_duplicateFiles);
600         if (i.hasNext()) {
601             i.next();
602             KUrl startJobSrc = i.key();
603             KUrl startJobDst = i.value();
604             m_duplicateFiles.remove(startJobSrc);
605             KIO::CopyJob *job = KIO::copyAs(startJobSrc, startJobDst, KIO::HideProgressInfo);
606             connect(job, SIGNAL(result(KJob *)), this, SLOT(slotArchivingFinished(KJob *)));
607             connect(job, SIGNAL(processedSize(KJob *, qulonglong)), this, SLOT(slotArchivingProgress(KJob *, qulonglong)));
608         }
609         return true;
610     }
611
612     if (isArchive) {
613         m_foldersList.append(destPath);
614         for (int i = 0; i < files.count(); i++) {
615             m_filesList.insert(files.at(i).path(), destPath + files.at(i).fileName());
616         }
617         slotArchivingFinished();
618     }
619     else if (files.isEmpty()) {
620         slotStartArchiving(false);
621     }
622     else {
623         KIO::NetAccess::mkdir(destUrl, this);
624         m_copyJob = KIO::copy (files, destUrl, KIO::HideProgressInfo);
625         connect(m_copyJob, SIGNAL(result(KJob *)), this, SLOT(slotArchivingFinished(KJob *)));
626         connect(m_copyJob, SIGNAL(processedSize(KJob *, qulonglong)), this, SLOT(slotArchivingProgress(KJob *, qulonglong)));
627     }
628     if (firstPass) {
629         progressBar->setValue(0);
630         buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Abort"));
631     }
632     return true;
633 }
634
635 void ArchiveWidget::slotArchivingFinished(KJob *job, bool finished)
636 {
637     if (job == NULL || job->error() == 0) {
638         if (!finished && slotStartArchiving(false)) {
639             // We still have files to archive
640             return;
641         }
642         else if (!compressed_archive->isChecked()) {
643             // Archiving finished
644             progressBar->setValue(100);
645             if (processProjectFile()) {
646                 slotJobResult(true, i18n("Project was successfully archived."));
647             }
648             else {
649                 slotJobResult(false, i18n("There was an error processing project file"));
650             }
651         } else processProjectFile();
652     }
653     else {
654         m_copyJob = NULL;
655         slotJobResult(false, i18n("There was an error while copying the files: %1", job->errorString()));
656     }
657     if (!compressed_archive->isChecked()) {
658         buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive"));
659         archive_url->setEnabled(true);
660         proxy_only->setEnabled(true);
661         compressed_archive->setEnabled(true);
662         for (int i = 0; i < files_list->topLevelItemCount(); i++) {
663             files_list->topLevelItem(i)->setDisabled(false);
664             for (int j = 0; j < files_list->topLevelItem(i)->childCount(); j++)
665                 files_list->topLevelItem(i)->child(j)->setDisabled(false);        
666         }
667     }
668 }
669
670 void ArchiveWidget::slotArchivingProgress(KJob *, qulonglong size)
671 {
672     progressBar->setValue((int) 100 * size / m_requestedSize);
673 }
674
675
676 bool ArchiveWidget::processProjectFile()
677 {
678     KUrl destUrl;
679     QTreeWidgetItem *item;
680     bool isArchive = compressed_archive->isChecked();
681
682     for (int i = 0; i < files_list->topLevelItemCount(); i++) {
683         QTreeWidgetItem *parentItem = files_list->topLevelItem(i);
684         if (parentItem->childCount() > 0) {
685             destUrl = KUrl(archive_url->url().path(KUrl::AddTrailingSlash) + parentItem->data(0, Qt::UserRole).toString());
686             bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == "slideshows";
687             for (int j = 0; j < parentItem->childCount(); j++) {
688                 item = parentItem->child(j);
689                 KUrl src(item->text(0));
690                 KUrl dest = destUrl;
691                 if (isSlideshow) {
692                     dest = KUrl(destUrl.path(KUrl::AddTrailingSlash) + item->data(0, Qt::UserRole).toString() + "/" + src.fileName());
693                 }
694                 else if (item->data(0, Qt::UserRole).isNull()) {
695                     dest.addPath(src.fileName());
696                 }
697                 else {
698                     dest.addPath(item->data(0, Qt::UserRole).toString());
699                 }
700                 m_replacementList.insert(src, dest);
701             }
702         }
703     }
704     
705     QDomElement mlt = m_doc.documentElement();
706     QString root = mlt.attribute("root") + "/";
707
708     // Adjust global settings
709     QString basePath;
710     if (isArchive) basePath = "$CURRENTPATH";
711     else basePath = archive_url->url().path(KUrl::RemoveTrailingSlash);
712     mlt.setAttribute("root", basePath);
713     QDomElement project = mlt.firstChildElement("kdenlivedoc");
714     project.setAttribute("projectfolder", basePath);
715
716     // process kdenlive producers
717     QDomNodeList prods = mlt.elementsByTagName("kdenlive_producer");
718     for (int i = 0; i < prods.count(); i++) {
719         QDomElement e = prods.item(i).toElement();
720         if (e.isNull()) continue;
721         if (e.hasAttribute("resource")) {
722             KUrl src(e.attribute("resource"));
723             KUrl dest = m_replacementList.value(src);
724             if (!dest.isEmpty()) e.setAttribute("resource", dest.path());
725         }
726         if (e.hasAttribute("proxy") && e.attribute("proxy") != "-") {
727             KUrl src(e.attribute("proxy"));
728             KUrl dest = m_replacementList.value(src);
729             if (!dest.isEmpty()) e.setAttribute("proxy", dest.path());
730         }
731     }
732
733     // process mlt producers
734     prods = mlt.elementsByTagName("producer");
735     for (int i = 0; i < prods.count(); i++) {
736         QDomElement e = prods.item(i).toElement();
737         if (e.isNull()) continue;
738         QString src = EffectsList::property(e, "resource");
739         if (!src.isEmpty()) {
740             if (!src.startsWith('/')) src.prepend(root);
741             KUrl srcUrl(src);
742             KUrl dest = m_replacementList.value(src);
743             if (!dest.isEmpty()) EffectsList::setProperty(e, "resource", dest.path());
744         }
745     }
746
747     // process mlt transitions (for luma files)
748     prods = mlt.elementsByTagName("transition");
749     QString attribute;
750     for (int i = 0; i < prods.count(); i++) {
751         QDomElement e = prods.item(i).toElement();
752         if (e.isNull()) continue;
753         attribute = "resource";
754         QString src = EffectsList::property(e, attribute);
755         if (src.isEmpty()) attribute = "luma";
756         src = EffectsList::property(e, attribute);
757         if (!src.isEmpty()) {
758             if (!src.startsWith('/')) src.prepend(root);
759             KUrl srcUrl(src);
760             KUrl dest = m_replacementList.value(src);
761             if (!dest.isEmpty()) EffectsList::setProperty(e, attribute, dest.path());
762         }
763     }
764
765     QString playList = m_doc.toString();
766     if (isArchive) {
767         QString startString("\"");
768         startString.append(archive_url->url().path(KUrl::RemoveTrailingSlash));
769         QString endString("\"");
770         endString.append(basePath);
771         playList.replace(startString, endString);
772         startString = ">" + archive_url->url().path(KUrl::RemoveTrailingSlash);
773         endString = ">" + basePath;
774         playList.replace(startString, endString);
775     }
776
777     if (isArchive) {
778         m_temp = new KTemporaryFile;
779         if (!m_temp->open()) KMessageBox::error(this, i18n("Cannot create temporary file"));
780         m_temp->write(playList.toUtf8());
781         m_temp->close();
782         m_archiveThread = QtConcurrent::run(this, &ArchiveWidget::createArchive);
783         return true;
784     }
785     
786     QString path = archive_url->url().path(KUrl::AddTrailingSlash) + m_name + ".kdenlive";
787     QFile file(path);
788     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
789         kWarning() << "//////  ERROR writing to file: " << path;
790         KMessageBox::error(this, i18n("Cannot write to file %1", path));
791         return false;
792     }
793
794     file.write(m_doc.toString().toUtf8());
795     if (file.error() != QFile::NoError) {
796         KMessageBox::error(this, i18n("Cannot write to file %1", path));
797         file.close();
798         return false;
799     }
800     file.close();
801     return true;
802 }
803
804 void ArchiveWidget::createArchive()
805 {
806     QFileInfo dirInfo(archive_url->url().path());
807     QString user = dirInfo.owner();
808     QString group = dirInfo.group();
809     KTar archive(archive_url->url().path(KUrl::AddTrailingSlash) + m_name + ".tar.gz", "application/x-gzip");
810     archive.open( QIODevice::WriteOnly );
811
812     // Create folders
813     foreach(const QString &path, m_foldersList) {
814         archive.writeDir(path, user, group);
815     }
816
817     // Add files
818     int ix = 0;
819     QMapIterator<QString, QString> i(m_filesList);
820     while (i.hasNext()) {
821         i.next();
822         archive.addLocalFile(i.key(), i.value());
823         emit archiveProgress((int) 100 * ix / m_filesList.count());
824         ix++;
825     }
826
827     // Add project file
828     archive.addLocalFile(m_temp->fileName(), m_name + ".kdenlive");
829     bool result = archive.close();
830     delete m_temp;
831     emit archivingFinished(result);
832 }
833
834 void ArchiveWidget::slotArchivingFinished(bool result)
835 {
836     if (result) {
837         slotJobResult(true, i18n("Project was successfully archived."));
838         buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
839     }
840     else {
841         slotJobResult(false, i18n("There was an error processing project file"));
842     }
843     progressBar->setValue(100);
844     buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive"));
845     archive_url->setEnabled(true);
846     proxy_only->setEnabled(true);
847     compressed_archive->setEnabled(true);
848     for (int i = 0; i < files_list->topLevelItemCount(); i++) {
849         files_list->topLevelItem(i)->setDisabled(false);
850         for (int j = 0; j < files_list->topLevelItem(i)->childCount(); j++)
851             files_list->topLevelItem(i)->child(j)->setDisabled(false);
852     }
853 }
854
855 void ArchiveWidget::slotArchivingProgress(int p)
856 {
857     progressBar->setValue(p);
858 }
859
860 void ArchiveWidget::slotStartExtracting()
861 {
862     if (m_archiveThread.isRunning()) {
863         //TODO: abort extracting
864         return;
865     }
866     QFileInfo f(m_extractUrl.path());
867     m_requestedSize = f.size();
868     KIO::NetAccess::mkdir(archive_url->url().path(KUrl::RemoveTrailingSlash), this);
869     slotDisplayMessage("system-run", i18n("Extracting..."));
870     buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Abort"));
871     m_progressTimer = new QTimer;
872     m_progressTimer->setInterval(800);
873     m_progressTimer->setSingleShot(false);
874     connect(m_progressTimer, SIGNAL(timeout()), this, SLOT(slotExtractProgress()));
875     m_archiveThread = QtConcurrent::run(this, &ArchiveWidget::doExtracting);
876     m_progressTimer->start();
877 }
878
879 void ArchiveWidget::slotExtractProgress()
880 {
881     KIO::DirectorySizeJob *job = KIO::directorySize(archive_url->url());
882     connect(job, SIGNAL(result(KJob*)), this, SLOT(slotGotProgress(KJob*)));
883 }
884
885 void ArchiveWidget::slotGotProgress(KJob* job)
886 {
887     if (!job->error()) {
888         KIO::DirectorySizeJob *j = static_cast <KIO::DirectorySizeJob *>(job);
889         progressBar->setValue((int) 100 * j->totalSize() / m_requestedSize);
890     }
891     job->deleteLater();
892 }
893
894 void ArchiveWidget::doExtracting()
895 {
896     m_extractArchive->directory()->copyTo(archive_url->url().path(KUrl::AddTrailingSlash));
897     m_extractArchive->close();
898     emit extractingFinished();    
899 }
900
901 QString ArchiveWidget::extractedProjectFile()
902 {
903     return archive_url->url().path(KUrl::AddTrailingSlash) + m_projectName;
904 }
905
906 void ArchiveWidget::slotExtractingFinished()
907 {
908     m_progressTimer->stop();
909     delete m_progressTimer;
910     // Process project file
911     QFile file(extractedProjectFile());
912     bool error = false;
913     if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
914         error = true;
915     }
916     else {
917         QString playList = file.readAll();
918         file.close();
919         if (playList.isEmpty()) {
920             error = true;
921         }
922         else {
923             playList.replace("$CURRENTPATH", archive_url->url().path(KUrl::RemoveTrailingSlash));
924             if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
925                 kWarning() << "//////  ERROR writing to file: ";
926                 error = true;
927             }
928             else {
929                 file.write(playList.toUtf8());
930                 if (file.error() != QFile::NoError) {
931                     error = true;
932                 }
933                 file.close();
934             }
935         }
936     }
937     if (error) {
938         KMessageBox::sorry(kapp->activeWindow(), i18n("Cannot open project file %1", extractedProjectFile()), i18n("Cannot open file"));
939         reject();
940     }
941     else accept();
942 }
943
944 void ArchiveWidget::slotProxyOnly(int onlyProxy)
945 {
946     m_requestedSize = 0;
947     if (onlyProxy == Qt::Checked) {
948         // Archive proxy clips
949         QStringList proxyIdList;
950         QTreeWidgetItem *parentItem = NULL;
951
952         // Build list of existing proxy ids
953         for (int i = 0; i < files_list->topLevelItemCount(); i++) {
954             parentItem = files_list->topLevelItem(i);
955             if (parentItem->data(0, Qt::UserRole).toString() == "proxy") break;
956         }
957         if (!parentItem) return;
958         int items = parentItem->childCount();
959         for (int j = 0; j < items; j++) {
960             proxyIdList << parentItem->child(j)->data(0, Qt::UserRole + 2).toString();
961         }
962         
963         // Parse all items to disable original clips for existing proxies
964         for (int i = 0; i < proxyIdList.count(); i++) {
965             QString id = proxyIdList.at(i);
966             if (id.isEmpty()) continue;
967             for (int j = 0; j < files_list->topLevelItemCount(); j++) {
968                 parentItem = files_list->topLevelItem(j);
969                 if (parentItem->data(0, Qt::UserRole).toString() == "proxy") continue;
970                 items = parentItem->childCount();
971                 for (int k = 0; k < items; k++) {
972                     if (parentItem->child(k)->data(0, Qt::UserRole + 2).toString() == id) {
973                         // This item has a proxy, do not archive it
974                         parentItem->child(k)->setFlags(Qt::ItemIsSelectable);
975                         break;
976                     }
977                 }
978             }
979         }
980     }
981     else {
982         // Archive all clips
983         for (int i = 0; i < files_list->topLevelItemCount(); i++) {
984             QTreeWidgetItem *parentItem = files_list->topLevelItem(i);
985             int items = parentItem->childCount();
986             for (int j = 0; j < items; j++) {
987                 parentItem->child(j)->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
988             }
989         }
990     }
991     
992     // Calculate requested size
993     int total = 0;
994     for (int i = 0; i < files_list->topLevelItemCount(); i++) {
995         QTreeWidgetItem *parentItem = files_list->topLevelItem(i);
996         int items = parentItem->childCount();
997         int itemsCount = 0;
998         bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == "slideshows";
999         
1000         for (int j = 0; j < items; j++) {
1001             if (!parentItem->child(j)->isDisabled()) {
1002                 m_requestedSize += parentItem->child(j)->data(0, Qt::UserRole + 3).toInt();
1003                 if (isSlideshow) total += parentItem->child(j)->data(0, Qt::UserRole + 1).toStringList().count();
1004                 else total ++;
1005                 itemsCount ++;
1006             }
1007         }
1008         parentItem->setText(0, parentItem->text(0).section("(", 0, 0) + i18np("(%1 item)", "(%1 items)", itemsCount));
1009     }
1010     project_files->setText(i18np("%1 file to archive, requires %2", "%1 files to archive, requires %2", total, KIO::convertSize(m_requestedSize)));
1011     slotCheckSpace();
1012 }
1013