]> git.sesse.net Git - kdenlive/blob - src/documentchecker.cpp
Fix crash on clip deletion, fix issues with placeholder clips
[kdenlive] / src / documentchecker.cpp
1 /***************************************************************************
2  *   Copyright (C) 2008 by Jean-Baptiste Mardelle (jb@kdenlive.org)        *
3  *                                                                         *
4  *   This program is free software; you can redistribute it and/or modify  *
5  *   it under the terms of the GNU General Public License as published by  *
6  *   the Free Software Foundation; either version 2 of the License, or     *
7  *   (at your option) any later version.                                   *
8  *                                                                         *
9  *   This program is distributed in the hope that it will be useful,       *
10  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
11  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
12  *   GNU General Public License for more details.                          *
13  *                                                                         *
14  *   You should have received a copy of the GNU General Public License     *
15  *   along with this program; if not, write to the                         *
16  *   Free Software Foundation, Inc.,                                       *
17  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA          *
18  ***************************************************************************/
19
20
21 #include "documentchecker.h"
22 #include "kthumb.h"
23 #include "docclipbase.h"
24 #include "titlewidget.h"
25 #include "definitions.h"
26 #include "kdenlivesettings.h"
27
28 #include <KDebug>
29 #include <KGlobalSettings>
30 #include <KFileItem>
31 #include <KIO/NetAccess>
32 #include <KFileDialog>
33 #include <KApplication>
34 #include <KUrlRequesterDialog>
35 #include <KMessageBox>
36
37 #include <QTreeWidgetItem>
38 #include <QFile>
39 #include <QHeaderView>
40 #include <QIcon>
41 #include <QPixmap>
42 #include <QTimer>
43 #include <QCryptographicHash>
44
45 const int hashRole = Qt::UserRole;
46 const int sizeRole = Qt::UserRole + 1;
47 const int idRole = Qt::UserRole + 2;
48 const int statusRole = Qt::UserRole + 3;
49 const int typeRole = Qt::UserRole + 4;
50 const int typeOriginalResource = Qt::UserRole + 5;
51
52 const int CLIPMISSING = 0;
53 const int CLIPOK = 1;
54 const int CLIPPLACEHOLDER = 2;
55 const int LUMAMISSING = 10;
56 const int LUMAOK = 11;
57 const int LUMAPLACEHOLDER = 12;
58
59 enum TITLECLIPTYPE { TITLE_IMAGE_ELEMENT = 20, TITLE_FONT_ELEMENT = 21 };
60
61 DocumentChecker::DocumentChecker(QDomNodeList infoproducers, QDomDocument doc):
62         m_info(infoproducers), m_doc(doc), m_dialog(NULL)
63 {
64
65 }
66
67
68 bool DocumentChecker::hasMissingClips()
69 {
70     int clipType;
71     QDomElement e;
72     QString id;
73     QString resource;
74     QList <QDomElement> missingClips;
75     for (int i = 0; i < m_info.count(); i++) {
76         e = m_info.item(i).toElement();
77         clipType = e.attribute("type").toInt();
78         if (clipType == COLOR) continue;
79         if (clipType == TEXT) {
80             //TODO: Check is clip template is missing (xmltemplate) or hash changed
81             QStringList images = TitleWidget::extractImageList(e.attribute("xmldata"));
82             QStringList fonts = TitleWidget::extractFontList(e.attribute("xmldata"));
83             checkMissingImages(missingClips, images, fonts, e.attribute("id"), e.attribute("name"));
84             continue;
85         }
86         id = e.attribute("id");
87         resource = e.attribute("resource");
88         if (clipType == SLIDESHOW) resource = KUrl(resource).directory();
89         if (!KIO::NetAccess::exists(KUrl(resource), KIO::NetAccess::SourceSide, 0)) {
90             // Missing clip found
91             missingClips.append(e);
92         } else {
93             // Check if the clip has changed
94             if (clipType != SLIDESHOW && e.hasAttribute("file_hash")) {
95                 if (e.attribute("file_hash") != DocClipBase::getHash(e.attribute("resource")))
96                     e.removeAttribute("file_hash");
97             }
98         }
99     }
100
101     QStringList missingLumas;
102     QDomNodeList trans = m_doc.elementsByTagName("transition");
103     for (int i = 0; i < trans.count(); i++) {
104         QString luma = getProperty(trans.at(i).toElement(), "luma");
105         if (!luma.isEmpty() && !QFile::exists(luma)) {
106             if (!missingLumas.contains(luma)) {
107                 missingLumas.append(luma);
108             }
109         }
110     }
111
112     if (missingClips.isEmpty() && missingLumas.isEmpty()) {
113         return false;
114     }
115     m_dialog = new QDialog();
116     m_dialog->setFont(KGlobalSettings::toolBarFont());
117     m_ui.setupUi(m_dialog);
118
119     foreach(const QString l, missingLumas) {
120         QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Luma file") << l);
121         item->setIcon(0, KIcon("dialog-close"));
122         item->setData(0, idRole, l);
123         item->setData(0, statusRole, LUMAMISSING);
124     }
125
126     m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
127     for (int i = 0; i < missingClips.count(); i++) {
128         e = missingClips.at(i).toElement();
129         QString clipType;
130         int t = e.attribute("type").toInt();
131         switch (t) {
132         case AV:
133             clipType = i18n("Video clip");
134             break;
135         case VIDEO:
136             clipType = i18n("Mute video clip");
137             break;
138         case AUDIO:
139             clipType = i18n("Audio clip");
140             break;
141         case PLAYLIST:
142             clipType = i18n("Playlist clip");
143             break;
144         case IMAGE:
145             clipType = i18n("Image clip");
146             break;
147         case SLIDESHOW:
148             clipType = i18n("Slideshow clip");
149             break;
150         case TITLE_IMAGE_ELEMENT:
151             clipType = i18n("Title Image");
152             break;
153         case TITLE_FONT_ELEMENT:
154             clipType = i18n("Title Font");
155             break;
156         default:
157             clipType = i18n("Video clip");
158         }
159         QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << clipType);
160         if (t == TITLE_IMAGE_ELEMENT) {
161             item->setIcon(0, KIcon("dialog-warning"));
162             item->setToolTip(1, e.attribute("name"));
163             item->setText(1, e.attribute("resource"));
164             item->setData(0, statusRole, CLIPPLACEHOLDER);
165             item->setData(0, typeOriginalResource, e.attribute("resource"));
166         } else if (t == TITLE_FONT_ELEMENT) {
167             item->setIcon(0, KIcon("dialog-warning"));
168             item->setToolTip(1, e.attribute("name"));
169             QString ft = e.attribute("resource");
170             QString newft = QFontInfo(QFont(ft)).family();
171             item->setText(1, i18n("%1, will be replaced by %2", ft, newft));
172             item->setData(0, statusRole, CLIPPLACEHOLDER);
173         } else {
174             item->setIcon(0, KIcon("dialog-close"));
175             item->setText(1, e.attribute("resource"));
176             item->setData(0, hashRole, e.attribute("file_hash"));
177             item->setData(0, sizeRole, e.attribute("file_size"));
178             item->setData(0, statusRole, CLIPMISSING);
179         }
180         item->setData(0, typeRole, t);
181         item->setData(0, idRole, e.attribute("id"));
182     }
183     connect(m_ui.recursiveSearch, SIGNAL(pressed()), this, SLOT(slotSearchClips()));
184     connect(m_ui.usePlaceholders, SIGNAL(pressed()), this, SLOT(slotPlaceholders()));
185     connect(m_ui.removeSelected, SIGNAL(pressed()), this, SLOT(slotDeleteSelected()));
186     connect(m_ui.treeWidget, SIGNAL(itemDoubleClicked(QTreeWidgetItem *, int)), this, SLOT(slotEditItem(QTreeWidgetItem *, int)));
187     connect(m_ui.treeWidget, SIGNAL(itemSelectionChanged()), this, SLOT(slotCheckButtons()));
188     //adjustSize();
189     if (m_ui.treeWidget->topLevelItem(0)) m_ui.treeWidget->setCurrentItem(m_ui.treeWidget->topLevelItem(0));
190     checkStatus();
191     int acceptMissing = m_dialog->exec();
192     if (acceptMissing == QDialog::Accepted) acceptDialog();
193     return (acceptMissing != QDialog::Accepted);
194 }
195
196 DocumentChecker::~DocumentChecker()
197 {
198     if (m_dialog) delete m_dialog;
199 }
200
201
202 QString DocumentChecker::getProperty(QDomElement effect, const QString &name)
203 {
204     QDomNodeList params = effect.elementsByTagName("property");
205     for (int i = 0; i < params.count(); i++) {
206         QDomElement e = params.item(i).toElement();
207         if (e.attribute("name") == name) {
208             return e.firstChild().nodeValue();
209         }
210     }
211     return QString();
212 }
213
214 void DocumentChecker::setProperty(QDomElement effect, const QString &name, const QString value)
215 {
216     QDomNodeList params = effect.elementsByTagName("property");
217     for (int i = 0; i < params.count(); i++) {
218         QDomElement e = params.item(i).toElement();
219         if (e.attribute("name") == name) {
220             e.firstChild().setNodeValue(value);
221         }
222     }
223 }
224
225 void DocumentChecker::slotSearchClips()
226 {
227     QString newpath = KFileDialog::getExistingDirectory(KUrl("kfiledialog:///clipfolder"), kapp->activeWindow(), i18n("Clips folder"));
228     if (newpath.isEmpty()) return;
229     int ix = 0;
230     m_ui.recursiveSearch->setEnabled(false);
231     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
232     while (child) {
233         if (child->data(0, statusRole).toInt() == CLIPMISSING) {
234             QString clipPath = searchFileRecursively(QDir(newpath), child->data(0, sizeRole).toString(), child->data(0, hashRole).toString());
235             if (!clipPath.isEmpty()) {
236                 child->setText(1, clipPath);
237                 child->setIcon(0, KIcon("dialog-ok"));
238                 child->setData(0, statusRole, CLIPOK);
239             }
240         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
241             QString fileName = searchLuma(child->data(0, idRole).toString());
242             if (!fileName.isEmpty()) {
243                 child->setText(1, fileName);
244                 child->setIcon(0, KIcon("dialog-ok"));
245                 child->setData(0, statusRole, LUMAOK);
246             }
247         }
248         ix++;
249         child = m_ui.treeWidget->topLevelItem(ix);
250     }
251     m_ui.recursiveSearch->setEnabled(true);
252     checkStatus();
253 }
254
255
256 QString DocumentChecker::searchLuma(QString file) const
257 {
258     KUrl searchPath(KdenliveSettings::mltpath());
259     if (file.contains("PAL"))
260         searchPath.cd("../lumas/PAL");
261     else
262         searchPath.cd("../lumas/NTSC");
263     QString result = searchPath.path(KUrl::AddTrailingSlash) + KUrl(file).fileName();
264     if (QFile::exists(result))
265         return result;
266     return QString();
267 }
268
269
270 QString DocumentChecker::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const
271 {
272     QString foundFileName;
273     QByteArray fileData;
274     QByteArray fileHash;
275     QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable);
276     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
277         QFile file(dir.absoluteFilePath(filesAndDirs.at(i)));
278         if (QString::number(file.size()) == matchSize) {
279             if (file.open(QIODevice::ReadOnly)) {
280                 /*
281                 * 1 MB = 1 second per 450 files (or faster)
282                 * 10 MB = 9 seconds per 450 files (or faster)
283                 */
284                 if (file.size() > 1000000*2) {
285                     fileData = file.read(1000000);
286                     if (file.seek(file.size() - 1000000))
287                         fileData.append(file.readAll());
288                 } else
289                     fileData = file.readAll();
290                 file.close();
291                 fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
292                 if (QString(fileHash.toHex()) == matchHash)
293                     return file.fileName();
294             }
295         }
296         //kDebug() << filesAndDirs.at(i) << file.size() << fileHash.toHex();
297     }
298     filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot);
299     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
300         foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash);
301         if (!foundFileName.isEmpty())
302             break;
303     }
304     return foundFileName;
305 }
306
307 void DocumentChecker::slotEditItem(QTreeWidgetItem *item, int)
308 {
309     int t = item->data(0, typeRole).toInt();
310     if (t == TITLE_FONT_ELEMENT) return;
311     //|| t == TITLE_IMAGE_ELEMENT) {
312
313     KUrl url = KUrlRequesterDialog::getUrl(item->text(1), m_dialog, i18n("Enter new location for file"));
314     if (url.isEmpty()) return;
315     item->setText(1, url.path());
316     if (KIO::NetAccess::exists(url, KIO::NetAccess::SourceSide, 0)) {
317         item->setIcon(0, KIcon("dialog-ok"));
318         int id = item->data(0, statusRole).toInt();
319         if (id < 10) item->setData(0, statusRole, CLIPOK);
320         else item->setData(0, statusRole, LUMAOK);
321         checkStatus();
322     } else {
323         item->setIcon(0, KIcon("dialog-close"));
324         int id = item->data(0, statusRole).toInt();
325         if (id < 10) item->setData(0, statusRole, CLIPMISSING);
326         else item->setData(0, statusRole, LUMAMISSING);
327         checkStatus();
328     }
329 }
330
331
332 void DocumentChecker::acceptDialog()
333 {
334     QDomElement e, property;
335     QDomNodeList producers = m_doc.elementsByTagName("producer");
336     QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
337     QDomNodeList properties;
338     int ix = 0;
339
340     // prepare transitions
341     QDomNodeList trans = m_doc.elementsByTagName("transition");
342
343     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
344     while (child) {
345         int t = child->data(0, typeRole).toInt();
346         if (child->data(0, statusRole).toInt() == CLIPOK) {
347             QString id = child->data(0, idRole).toString();
348             if (t == TITLE_IMAGE_ELEMENT) {
349                 // edit images embedded in titles
350                 for (int i = 0; i < infoproducers.count(); i++) {
351                     e = infoproducers.item(i).toElement();
352                     if (e.attribute("id") == id) {
353                         // Fix clip
354                         QString xml = e.attribute("xmldata");
355                         xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
356                         e.setAttribute("xmldata", xml);
357                         break;
358                     }
359                 }
360                 for (int i = 0; i < producers.count(); i++) {
361                     e = producers.item(i).toElement();
362                     if (e.attribute("id").section('_', 0, 0) == id) {
363                         // Fix clip
364                         properties = e.childNodes();
365                         for (int j = 0; j < properties.count(); ++j) {
366                             property = properties.item(j).toElement();
367                             if (property.attribute("name") == "xmldata") {
368                                 QString xml = property.firstChild().nodeValue();
369                                 xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
370                                 property.firstChild().setNodeValue(xml);
371                                 break;
372                             }
373                         }
374                     }
375                 }
376             } else {
377                 // edit clip url
378                 for (int i = 0; i < infoproducers.count(); i++) {
379                     e = infoproducers.item(i).toElement();
380                     if (e.attribute("id") == id) {
381                         // Fix clip
382                         e.setAttribute("resource", child->text(1));
383                         e.setAttribute("name", KUrl(child->text(1)).fileName());
384                         break;
385                     }
386                 }
387                 for (int i = 0; i < producers.count(); i++) {
388                     e = producers.item(i).toElement();
389                     if (e.attribute("id").section('_', 0, 0) == id) {
390                         // Fix clip
391                         properties = e.childNodes();
392                         for (int j = 0; j < properties.count(); ++j) {
393                             property = properties.item(j).toElement();
394                             if (property.attribute("name") == "resource") {
395                                 property.firstChild().setNodeValue(child->text(1));
396                                 break;
397                             }
398                         }
399                     }
400                 }
401             }
402         } else if (child->data(0, statusRole).toInt() == CLIPPLACEHOLDER && t != TITLE_FONT_ELEMENT && t != TITLE_IMAGE_ELEMENT) {
403             QString id = child->data(0, idRole).toString();
404             for (int i = 0; i < infoproducers.count(); i++) {
405                 e = infoproducers.item(i).toElement();
406                 if (e.attribute("id") == id) {
407                     // Fix clip
408                     e.setAttribute("placeholder", '1');
409                     break;
410                 }
411             }
412         } else if (child->data(0, statusRole).toInt() == LUMAOK) {
413             for (int i = 0; i < trans.count(); i++) {
414                 QString luma = getProperty(trans.at(i).toElement(), "luma");
415                 kDebug() << "luma: " << luma;
416                 if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
417                     setProperty(trans.at(i).toElement(), "luma", child->text(1));
418                     kDebug() << "replace with; " << child->text(1);
419                 }
420             }
421         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
422             for (int i = 0; i < trans.count(); i++) {
423                 QString luma = getProperty(trans.at(i).toElement(), "luma");
424                 if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
425                     setProperty(trans.at(i).toElement(), "luma", QString());
426                 }
427             }
428         }
429         ix++;
430         child = m_ui.treeWidget->topLevelItem(ix);
431     }
432     //QDialog::accept();
433 }
434
435 void DocumentChecker::slotPlaceholders()
436 {
437     int ix = 0;
438     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
439     while (child) {
440         if (child->data(0, statusRole).toInt() == CLIPMISSING) {
441             child->setData(0, statusRole, CLIPPLACEHOLDER);
442             child->setIcon(0, KIcon("dialog-ok"));
443         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
444             child->setData(0, statusRole, LUMAPLACEHOLDER);
445             child->setIcon(0, KIcon("dialog-ok"));
446         }
447         ix++;
448         child = m_ui.treeWidget->topLevelItem(ix);
449     }
450     checkStatus();
451 }
452
453
454 void DocumentChecker::checkStatus()
455 {
456     bool status = true;
457     int ix = 0;
458     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
459     while (child) {
460         if (child->data(0, statusRole).toInt() == CLIPMISSING || child->data(0, statusRole).toInt() == LUMAMISSING) {
461             status = false;
462             break;
463         }
464         ix++;
465         child = m_ui.treeWidget->topLevelItem(ix);
466     }
467     m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status);
468 }
469
470
471 void DocumentChecker::slotDeleteSelected()
472 {
473     if (KMessageBox::warningContinueCancel(m_dialog, i18np("This will remove the selected clip from this project", "This will remove the selected clips from this project", m_ui.treeWidget->selectedItems().count()), i18n("Remove clips")) == KMessageBox::Cancel) return;
474     int ix = 0;
475     QStringList deletedIds;
476     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
477     QDomNodeList playlists = m_doc.elementsByTagName("playlist");
478
479     while (child) {
480         int id = child->data(0, statusRole).toInt();
481         if (child->isSelected() && id < 10) {
482             QString id = child->data(0, idRole).toString();
483             deletedIds.append(id);
484             for (int j = 0; j < playlists.count(); j++)
485                 deletedIds.append(id + '_' + QString::number(j));
486             delete child;
487         } else ix++;
488         child = m_ui.treeWidget->topLevelItem(ix);
489     }
490     kDebug() << "// Clips to delete: " << deletedIds;
491
492     if (!deletedIds.isEmpty()) {
493         QDomElement e;
494         QDomNodeList producers = m_doc.elementsByTagName("producer");
495         QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
496
497         QDomElement mlt = m_doc.firstChildElement("mlt");
498         QDomElement kdenlivedoc = mlt.firstChildElement("kdenlivedoc");
499
500         for (int i = 0; i < infoproducers.count(); i++) {
501             e = infoproducers.item(i).toElement();
502             if (deletedIds.contains(e.attribute("id"))) {
503                 // Remove clip
504                 kdenlivedoc.removeChild(e);
505                 break;
506             }
507         }
508
509         for (int i = 0; i < producers.count(); i++) {
510             e = producers.item(i).toElement();
511             if (deletedIds.contains(e.attribute("id"))) {
512                 // Remove clip
513                 mlt.removeChild(e);
514                 break;
515             }
516         }
517
518         for (int i = 0; i < playlists.count(); i++) {
519             QDomNodeList entries = playlists.at(i).toElement().elementsByTagName("entry");
520             for (int j = 0; j < playlists.count(); j++) {
521                 e = entries.item(j).toElement();
522                 if (deletedIds.contains(e.attribute("producer"))) {
523                     // Replace clip with blank
524                     e.setTagName("blank");
525                     e.removeAttribute("producer");
526                     int length = e.attribute("out").toInt() - e.attribute("in").toInt();
527                     e.setAttribute("length", length);
528                 }
529             }
530         }
531         checkStatus();
532     }
533 }
534
535 void DocumentChecker::checkMissingImages(QList <QDomElement>&missingClips, QStringList images, QStringList fonts, QString id, QString baseClip)
536 {
537     QDomDocument doc;
538     foreach(const QString &img, images) {
539         if (!KIO::NetAccess::exists(KUrl(img), KIO::NetAccess::SourceSide, 0)) {
540             QDomElement e = doc.createElement("missingclip");
541             e.setAttribute("type", TITLE_IMAGE_ELEMENT);
542             e.setAttribute("resource", img);
543             e.setAttribute("id", id);
544             e.setAttribute("name", baseClip);
545             missingClips.append(e);
546         }
547     }
548     kDebug() << "/ / / CHK FONTS: " << fonts;
549     foreach(const QString &fontelement, fonts) {
550         QFont f(fontelement);
551         kDebug() << "/ / / CHK FONTS: " << fontelement << " = " << QFontInfo(f).family();
552         if (fontelement != QFontInfo(f).family()) {
553             QDomElement e = doc.createElement("missingclip");
554             e.setAttribute("type", TITLE_FONT_ELEMENT);
555             e.setAttribute("resource", fontelement);
556             e.setAttribute("id", id);
557             e.setAttribute("name", baseClip);
558             missingClips.append(e);
559         }
560     }
561 }
562
563
564 void DocumentChecker::slotCheckButtons()
565 {
566     if (m_ui.treeWidget->currentItem()) {
567         QTreeWidgetItem *item = m_ui.treeWidget->currentItem();
568         int t = item->data(0, typeRole).toInt();
569         if (t == TITLE_FONT_ELEMENT || t == TITLE_IMAGE_ELEMENT) {
570             m_ui.removeSelected->setEnabled(false);
571         } else m_ui.removeSelected->setEnabled(true);
572     }
573
574 }
575
576 #include "documentchecker.moc"
577
578