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(const QDomNodeList &infoproducers, const 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();
342 QString realPath = e.attribute("resource");
343 QString id = e.attribute("id");
344 // Tell Kdenlive to recreate proxy
345 e.setAttribute("_replaceproxy", "1");
346 // Replace proxy url with real clip in MLT producers
347 QDomNodeList properties;
349 QDomElement property;
350 int prodsCount = documentProducers.count();
351 for (int j = 0; j < prodsCount; j++) {
352 mltProd = documentProducers.at(j).toElement();
353 QString prodId = mltProd.attribute("id");
354 bool slowmotion = false;
355 if (prodId.startsWith("slowmotion")) {
357 prodId = prodId.section(':', 1, 1);
359 if (prodId.contains('_')) prodId = prodId.section('_', 0, 0);
361 // Hit, we must replace url
362 properties = mltProd.childNodes();
363 for (int k = 0; k < properties.count(); ++k) {
364 property = properties.item(k).toElement();
365 if (property.attribute("name") == "resource") {
366 QString resource = property.firstChild().nodeValue();
368 if (slowmotion) suffix = '?' + resource.section('?', -1);
369 property.firstChild().setNodeValue(realPath + suffix);
378 // original doc was modified
379 QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
380 infoXml.setAttribute("modified", "1");
383 // Check clips with available proxies but missing original source clips
384 max = missingSources.count();
386 QTreeWidgetItem *item = new QTreeWidgetItem(m_ui.treeWidget, QStringList() << i18n("Source clip"));
387 item->setIcon(0, KIcon("dialog-warning"));
388 item->setText(1, i18n("%1 missing source clips, you can only use the proxies", max));
389 item->setData(0, hashRole, e.attribute("file_hash"));
390 item->setData(0, statusRole, SOURCEMISSING);
391 item->setToolTip(0, i18n("Missing source clip"));
392 for (int i = 0; i < max; ++i) {
393 e = missingSources.at(i).toElement();
395 QString realPath = e.attribute("resource");
396 QString id = e.attribute("id");
397 // Tell Kdenlive the source is missing
398 e.setAttribute("_missingsource", "1");
399 QTreeWidgetItem *subitem = new QTreeWidgetItem(item, QStringList() << i18n("Source clip"));
400 kDebug()<<"// Adding missing source clip: "<<realPath;
401 subitem->setIcon(0, KIcon("dialog-close"));
402 subitem->setText(1, realPath);
403 subitem->setData(0, hashRole, e.attribute("file_hash"));
404 subitem->setData(0, sizeRole, e.attribute("file_size"));
405 subitem->setData(0, statusRole, CLIPMISSING);
406 int t = e.attribute("type").toInt();
407 subitem->setData(0, typeRole, t);
408 subitem->setData(0, idRole, id);
413 // original doc was modified
414 QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
415 infoXml.setAttribute("modified", "1");
418 connect(m_ui.recursiveSearch, SIGNAL(pressed()), this, SLOT(slotSearchClips()));
419 connect(m_ui.usePlaceholders, SIGNAL(pressed()), this, SLOT(slotPlaceholders()));
420 connect(m_ui.removeSelected, SIGNAL(pressed()), this, SLOT(slotDeleteSelected()));
421 connect(m_ui.fixDuration, SIGNAL(pressed()), this, SLOT(slotFixDuration()));
422 connect(m_ui.treeWidget, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)), this, SLOT(slotEditItem(QTreeWidgetItem*,int)));
423 connect(m_ui.treeWidget, SIGNAL(itemSelectionChanged()), this, SLOT(slotCheckButtons()));
425 if (m_ui.treeWidget->topLevelItem(0)) m_ui.treeWidget->setCurrentItem(m_ui.treeWidget->topLevelItem(0));
427 int acceptMissing = m_dialog->exec();
428 if (acceptMissing == QDialog::Accepted) acceptDialog();
429 return (acceptMissing != QDialog::Accepted);
432 DocumentChecker::~DocumentChecker()
438 QString DocumentChecker::getProperty(QDomElement effect, const QString &name)
440 QDomNodeList params = effect.elementsByTagName("property");
441 for (int i = 0; i < params.count(); ++i) {
442 QDomElement e = params.item(i).toElement();
443 if (e.attribute("name") == name) {
444 return e.firstChild().nodeValue();
450 void DocumentChecker::setProperty(QDomElement effect, const QString &name, const QString &value)
452 QDomNodeList params = effect.elementsByTagName("property");
453 for (int i = 0; i < params.count(); ++i) {
454 QDomElement e = params.item(i).toElement();
455 if (e.attribute("name") == name) {
456 e.firstChild().setNodeValue(value);
461 void DocumentChecker::slotSearchClips()
463 QString newpath = KFileDialog::getExistingDirectory(KUrl("kfiledialog:///clipfolder"), kapp->activeWindow(), i18n("Clips folder"));
464 if (newpath.isEmpty()) return;
467 m_ui.recursiveSearch->setChecked(true);
468 qApp->processEvents();
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->setChecked(false);
518 m_ui.recursiveSearch->setEnabled(true);
520 // original doc was modified
521 QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
522 infoXml.setAttribute("modified", "1");
528 QString DocumentChecker::searchLuma(const QDir &dir, const QString &file) const
530 KUrl searchPath(KdenliveSettings::mltpath());
531 QString fname = KUrl(file).fileName();
532 if (file.contains("PAL"))
533 searchPath.cd("../lumas/PAL");
535 searchPath.cd("../lumas/NTSC");
536 QString result = searchPath.path(KUrl::AddTrailingSlash) + fname;
537 if (QFile::exists(result))
539 // try to find luma in application path
541 searchPath = KUrl(QCoreApplication::applicationDirPath());
542 searchPath.cd("../share/apps/kdenlive/lumas");
543 result = searchPath.path(KUrl::AddTrailingSlash) + fname;
544 if (QFile::exists(result))
546 // Try in Kdenlive's standard KDE path
547 result = KStandardDirs::locate("appdata", "lumas/" + fname);
548 if (!result.isEmpty()) return result;
549 // Try in user's chosen folder
550 return searchPathRecursively(dir, fname);
553 QString DocumentChecker::searchPathRecursively(const QDir &dir, const QString &fileName) const
555 QString foundFileName;
559 searchDir.setNameFilters(filters);
560 QStringList filesAndDirs = searchDir.entryList(QDir::Files | QDir::Readable);
561 if (!filesAndDirs.isEmpty()) return searchDir.absoluteFilePath(filesAndDirs.at(0));
562 searchDir.setNameFilters(QStringList());
563 filesAndDirs = searchDir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot);
564 for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) {
565 foundFileName = searchPathRecursively(searchDir.absoluteFilePath(filesAndDirs.at(i)), fileName);
566 if (!foundFileName.isEmpty())
569 return foundFileName;
572 QString DocumentChecker::searchFileRecursively(const QDir &dir, const QString &matchSize, const QString &matchHash) const
574 QString foundFileName;
577 QStringList filesAndDirs = dir.entryList(QDir::Files | QDir::Readable);
578 for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) {
579 QFile file(dir.absoluteFilePath(filesAndDirs.at(i)));
580 if (QString::number(file.size()) == matchSize) {
581 if (file.open(QIODevice::ReadOnly)) {
583 * 1 MB = 1 second per 450 files (or faster)
584 * 10 MB = 9 seconds per 450 files (or faster)
586 if (file.size() > 1000000 * 2) {
587 fileData = file.read(1000000);
588 if (file.seek(file.size() - 1000000))
589 fileData.append(file.readAll());
591 fileData = file.readAll();
593 fileHash = QCryptographicHash::hash(fileData, QCryptographicHash::Md5);
594 if (QString(fileHash.toHex()) == matchHash)
595 return file.fileName();
598 //kDebug() << filesAndDirs.at(i) << file.size() << fileHash.toHex();
600 filesAndDirs = dir.entryList(QDir::Dirs | QDir::Readable | QDir::Executable | QDir::NoDotAndDotDot);
601 for (int i = 0; i < filesAndDirs.size() && foundFileName.isEmpty(); ++i) {
602 foundFileName = searchFileRecursively(dir.absoluteFilePath(filesAndDirs.at(i)), matchSize, matchHash);
603 if (!foundFileName.isEmpty())
606 return foundFileName;
609 void DocumentChecker::slotEditItem(QTreeWidgetItem *item, int)
611 int t = item->data(0, typeRole).toInt();
612 if (t == TITLE_FONT_ELEMENT || t == UNKNOWN) return;
613 //|| t == TITLE_IMAGE_ELEMENT) {
615 KUrl url = KUrlRequesterDialog::getUrl(item->text(1), m_dialog, i18n("Enter new location for file"));
616 if (url.isEmpty()) return;
617 item->setText(1, url.path());
618 if (KIO::NetAccess::exists(url, KIO::NetAccess::SourceSide, 0)) {
619 item->setIcon(0, KIcon("dialog-ok"));
620 int id = item->data(0, statusRole).toInt();
621 if (id < 10) item->setData(0, statusRole, CLIPOK);
622 else item->setData(0, statusRole, LUMAOK);
625 item->setIcon(0, KIcon("dialog-close"));
626 int id = item->data(0, statusRole).toInt();
627 if (id < 10) item->setData(0, statusRole, CLIPMISSING);
628 else item->setData(0, statusRole, LUMAMISSING);
634 void DocumentChecker::acceptDialog()
636 QDomNodeList producers = m_doc.elementsByTagName("producer");
637 QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
640 // prepare transitions
641 QDomNodeList trans = m_doc.elementsByTagName("transition");
643 // Mark document as modified
644 m_doc.documentElement().setAttribute("modified", 1);
646 QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
648 if (child->data(0, statusRole).toInt() == SOURCEMISSING) {
649 for (int j = 0; j < child->childCount(); j++) {
650 fixClipItem(child->child(j), producers, infoproducers, trans);
653 else fixClipItem(child, producers, infoproducers, trans);
655 child = m_ui.treeWidget->topLevelItem(ix);
660 void DocumentChecker::fixClipItem(QTreeWidgetItem *child, QDomNodeList producers, QDomNodeList infoproducers, QDomNodeList trans)
662 QDomElement e, property;
663 QDomNodeList properties;
664 int t = child->data(0, typeRole).toInt();
665 if (child->data(0, statusRole).toInt() == CLIPOK) {
666 QString id = child->data(0, idRole).toString();
667 if (t == TITLE_IMAGE_ELEMENT) {
668 // edit images embedded in titles
669 for (int i = 0; i < infoproducers.count(); ++i) {
670 e = infoproducers.item(i).toElement();
671 if (e.attribute("id") == id) {
673 QString xml = e.attribute("xmldata");
674 xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
675 e.setAttribute("xmldata", xml);
679 for (int i = 0; i < producers.count(); ++i) {
680 e = producers.item(i).toElement();
681 if (e.attribute("id").section('_', 0, 0) == id) {
683 properties = e.childNodes();
684 for (int j = 0; j < properties.count(); ++j) {
685 property = properties.item(j).toElement();
686 if (property.attribute("name") == "xmldata") {
687 QString xml = property.firstChild().nodeValue();
688 xml.replace(child->data(0, typeOriginalResource).toString(), child->text(1));
689 property.firstChild().setNodeValue(xml);
697 for (int i = 0; i < infoproducers.count(); ++i) {
698 e = infoproducers.item(i).toElement();
699 if (e.attribute("id") == id) {
701 e.setAttribute("resource", child->text(1));
702 e.setAttribute("name", KUrl(child->text(1)).fileName());
703 e.removeAttribute("_missingsource");
707 for (int i = 0; i < producers.count(); ++i) {
708 e = producers.item(i).toElement();
709 if (e.attribute("id").section('_', 0, 0) == id || e.attribute("id").section(':', 1, 1) == id) {
711 properties = e.childNodes();
712 for (int j = 0; j < properties.count(); ++j) {
713 property = properties.item(j).toElement();
714 if (property.attribute("name") == "resource") {
715 QString resource = property.firstChild().nodeValue();
716 if (resource.contains(QRegExp("\\?[0-9]+\\.[0-9]+(&strobe=[0-9]+)?$")))
717 property.firstChild().setNodeValue(child->text(1) + '?' + resource.section('?', -1));
719 property.firstChild().setNodeValue(child->text(1));
726 } else if (child->data(0, statusRole).toInt() == CLIPPLACEHOLDER && t != TITLE_FONT_ELEMENT && t != TITLE_IMAGE_ELEMENT) {
727 QString id = child->data(0, idRole).toString();
728 for (int i = 0; i < infoproducers.count(); ++i) {
729 e = infoproducers.item(i).toElement();
730 if (e.attribute("id") == id) {
732 e.setAttribute("placeholder", '1');
736 } else if (child->data(0, statusRole).toInt() == LUMAOK) {
737 for (int i = 0; i < trans.count(); ++i) {
738 QString luma = getProperty(trans.at(i).toElement(), "luma");
739 if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
740 setProperty(trans.at(i).toElement(), "luma", child->text(1));
741 kDebug() << "replace with; " << child->text(1);
744 } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
745 for (int i = 0; i < trans.count(); ++i) {
746 QString luma = getProperty(trans.at(i).toElement(), "luma");
747 if (!luma.isEmpty() && luma == child->data(0, idRole).toString()) {
748 setProperty(trans.at(i).toElement(), "luma", QString());
754 void DocumentChecker::slotPlaceholders()
757 QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
759 if (child->data(0, statusRole).toInt() == CLIPMISSING) {
760 child->setData(0, statusRole, CLIPPLACEHOLDER);
761 child->setIcon(0, KIcon("dialog-ok"));
762 } else if (child->data(0, statusRole).toInt() == LUMAMISSING) {
763 child->setData(0, statusRole, LUMAPLACEHOLDER);
764 child->setIcon(0, KIcon("dialog-ok"));
767 child = m_ui.treeWidget->topLevelItem(ix);
772 void DocumentChecker::slotFixDuration()
775 QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
776 QDomNodeList documentProducers = m_doc.elementsByTagName("producer");
778 if (child->data(0, statusRole).toInt() == CLIPWRONGDURATION) {
779 QString id = child->data(0, idRole).toString();
780 bool resetDuration = child->data(0, resetDurationRole).toInt();
782 for (int i = 0; i < m_info.count(); ++i) {
783 QDomElement e = m_info.at(i).toElement();
784 if (e.attribute("id") == id) {
785 if (m_missingClips.contains(e)) {
786 // we cannot fix duration of missing clips
787 resetDuration = false;
790 if (resetDuration) e.removeAttribute("duration");
791 else e.setAttribute("duration", child->data(0, sizeRole).toString());
792 child->setData(0, statusRole, CLIPOK);
793 child->setIcon(0, KIcon("dialog-ok"));
799 // something is wrong in clip durations, so remove them so mlt fetches them again
800 for (int j = 0; j < documentProducers.count(); j++) {
801 QDomElement mltProd = documentProducers.at(j).toElement();
802 QString prodId = mltProd.attribute("id");
803 if (prodId == id || prodId.startsWith(id + '_')) {
804 EffectsList::removeProperty(mltProd, "length");
810 child = m_ui.treeWidget->topLevelItem(ix);
812 QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
813 infoXml.setAttribute("modified", "1");
814 m_ui.fixDuration->setEnabled(false);
819 void DocumentChecker::checkStatus()
823 QTreeWidgetItem *child = m_ui.treeWidget->topLevelItem(ix);
825 int status = child->data(0, statusRole).toInt();
826 if (status == CLIPMISSING || status == LUMAMISSING || status == CLIPWRONGDURATION) {
831 child = m_ui.treeWidget->topLevelItem(ix);
833 m_ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(status);
837 void DocumentChecker::slotDeleteSelected()
839 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)
841 QStringList deletedIds;
842 QStringList deletedLumas;
843 QDomNodeList playlists = m_doc.elementsByTagName("playlist");
845 foreach(QTreeWidgetItem *child, m_ui.treeWidget->selectedItems()) {
846 int id = child->data(0, statusRole).toInt();
847 if (id == CLIPMISSING) {
848 deletedIds.append(child->data(0, idRole).toString());
851 else if (id == LUMAMISSING) {
852 deletedLumas.append(child->data(0, idRole).toString());
857 if (!deletedLumas.isEmpty()) {
859 QDomNodeList transitions = m_doc.elementsByTagName("transition");
860 foreach (const QString &lumaPath, deletedLumas) {
861 for (int i = 0; i < transitions.count(); ++i) {
862 e = transitions.item(i).toElement();
863 QString resource = EffectsList::property(e, "luma");
864 if (resource == lumaPath) EffectsList::removeProperty(e, "luma");
869 if (!deletedIds.isEmpty()) {
871 QDomNodeList producers = m_doc.elementsByTagName("producer");
872 QDomNodeList infoproducers = m_doc.elementsByTagName("kdenlive_producer");
874 QDomNode mlt = m_doc.elementsByTagName("mlt").at(0);
875 QDomNode kdenlivedoc = m_doc.elementsByTagName("kdenlivedoc").at(0);
877 for (int i = 0, j = 0; i < infoproducers.count() && j < deletedIds.count(); ++i) {
878 e = infoproducers.item(i).toElement();
879 if (deletedIds.contains(e.attribute("id"))) {
881 kdenlivedoc.removeChild(e);
887 for (int i = 0; i < producers.count(); ++i) {
888 e = producers.item(i).toElement();
889 if (deletedIds.contains(e.attribute("id").section('_', 0, 0)) || deletedIds.contains(e.attribute("id").section(':', 1, 1).section('_', 0, 0))) {
896 for (int i = 0; i < playlists.count(); ++i) {
897 QDomNodeList entries = playlists.at(i).toElement().elementsByTagName("entry");
898 for (int j = 0; j < entries.count(); j++) {
899 e = entries.item(j).toElement();
900 if (deletedIds.contains(e.attribute("producer").section('_', 0, 0)) || deletedIds.contains(e.attribute("producer").section(':', 1, 1).section('_', 0, 0))) {
901 // Replace clip with blank
902 while (e.childNodes().count() > 0)
903 e.removeChild(e.firstChild());
904 e.setTagName("blank");
905 e.removeAttribute("producer");
906 int length = e.attribute("out").toInt() - e.attribute("in").toInt();
907 e.setAttribute("length", length);
912 QDomElement infoXml = m_doc.elementsByTagName("kdenlivedoc").at(0).toElement();
913 infoXml.setAttribute("modified", "1");
918 void DocumentChecker::checkMissingImagesAndFonts(const QStringList &images, const QStringList &fonts, const QString &id, const QString &baseClip)
921 foreach(const QString &img, images) {
922 if (m_safeImages.contains(img)) continue;
923 if (!KIO::NetAccess::exists(KUrl(img), KIO::NetAccess::SourceSide, 0)) {
924 QDomElement e = doc.createElement("missingclip");
925 e.setAttribute("type", TITLE_IMAGE_ELEMENT);
926 e.setAttribute("resource", img);
927 e.setAttribute("id", id);
928 e.setAttribute("name", baseClip);
929 m_missingClips.append(e);
931 else m_safeImages.append(img);
933 foreach(const QString &fontelement, fonts) {
934 if (m_safeFonts.contains(fontelement)) continue;
935 QFont f(fontelement);
936 //kDebug() << "/ / / CHK FONTS: " << fontelement << " = " << QFontInfo(f).family();
937 if (fontelement != QFontInfo(f).family()) {
938 QDomElement e = doc.createElement("missingclip");
939 e.setAttribute("type", TITLE_FONT_ELEMENT);
940 e.setAttribute("resource", fontelement);
941 e.setAttribute("id", id);
942 e.setAttribute("name", baseClip);
943 m_missingClips.append(e);
945 else m_safeFonts.append(fontelement);
950 void DocumentChecker::slotCheckButtons()
952 if (m_ui.treeWidget->currentItem()) {
953 QTreeWidgetItem *item = m_ui.treeWidget->currentItem();
954 int t = item->data(0, typeRole).toInt();
955 int s = item->data(0, statusRole).toInt();
956 if (t == TITLE_FONT_ELEMENT || t == TITLE_IMAGE_ELEMENT || s == PROXYMISSING) {
957 m_ui.removeSelected->setEnabled(false);
958 } else m_ui.removeSelected->setEnabled(true);
963 #include "documentchecker.moc"