File indexing completed on 2024-04-28 16:31:56
0001 /*************************************************************************** 0002 Copyright (C) 2001-2009 Robby Stephenson <robby@periapsis.org> 0003 ***************************************************************************/ 0004 0005 /*************************************************************************** 0006 * * 0007 * This program is free software; you can redistribute it and/or * 0008 * modify it under the terms of the GNU General Public License as * 0009 * published by the Free Software Foundation; either version 2 of * 0010 * the License or (at your option) version 3 or any later version * 0011 * accepted by the membership of KDE e.V. (or its successor approved * 0012 * by the membership of KDE e.V.), which shall act as a proxy * 0013 * defined in Section 14 of version 3 of the license. * 0014 * * 0015 * This program is distributed in the hope that it will be useful, * 0016 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 0017 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 0018 * GNU General Public License for more details. * 0019 * * 0020 * You should have received a copy of the GNU General Public License * 0021 * along with this program. If not, see <http://www.gnu.org/licenses/>. * 0022 * * 0023 ***************************************************************************/ 0024 0025 #include "document.h" 0026 #include "collectionfactory.h" 0027 #include "translators/tellicoimporter.h" 0028 #include "translators/tellicozipexporter.h" 0029 #include "translators/tellicoxmlexporter.h" 0030 #include "collection.h" 0031 #include "core/filehandler.h" 0032 #include "borrower.h" 0033 #include "fieldformat.h" 0034 #include "core/tellico_strings.h" 0035 #include "images/imagefactory.h" 0036 #include "images/imagedirectory.h" 0037 #include "images/image.h" 0038 #include "images/imageinfo.h" 0039 #include "utils/stringset.h" 0040 #include "utils/mergeconflictresolver.h" 0041 #include "progressmanager.h" 0042 #include "config/tellico_config.h" 0043 #include "entrycomparison.h" 0044 #include "utils/guiproxy.h" 0045 #include "tellico_debug.h" 0046 0047 #include <KMessageBox> 0048 #include <KLocalizedString> 0049 0050 #include <QApplication> 0051 0052 using namespace Tellico; 0053 using Tellico::Data::Document; 0054 Document* Document::s_self = nullptr; 0055 0056 Document::Document() : QObject(), m_coll(nullptr), m_isModified(false), 0057 m_loadAllImages(false), m_validFile(false), m_importer(nullptr), m_cancelImageWriting(true), 0058 m_fileFormat(Import::TellicoImporter::Unknown), m_loadImagesTimer(this) { 0059 m_allImagesOnDisk = Config::imageLocation() != Config::ImagesInFile; 0060 m_loadImagesTimer.setSingleShot(true); 0061 m_loadImagesTimer.setInterval(500); 0062 connect(&m_loadImagesTimer, &QTimer::timeout, this, &Document::slotLoadAllImages); 0063 newDocument(Collection::Book); 0064 } 0065 0066 Document::~Document() { 0067 delete m_importer; 0068 m_importer = nullptr; 0069 } 0070 0071 Tellico::Data::CollPtr Document::collection() const { 0072 return m_coll; 0073 } 0074 0075 void Document::setURL(const QUrl& url_) { 0076 m_url = url_; 0077 if(m_url.fileName() != i18n(Tellico::untitledFilename)) { 0078 ImageFactory::setLocalDirectory(m_url); 0079 EntryComparison::setDocumentUrl(m_url); 0080 } 0081 } 0082 0083 void Document::setModified(bool modified_) { 0084 if(modified_ != m_isModified) { 0085 m_isModified = modified_; 0086 emit signalModified(m_isModified); 0087 } 0088 } 0089 0090 void Document::slotSetModified() { 0091 setModified(true); 0092 } 0093 0094 /** 0095 * Since QUndoStack emits cleanChanged(), the behavior is opposite 0096 * the document modified flag 0097 */ 0098 void Document::slotSetClean(bool clean_) { 0099 setModified(!clean_); 0100 } 0101 0102 bool Document::newDocument(int type_) { 0103 if(m_importer) { 0104 m_importer->deleteLater(); 0105 m_importer = nullptr; 0106 } 0107 deleteContents(); 0108 0109 m_coll = CollectionFactory::collection(type_, true); 0110 m_coll->setTrackGroups(true); 0111 0112 emit signalCollectionAdded(m_coll); 0113 emit signalCollectionImagesLoaded(m_coll); 0114 0115 setModified(false); 0116 QUrl url = QUrl::fromLocalFile(i18n(Tellico::untitledFilename)); 0117 setURL(url); 0118 m_validFile = false; 0119 m_fileFormat = Import::TellicoImporter::Unknown; 0120 0121 return true; 0122 } 0123 0124 bool Document::openDocument(const QUrl& url_) { 0125 MARK; 0126 // delayed image loading only works for local files 0127 m_loadAllImages = !url_.isLocalFile(); 0128 m_loadImagesTimer.stop(); // avoid potential race condition 0129 0130 if(m_importer) { 0131 m_importer->deleteLater(); 0132 } 0133 m_importer = new Import::TellicoImporter(url_, m_loadAllImages); 0134 0135 ProgressItem& item = ProgressManager::self()->newProgressItem(m_importer, m_importer->progressLabel(), true); 0136 connect(m_importer, &Import::Importer::signalTotalSteps, 0137 ProgressManager::self(), &ProgressManager::setTotalSteps); 0138 connect(m_importer, &Import::Importer::signalProgress, 0139 ProgressManager::self(), &ProgressManager::setProgress); 0140 connect(&item, &ProgressItem::signalCancelled, m_importer, &Import::Importer::slotCancel); 0141 ProgressItem::Done done(m_importer); 0142 0143 CollPtr coll = m_importer->collection(); 0144 if(!m_importer) { 0145 myDebug() << "The importer was deleted out from under us"; 0146 return false; 0147 } 0148 // delayed image loading only works for zip files 0149 // format is only known AFTER collection() is called 0150 0151 m_fileFormat = m_importer->format(); 0152 m_allImagesOnDisk = !m_importer->hasImages(); 0153 if(!m_importer->hasImages() || m_fileFormat != Import::TellicoImporter::Zip) { 0154 m_loadAllImages = true; 0155 } 0156 ImageFactory::setZipArchive(m_importer->takeImages()); 0157 0158 if(!coll) { 0159 // myDebug() << "returning false"; 0160 GUI::Proxy::sorry(m_importer->statusMessage()); 0161 m_validFile = false; 0162 return false; 0163 } 0164 deleteContents(); 0165 m_coll = coll; 0166 m_coll->setTrackGroups(true); 0167 setURL(url_); 0168 m_validFile = true; 0169 0170 emit signalCollectionAdded(m_coll); 0171 0172 // m_importer might have been deleted? 0173 setModified(m_importer && m_importer->modifiedOriginal()); 0174 // if(pruneImages()) { 0175 // slotSetModified(true); 0176 // } 0177 if(m_importer && m_importer->hasImages()) { 0178 m_cancelImageWriting = false; 0179 m_loadImagesTimer.start(); 0180 } else { 0181 emit signalCollectionImagesLoaded(m_coll); 0182 if(m_importer) { 0183 m_importer->deleteLater(); 0184 m_importer = nullptr; 0185 } 0186 } 0187 return true; 0188 } 0189 0190 bool Document::saveDocument(const QUrl& url_, bool force_) { 0191 // FileHandler::queryExists calls FileHandler::writeBackupFile 0192 // so the only reason to check queryExists() is if the url to write to is different than the current one 0193 if(url_ == m_url) { 0194 if(!FileHandler::writeBackupFile(url_)) { 0195 return false; 0196 } 0197 } else { 0198 if(!force_ && !FileHandler::queryExists(url_)) { 0199 return false; 0200 } 0201 } 0202 0203 // in case we're still loading images, give that a chance to cancel 0204 m_cancelImageWriting = true; 0205 qApp->processEvents(); 0206 0207 ProgressItem& item = ProgressManager::self()->newProgressItem(this, i18n("Saving file..."), false); 0208 ProgressItem::Done done(this); 0209 0210 // will always save as zip file, no matter if has images or not 0211 int imageLocation = Config::imageLocation(); 0212 bool includeImages = imageLocation == Config::ImagesInFile; 0213 int totalSteps; 0214 // write all images to disk cache if needed 0215 // have to do this before executing exporter in case 0216 // the user changed the imageInFile setting from Yes to No, in which 0217 // case saving will overwrite the old file that has the images in it! 0218 if(includeImages) { 0219 totalSteps = 10; 0220 item.setTotalSteps(totalSteps); 0221 // since TellicoZipExporter uses 100 steps, then it will get 100/110 of the total progress 0222 } else { 0223 totalSteps = 100; 0224 item.setTotalSteps(totalSteps); 0225 m_cancelImageWriting = false; 0226 writeAllImages(imageLocation == Config::ImagesInAppDir ? ImageFactory::DataDir : ImageFactory::LocalDir, url_); 0227 } 0228 QScopedPointer<Export::Exporter> exporter; 0229 if(m_fileFormat == Import::TellicoImporter::XML) { 0230 exporter.reset(new Export::TellicoXMLExporter(m_coll)); 0231 static_cast<Export::TellicoXMLExporter*>(exporter.data())->setIncludeImages(includeImages); 0232 } else { 0233 exporter.reset(new Export::TellicoZipExporter(m_coll)); 0234 static_cast<Export::TellicoZipExporter*>(exporter.data())->setIncludeImages(includeImages); 0235 } 0236 item.setProgress(int(0.8*totalSteps)); 0237 exporter->setEntries(m_coll->entries()); 0238 exporter->setURL(url_); 0239 // since we already asked about overwriting the file, force the save 0240 long opt = exporter->options() | Export::ExportForce | Export::ExportComplete | Export::ExportProgress; 0241 // only write the image sizes if they're known already 0242 opt &= ~Export::ExportImageSize; 0243 exporter->setOptions(opt); 0244 const bool success = exporter->exec(); 0245 item.setProgress(int(0.9*totalSteps)); 0246 0247 if(success) { 0248 setURL(url_); 0249 // if successful, doc is no longer modified 0250 setModified(false); 0251 } else { 0252 myDebug() << "Document::saveDocument() - not successful saving to" << url_.url(); 0253 } 0254 return success; 0255 } 0256 0257 bool Document::closeDocument() { 0258 if(m_importer) { 0259 m_importer->deleteLater(); 0260 m_importer = nullptr; 0261 } 0262 deleteContents(); 0263 return true; 0264 } 0265 0266 void Document::deleteContents() { 0267 if(m_coll) { 0268 emit signalCollectionDeleted(m_coll); 0269 } 0270 // don't delete the m_importer here, bad things will happen 0271 0272 // since the collection holds a pointer to each entry and each entry 0273 // hold a pointer to the collection, and they're both sharedptrs, 0274 // neither will ever get deleted, unless the entries are removed from the collection 0275 if(m_coll) { 0276 m_coll->clear(); 0277 } 0278 m_coll = nullptr; // old collection gets deleted as refcount goes to 0 0279 m_cancelImageWriting = true; 0280 } 0281 0282 void Document::appendCollection(Tellico::Data::CollPtr coll_, bool* structuralChange_) { 0283 appendCollection(m_coll, coll_, structuralChange_); 0284 } 0285 0286 void Document::appendCollection(Tellico::Data::CollPtr coll1_, Tellico::Data::CollPtr coll2_, bool* structuralChange_) { 0287 if(structuralChange_) *structuralChange_ = false; 0288 if(!coll1_ || !coll2_) { 0289 return; 0290 } 0291 0292 coll1_->blockSignals(true); 0293 0294 foreach(FieldPtr field, coll2_->fields()) { 0295 bool collChange = coll1_->mergeField(field); 0296 if(collChange && structuralChange_) *structuralChange_ = true; 0297 } 0298 0299 Data::EntryList newEntries; 0300 foreach(EntryPtr entry, coll2_->entries()) { 0301 Data::EntryPtr newEntry(new Data::Entry(*entry)); 0302 newEntry->setCollection(coll1_); 0303 newEntries << newEntry; 0304 } 0305 coll1_->addEntries(newEntries); 0306 // TODO: merge filters and loans 0307 coll1_->blockSignals(false); 0308 } 0309 0310 Tellico::Data::MergePair Document::mergeCollection(Tellico::Data::CollPtr coll_, bool* structuralChange_) { 0311 return mergeCollection(m_coll, coll_, structuralChange_); 0312 } 0313 0314 Tellico::Data::MergePair Document::mergeCollection(Tellico::Data::CollPtr coll1_, Tellico::Data::CollPtr coll2_, bool* structuralChange_) { 0315 if(structuralChange_) *structuralChange_ = false; 0316 MergePair pair; 0317 if(!coll1_ || !coll2_) { 0318 return pair; 0319 } 0320 0321 coll1_->blockSignals(true); 0322 Data::FieldList fields = coll2_->fields(); 0323 foreach(FieldPtr field, fields) { 0324 bool collChange = coll1_->mergeField(field); 0325 if(collChange && structuralChange_) *structuralChange_ = true; 0326 } 0327 0328 EntryList currEntries = coll1_->entries(); 0329 EntryList newEntries = coll2_->entries(); 0330 std::sort(currEntries.begin(), currEntries.end(), Data::EntryCmp(QStringLiteral("title"))); 0331 std::sort(newEntries.begin(), newEntries.end(), Data::EntryCmp(QStringLiteral("title"))); 0332 0333 const int currTotal = currEntries.count(); 0334 int lastMatchId = 0; 0335 bool checkSameId = false; // if the matching entries have the same id, then check that first for later comparisons 0336 foreach(EntryPtr newEntry, newEntries) { 0337 int bestMatch = 0; 0338 Data::EntryPtr matchEntry, currEntry; 0339 // first, if we're checking against same ID 0340 if(checkSameId) { 0341 currEntry = coll1_->entryById(newEntry->id()); 0342 if(currEntry && coll1_->sameEntry(currEntry, newEntry) >= EntryComparison::ENTRY_PERFECT_MATCH) { 0343 // only have to compare against perfect match 0344 matchEntry = currEntry; 0345 } 0346 } 0347 if(!matchEntry) { 0348 // alternative is to loop over them all 0349 for(int i = 0; i < currTotal; ++i) { 0350 // since we're sorted by title, track the index of the previous match and start comparison there 0351 currEntry = currEntries.at((i+lastMatchId) % currTotal); 0352 const int match = coll1_->sameEntry(currEntry, newEntry); 0353 if(match >= EntryComparison::ENTRY_PERFECT_MATCH) { 0354 matchEntry = currEntry; 0355 lastMatchId = (i+lastMatchId) % currTotal; 0356 break; 0357 } else if(match >= EntryComparison::ENTRY_GOOD_MATCH && match > bestMatch) { 0358 bestMatch = match; 0359 matchEntry = currEntry; 0360 lastMatchId = (i+lastMatchId) % currTotal; 0361 // don't break, keep looking for better one 0362 } 0363 } 0364 } 0365 if(matchEntry) { 0366 checkSameId = checkSameId || (matchEntry->id() == newEntry->id()); 0367 Merge::mergeEntry(matchEntry, newEntry); 0368 } else { 0369 Data::EntryPtr e(new Data::Entry(*newEntry)); 0370 e->setCollection(coll1_); 0371 // keep track of which entries got added 0372 pair.first.append(e); 0373 } 0374 } 0375 coll1_->addEntries(pair.first); 0376 // TODO: merge filters and loans 0377 coll1_->blockSignals(false); 0378 return pair; 0379 } 0380 0381 void Document::replaceCollection(Tellico::Data::CollPtr coll_) { 0382 if(!coll_) { 0383 return; 0384 } 0385 0386 QUrl url = QUrl::fromLocalFile(i18n(Tellico::untitledFilename)); 0387 setURL(url); 0388 m_validFile = false; 0389 0390 // the collection gets cleared by the CollectionCommand that called this function 0391 // no need to do it here 0392 0393 m_coll = coll_; 0394 m_coll->setTrackGroups(true); 0395 m_cancelImageWriting = true; 0396 // CollectionCommand takes care of calling Controller signals 0397 } 0398 0399 void Document::unAppendCollection(Tellico::Data::FieldList origFields_, QList<int> addedEntries_) { 0400 m_coll->blockSignals(true); 0401 0402 StringSet origFieldNames; 0403 foreach(FieldPtr field, origFields_) { 0404 m_coll->modifyField(field); 0405 origFieldNames.add(field->name()); 0406 } 0407 0408 EntryList entriesToRemove; 0409 foreach(int id, addedEntries_) { 0410 auto e = m_coll->entryById(id); 0411 if(e) entriesToRemove << e; 0412 } 0413 m_coll->removeEntries(entriesToRemove); 0414 0415 // since Collection::removeField() iterates over all entries to reset the value of the field 0416 // don't removeField() until after removeEntry() is done 0417 FieldList currFields = m_coll->fields(); 0418 foreach(FieldPtr field, currFields) { 0419 if(!origFieldNames.has(field->name())) { 0420 m_coll->removeField(field); 0421 } 0422 } 0423 m_coll->blockSignals(false); 0424 } 0425 0426 void Document::unMergeCollection(Tellico::Data::FieldList origFields_, Tellico::Data::MergePair entryPair_) { 0427 m_coll->blockSignals(true); 0428 0429 QStringList origFieldNames; 0430 foreach(FieldPtr field, origFields_) { 0431 m_coll->modifyField(field); 0432 origFieldNames << field->name(); 0433 } 0434 0435 // first item in pair are the entries added by the operation, remove them 0436 EntryList entries = entryPair_.first; 0437 m_coll->removeEntries(entries); 0438 0439 // second item in pair are the entries which got modified by the original merge command 0440 const QString track = QStringLiteral("track"); 0441 PairVector trackChanges = entryPair_.second; 0442 // need to go through them in reverse since one entry may have been modified multiple times 0443 // first item in the pair is the entry pointer 0444 // second item is the old value of the track field 0445 for(int i = trackChanges.count()-1; i >= 0; --i) { 0446 trackChanges[i].first->setField(track, trackChanges[i].second); 0447 } 0448 0449 // since Collection::removeField() iterates over all entries to reset the value of the field 0450 // don't removeField() until after removeEntry() is done 0451 FieldList currFields = m_coll->fields(); 0452 foreach(FieldPtr field, currFields) { 0453 if(origFieldNames.indexOf(field->name()) == -1) { 0454 m_coll->removeField(field); 0455 } 0456 } 0457 m_coll->blockSignals(false); 0458 } 0459 0460 bool Document::isEmpty() const { 0461 //an empty doc may contain a collection, but no entries 0462 return (!m_coll || m_coll->entries().isEmpty()); 0463 } 0464 0465 bool Document::loadAllImagesNow() const { 0466 // DEBUG_LINE; 0467 if(!m_coll || !m_validFile) { 0468 return false; 0469 } 0470 if(m_loadAllImages) { 0471 myDebug() << "Document::loadAllImagesNow() - all valid images should already be loaded!"; 0472 return false; 0473 } 0474 return Import::TellicoImporter::loadAllImages(m_url); 0475 } 0476 0477 Tellico::Data::EntryList Document::filteredEntries(Tellico::FilterPtr filter_) const { 0478 Data::EntryList matches; 0479 Data::EntryList entries = m_coll->entries(); 0480 foreach(EntryPtr entry, entries) { 0481 if(filter_->matches(entry)) { 0482 matches.append(entry); 0483 } 0484 } 0485 return matches; 0486 } 0487 0488 void Document::checkOutEntry(Tellico::Data::EntryPtr entry_) { 0489 if(!entry_) { 0490 return; 0491 } 0492 0493 const QString loaned = QStringLiteral("loaned"); 0494 if(!m_coll->hasField(loaned)) { 0495 FieldPtr f(new Field(loaned, i18n("Loaned"), Field::Bool)); 0496 f->setFlags(Field::AllowGrouped); 0497 f->setCategory(i18n("Personal")); 0498 m_coll->addField(f); 0499 } 0500 entry_->setField(loaned, QStringLiteral("true")); 0501 EntryList vec; 0502 vec.append(entry_); 0503 m_coll->updateDicts(vec, QStringList() << loaned); 0504 } 0505 0506 void Document::checkInEntry(Tellico::Data::EntryPtr entry_) { 0507 if(!entry_) { 0508 return; 0509 } 0510 0511 const QString loaned = QStringLiteral("loaned"); 0512 if(!m_coll->hasField(loaned)) { 0513 return; 0514 } 0515 entry_->setField(loaned, QString()); 0516 m_coll->updateDicts(EntryList() << entry_, QStringList() << loaned); 0517 } 0518 0519 void Document::renameCollection(const QString& newTitle_) { 0520 m_coll->setTitle(newTitle_); 0521 } 0522 0523 // this only gets called when a zip file with images is opened 0524 // by loading every image, it gets pulled out of the zip file and 0525 // copied to disk. Then the zip file can be closed and not retained in memory 0526 void Document::slotLoadAllImages() { 0527 QString id; 0528 StringSet images; 0529 foreach(EntryPtr entry, m_coll->entries()) { 0530 foreach(FieldPtr field, m_coll->imageFields()) { 0531 id = entry->field(field); 0532 if(id.isEmpty() || images.has(id)) { 0533 continue; 0534 } 0535 // this is the early loading, so just by calling imageById() 0536 // the image gets sucked from the zip file and written to disk 0537 // by ImageFactory::imageById() 0538 // TODO:: does this need to check against images with link only? 0539 if(ImageFactory::imageById(id).isNull()) { 0540 myDebug() << "Null image for entry:" << entry->title() << id; 0541 } 0542 images.add(id); 0543 if(m_cancelImageWriting) { 0544 break; 0545 } 0546 } 0547 if(m_cancelImageWriting) { 0548 break; 0549 } 0550 // stay responsive, do this in the background 0551 qApp->processEvents(); 0552 } 0553 0554 if(m_cancelImageWriting) { 0555 myLog() << "slotLoadAllImages() - cancel image writing"; 0556 } else { 0557 emit signalCollectionImagesLoaded(m_coll); 0558 } 0559 0560 m_cancelImageWriting = false; 0561 if(m_importer) { 0562 m_importer->deleteLater(); 0563 m_importer = nullptr; 0564 } 0565 } 0566 0567 // cacheDir_ is the location dir to write the images 0568 // localDir_ provide the new file location which is only needed if cacheDir == LocalDir 0569 void Document::writeAllImages(int cacheDir_, const QUrl& localDir_) { 0570 // images get 80 steps in saveDocument() 0571 const uint stepSize = 1 + qMax(1, m_coll->entryCount()/80); // add 1 since it could round off 0572 uint j = 1; 0573 0574 ImageFactory::CacheDir cacheDir = static_cast<ImageFactory::CacheDir>(cacheDir_); 0575 QScopedPointer<ImageDirectory> imgDir; 0576 if(cacheDir == ImageFactory::LocalDir) { 0577 imgDir.reset(new ImageDirectory(ImageFactory::localDirectory(localDir_))); 0578 } 0579 0580 QString id; 0581 StringSet images; 0582 EntryList entries = m_coll->entries(); 0583 FieldList imageFields = m_coll->imageFields(); 0584 foreach(EntryPtr entry, entries) { 0585 foreach(FieldPtr field, imageFields) { 0586 id = entry->field(field); 0587 if(id.isEmpty() || images.has(id)) { 0588 continue; 0589 } 0590 images.add(id); 0591 if(ImageFactory::imageInfo(id).linkOnly) { 0592 continue; 0593 } 0594 // careful here, if we're writing to LocalDir, need to read from the old LocalDir and write to new 0595 bool success; 0596 if(cacheDir == ImageFactory::LocalDir) { 0597 success = ImageFactory::writeCachedImage(id, imgDir.data()); 0598 } else { 0599 success = ImageFactory::writeCachedImage(id, cacheDir); 0600 } 0601 if(!success) { 0602 myDebug() << "did not write image for entry title:" << entry->title(); 0603 } 0604 if(m_cancelImageWriting) { 0605 break; 0606 } 0607 } 0608 if(j%stepSize == 0) { 0609 ProgressManager::self()->setProgress(this, j/stepSize); 0610 } 0611 ++j; 0612 if(m_cancelImageWriting) { 0613 break; 0614 } 0615 } 0616 0617 if(m_cancelImageWriting) { 0618 myDebug() << "Document::writeAllImages() - cancel image writing"; 0619 } 0620 0621 m_cancelImageWriting = false; 0622 } 0623 0624 bool Document::pruneImages() { 0625 bool found = false; 0626 QString id; 0627 StringSet images; 0628 Data::EntryList entries = m_coll->entries(); 0629 Data::FieldList imageFields = m_coll->imageFields(); 0630 foreach(EntryPtr entry, entries) { 0631 foreach(FieldPtr field, imageFields) { 0632 id = entry->field(field); 0633 if(id.isEmpty() || images.has(id)) { 0634 continue; 0635 } 0636 const Data::Image& img = ImageFactory::imageById(id); 0637 if(img.isNull()) { 0638 entry->setField(field, QString()); 0639 found = true; 0640 myDebug() << "removing null image for" << entry->title() << ":" << id; 0641 } else { 0642 images.add(id); 0643 } 0644 } 0645 } 0646 return found; 0647 } 0648 0649 int Document::imageCount() const { 0650 if(!m_coll) { 0651 return 0; 0652 } 0653 StringSet images; 0654 FieldList fields = m_coll->imageFields(); 0655 EntryList entries = m_coll->entries(); 0656 foreach(FieldPtr field, fields) { 0657 foreach(EntryPtr entry, entries) { 0658 images.add(entry->field(field)); 0659 } 0660 } 0661 return images.count(); 0662 } 0663 0664 void Document::removeImagesNotInCollection(Tellico::Data::EntryList entries_, Tellico::Data::EntryList entriesToKeep_) { 0665 // first get list of all images in collection 0666 StringSet images; 0667 FieldList fields = m_coll->imageFields(); 0668 EntryList allEntries = m_coll->entries(); 0669 foreach(FieldPtr field, fields) { 0670 foreach(EntryPtr entry, allEntries) { 0671 images.add(entry->field(field)); 0672 } 0673 foreach(EntryPtr entry, entriesToKeep_) { 0674 images.add(entry->field(field)); 0675 } 0676 } 0677 0678 // now for all images not in the cache, we can clear them 0679 StringSet imagesToCheck = ImageFactory::imagesNotInCache(); 0680 0681 // if entries_ is not empty, that means we want to limit the images removed 0682 // to those that are referenced in those entries 0683 StringSet imagesToRemove; 0684 foreach(FieldPtr field, fields) { 0685 foreach(EntryPtr entry, entries_) { 0686 QString id = entry->field(field); 0687 if(!id.isEmpty() && imagesToCheck.has(id) && !images.has(id)) { 0688 imagesToRemove.add(id); 0689 } 0690 } 0691 } 0692 0693 const QStringList realImagesToRemove = imagesToRemove.values(); 0694 for(QStringList::ConstIterator it = realImagesToRemove.begin(); it != realImagesToRemove.end(); ++it) { 0695 ImageFactory::removeImage(*it, false); // doesn't delete, just remove link 0696 } 0697 }