1 /***************************************************************************
2 * Copyright (C) 2008 by Jean-Baptiste Mardelle (jb@kdenlive.org) *
4 * This program is free software; you can redistribute it and/or modify *
5 * it under the terms of the GNU General Public License as published by *
6 * the Free Software Foundation; either version 2 of the License, or *
7 * (at your option) any later version. *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
14 * You should have received a copy of the GNU General Public License *
15 * along with this program; if not, write to the *
16 * Free Software Foundation, Inc., *
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
18 ***************************************************************************/
21 #include "documentchecker.h"
23 #include "docclipbase.h"
24 #include "titlewidget.h"
25 #include "definitions.h"
26 #include "kdenlivesettings.h"
29 #include <KGlobalSettings>
31 #include <KIO/NetAccess>
32 #include <KFileDialog>
33 #include <KApplication>
34 #include <KUrlRequesterDialog>
35 #include <KMessageBox>
36 #include <KStandardDirs>
38 #include <QTreeWidgetItem>
40 #include <QHeaderView>
44 #include <QCryptographicHash>
46 const int hashRole = Qt::UserRole;
47 const int sizeRole = Qt::UserRole + 1;
48 const int idRole = Qt::UserRole + 2;
49 const int statusRole = Qt::UserRole + 3;
50 const int typeRole = Qt::UserRole + 4;
51 const int typeOriginalResource = Qt::UserRole + 5;
52 const int resetDurationRole = Qt::UserRole + 6;
54 const int CLIPMISSING = 0;
56 const int CLIPPLACEHOLDER = 2;
57 const int CLIPWRONGDURATION = 3;
58 const int PROXYMISSING = 4;
59 const int SOURCEMISSING = 5;
61 const int LUMAMISSING = 10;
62 const int LUMAOK = 11;
63 const int LUMAPLACEHOLDER = 12;
65 enum TITLECLIPTYPE { TITLE_IMAGE_ELEMENT = 20, TITLE_FONT_ELEMENT = 21 };
67 DocumentChecker::DocumentChecker(QDomNodeList infoproducers, QDomDocument doc):
68 m_info(infoproducers), m_doc(doc), m_dialog(NULL)
74 bool DocumentChecker::hasErrorInClips()
80 QDomNodeList documentProducers = m_doc.elementsByTagName("producer");
81 QList <QDomElement> wrongDurationClips;
82 // List clips whose proxy is missing
83 QList <QDomElement> missingProxies;
84 // List clips who have a working proxy but no source clip
85 QList <QDomElement> missingSources;
89 for (int i = 0; i < max; i++) {
90 e = m_info.item(i).toElement();
91 clipType = e.attribute("type").toInt();
92 if (clipType == COLOR) continue;
93 if (clipType != TEXT && clipType != IMAGE && clipType != SLIDESHOW) {
94 QString id = e.attribute("id");
95 int duration = e.attribute("duration").toInt();
99 // Check that the duration is in sync between Kdenlive's info and MLT's playlist
100 int prodsCount = documentProducers.count();
101 for (int j = 0; j < prodsCount; j++) {
102 mltProd = documentProducers.at(j).toElement();
103 prodId = mltProd.attribute("id");
104 // Don't check slowmotion clips for now... (TODO?)
105 if (prodId.startsWith("slowmotion")) continue;
106 if (prodId.contains('_')) prodId = prodId.section('_', 0, 0);
107 if (prodId != id) continue;
108 if (mltDuration > 0 ) {
109 // We have several MLT producers for the same clip (probably track producers)
110 int newLength = EffectsList::property(mltProd, "length").toInt();
111 if (newLength != mltDuration) {
112 // we have a different duration for the same clip, that is not safe
113 e.setAttribute("_resetDuration", 1);
116 mltDuration = EffectsList::property(mltProd, "length").toInt();
117 if (mltDuration != duration) {
119 e.setAttribute("_mismatch", mltDuration);
120 if (mltDuration == 15000) {
121 // a length of 15000 might indicate a wrong clip length since it is a default length
122 e.setAttribute("_resetDuration", 1);
124 if (!wrongDurationClips.contains(e)) wrongDurationClips.append(e);
129 if (clipType == TEXT) {
130 //TODO: Check is clip template is missing (xmltemplate) or hash changed
131 QStringList images = TitleWidget::extractImageList(e.attribute("xmldata"));
132 QStringList fonts = TitleWidget::extractFontList(e.attribute("xmldata"));
133 checkMissingImagesAndFonts(images, fonts, e.attribute("id"), e.attribute("name"));
136 resource = e.attribute("resource");
137 if (e.hasAttribute("proxy")) {
138 QString proxyresource = e.attribute("proxy");
139 if (!proxyresource.isEmpty() && proxyresource != "-") {
141 if (!KIO::NetAccess::exists(KUrl(proxyresource), KIO::NetAccess::SourceSide, 0)) {
142 // Missing clip found
143 missingProxies.append(e);
145 else if (!KIO::NetAccess::exists(KUrl(resource), KIO::NetAccess::SourceSide, 0)) {
146 // clip has proxy but original clip is missing
147 missingSources.append(e);
152 if (clipType == SLIDESHOW) resource = KUrl(resource).directory();
153 if (!KIO::NetAccess::exists(KUrl(resource), KIO::NetAccess::SourceSide, 0)) {
154 // Missing clip found
155 m_missingClips.append(e);
157 // Check if the clip has changed
158 if (clipType != SLIDESHOW && e.hasAttribute("file_hash")) {
159 if (e.attribute("file_hash") != DocClipBase::getHash(e.attribute("resource")))
160 e.removeAttribute("file_hash");
165 // Get list of used Luma files
166 QStringList missingLumas;
167 QStringList filesToCheck;
169 QString root = m_doc.documentElement().attribute("root");
170 if (!root.isEmpty()) root = KUrl(root).path(KUrl::AddTrailingSlash);
171 QDomNodeList trans = m_doc.elementsByTagName("transition");
173 for (int i = 0; i < max; i++) {
174 QString luma = getProperty(trans.at(i).toElement(), "luma");
175 if (!luma.isEmpty() && !filesToCheck.contains(luma))
176 filesToCheck.append(luma);
178 // Check existence of luma files
179 foreach (const QString &lumafile, filesToCheck) {
181 if (!filePath.startsWith('/')) filePath.prepend(root);
182 if (!QFile::exists(filePath)) {
183 missingLumas.append(lumafile);
189 if (m_missingClips.isEmpty() && missingLumas.isEmpty() && wrongDurationClips.isEmpty() && missingProxies.isEmpty() && missingSources.isEmpty())
192 m_dialog = new QDialog();
193 m_dialog->setFont(KGlobalSettings::toolBarFont());
194 m_ui.setupUi(m_dialog);
196 foreach(const QString &l, missingLumas) {
197 QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Luma file") << l);
198 item->setIcon(0, KIcon("dialog-close"));
199 item->setData(0, idRole, l);
200 item->setData(0, statusRole, LUMAMISSING);
203 m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
204 max = m_missingClips.count();
205 for (int i = 0; i < max; i++) {
206 e = m_missingClips.at(i).toElement();
208 int t = e.attribute("type").toInt();
211 clipType = i18n("Video clip");
214 clipType = i18n("Mute video clip");
217 clipType = i18n("Audio clip");
220 clipType = i18n("Playlist clip");
223 clipType = i18n("Image clip");
226 clipType = i18n("Slideshow clip");
228 case TITLE_IMAGE_ELEMENT:
229 clipType = i18n("Title Image");
231 case TITLE_FONT_ELEMENT:
232 clipType = i18n("Title Font");
235 clipType = i18n("Video clip");
237 QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << clipType);
238 if (t == TITLE_IMAGE_ELEMENT) {
239 item->setIcon(0, KIcon("dialog-warning"));
240 item->setToolTip(1, e.attribute("name"));
241 item->setText(1, e.attribute("resource"));
242 item->setData(0, statusRole, CLIPPLACEHOLDER);
243 item->setData(0, typeOriginalResource, e.attribute("resource"));
244 } else if (t == TITLE_FONT_ELEMENT) {
245 item->setIcon(0, KIcon("dialog-warning"));
246 item->setToolTip(1, e.attribute("name"));
247 QString ft = e.attribute("resource");
248 QString newft = QFontInfo(QFont(ft)).family();
249 item->setText(1, i18n("%1 will be replaced by %2", ft, newft));
250 item->setData(0, statusRole, CLIPPLACEHOLDER);
252 item->setIcon(0, KIcon("dialog-close"));
253 item->setText(1, e.attribute("resource"));
254 item->setData(0, hashRole, e.attribute("file_hash"));
255 item->setData(0, sizeRole, e.attribute("file_size"));
256 item->setData(0, statusRole, CLIPMISSING);
258 item->setData(0, typeRole, t);
259 item->setData(0, idRole, e.attribute("id"));
260 item->setToolTip(0, i18n("Missing item"));
263 if (m_missingClips.count() > 0) {
264 if (wrongDurationClips.count() > 0) {
265 m_ui.infoLabel->setText(i18n("The project file contains missing clips or files and clip duration mismatch"));
268 m_ui.infoLabel->setText(i18n("The project file contains missing clips or files"));
271 else if (wrongDurationClips.count() > 0) {
272 m_ui.infoLabel->setText(i18n("The project file contains clips with duration mismatch"));
274 if (missingProxies.count() > 0) {
275 if (!m_ui.infoLabel->text().isEmpty()) m_ui.infoLabel->setText(m_ui.infoLabel->text() + ". ");
276 m_ui.infoLabel->setText(m_ui.infoLabel->text() + i18n("Missing proxies will be recreated after opening."));
278 if (missingSources.count() > 0) {
279 if (!m_ui.infoLabel->text().isEmpty()) m_ui.infoLabel->setText(m_ui.infoLabel->text() + ". ");
280 m_ui.infoLabel->setText(m_ui.infoLabel->text() + i18np("The project file contains a missing clip, you can still work with its proxy.", "The project file contains missing clips, you can still work with their proxies.", missingSources.count()));
283 m_ui.removeSelected->setEnabled(!m_missingClips.isEmpty());
284 m_ui.recursiveSearch->setEnabled(!m_missingClips.isEmpty() || !missingLumas.isEmpty() || !missingSources.isEmpty());
285 m_ui.usePlaceholders->setEnabled(!m_missingClips.isEmpty());
286 m_ui.fixDuration->setEnabled(!wrongDurationClips.isEmpty());
288 max = wrongDurationClips.count();
289 for (int i = 0; i < max; i++) {
290 e = wrongDurationClips.at(i).toElement();
292 int t = e.attribute("type").toInt();
295 clipType = i18n("Video clip");
298 clipType = i18n("Mute video clip");
301 clipType = i18n("Audio clip");
304 clipType = i18n("Playlist clip");
307 clipType = i18n("Image clip");
310 clipType = i18n("Slideshow clip");
313 clipType = i18n("Video clip");
315 QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << clipType);
316 item->setIcon(0, KIcon("timeadjust"));
317 item->setText(1, e.attribute("resource"));
318 item->setData(0, hashRole, e.attribute("file_hash"));
319 item->setData(0, sizeRole, e.attribute("_mismatch"));
320 e.removeAttribute("_mismatch");
321 item->setData(0, resetDurationRole, (int) e.hasAttribute("_resetDuration"));
322 e.removeAttribute("_resetDuration");
323 item->setData(0, statusRole, CLIPWRONGDURATION);
324 item->setData(0, typeRole, t);
325 item->setData(0, idRole, e.attribute("id"));
326 item->setToolTip(0, i18n("Duration mismatch"));
329 // Check missing proxies
330 max = missingProxies.count();
332 QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Proxy clip"));
333 item->setIcon(0, KIcon("dialog-warning"));
334 item->setText(1, i18n("%1 missing proxy clips, will be recreated on project opening", max));
335 item->setData(0, hashRole, e.attribute("file_hash"));
336 item->setData(0, statusRole, PROXYMISSING);
337 item->setToolTip(0, i18n("Missing proxy"));
340 for (int i = 0; i < max; i++) {
341 e = missingProxies.at(i).toElement();
343 QString realPath = e.attribute("resource");
344 QString id = e.attribute("id");
345 // Tell Kdenlive to recreate proxy
346 e.setAttribute("_replaceproxy", "1");
347 // Replace proxy url with real clip in MLT producers
348 QDomNodeList properties;
350 QDomElement property;
351 int prodsCount = documentProducers.count();
352 for (int j = 0; j < prodsCount; j++) {
353 mltProd = documentProducers.at(j).toElement();
354 QString prodId = mltProd.attribute("id");
355 bool slowmotion = false;
356 if (prodId.startsWith("slowmotion")) {
358 prodId = prodId.section(':', 1, 1);
360 if (prodId.contains('_')) prodId = prodId.section('_', 0, 0);
362 // Hit, we must replace url
363 properties = mltProd.childNodes();
364 for (int k = 0; k < properties.count(); ++k) {
365 property = properties.item(k).toElement();
366 if (property.attribute("name") == "resource") {
367 QString resource = property.firstChild().nodeValue();
369 if (slowmotion) suffix = '?' + resource.section('?', -1);
370 property.firstChild().setNodeValue(realPath + suffix);
379 // original doc was modified
380 QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
381 infoXml.setAttribute("modified", "1");
384 // Check clips with available proxies but missing original source clips
385 max = missingSources.count();
387 QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Source clip"));
388 item->setIcon(0, KIcon("dialog-warning"));
389 item->setText(1, i18n("%1 missing source clips, you can only use the proxies", max));
390 item->setData(0, hashRole, e.attribute("file_hash"));
391 item->setData(0, statusRole, SOURCEMISSING);
392 item->setToolTip(0, i18n("Missing source clip"));
393 for (int i = 0; i < max; i++) {
394 e = missingSources.at(i).toElement();
396 QString realPath = e.attribute("resource");
397 QString id = e.attribute("id");
398 // Tell Kdenlive the source is missing
399 e.setAttribute("_missingsource", "1");
400 QTreeWidgetItem *subitem = new QTreeWidgetItem(item, QStringList() << i18n("Source clip"));
401 kDebug()<<"// Adding missing source clip: "<<realPath;
402 subitem->setIcon(0, KIcon("dialog-close"));
403 subitem->setText(1, realPath);
404 subitem->setData(0, hashRole, e.attribute("file_hash"));
405 subitem->setData(0, sizeRole, e.attribute("file_size"));
406 subitem->setData(0, statusRole, CLIPMISSING);
407 int t = e.attribute("type").toInt();
408 subitem->setData(0, typeRole, t);
409 subitem->setData(0, idRole, id);
414 // original doc was modified
415 QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
416 infoXml.setAttribute("modified", "1");
419 connect(m_ui.recursiveSearch, SIGNAL(pressed()), this, SLOT(slotSearchClips()));
420 connect(m_ui.usePlaceholders, SIGNAL(pressed()), this, SLOT(slotPlaceholders()));
421 connect(m_ui.removeSelected, SIGNAL(pressed()), this, SLOT(slotDeleteSelected()));
422 connect(m_ui.fixDuration, SIGNAL(pressed()), this, SLOT(slotFixDuration()));
423 connect(m_ui.treeWidget, SIGNAL(itemDoubleClicked(QTreeWidgetItem *, int)), this, SLOT(slotEditItem(QTreeWidgetItem *, int)));
424 connect(m_ui.treeWidget, SIGNAL(itemSelectionChanged()), this, SLOT(slotCheckButtons()));
426 if (m_ui.treeWidget->topLevelItem(0)) m_ui.treeWidget->setCurrentItem(m_ui.treeWidget->topLevelItem(0));
428 int acceptMissing = m_dialog->exec();
429 if (acceptMissing == QDialog::Accepted) acceptDialog();
430 return (acceptMissing != QDialog::Accepted);
433 DocumentChecker::~DocumentChecker()
435 if (m_dialog) delete m_dialog;
439 QString DocumentChecker::getProperty(QDomElement effect, const QString &name)
441 QDomNodeList params = effect.elementsByTagName("property");
442 for (int i = 0; i < params.count(); i++) {
443 QDomElement e = params.item(i).toElement();
444 if (e.attribute("name") == name) {
445 return e.firstChild().nodeValue();
451 void DocumentChecker::setProperty(QDomElement effect, const QString &name, const QString value)
453 QDomNodeList params = effect.elementsByTagName("property");
454 for (int i = 0; i < params.count(); i++) {
455 QDomElement e = params.item(i).toElement();
456 if (e.attribute("name") == name) {
457 e.firstChild().setNodeValue(value);
462 void DocumentChecker::slotSearchClips()
464 QString newpath = KFileDialog::getExistingDirectory(KUrl("kfiledialog:///clipfolder"), kapp->activeWindow(), i18n("Clips folder"));
465 if (newpath.isEmpty()) return;
468 m_ui.recursiveSearch->setEnabled(false);
469 QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
470 QDir searchDir(newpath);
472 if (child->data(0, statusRole).toInt() == SOURCEMISSING) {
473 for (int j = 0; j < child->childCount(); j++) {
474 QTreeWidgetItem *subchild = child->child(j);
475 QString clipPath = searchFileRecursively(searchDir, subchild->data(0, sizeRole).toString(), subchild->data(0, hashRole).toString());
476 if (!clipPath.isEmpty()) {
479 subchild->setText(1, clipPath);
480 subchild->setIcon(0, KIcon("dialog-ok"));
481 subchild->setData(0, statusRole, CLIPOK);
485 else if (child->data(0, statusRole).toInt() == CLIPMISSING) {
486 QString clipPath = searchFileRecursively(searchDir, child->data(0, sizeRole).toString(), child->data(0, hashRole).toString());
487 if (!clipPath.isEmpty()) {
489 child->setText(1, clipPath);
490 child->setIcon(0, KIcon("dialog-ok"));
491 child->setData(0, statusRole, CLIPOK);
493 } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
494 QString fileName = searchLuma(searchDir, child->data(0, idRole).toString());
495 if (!fileName.isEmpty()) {
497 child->setText(1, fileName);
498 child->setIcon(0, KIcon("dialog-ok"));
499 child->setData(0, statusRole, LUMAOK);
502 else if (child->data(0, typeRole).toInt() == TITLE_IMAGE_ELEMENT && child->data(0, statusRole).toInt() == CLIPPLACEHOLDER) {
503 // Search missing title images
504 QString missingFileName = KUrl(child->text(1)).fileName();
505 QString newPath = searchPathRecursively(searchDir, missingFileName);
506 if (!newPath.isEmpty()) {
509 child->setText(1, newPath);
510 child->setIcon(0, KIcon("dialog-ok"));
511 child->setData(0, statusRole, CLIPOK);
515 child = m_ui.treeWidget->topLevelItem(ix);
517 m_ui.recursiveSearch->setEnabled(true);
519 // original doc was modified
520 QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
521 infoXml.setAttribute("modified", "1");
527 QString DocumentChecker::searchLuma(const QDir &dir, const QString &file) const
529 KUrl searchPath(KdenliveSettings::mltpath());
530 QString fname = KUrl(file).fileName();
531 if (file.contains("PAL"))
532 searchPath.cd("../lumas/PAL");
534 searchPath.cd("../lumas/NTSC");
535 QString result = searchPath.path(KUrl::AddTrailingSlash) + fname;
536 if (QFile::exists(result))
538 // try to find luma in application path
540 searchPath = KUrl(QCoreApplication::applicationDirPath());
541 searchPath.cd("../share/apps/kdenlive/lumas");
542 result = searchPath.path(KUrl::AddTrailingSlash) + fname;
543 if (QFile::exists(result))
545 // Try in Kdenlive's standard KDE path
546 result = KStandardDirs::locate("appdata", "lumas/" + fname);
547 if (!result.isEmpty()) return result;
548 // Try in user's chosen folder
549 return searchPathRecursively(dir, fname);
552 QString DocumentChecker::searchPathRecursively(const QDir &dir, const QString &fileName) const
554 QString foundFileName;
558 searchDir.setNameFilters(filters);
559 QStringList filesAndDirs = searchDir.entryList(QDir::Files | QDir::Readable);
560 if (!filesAndDirs.isEmpty()) return searchDir.absoluteFilePath(filesAndDirs.at(0));
561 searchDir.setNameFilters(QStringList());
562 filesAndDirs = searchDir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot);
563 for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
564 foundFileName = searchPathRecursively(searchDir.absoluteFilePath(filesAndDirs.at(i)), fileName);
565 if (!foundFileName.isEmpty())
568 return foundFileName;
571 QString DocumentChecker::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const
573 QString foundFileName;
576 QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable);
577 for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
578 QFile file(dir.absoluteFilePath(filesAndDirs.at(i)));
579 if (QString::number(file.size()) == matchSize) {
580 if (file.open(QIODevice::ReadOnly)) {
582 * 1 MB = 1 second per 450 files (or faster)
583 * 10 MB = 9 seconds per 450 files (or faster)
585 if (file.size() > 1000000 * 2) {
586 fileData = file.read(1000000);
587 if (file.seek(file.size() - 1000000))
588 fileData.append(file.readAll());
590 fileData = file.readAll();
592 fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
593 if (QString(fileHash.toHex()) == matchHash)
594 return file.fileName();
597 //kDebug() << filesAndDirs.at(i) << file.size() << fileHash.toHex();
599 filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot);
600 for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); i++) {
601 foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash);
602 if (!foundFileName.isEmpty())
605 return foundFileName;
608 void DocumentChecker::slotEditItem(QTreeWidgetItem *item, int)
610 int t = item->data(0, typeRole).toInt();
611 if (t == TITLE_FONT_ELEMENT || t == UNKNOWN) return;
612 //|| t == TITLE_IMAGE_ELEMENT) {
614 KUrl url = KUrlRequesterDialog::getUrl(item->text(1), m_dialog, i18n("Enter new location for file"));
615 if (url.isEmpty()) return;
616 item->setText(1, url.path());
617 if (KIO::NetAccess::exists(url, KIO::NetAccess::SourceSide, 0)) {
618 item->setIcon(0, KIcon("dialog-ok"));
619 int id = item->data(0, statusRole).toInt();
620 if (id < 10) item->setData(0, statusRole, CLIPOK);
621 else item->setData(0, statusRole, LUMAOK);
624 item->setIcon(0, KIcon("dialog-close"));
625 int id = item->data(0, statusRole).toInt();
626 if (id < 10) item->setData(0, statusRole, CLIPMISSING);
627 else item->setData(0, statusRole, LUMAMISSING);
633 void DocumentChecker::acceptDialog()
635 QDomNodeList producers = m_doc.elementsByTagName("producer");
636 QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
639 // prepare transitions
640 QDomNodeList trans = m_doc.elementsByTagName("transition");
642 // Mark document as modified
643 m_doc.documentElement().setAttribute("modified", 1);
645 QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
647 if (child->data(0, statusRole).toInt() == SOURCEMISSING) {
648 for (int j = 0; j < child->childCount(); j++) {
649 fixClipItem(child->child(j), producers, infoproducers, trans);
652 else fixClipItem(child, producers, infoproducers, trans);
654 child = m_ui.treeWidget->topLevelItem(ix);
659 void DocumentChecker::fixClipItem(QTreeWidgetItem *child, QDomNodeList producers, QDomNodeList infoproducers, QDomNodeList trans)
661 QDomElement e, property;
662 QDomNodeList properties;
663 int t = child->data(0, typeRole).toInt();
664 if (child->data(0, statusRole).toInt() == CLIPOK) {
665 QString id = child->data(0, idRole).toString();
666 if (t == TITLE_IMAGE_ELEMENT) {
667 // edit images embedded in titles
668 for (int i = 0; i < infoproducers.count(); i++) {
669 e = infoproducers.item(i).toElement();
670 if (e.attribute("id") == id) {
672 QString xml = e.attribute("xmldata");
673 xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
674 e.setAttribute("xmldata", xml);
678 for (int i = 0; i < producers.count(); i++) {
679 e = producers.item(i).toElement();
680 if (e.attribute("id").section('_', 0, 0) == id) {
682 properties = e.childNodes();
683 for (int j = 0; j < properties.count(); ++j) {
684 property = properties.item(j).toElement();
685 if (property.attribute("name") == "xmldata") {
686 QString xml = property.firstChild().nodeValue();
687 xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
688 property.firstChild().setNodeValue(xml);
696 for (int i = 0; i < infoproducers.count(); i++) {
697 e = infoproducers.item(i).toElement();
698 if (e.attribute("id") == id) {
700 e.setAttribute("resource", child->text(1));
701 e.setAttribute("name", KUrl(child->text(1)).fileName());
702 e.removeAttribute("_missingsource");
706 for (int i = 0; i < producers.count(); i++) {
707 e = producers.item(i).toElement();
708 if (e.attribute("id").section('_', 0, 0) == id || e.attribute("id").section(':', 1, 1) == id) {
710 properties = e.childNodes();
711 for (int j = 0; j < properties.count(); ++j) {
712 property = properties.item(j).toElement();
713 if (property.attribute("name") == "resource") {
714 QString resource = property.firstChild().nodeValue();
715 if (resource.contains(QRegExp("\\?[0-9]+\\.[0-9]+(&strobe=[0-9]+)?$")))
716 property.firstChild().setNodeValue(child->text(1) + '?' + resource.section('?', -1));
718 property.firstChild().setNodeValue(child->text(1));
725 } else if (child->data(0, statusRole).toInt() == CLIPPLACEHOLDER && t != TITLE_FONT_ELEMENT && t != TITLE_IMAGE_ELEMENT) {
726 QString id = child->data(0, idRole).toString();
727 for (int i = 0; i < infoproducers.count(); i++) {
728 e = infoproducers.item(i).toElement();
729 if (e.attribute("id") == id) {
731 e.setAttribute("placeholder", '1');
735 } else if (child->data(0, statusRole).toInt() == LUMAOK) {
736 for (int i = 0; i < trans.count(); i++) {
737 QString luma = getProperty(trans.at(i).toElement(), "luma");
738 if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
739 setProperty(trans.at(i).toElement(), "luma", child->text(1));
740 kDebug() << "replace with; " << child->text(1);
743 } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
744 for (int i = 0; i < trans.count(); i++) {
745 QString luma = getProperty(trans.at(i).toElement(), "luma");
746 if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
747 setProperty(trans.at(i).toElement(), "luma", QString());
753 void DocumentChecker::slotPlaceholders()
756 QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
758 if (child->data(0, statusRole).toInt() == CLIPMISSING) {
759 child->setData(0, statusRole, CLIPPLACEHOLDER);
760 child->setIcon(0, KIcon("dialog-ok"));
761 } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
762 child->setData(0, statusRole, LUMAPLACEHOLDER);
763 child->setIcon(0, KIcon("dialog-ok"));
766 child = m_ui.treeWidget->topLevelItem(ix);
771 void DocumentChecker::slotFixDuration()
774 QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
775 QDomNodeList documentProducers = m_doc.elementsByTagName("producer");
777 if (child->data(0, statusRole).toInt() == CLIPWRONGDURATION) {
778 QString id = child->data(0, idRole).toString();
779 bool resetDuration = child->data(0, resetDurationRole).toInt();
781 for (int i = 0; i < m_info.count(); i++) {
782 QDomElement e = m_info.at(i).toElement();
783 if (e.attribute("id") == id) {
784 if (m_missingClips.contains(e)) {
785 // we cannot fix duration of missing clips
786 resetDuration = false;
789 if (resetDuration) e.removeAttribute("duration");
790 else e.setAttribute("duration", child->data(0, sizeRole).toString());
791 child->setData(0, statusRole, CLIPOK);
792 child->setIcon(0, KIcon("dialog-ok"));
798 // something is wrong in clip durations, so remove them so mlt fetches them again
799 for (int j = 0; j < documentProducers.count(); j++) {
800 QDomElement mltProd = documentProducers.at(j).toElement();
801 QString prodId = mltProd.attribute("id");
802 if (prodId == id || prodId.startsWith(id + '_')) {
803 EffectsList::removeProperty(mltProd, "length");
809 child = m_ui.treeWidget->topLevelItem(ix);
811 QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
812 infoXml.setAttribute("modified", "1");
813 m_ui.fixDuration->setEnabled(false);
818 void DocumentChecker::checkStatus()
822 QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
824 int status = child->data(0, statusRole).toInt();
825 if (status == CLIPMISSING || status == LUMAMISSING || status == CLIPWRONGDURATION) {
830 child = m_ui.treeWidget->topLevelItem(ix);
832 m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status);
836 void DocumentChecker::slotDeleteSelected()
838 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)
840 QStringList deletedIds;
841 QStringList deletedLumas;
842 QDomNodeList playlists = m_doc.elementsByTagName("playlist");
844 foreach(QTreeWidgetItem *child, m_ui.treeWidget->selectedItems()) {
845 int id = child->data(0, statusRole).toInt();
846 if (id == CLIPMISSING) {
847 deletedIds.append(child->data(0, idRole).toString());
850 else if (id == LUMAMISSING) {
851 deletedLumas.append(child->data(0, idRole).toString());
856 if (!deletedLumas.isEmpty()) {
858 QDomNodeList transitions = m_doc.elementsByTagName("transition");
859 foreach (const QString &lumaPath, deletedLumas) {
860 for (int i = 0; i < transitions.count(); i++) {
861 e = transitions.item(i).toElement();
862 QString resource = EffectsList::property(e, "luma");
863 if (resource == lumaPath) EffectsList::removeProperty(e, "luma");
868 if (!deletedIds.isEmpty()) {
870 QDomNodeList producers = m_doc.elementsByTagName("producer");
871 QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
873 QDomNode mlt = m_doc.elementsByTagName("mlt").at(0);
874 QDomNode kdenlivedoc = m_doc.elementsByTagName("kdenlivedoc").at(0);
876 for (int i = 0, j = 0; i < infoproducers.count() && j < deletedIds.count(); i++) {
877 e = infoproducers.item(i).toElement();
878 if (deletedIds.contains(e.attribute("id"))) {
880 kdenlivedoc.removeChild(e);
886 for (int i = 0; i < producers.count(); i++) {
887 e = producers.item(i).toElement();
888 if (deletedIds.contains(e.attribute("id").section('_', 0, 0)) || deletedIds.contains(e.attribute("id").section(':', 1, 1).section('_', 0, 0))) {
895 for (int i = 0; i < playlists.count(); i++) {
896 QDomNodeList entries = playlists.at(i).toElement().elementsByTagName("entry");
897 for (int j = 0; j < entries.count(); j++) {
898 e = entries.item(j).toElement();
899 if (deletedIds.contains(e.attribute("producer").section('_', 0, 0)) || deletedIds.contains(e.attribute("producer").section(':', 1, 1).section('_', 0, 0))) {
900 // Replace clip with blank
901 while (e.childNodes().count() > 0)
902 e.removeChild(e.firstChild());
903 e.setTagName("blank");
904 e.removeAttribute("producer");
905 int length = e.attribute("out").toInt() - e.attribute("in").toInt();
906 e.setAttribute("length", length);
911 QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
912 infoXml.setAttribute("modified", "1");
917 void DocumentChecker::checkMissingImagesAndFonts(QStringList images, QStringList fonts, const QString &id, const QString &baseClip)
920 foreach(const QString &img, images) {
921 if (m_safeImages.contains(img)) continue;
922 if (!KIO::NetAccess::exists(KUrl(img), KIO::NetAccess::SourceSide, 0)) {
923 QDomElement e = doc.createElement("missingclip");
924 e.setAttribute("type", TITLE_IMAGE_ELEMENT);
925 e.setAttribute("resource", img);
926 e.setAttribute("id", id);
927 e.setAttribute("name", baseClip);
928 m_missingClips.append(e);
930 else m_safeImages.append(img);
932 foreach(const QString &fontelement, fonts) {
933 if (m_safeFonts.contains(fontelement)) continue;
934 QFont f(fontelement);
935 //kDebug() << "/ / / CHK FONTS: " << fontelement << " = " << QFontInfo(f).family();
936 if (fontelement != QFontInfo(f).family()) {
937 QDomElement e = doc.createElement("missingclip");
938 e.setAttribute("type", TITLE_FONT_ELEMENT);
939 e.setAttribute("resource", fontelement);
940 e.setAttribute("id", id);
941 e.setAttribute("name", baseClip);
942 m_missingClips.append(e);
944 else m_safeFonts.append(fontelement);
949 void DocumentChecker::slotCheckButtons()
951 if (m_ui.treeWidget->currentItem()) {
952 QTreeWidgetItem *item = m_ui.treeWidget->currentItem();
953 int t = item->data(0, typeRole).toInt();
954 int s = item->data(0, statusRole).toInt();
955 if (t == TITLE_FONT_ELEMENT || t == TITLE_IMAGE_ELEMENT || s == PROXYMISSING) {
956 m_ui.removeSelected->setEnabled(false);
957 } else m_ui.removeSelected->setEnabled(true);
962 #include "documentchecker.moc"