]> git.sesse.net Git - kdenlive/blob - src/archivewidget.cpp
Several small fixes for project archiving
[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
32 #include <KDebug>
33 #include <QTreeWidget>
34 #include "projectsettings.h"
35
36
37 ArchiveWidget::ArchiveWidget(QDomDocument doc, QList <DocClipBase*> list, QStringList luma_list, QWidget * parent) :
38         QDialog(parent),
39         m_requestedSize(0),
40         m_copyJob(NULL),
41         m_doc(doc)
42 {
43     setAttribute(Qt::WA_DeleteOnClose);
44     setupUi(this);
45     setWindowTitle(i18n("Archive Project"));
46     archive_url->setUrl(KUrl(QDir::homePath()));
47     connect(archive_url, SIGNAL(textChanged (const QString &)), this, SLOT(slotCheckSpace()));
48
49     // Setup categories
50     QTreeWidgetItem *videos = new QTreeWidgetItem(files_list, QStringList() << i18n("Video clips"));
51     videos->setIcon(0, KIcon("video-x-generic"));
52     videos->setData(0, Qt::UserRole, "videos");
53     videos->setExpanded(false);
54     QTreeWidgetItem *sounds = new QTreeWidgetItem(files_list, QStringList() << i18n("Audio clips"));
55     sounds->setIcon(0, KIcon("audio-x-generic"));
56     sounds->setData(0, Qt::UserRole, "sounds");
57     sounds->setExpanded(false);
58     QTreeWidgetItem *images = new QTreeWidgetItem(files_list, QStringList() << i18n("Image clips"));
59     images->setIcon(0, KIcon("image-x-generic"));
60     images->setData(0, Qt::UserRole, "images");
61     images->setExpanded(false);
62     QTreeWidgetItem *slideshows = new QTreeWidgetItem(files_list, QStringList() << i18n("Slideshow clips"));
63     slideshows->setIcon(0, KIcon("image-x-generic"));
64     slideshows->setData(0, Qt::UserRole, "slideshows");
65     slideshows->setExpanded(false);
66     QTreeWidgetItem *texts = new QTreeWidgetItem(files_list, QStringList() << i18n("Text clips"));
67     texts->setIcon(0, KIcon("text-plain"));
68     texts->setData(0, Qt::UserRole, "texts");
69     texts->setExpanded(false);
70     QTreeWidgetItem *others = new QTreeWidgetItem(files_list, QStringList() << i18n("Other clips"));
71     others->setIcon(0, KIcon("unknown"));
72     others->setData(0, Qt::UserRole, "others");
73     others->setExpanded(false);
74     QTreeWidgetItem *lumas = new QTreeWidgetItem(files_list, QStringList() << i18n("Luma files"));
75     lumas->setIcon(0, KIcon("image-x-generic"));
76     lumas->setData(0, Qt::UserRole, "lumas");
77     lumas->setExpanded(false);
78     
79     QTreeWidgetItem *proxies = new QTreeWidgetItem(files_list, QStringList() << i18n("Proxy clips"));
80     proxies->setIcon(0, KIcon("video-x-generic"));
81     proxies->setData(0, Qt::UserRole, "proxy");
82     proxies->setExpanded(false);
83     
84     // process all files
85     QStringList allFonts;
86     KUrl::List fileUrls;
87     QStringList fileNames;
88     generateItems(lumas, luma_list);
89
90     QStringList slideUrls;
91     QStringList audioUrls;
92     QStringList videoUrls;
93     QStringList imageUrls;
94     QStringList otherUrls;
95     QStringList proxyUrls;
96
97     for (int i = 0; i < list.count(); i++) {
98         DocClipBase *clip = list.at(i);
99         CLIPTYPE t = clip->clipType();
100         if (t == SLIDESHOW) {
101             KUrl slideUrl = clip->fileURL();
102             //TODO: Slideshow files
103             slideUrls << slideUrl.path();
104         }
105         else if (t == IMAGE) imageUrls << clip->fileURL().path();
106         else if (t == TEXT) {
107             QStringList imagefiles = TitleWidget::extractImageList(clip->getProperty("xmldata"));
108             QStringList fonts = TitleWidget::extractFontList(clip->getProperty("xmldata"));
109             imageUrls << imagefiles;
110             allFonts << fonts;
111         } else if (t == PLAYLIST) {
112             QStringList files = ProjectSettings::extractPlaylistUrls(clip->fileURL().path());
113             otherUrls << files;
114         }
115         else if (!clip->fileURL().isEmpty()) {
116             if (t == AUDIO) audioUrls << clip->fileURL().path();
117             else {
118                 videoUrls << clip->fileURL().path();
119                 // Check if we have a proxy
120                 QString proxy = clip->getProperty("proxy");
121                 if (!proxy.isEmpty() && proxy != "-" && QFile::exists(proxy)) proxyUrls << proxy;
122             }
123         }
124     }
125
126     generateItems(sounds, audioUrls);
127     generateItems(videos, videoUrls);
128     generateItems(images, imageUrls);
129     generateItems(slideshows, slideUrls);
130     generateItems(others, otherUrls);
131     generateItems(proxies, proxyUrls);
132     
133 #if QT_VERSION >= 0x040500
134     allFonts.removeDuplicates();
135 #endif
136
137     //TODO: fonts
138
139     // Hide unused categories, add item count
140     int total = 0;
141     for (int i = 0; i < files_list->topLevelItemCount(); i++) {
142         QTreeWidgetItem *parentItem = files_list->topLevelItem(i);
143         int items = parentItem->childCount();
144         if (items == 0) {
145             files_list->topLevelItem(i)->setHidden(true);
146         }
147         else {
148             if (parentItem->data(0, Qt::UserRole).toString() == "slideshows")
149             {
150                 // Special case: slideshows contain several files
151                 for (int j = 0; j < items; j++) {
152                     total += parentItem->child(j)->data(0, Qt::UserRole + 1).toStringList().count();
153                 }
154             }
155             else total += items;
156             parentItem->setText(0, files_list->topLevelItem(i)->text(0) + " " + i18np("(%1 item)", "(%1 items)", items));
157         }
158     }
159     
160     project_files->setText(i18np("%1 file to archive, requires %2", "%1 files to archive, requires %2", total, KIO::convertSize(m_requestedSize)));
161     buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive"));
162     connect(buttonBox->button(QDialogButtonBox::Apply), SIGNAL(clicked()), this, SLOT(slotStartArchiving()));
163     buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
164     
165     slotCheckSpace();
166 }
167
168 ArchiveWidget::~ArchiveWidget()
169 {
170 }
171
172 void ArchiveWidget::done ( int r )
173 {
174     if (closeAccepted()) QDialog::done(r);
175 }
176
177 void ArchiveWidget::closeEvent ( QCloseEvent * e )
178 {
179
180     if (closeAccepted()) e->accept();
181     else e->ignore();
182 }
183
184
185 bool ArchiveWidget::closeAccepted()
186 {
187     if (!archive_url->isEnabled()) {
188         // Archiving in progress, should we stop?
189         if (KMessageBox::warningContinueCancel(this, i18n("Archiving in progress, do you want to stop it?"), i18n("Stop Archiving"), KGuiItem(i18n("Stop Archiving"))) != KMessageBox::Continue) {
190             return false;
191         }
192         if (m_copyJob) m_copyJob->kill();
193     }
194     return true;
195 }
196
197
198 void ArchiveWidget::generateItems(QTreeWidgetItem *parentItem, QStringList items)
199 {
200     QStringList filesList;
201     QString fileName;
202     int ix = 0;
203     bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == "slideshows";
204     foreach(const QString & file, items) {
205         QTreeWidgetItem *item = new QTreeWidgetItem(parentItem, QStringList() << file);
206         fileName = KUrl(file).fileName();
207         if (isSlideshow) {
208             // we store each slideshow in a separate subdirectory
209             item->setData(0, Qt::UserRole, ix);
210             ix++;
211             KUrl slideUrl(file);
212             QDir dir(slideUrl.directory(KUrl::AppendTrailingSlash));
213             if (slideUrl.fileName().startsWith(".all.")) {
214                 // mimetype slideshow (for example *.png)
215                     QStringList filters;
216                     QString extension;
217                     // TODO: improve jpeg image detection with extension like jpeg, requires change in MLT image producers
218                     filters << "*." + slideUrl.fileName().section('.', -1);
219                     dir.setNameFilters(filters);
220                     QFileInfoList resultList = dir.entryInfoList(QDir::Files);
221                     QStringList slideImages;
222                     for (int i = 0; i < resultList.count(); i++) {
223                         m_requestedSize += resultList.at(i).size();
224                         slideImages << resultList.at(i).absoluteFilePath();
225                     }
226                     item->setData(0, Qt::UserRole + 1, slideImages);
227             }
228             else {
229                 // pattern url (like clip%.3d.png)
230                 QStringList result = dir.entryList(QDir::Files);
231                 QString filter = slideUrl.fileName();
232                 QString ext = filter.section('.', -1);
233                 filter = filter.section('%', 0, -2);
234                 QString regexp = "^" + filter + "\\d+\\." + ext + "$";
235                 QRegExp rx(regexp);
236                 QStringList slideImages;
237                 QString directory = dir.absolutePath();
238                 if (!directory.endsWith('/')) directory.append('/');
239                 foreach(const QString & path, result) {
240                     if (rx.exactMatch(path)) {
241                         m_requestedSize += QFileInfo(directory + path).size();
242                         slideImages <<  directory + path;
243                     }
244                 }
245                 item->setData(0, Qt::UserRole + 1, slideImages);
246             }                    
247         }
248         else if (filesList.contains(fileName)) {
249             // we have 2 files with same name
250             int ix = 0;
251             QString newFileName = fileName.section('.', 0, -2) + "_" + QString::number(ix) + "." + fileName.section('.', -1);
252             while (filesList.contains(newFileName)) {
253                 ix ++;
254                 newFileName = fileName.section('.', 0, -2) + "_" + QString::number(ix) + "." + fileName.section('.', -1);
255             }
256             fileName = newFileName;
257             item->setData(0, Qt::UserRole, fileName);
258         }
259         if (!isSlideshow) {
260             m_requestedSize += QFileInfo(file).size();
261             filesList << fileName;
262         }
263     }
264 }
265
266 void ArchiveWidget::slotCheckSpace()
267 {
268     KDiskFreeSpaceInfo inf = KDiskFreeSpaceInfo::freeSpaceInfo( archive_url->url().path());
269     KIO::filesize_t freeSize = inf.available();;
270     if (freeSize > m_requestedSize) {
271         // everything is ok
272         buttonBox->button(QDialogButtonBox::Apply)->setEnabled(true);
273         icon_info->setPixmap(KIcon("dialog-ok").pixmap(16, 16));
274         text_info->setText(i18n("Available space on drive: %1", KIO::convertSize(freeSize)));
275     }
276     else {
277         buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
278         icon_info->setPixmap(KIcon("dialog-close").pixmap(16, 16));
279         text_info->setText(i18n("Not enough space on drive, free space: %1", KIO::convertSize(freeSize)));
280     }
281 }
282
283 bool ArchiveWidget::slotStartArchiving(bool firstPass)
284 {
285     if (firstPass && m_copyJob) {
286         // archiving in progress, abort
287         m_copyJob->kill(KJob::EmitResult);
288         return true;
289     }
290     if (!firstPass) m_copyJob = NULL;
291     else {
292         //starting archiving
293         m_duplicateFiles.clear();
294         m_replacementList.clear();
295         archive_url->setEnabled(false);
296     }
297     KUrl::List files;
298     KUrl destUrl;
299     QTreeWidgetItem *parentItem;
300     bool isSlideshow = false;
301     for (int i = 0; i < files_list->topLevelItemCount(); i++) {
302         parentItem = files_list->topLevelItem(i);
303         if (parentItem->childCount() > 0 && !parentItem->isDisabled()) {
304             if (parentItem->data(0, Qt::UserRole).toString() == "slideshows") {
305                 KIO::NetAccess::mkdir(KUrl(archive_url->url().path(KUrl::AddTrailingSlash) + "slideshows"), this);
306                 isSlideshow = true;
307             }
308             files_list->setCurrentItem(parentItem);
309             if (!isSlideshow) parentItem->setDisabled(true);
310             destUrl = KUrl(archive_url->url().path(KUrl::AddTrailingSlash) + parentItem->data(0, Qt::UserRole).toString());
311             QTreeWidgetItem *item;
312             for (int j = 0; j < parentItem->childCount(); j++) {
313                 item = parentItem->child(j);
314                 // Special case: slideshows
315                 if (isSlideshow) {
316                     if (item->isDisabled()) {
317                         continue;
318                     }
319                     destUrl = KUrl(destUrl.path(KUrl::AddTrailingSlash) + item->data(0, Qt::UserRole).toString() + "/");
320                     QStringList srcFiles = item->data(0, Qt::UserRole + 1).toStringList();
321                     for (int k = 0; k < srcFiles.count(); k++) {
322                         files << KUrl(srcFiles.at(k));
323                     }
324                     item->setDisabled(true);
325                     if (parentItem->indexOfChild(item) == parentItem->childCount() - 1) {
326                         // We have processed all slideshows
327                         parentItem->setDisabled(true);
328                     }
329                     break;
330                 }
331                 else if (item->data(0, Qt::UserRole).isNull()) {
332                     files << KUrl(item->text(0));
333                 }
334                 else {
335                     // We must rename the destination file, since another file with same name exists
336                     //TODO: monitor progress
337                     m_duplicateFiles.insert(KUrl(item->text(0)), KUrl(destUrl.path(KUrl::AddTrailingSlash) + item->data(0, Qt::UserRole).toString()));
338                 }
339             }
340             break;
341         }
342     }
343
344     if (destUrl.isEmpty()) {
345         if (m_duplicateFiles.isEmpty()) return false;        
346         QMapIterator<KUrl, KUrl> i(m_duplicateFiles);
347         if (i.hasNext()) {
348             i.next();
349             KUrl startJobSrc = i.key();
350             KUrl startJobDst = i.value();
351             m_duplicateFiles.remove(startJobSrc);
352             KIO::CopyJob *job = KIO::copyAs(startJobSrc, startJobDst, KIO::HideProgressInfo);
353             connect(job, SIGNAL(result(KJob *)), this, SLOT(slotArchivingFinished(KJob *)));
354             connect(job, SIGNAL(processedSize(KJob *, qulonglong)), this, SLOT(slotArchivingProgress(KJob *, qulonglong)));
355         }
356         return true;
357     }
358
359     if (files.isEmpty()) slotStartArchiving(false);
360     KIO::NetAccess::mkdir(destUrl, this);
361     m_copyJob = KIO::copy (files, destUrl, KIO::HideProgressInfo);
362     connect(m_copyJob, SIGNAL(result(KJob *)), this, SLOT(slotArchivingFinished(KJob *)));
363     connect(m_copyJob, SIGNAL(processedSize(KJob *, qulonglong)), this, SLOT(slotArchivingProgress(KJob *, qulonglong)));
364
365     if (firstPass) {
366         progressBar->setValue(0);
367         buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Abort"));
368     }
369     return true;
370 }
371
372 void ArchiveWidget::slotArchivingFinished(KJob *job)
373 {
374     if (job->error() == 0) {
375         if (slotStartArchiving(false)) {
376             // We still have files to archive
377             return;
378         }
379         else {
380             // Archiving finished
381             progressBar->setValue(100);
382             if (processProjectFile()) {
383                 icon_info->setPixmap(KIcon("dialog-ok").pixmap(16, 16));
384                 text_info->setText(i18n("Project was successfully archived."));
385             }
386             else {
387                 icon_info->setPixmap(KIcon("dialog-close").pixmap(16, 16));
388                 text_info->setText(i18n("There was an error processing project file"));
389             }
390         }
391     }
392     else {
393         m_copyJob = NULL;
394         icon_info->setPixmap(KIcon("dialog-close").pixmap(16, 16));
395         text_info->setText(i18n("There was an error while copying the files: %1", job->errorString()));
396     }
397     buttonBox->button(QDialogButtonBox::Apply)->setText(i18n("Archive"));
398     archive_url->setEnabled(true);
399     for (int i = 0; i < files_list->topLevelItemCount(); i++) {
400         files_list->topLevelItem(i)->setDisabled(false);
401         for (int j = 0; j < files_list->topLevelItem(i)->childCount(); j++)
402             files_list->topLevelItem(i)->child(j)->setDisabled(false);
403         
404     }
405 }
406
407 void ArchiveWidget::slotArchivingProgress(KJob *, qulonglong size)
408 {
409     progressBar->setValue((int) 100 * size / m_requestedSize);
410 }
411
412
413 bool ArchiveWidget::processProjectFile()
414 {
415     KUrl destUrl;
416     QTreeWidgetItem *item;
417     for (int i = 0; i < files_list->topLevelItemCount(); i++) {
418         QTreeWidgetItem *parentItem = files_list->topLevelItem(i);
419         if (parentItem->childCount() > 0) {
420             destUrl = KUrl(archive_url->url().path(KUrl::AddTrailingSlash) + parentItem->data(0, Qt::UserRole).toString());
421             bool isSlideshow = parentItem->data(0, Qt::UserRole).toString() == "slideshows";
422             for (int j = 0; j < parentItem->childCount(); j++) {
423                 item = parentItem->child(j);
424                 KUrl src(item->text(0));
425                 KUrl dest = destUrl;
426                 if (isSlideshow) {
427                     dest = KUrl(destUrl.path(KUrl::AddTrailingSlash) + item->data(0, Qt::UserRole).toString() + "/" + src.fileName());
428                 }
429                 else if (item->data(0, Qt::UserRole).isNull()) {
430                     dest.addPath(src.fileName());
431                 }
432                 else {
433                     dest.addPath(item->data(0, Qt::UserRole).toString());
434                 }
435                 m_replacementList.insert(src, dest);
436                 kDebug()<<"___ PROCESS ITEM :"<<src << "="<<dest;
437             }
438         }
439     }
440     
441     // process kdenlive producers           
442     QDomElement mlt = m_doc.documentElement();
443     QString root = mlt.attribute("root") + "/";
444     mlt.setAttribute("root", archive_url->url().path(KUrl::RemoveTrailingSlash));
445     QDomNodeList prods = mlt.elementsByTagName("kdenlive_producer");
446     for (int i = 0; i < prods.count(); i++) {
447         QDomElement e = prods.item(i).toElement();
448         if (e.isNull()) continue;
449         if (e.hasAttribute("resource")) {
450             KUrl src(e.attribute("resource"));
451             KUrl dest = m_replacementList.value(src);
452             if (!dest.isEmpty()) e.setAttribute("resource", dest.path());
453         }
454         if (e.hasAttribute("proxy") && e.attribute("proxy") != "-") {
455             KUrl src(e.attribute("proxy"));
456             KUrl dest = m_replacementList.value(src);
457             if (!dest.isEmpty()) e.setAttribute("proxy", dest.path());
458         }
459     }
460
461     // process mlt producers
462     prods = mlt.elementsByTagName("producer");
463     for (int i = 0; i < prods.count(); i++) {
464         QDomElement e = prods.item(i).toElement();
465         if (e.isNull()) continue;
466         QString src = EffectsList::property(e, "resource");
467         if (!src.isEmpty()) {
468             if (!src.startsWith('/')) src.prepend(root);
469             KUrl srcUrl(src);
470             KUrl dest = m_replacementList.value(src);
471             if (!dest.isEmpty()) EffectsList::setProperty(e, "resource", dest.path());
472         }
473     }
474
475     // process mlt transitions (for luma files)
476     prods = mlt.elementsByTagName("transition");
477     QString attribute;
478     for (int i = 0; i < prods.count(); i++) {
479         QDomElement e = prods.item(i).toElement();
480         if (e.isNull()) continue;
481         attribute = "resource";
482         QString src = EffectsList::property(e, attribute);
483         if (src.isEmpty()) attribute = "luma";
484         src = EffectsList::property(e, attribute);
485         if (!src.isEmpty()) {
486             if (!src.startsWith('/')) src.prepend(root);
487             KUrl srcUrl(src);
488             KUrl dest = m_replacementList.value(src);
489             if (!dest.isEmpty()) EffectsList::setProperty(e, attribute, dest.path());
490         }
491     }
492
493     QString path = archive_url->url().path(KUrl::AddTrailingSlash) + "project.kdenlive";
494     QFile file(path);
495     if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
496         kWarning() << "//////  ERROR writing to file: " << path;
497         KMessageBox::error(this, i18n("Cannot write to file %1", path));
498         return false;
499     }
500
501     file.write(m_doc.toString().toUtf8());
502     if (file.error() != QFile::NoError) {
503         KMessageBox::error(this, i18n("Cannot write to file %1", path));
504         file.close();
505         return false;
506     }
507     file.close();
508     return true;
509 }
510