]> git.sesse.net Git - kdenlive/blob - src/documentchecker.cpp
Small optimisation of document loading (do not check for missing files twice)
[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 const int resetDurationRole = Qt::UserRole + 6;
52
53 const int CLIPMISSING = 0;
54 const int CLIPOK = 1;
55 const int CLIPPLACEHOLDER = 2;
56 const int CLIPWRONGDURATION = 3;
57 const int PROXYMISSING = 4;
58
59 const int LUMAMISSING = 10;
60 const int LUMAOK = 11;
61 const int LUMAPLACEHOLDER = 12;
62
63 enum TITLECLIPTYPE { TITLE_IMAGE_ELEMENT = 20, TITLE_FONT_ELEMENT = 21 };
64
65 DocumentChecker::DocumentChecker(QDomNodeList infoproducers, QDomDocument doc):
66     m_info(infoproducers), m_doc(doc), m_dialog(NULL)
67 {
68
69 }
70
71
72 bool DocumentChecker::hasErrorInClips()
73 {
74     int clipType;
75     QDomElement e;
76     QString resource;
77     int max;
78     QDomNodeList documentProducers = m_doc.elementsByTagName("producer");
79     QList <QDomElement> wrongDurationClips;
80     QList <QDomElement> missingProxies;
81     max = m_info.count();
82     for (int i = 0; i < max; i++) {
83         e = m_info.item(i).toElement();
84         clipType = e.attribute("type").toInt();
85         if (clipType == COLOR) continue;
86         if (clipType != TEXT && clipType != IMAGE && clipType != SLIDESHOW) {
87             QString id = e.attribute("id");
88             int duration = e.attribute("duration").toInt();
89             int mltDuration = -1;
90             QDomElement mltProd;
91             QString prodId;
92             // Check that the duration is in sync between Kdenlive's info and MLT's playlist
93             int prodsCount = documentProducers.count();
94             for (int j = 0; j < prodsCount; j++) {
95                 mltProd = documentProducers.at(j).toElement();
96                 prodId = mltProd.attribute("id");
97                 // Don't check slowmotion clips for now... (TODO?)
98                 if (prodId.startsWith("slowmotion")) continue;
99                 if (prodId.contains("_")) prodId = prodId.section("_", 0, 0);
100                 if (prodId != id) continue;
101                 if (mltDuration > 0 ) {
102                     // We have several MLT producers for the same clip (probably track producers)
103                     int newLength = EffectsList::property(mltProd, "length").toInt();
104                     if (newLength != mltDuration) {
105                         // we have a different duration for the same clip, that is not safe
106                         e.setAttribute("_resetDuration", 1);
107                     }
108                 }
109                 mltDuration = EffectsList::property(mltProd, "length").toInt();
110                 if (mltDuration != duration) {
111                     // Duration mismatch
112                     e.setAttribute("_mismatch", mltDuration);
113                     if (mltDuration == 15000) {
114                         // a length of 15000 might indicate a wrong clip length since it is a default length
115                         e.setAttribute("_resetDuration", 1);
116                     }
117                     if (!wrongDurationClips.contains(e)) wrongDurationClips.append(e);
118                 }
119             }
120         }
121         
122         QStringList safeImages;
123         if (clipType == TEXT) {
124             //TODO: Check is clip template is missing (xmltemplate) or hash changed
125             QStringList images = TitleWidget::extractImageList(e.attribute("xmldata"));
126             QStringList fonts = TitleWidget::extractFontList(e.attribute("xmldata"));
127             // Make sure we don't check the same images twice
128             foreach(const QString &existingImage, safeImages) {
129                 images.removeAll(existingImage);
130             }
131             safeImages.append(checkMissingImages(images, fonts, e.attribute("id"), e.attribute("name")));
132             continue;
133         }
134         resource = e.attribute("resource");
135         if (e.hasAttribute("proxy")) {
136             QString proxyresource = e.attribute("proxy");
137             if (!proxyresource.isEmpty() && proxyresource != "-" && !KIO::NetAccess::exists(KUrl(proxyresource), KIO::NetAccess::SourceSide, 0)) {
138                 // Missing clip found
139                 missingProxies.append(e);
140             }
141         }
142         if (clipType == SLIDESHOW) resource = KUrl(resource).directory();
143         if (!KIO::NetAccess::exists(KUrl(resource), KIO::NetAccess::SourceSide, 0)) {
144             // Missing clip found
145             m_missingClips.append(e);
146         } else {
147             // Check if the clip has changed
148             if (clipType != SLIDESHOW && e.hasAttribute("file_hash")) {
149                 if (e.attribute("file_hash") != DocClipBase::getHash(e.attribute("resource")))
150                     e.removeAttribute("file_hash");
151             }
152         }
153     }
154
155     // Get list of used Luma files
156     QStringList missingLumas;
157     QStringList filesToCheck;
158     QString filePath;
159     QString root = m_doc.documentElement().attribute("root");
160     if (!root.isEmpty()) root = KUrl(root).path(KUrl::AddTrailingSlash);
161     QDomNodeList trans = m_doc.elementsByTagName("transition");
162     max = trans.count();
163     for (int i = 0; i < max; i++) {
164         QString luma = getProperty(trans.at(i).toElement(), "luma");
165         if (!luma.isEmpty() && !filesToCheck.contains(luma))
166             filesToCheck.append(luma);
167     }
168     // Check existence of luma files
169     foreach (const QString lumafile, filesToCheck) {
170         filePath = lumafile;
171         if (!filePath.startsWith('/')) filePath.prepend(root);
172         if (!QFile::exists(filePath)) {
173             missingLumas.append(lumafile);
174         }
175     }
176     
177     
178
179     if (m_missingClips.isEmpty() && missingLumas.isEmpty() && wrongDurationClips.isEmpty() && missingProxies.isEmpty())
180         return false;
181
182     m_dialog = new QDialog();
183     m_dialog->setFont(KGlobalSettings::toolBarFont());
184     m_ui.setupUi(m_dialog);
185
186     foreach(const QString l, missingLumas) {
187         QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Luma file") << l);
188         item->setIcon(0, KIcon("dialog-close"));
189         item->setData(0, idRole, l);
190         item->setData(0, statusRole, LUMAMISSING);
191     }
192
193     m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
194     max = m_missingClips.count();
195     for (int i = 0; i < max; i++) {
196         e = m_missingClips.at(i).toElement();
197         QString clipType;
198         int t = e.attribute("type").toInt();
199         switch (t) {
200         case AV:
201             clipType = i18n("Video clip");
202             break;
203         case VIDEO:
204             clipType = i18n("Mute video clip");
205             break;
206         case AUDIO:
207             clipType = i18n("Audio clip");
208             break;
209         case PLAYLIST:
210             clipType = i18n("Playlist clip");
211             break;
212         case IMAGE:
213             clipType = i18n("Image clip");
214             break;
215         case SLIDESHOW:
216             clipType = i18n("Slideshow clip");
217             break;
218         case TITLE_IMAGE_ELEMENT:
219             clipType = i18n("Title Image");
220             break;
221         case TITLE_FONT_ELEMENT:
222             clipType = i18n("Title Font");
223             break;
224         default:
225             clipType = i18n("Video clip");
226         }
227         QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << clipType);
228         if (t == TITLE_IMAGE_ELEMENT) {
229             item->setIcon(0, KIcon("dialog-warning"));
230             item->setToolTip(1, e.attribute("name"));
231             item->setText(1, e.attribute("resource"));
232             item->setData(0, statusRole, CLIPPLACEHOLDER);
233             item->setData(0, typeOriginalResource, e.attribute("resource"));
234         } else if (t == TITLE_FONT_ELEMENT) {
235             item->setIcon(0, KIcon("dialog-warning"));
236             item->setToolTip(1, e.attribute("name"));
237             QString ft = e.attribute("resource");
238             QString newft = QFontInfo(QFont(ft)).family();
239             item->setText(1, i18n("%1 will be replaced by %2", ft, newft));
240             item->setData(0, statusRole, CLIPPLACEHOLDER);
241         } else {
242             item->setIcon(0, KIcon("dialog-close"));
243             item->setText(1, e.attribute("resource"));
244             item->setData(0, hashRole, e.attribute("file_hash"));
245             item->setData(0, sizeRole, e.attribute("file_size"));
246             item->setData(0, statusRole, CLIPMISSING);
247         }
248         item->setData(0, typeRole, t);
249         item->setData(0, idRole, e.attribute("id"));
250         item->setToolTip(0, i18n("Missing item"));
251     }
252
253     if (m_missingClips.count() > 0) {
254         if (wrongDurationClips.count() > 0) {
255             m_ui.infoLabel->setText(i18n("The project file contains missing clips or files and clip duration mismatch"));
256         }
257         else {
258             m_ui.infoLabel->setText(i18n("The project file contains missing clips or files"));
259         }
260     }
261     else if (wrongDurationClips.count() > 0) {
262         m_ui.infoLabel->setText(i18n("The project file contains clips with duration mismatch"));
263     }
264     if (missingProxies.count() > 0) {
265         if (!m_ui.infoLabel->text().isEmpty()) m_ui.infoLabel->setText(m_ui.infoLabel->text() + ". ");
266         m_ui.infoLabel->setText(m_ui.infoLabel->text() + i18n("Missing proxies will be recreated after opening."));
267     }
268
269     m_ui.removeSelected->setEnabled(!m_missingClips.isEmpty());
270     m_ui.recursiveSearch->setEnabled(!m_missingClips.isEmpty() || !missingLumas.isEmpty());
271     m_ui.usePlaceholders->setEnabled(!m_missingClips.isEmpty());
272     m_ui.fixDuration->setEnabled(!wrongDurationClips.isEmpty());
273
274     max = wrongDurationClips.count();
275     for (int i = 0; i < max; i++) {
276         e = wrongDurationClips.at(i).toElement();
277         QString clipType;
278         int t = e.attribute("type").toInt();
279         switch (t) {
280         case AV:
281             clipType = i18n("Video clip");
282             break;
283         case VIDEO:
284             clipType = i18n("Mute video clip");
285             break;
286         case AUDIO:
287             clipType = i18n("Audio clip");
288             break;
289         case PLAYLIST:
290             clipType = i18n("Playlist clip");
291             break;
292         case IMAGE:
293             clipType = i18n("Image clip");
294             break;
295         case SLIDESHOW:
296             clipType = i18n("Slideshow clip");
297             break;
298         default:
299             clipType = i18n("Video clip");
300         }
301         QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << clipType);
302         item->setIcon(0, KIcon("timeadjust"));
303         item->setText(1, e.attribute("resource"));
304         item->setData(0, hashRole, e.attribute("file_hash"));
305         item->setData(0, sizeRole, e.attribute("_mismatch"));
306         e.removeAttribute("_mismatch");
307         item->setData(0, resetDurationRole, (int) e.hasAttribute("_resetDuration"));
308         e.removeAttribute("_resetDuration");
309         item->setData(0, statusRole, CLIPWRONGDURATION);
310         item->setData(0, typeRole, t);
311         item->setData(0, idRole, e.attribute("id"));
312         item->setToolTip(0, i18n("Duration mismatch"));
313     }
314
315     if (missingProxies.count() > 0) {
316         QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Proxy clip"));
317         item->setIcon(0, KIcon("dialog-warning"));
318         item->setText(1, i18n("%1 missing proxy clips, will be recreated on project opening", missingProxies.count()));
319         item->setData(0, hashRole, e.attribute("file_hash"));
320         item->setData(0, statusRole, PROXYMISSING);
321         item->setToolTip(0, i18n("Missing proxy"));
322     }
323
324     max = missingProxies.count();
325     for (int i = 0; i < max; i++) {
326         e = missingProxies.at(i).toElement();
327         QString clipType;
328         QString realPath = e.attribute("resource");
329         QString id = e.attribute("id");
330         // Replace proxy url with real clip in MLT producers
331         QDomNodeList properties;
332         QDomElement mltProd;
333         QDomElement property;
334         int prodsCount = documentProducers.count();
335         for (int j = 0; j < prodsCount; j++) {
336             mltProd = documentProducers.at(j).toElement();
337             QString prodId = mltProd.attribute("id");
338             bool slowmotion = false;
339             if (prodId.startsWith("slowmotion")) {
340                 slowmotion = true;
341                 prodId = prodId.section(':', 1, 1);
342             }
343             if (prodId.contains('_')) prodId = prodId.section('_', 0, 0);
344             if (prodId == id) {
345                 // Hit, we must replace url
346                 properties = mltProd.childNodes();
347                 for (int k = 0; k < properties.count(); ++k) {
348                     property = properties.item(k).toElement();
349                     if (property.attribute("name") == "resource") {
350                         QString resource = property.firstChild().nodeValue();                    
351                         QString suffix;
352                         if (slowmotion) suffix = "?" + resource.section('?', -1);
353                         property.firstChild().setNodeValue(realPath + suffix);
354                         break;
355                     }
356                 }
357             }
358         }
359     }
360     
361     if (missingProxies.count() > 0) {
362         // original doc was modified
363         QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
364         infoXml.setAttribute("modified", "1");
365     }
366     
367     connect(m_ui.recursiveSearch, SIGNAL(pressed()), this, SLOT(slotSearchClips()));
368     connect(m_ui.usePlaceholders, SIGNAL(pressed()), this, SLOT(slotPlaceholders()));
369     connect(m_ui.removeSelected, SIGNAL(pressed()), this, SLOT(slotDeleteSelected()));
370     connect(m_ui.fixDuration, SIGNAL(pressed()), this, SLOT(slotFixDuration()));
371     connect(m_ui.treeWidget, SIGNAL(itemDoubleClicked(QTreeWidgetItem *, int)), this, SLOT(slotEditItem(QTreeWidgetItem *, int)));
372     connect(m_ui.treeWidget, SIGNAL(itemSelectionChanged()), this, SLOT(slotCheckButtons()));
373     //adjustSize();
374     if (m_ui.treeWidget->topLevelItem(0)) m_ui.treeWidget->setCurrentItem(m_ui.treeWidget->topLevelItem(0));
375     checkStatus();
376     int acceptMissing = m_dialog->exec();
377     if (acceptMissing == QDialog::Accepted) acceptDialog();
378     return (acceptMissing != QDialog::Accepted);
379 }
380
381 DocumentChecker::~DocumentChecker()
382 {
383     if (m_dialog) delete m_dialog;
384 }
385
386
387 QString DocumentChecker::getProperty(QDomElement effect, const QString &name)
388 {
389     QDomNodeList params = effect.elementsByTagName("property");
390     for (int i = 0; i < params.count(); i++) {
391         QDomElement e = params.item(i).toElement();
392         if (e.attribute("name") == name) {
393             return e.firstChild().nodeValue();
394         }
395     }
396     return QString();
397 }
398
399 void DocumentChecker::setProperty(QDomElement effect, const QString &name, const QString value)
400 {
401     QDomNodeList params = effect.elementsByTagName("property");
402     for (int i = 0; i < params.count(); i++) {
403         QDomElement e = params.item(i).toElement();
404         if (e.attribute("name") == name) {
405             e.firstChild().setNodeValue(value);
406         }
407     }
408 }
409
410 void DocumentChecker::slotSearchClips()
411 {
412     QString newpath = KFileDialog::getExistingDirectory(KUrl("kfiledialog:///clipfolder"), kapp->activeWindow(), i18n("Clips folder"));
413     if (newpath.isEmpty()) return;
414     int ix = 0;
415     bool fixed = false;
416     m_ui.recursiveSearch->setEnabled(false);
417     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
418     QDir searchDir(newpath);
419     while (child) {
420         if (child->data(0, statusRole).toInt() == CLIPMISSING) {
421             QString clipPath = searchFileRecursively(searchDir, child->data(0, sizeRole).toString(), child->data(0, hashRole).toString());
422             if (!clipPath.isEmpty()) {
423                 fixed = true;
424                 child->setText(1, clipPath);
425                 child->setIcon(0, KIcon("dialog-ok"));
426                 child->setData(0, statusRole, CLIPOK);
427             }
428         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
429             QString fileName = searchLuma(child->data(0, idRole).toString());
430             if (!fileName.isEmpty()) {
431                 fixed = true;
432                 child->setText(1, fileName);
433                 child->setIcon(0, KIcon("dialog-ok"));
434                 child->setData(0, statusRole, LUMAOK);
435             }
436         }
437         else if (child->data(0, typeRole).toInt() == TITLE_IMAGE_ELEMENT && child->data(0, statusRole).toInt() == CLIPPLACEHOLDER) {
438             // Search missing title images
439             QString missingFileName = KUrl(child->text(1)).fileName();
440             QString newPath = searchPathRecursively(searchDir, missingFileName);
441             if (!newPath.isEmpty()) {
442                 // File found
443                 fixed = true;
444                 child->setText(1, newPath);
445                 child->setIcon(0, KIcon("dialog-ok"));
446                 child->setData(0, statusRole, CLIPOK);
447             }
448         }
449         ix++;
450         child = m_ui.treeWidget->topLevelItem(ix);
451     }
452     m_ui.recursiveSearch->setEnabled(true);
453     if (fixed) {
454         // original doc was modified
455         QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
456         infoXml.setAttribute("modified", "1");
457     }
458     checkStatus();
459 }
460
461
462 QString DocumentChecker::searchLuma(QString file) const
463 {
464     KUrl searchPath(KdenliveSettings::mltpath());
465     if (file.contains("PAL"))
466         searchPath.cd("../lumas/PAL");
467     else
468         searchPath.cd("../lumas/NTSC");
469     QString result = searchPath.path(KUrl::AddTrailingSlash) + KUrl(file).fileName();
470     if (QFile::exists(result))
471         return result;
472     // try to find luma in application path
473     searchPath.clear();
474     searchPath = KUrl(QCoreApplication::applicationDirPath());
475     searchPath.cd("../share/apps/kdenlive/lumas");
476     result = searchPath.path(KUrl::AddTrailingSlash) + KUrl(file).fileName();
477     if (QFile::exists(result))
478         return result;
479     return QString();
480 }
481
482 QString DocumentChecker::searchPathRecursively(const QDir &dir, const QString &fileName) const
483 {
484     QString foundFileName;
485     QStringList filters;
486     filters << fileName;
487     QDir searchDir(dir);
488     searchDir.setNameFilters(filters);
489     QStringList filesAndDirs = searchDir.entryList(QDir::Files | QDir::Readable);
490     if (!filesAndDirs.isEmpty()) return searchDir.absoluteFilePath(filesAndDirs.at(0));
491     searchDir.setNameFilters(QStringList());
492     filesAndDirs = searchDir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot);
493     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
494         foundFileName = searchPathRecursively(searchDir.absoluteFilePath(filesAndDirs.at(i)), fileName);
495         if (!foundFileName.isEmpty())
496             break;
497     }
498     return foundFileName;
499 }
500
501 QString DocumentChecker::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const
502 {
503     QString foundFileName;
504     QByteArray fileData;
505     QByteArray fileHash;
506     QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable);
507     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
508         QFile file(dir.absoluteFilePath(filesAndDirs.at(i)));
509         if (QString::number(file.size()) == matchSize) {
510             if (file.open(QIODevice::ReadOnly)) {
511                 /*
512                 * 1 MB = 1 second per 450 files (or faster)
513                 * 10 MB = 9 seconds per 450 files (or faster)
514                 */
515                 if (file.size() > 1000000 * 2) {
516                     fileData = file.read(1000000);
517                     if (file.seek(file.size() - 1000000))
518                         fileData.append(file.readAll());
519                 } else
520                     fileData = file.readAll();
521                 file.close();
522                 fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
523                 if (QString(fileHash.toHex()) == matchHash)
524                     return file.fileName();
525             }
526         }
527         //kDebug() << filesAndDirs.at(i) << file.size() << fileHash.toHex();
528     }
529     filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot);
530     for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
531         foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash);
532         if (!foundFileName.isEmpty())
533             break;
534     }
535     return foundFileName;
536 }
537
538 void DocumentChecker::slotEditItem(QTreeWidgetItem *item, int)
539 {
540     int t = item->data(0, typeRole).toInt();
541     if (t == TITLE_FONT_ELEMENT) return;
542     //|| t == TITLE_IMAGE_ELEMENT) {
543
544     KUrl url = KUrlRequesterDialog::getUrl(item->text(1), m_dialog, i18n("Enter new location for file"));
545     if (url.isEmpty()) return;
546     item->setText(1, url.path());
547     if (KIO::NetAccess::exists(url, KIO::NetAccess::SourceSide, 0)) {
548         item->setIcon(0, KIcon("dialog-ok"));
549         int id = item->data(0, statusRole).toInt();
550         if (id < 10) item->setData(0, statusRole, CLIPOK);
551         else item->setData(0, statusRole, LUMAOK);
552         checkStatus();
553     } else {
554         item->setIcon(0, KIcon("dialog-close"));
555         int id = item->data(0, statusRole).toInt();
556         if (id < 10) item->setData(0, statusRole, CLIPMISSING);
557         else item->setData(0, statusRole, LUMAMISSING);
558         checkStatus();
559     }
560 }
561
562
563 void DocumentChecker::acceptDialog()
564 {
565     QDomElement e, property;
566     QDomNodeList producers = m_doc.elementsByTagName("producer");
567     QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
568     QDomNodeList properties;
569     int ix = 0;
570
571     // prepare transitions
572     QDomNodeList trans = m_doc.elementsByTagName("transition");
573
574     // Mark document as modified
575     m_doc.documentElement().setAttribute("modified", 1);
576
577     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
578     while (child) {
579         int t = child->data(0, typeRole).toInt();
580         if (child->data(0, statusRole).toInt() == CLIPOK) {
581             QString id = child->data(0, idRole).toString();
582             if (t == TITLE_IMAGE_ELEMENT) {
583                 // edit images embedded in titles
584                 for (int i = 0; i < infoproducers.count(); i++) {
585                     e = infoproducers.item(i).toElement();
586                     if (e.attribute("id") == id) {
587                         // Fix clip
588                         QString xml = e.attribute("xmldata");
589                         xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
590                         e.setAttribute("xmldata", xml);
591                         break;
592                     }
593                 }
594                 for (int i = 0; i < producers.count(); i++) {
595                     e = producers.item(i).toElement();
596                     if (e.attribute("id").section('_', 0, 0) == id) {
597                         // Fix clip
598                         properties = e.childNodes();
599                         for (int j = 0; j < properties.count(); ++j) {
600                             property = properties.item(j).toElement();
601                             if (property.attribute("name") == "xmldata") {
602                                 QString xml = property.firstChild().nodeValue();
603                                 xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
604                                 property.firstChild().setNodeValue(xml);
605                                 break;
606                             }
607                         }
608                     }
609                 }
610             } else {
611                 // edit clip url
612                 for (int i = 0; i < infoproducers.count(); i++) {
613                     e = infoproducers.item(i).toElement();
614                     if (e.attribute("id") == id) {
615                         // Fix clip
616                         e.setAttribute("resource", child->text(1));
617                         e.setAttribute("name", KUrl(child->text(1)).fileName());
618                         break;
619                     }
620                 }
621                 for (int i = 0; i < producers.count(); i++) {
622                     e = producers.item(i).toElement();
623                     if (e.attribute("id").section('_', 0, 0) == id || e.attribute("id").section(':', 1, 1) == id) {
624                         // Fix clip
625                         properties = e.childNodes();
626                         for (int j = 0; j < properties.count(); ++j) {
627                             property = properties.item(j).toElement();
628                             if (property.attribute("name") == "resource") {
629                                 QString resource = property.firstChild().nodeValue();
630                                 if (resource.contains(QRegExp("\\?[0-9]+\\.[0-9]+(&amp;strobe=[0-9]+)?$")))
631                                     property.firstChild().setNodeValue(child->text(1) + '?' + resource.section('?', -1));
632                                 else
633                                     property.firstChild().setNodeValue(child->text(1));
634                                 break;
635                             }
636                         }
637                     }
638                 }
639             }
640         } else if (child->data(0, statusRole).toInt() == CLIPPLACEHOLDER && t != TITLE_FONT_ELEMENT && t != TITLE_IMAGE_ELEMENT) {
641             QString id = child->data(0, idRole).toString();
642             for (int i = 0; i < infoproducers.count(); i++) {
643                 e = infoproducers.item(i).toElement();
644                 if (e.attribute("id") == id) {
645                     // Fix clip
646                     e.setAttribute("placeholder", '1');
647                     break;
648                 }
649             }
650         } else if (child->data(0, statusRole).toInt() == LUMAOK) {
651             for (int i = 0; i < trans.count(); i++) {
652                 QString luma = getProperty(trans.at(i).toElement(), "luma");
653                 
654                 kDebug() << "luma: " << luma;
655                 if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
656                     setProperty(trans.at(i).toElement(), "luma", child->text(1));
657                     kDebug() << "replace with; " << child->text(1);
658                 }
659             }
660         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
661             for (int i = 0; i < trans.count(); i++) {
662                 QString luma = getProperty(trans.at(i).toElement(), "luma");
663                 if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
664                     setProperty(trans.at(i).toElement(), "luma", QString());
665                 }
666             }
667         }
668         ix++;
669         child = m_ui.treeWidget->topLevelItem(ix);
670     }
671     //QDialog::accept();
672 }
673
674 void DocumentChecker::slotPlaceholders()
675 {
676     int ix = 0;
677     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
678     while (child) {
679         if (child->data(0, statusRole).toInt() == CLIPMISSING) {
680             child->setData(0, statusRole, CLIPPLACEHOLDER);
681             child->setIcon(0, KIcon("dialog-ok"));
682         } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
683             child->setData(0, statusRole, LUMAPLACEHOLDER);
684             child->setIcon(0, KIcon("dialog-ok"));
685         }
686         ix++;
687         child = m_ui.treeWidget->topLevelItem(ix);
688     }
689     checkStatus();
690 }
691
692 void DocumentChecker::slotFixDuration()
693 {
694     int ix = 0;
695     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
696     QDomNodeList documentProducers = m_doc.elementsByTagName("producer");
697     while (child) {
698         if (child->data(0, statusRole).toInt() == CLIPWRONGDURATION) {
699             QString id = child->data(0, idRole).toString();
700             bool resetDuration = child->data(0, resetDurationRole).toInt();
701
702             for (int i = 0; i < m_info.count(); i++) {
703                 QDomElement e = m_info.at(i).toElement();
704                 if (e.attribute("id") == id) {
705                     if (m_missingClips.contains(e)) {
706                         // we cannot fix duration of missing clips
707                         resetDuration = false;
708                     }
709                     else {
710                         if (resetDuration) e.removeAttribute("duration");
711                         else e.setAttribute("duration", child->data(0, sizeRole).toString());
712                         child->setData(0, statusRole, CLIPOK);
713                         child->setIcon(0, KIcon("dialog-ok"));
714                     }
715                     break;
716                 }
717             }
718             if (resetDuration) {
719                 // something is wrong in clip durations, so remove them so mlt fetches them again
720                 for (int j = 0; j < documentProducers.count(); j++) {
721                     QDomElement mltProd = documentProducers.at(j).toElement();
722                     QString prodId = mltProd.attribute("id");
723                     if (prodId == id || prodId.startsWith(id + "_")) {
724                         EffectsList::removeProperty(mltProd, "length");
725                     }
726                 }
727             }
728         }
729         ix++;
730         child = m_ui.treeWidget->topLevelItem(ix);
731     }
732     QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
733     infoXml.setAttribute("modified", "1");
734     m_ui.fixDuration->setEnabled(false);
735     checkStatus();
736 }
737
738
739 void DocumentChecker::checkStatus()
740 {
741     bool status = true;
742     int ix = 0;
743     QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
744     while (child) {
745         int status = child->data(0, statusRole).toInt();
746         if (status == CLIPMISSING || status == LUMAMISSING || status == CLIPWRONGDURATION) {
747             status = false;
748             break;
749         }
750         ix++;
751         child = m_ui.treeWidget->topLevelItem(ix);
752     }
753     m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status);
754 }
755
756
757 void DocumentChecker::slotDeleteSelected()
758 {
759     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)
760         return;
761     QStringList deletedIds;
762     QStringList deletedLumas;
763     QDomNodeList playlists = m_doc.elementsByTagName("playlist");
764
765     foreach(QTreeWidgetItem *child, m_ui.treeWidget->selectedItems()) {
766         int id = child->data(0, statusRole).toInt();
767         if (id == CLIPMISSING) {
768             deletedIds.append(child->data(0, idRole).toString());
769             delete child;
770         }
771         else if (id == LUMAMISSING) {
772             deletedLumas.append(child->data(0, idRole).toString());
773             delete child;
774         }
775     }
776
777     if (!deletedLumas.isEmpty()) {
778         QDomElement e;
779         QDomNodeList transitions = m_doc.elementsByTagName("transition");
780         foreach (QString lumaPath, deletedLumas) {
781             for (int i = 0; i < transitions.count(); i++) {
782                 e = transitions.item(i).toElement();
783                 QString resource = EffectsList::property(e, "luma");
784                 if (resource == lumaPath) EffectsList::removeProperty(e, "luma");
785             }
786         }
787     }
788
789     if (!deletedIds.isEmpty()) {
790         QDomElement e;
791         QDomNodeList producers = m_doc.elementsByTagName("producer");
792         QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
793
794         QDomNode mlt = m_doc.elementsByTagName("mlt").at(0);
795         QDomNode kdenlivedoc = m_doc.elementsByTagName("kdenlivedoc").at(0);
796
797         for (int i = 0, j = 0; i < infoproducers.count() && j < deletedIds.count(); i++) {
798             e = infoproducers.item(i).toElement();
799             if (deletedIds.contains(e.attribute("id"))) {
800                 // Remove clip
801                 kdenlivedoc.removeChild(e);
802                 i--;
803                 j++;
804             }
805         }
806
807         for (int i = 0; i < producers.count(); i++) {
808             e = producers.item(i).toElement();
809             if (deletedIds.contains(e.attribute("id").section('_', 0, 0)) || deletedIds.contains(e.attribute("id").section(':', 1, 1).section('_', 0, 0))) {
810                 // Remove clip
811                 mlt.removeChild(e);
812                 i--;
813             }
814         }
815
816         for (int i = 0; i < playlists.count(); i++) {
817             QDomNodeList entries = playlists.at(i).toElement().elementsByTagName("entry");
818             for (int j = 0; j < entries.count(); j++) {
819                 e = entries.item(j).toElement();
820                 if (deletedIds.contains(e.attribute("producer").section('_', 0, 0)) || deletedIds.contains(e.attribute("producer").section(':', 1, 1).section('_', 0, 0))) {
821                     // Replace clip with blank
822                     while (e.childNodes().count() > 0)
823                         e.removeChild(e.firstChild());
824                     e.setTagName("blank");
825                     e.removeAttribute("producer");
826                     int length = e.attribute("out").toInt() - e.attribute("in").toInt();
827                     e.setAttribute("length", length);
828                     j--;
829                 }
830             }
831         }
832         QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
833         infoXml.setAttribute("modified", "1");
834         checkStatus();
835     }
836 }
837
838 QStringList DocumentChecker::checkMissingImages(QStringList images, QStringList fonts, QString id, QString baseClip)
839 {
840     QDomDocument doc;
841     QStringList safeImages;
842     foreach(const QString &img, images) {
843         if (!KIO::NetAccess::exists(KUrl(img), KIO::NetAccess::SourceSide, 0)) {
844             QDomElement e = doc.createElement("missingclip");
845             e.setAttribute("type", TITLE_IMAGE_ELEMENT);
846             e.setAttribute("resource", img);
847             e.setAttribute("id", id);
848             e.setAttribute("name", baseClip);
849             m_missingClips.append(e);
850         }
851         else safeImages.append(img);
852     }
853     kDebug() << "/ / / CHK FONTS: " << fonts;
854     foreach(const QString &fontelement, fonts) {
855         QFont f(fontelement);
856         kDebug() << "/ / / CHK FONTS: " << fontelement << " = " << QFontInfo(f).family();
857         if (fontelement != QFontInfo(f).family()) {
858             QDomElement e = doc.createElement("missingclip");
859             e.setAttribute("type", TITLE_FONT_ELEMENT);
860             e.setAttribute("resource", fontelement);
861             e.setAttribute("id", id);
862             e.setAttribute("name", baseClip);
863             m_missingClips.append(e);
864         }
865     }
866     return safeImages;
867 }
868
869
870 void DocumentChecker::slotCheckButtons()
871 {
872     if (m_ui.treeWidget->currentItem()) {
873         QTreeWidgetItem *item = m_ui.treeWidget->currentItem();
874         int t = item->data(0, typeRole).toInt();
875         int s = item->data(0, statusRole).toInt();
876         if (t == TITLE_FONT_ELEMENT || t == TITLE_IMAGE_ELEMENT || s == PROXYMISSING) {
877             m_ui.removeSelected->setEnabled(false);
878         } else m_ui.removeSelected->setEnabled(true);
879     }
880
881 }
882
883 #include "documentchecker.moc"
884
885