1 /***************************************************************************
2     Copyright (C) 2001-2009 Robby Stephenson <robby@periapsis.org>
3  ***************************************************************************/
4 
5 /***************************************************************************
6  *                                                                         *
7  *   This program is free software; you can redistribute it and/or         *
8  *   modify it under the terms of the GNU General Public License as        *
9  *   published by the Free Software Foundation; either version 2 of        *
10  *   the License or (at your option) version 3 or any later version        *
11  *   accepted by the membership of KDE e.V. (or its successor approved     *
12  *   by the membership of KDE e.V.), which shall act as a proxy            *
13  *   defined in Section 14 of version 3 of the license.                    *
14  *                                                                         *
15  *   This program is distributed in the hope that it will be useful,       *
16  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
17  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
18  *   GNU General Public License for more details.                          *
19  *                                                                         *
20  *   You should have received a copy of the GNU General Public License     *
21  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
22  *                                                                         *
23  ***************************************************************************/
24 
25 #include "document.h"
26 #include "collectionfactory.h"
27 #include "translators/tellicoimporter.h"
28 #include "translators/tellicozipexporter.h"
29 #include "translators/tellicoxmlexporter.h"
30 #include "collection.h"
31 #include "core/filehandler.h"
32 #include "borrower.h"
33 #include "fieldformat.h"
34 #include "core/tellico_strings.h"
35 #include "images/imagefactory.h"
36 #include "images/imagedirectory.h"
37 #include "images/image.h"
38 #include "images/imageinfo.h"
39 #include "utils/stringset.h"
40 #include "utils/mergeconflictresolver.h"
41 #include "progressmanager.h"
42 #include "config/tellico_config.h"
43 #include "entrycomparison.h"
44 #include "utils/guiproxy.h"
45 #include "tellico_debug.h"
46 
47 #include <KMessageBox>
48 #include <KLocalizedString>
49 
50 #include <QApplication>
51 
52 using namespace Tellico;
53 using Tellico::Data::Document;
54 Document* Document::s_self = nullptr;
55 
Document()56 Document::Document() : QObject(), m_coll(nullptr), m_isModified(false),
57     m_loadAllImages(false), m_validFile(false), m_importer(nullptr), m_cancelImageWriting(true),
58     m_fileFormat(Import::TellicoImporter::Unknown), m_loadImagesTimer(this) {
59   m_allImagesOnDisk = Config::imageLocation() != Config::ImagesInFile;
60   m_loadImagesTimer.setSingleShot(true);
61   m_loadImagesTimer.setInterval(500);
62   connect(&m_loadImagesTimer, &QTimer::timeout, this, &Document::slotLoadAllImages);
63   newDocument(Collection::Book);
64 }
65 
~Document()66 Document::~Document() {
67   delete m_importer;
68   m_importer = nullptr;
69 }
70 
collection() const71 Tellico::Data::CollPtr Document::collection() const {
72   return m_coll;
73 }
74 
setURL(const QUrl & url_)75 void Document::setURL(const QUrl& url_) {
76   m_url = url_;
77   if(m_url.fileName() != i18n(Tellico::untitledFilename)) {
78     ImageFactory::setLocalDirectory(m_url);
79     EntryComparison::setDocumentUrl(m_url);
80   }
81 }
82 
setModified(bool modified_)83 void Document::setModified(bool modified_) {
84   if(modified_ != m_isModified) {
85     m_isModified = modified_;
86     emit signalModified(m_isModified);
87   }
88 }
89 
slotSetModified()90 void Document::slotSetModified() {
91   setModified(true);
92 }
93 
94 /**
95  * Since QUndoStack emits cleanChanged(), the behavior is opposite
96  * the document modified flag
97  */
slotSetClean(bool clean_)98 void Document::slotSetClean(bool clean_) {
99   setModified(!clean_);
100 }
101 
newDocument(int type_)102 bool Document::newDocument(int type_) {
103   if(m_importer) {
104     m_importer->deleteLater();
105     m_importer = nullptr;
106   }
107   deleteContents();
108 
109   m_coll = CollectionFactory::collection(type_, true);
110   m_coll->setTrackGroups(true);
111 
112   emit signalCollectionAdded(m_coll);
113   emit signalCollectionImagesLoaded(m_coll);
114 
115   setModified(false);
116   QUrl url = QUrl::fromLocalFile(i18n(Tellico::untitledFilename));
117   setURL(url);
118   m_validFile = false;
119   m_fileFormat = Import::TellicoImporter::Unknown;
120 
121   return true;
122 }
123 
openDocument(const QUrl & url_)124 bool Document::openDocument(const QUrl& url_) {
125   MARK;
126   // delayed image loading only works for local files
127   m_loadAllImages = !url_.isLocalFile();
128   m_loadImagesTimer.stop(); // avoid potential race condition
129 
130   if(m_importer) {
131     m_importer->deleteLater();
132   }
133   m_importer = new Import::TellicoImporter(url_, m_loadAllImages);
134 
135   ProgressItem& item = ProgressManager::self()->newProgressItem(m_importer, m_importer->progressLabel(), true);
136   connect(m_importer, &Import::Importer::signalTotalSteps,
137           ProgressManager::self(), &ProgressManager::setTotalSteps);
138   connect(m_importer, &Import::Importer::signalProgress,
139           ProgressManager::self(), &ProgressManager::setProgress);
140   connect(&item, &ProgressItem::signalCancelled, m_importer, &Import::Importer::slotCancel);
141   ProgressItem::Done done(m_importer);
142 
143   CollPtr coll = m_importer->collection();
144   if(!m_importer) {
145     myDebug() << "The importer was deleted out from under us";
146     return false;
147   }
148   // delayed image loading only works for zip files
149   // format is only known AFTER collection() is called
150 
151   m_fileFormat = m_importer->format();
152   m_allImagesOnDisk = !m_importer->hasImages();
153   if(!m_importer->hasImages() || m_fileFormat != Import::TellicoImporter::Zip) {
154     m_loadAllImages = true;
155   }
156   ImageFactory::setZipArchive(m_importer->takeImages());
157 
158   if(!coll) {
159 //    myDebug() << "returning false";
160     GUI::Proxy::sorry(m_importer->statusMessage());
161     m_validFile = false;
162     return false;
163   }
164   deleteContents();
165   m_coll = coll;
166   m_coll->setTrackGroups(true);
167   setURL(url_);
168   m_validFile = true;
169 
170   emit signalCollectionAdded(m_coll);
171 
172   // m_importer might have been deleted?
173   setModified(m_importer && m_importer->modifiedOriginal());
174 //  if(pruneImages()) {
175 //    slotSetModified(true);
176 //  }
177   if(m_importer && m_importer->hasImages()) {
178     m_cancelImageWriting = false;
179     m_loadImagesTimer.start();
180   } else {
181     emit signalCollectionImagesLoaded(m_coll);
182     if(m_importer) {
183       m_importer->deleteLater();
184       m_importer = nullptr;
185     }
186   }
187   return true;
188 }
189 
saveDocument(const QUrl & url_,bool force_)190 bool Document::saveDocument(const QUrl& url_, bool force_) {
191   // FileHandler::queryExists calls FileHandler::writeBackupFile
192   // so the only reason to check queryExists() is if the url to write to is different than the current one
193   if(url_ == m_url) {
194     if(!FileHandler::writeBackupFile(url_)) {
195       return false;
196     }
197   } else {
198     if(!force_ && !FileHandler::queryExists(url_)) {
199       return false;
200     }
201   }
202 
203   // in case we're still loading images, give that a chance to cancel
204   m_cancelImageWriting = true;
205   qApp->processEvents();
206 
207   ProgressItem& item = ProgressManager::self()->newProgressItem(this, i18n("Saving file..."), false);
208   ProgressItem::Done done(this);
209 
210   // will always save as zip file, no matter if has images or not
211   int imageLocation = Config::imageLocation();
212   bool includeImages = imageLocation == Config::ImagesInFile;
213   int totalSteps;
214   // write all images to disk cache if needed
215   // have to do this before executing exporter in case
216   // the user changed the imageInFile setting from Yes to No, in which
217   // case saving will overwrite the old file that has the images in it!
218   if(includeImages) {
219     totalSteps = 10;
220     item.setTotalSteps(totalSteps);
221     // since TellicoZipExporter uses 100 steps, then it will get 100/110 of the total progress
222   } else {
223     totalSteps = 100;
224     item.setTotalSteps(totalSteps);
225     m_cancelImageWriting = false;
226     writeAllImages(imageLocation == Config::ImagesInAppDir ? ImageFactory::DataDir : ImageFactory::LocalDir, url_);
227   }
228   QScopedPointer<Export::Exporter> exporter;
229   if(m_fileFormat == Import::TellicoImporter::XML) {
230     exporter.reset(new Export::TellicoXMLExporter(m_coll));
231     static_cast<Export::TellicoXMLExporter*>(exporter.data())->setIncludeImages(includeImages);
232   } else {
233     exporter.reset(new Export::TellicoZipExporter(m_coll));
234     static_cast<Export::TellicoZipExporter*>(exporter.data())->setIncludeImages(includeImages);
235   }
236   item.setProgress(int(0.8*totalSteps));
237   exporter->setEntries(m_coll->entries());
238   exporter->setURL(url_);
239   // since we already asked about overwriting the file, force the save
240   long opt = exporter->options() | Export::ExportForce | Export::ExportComplete | Export::ExportProgress;
241   // only write the image sizes if they're known already
242   opt &= ~Export::ExportImageSize;
243   exporter->setOptions(opt);
244   const bool success = exporter->exec();
245   item.setProgress(int(0.9*totalSteps));
246 
247   if(success) {
248     setURL(url_);
249     // if successful, doc is no longer modified
250     setModified(false);
251   } else {
252     myDebug() << "Document::saveDocument() - not successful saving to" << url_.url();
253   }
254   return success;
255 }
256 
closeDocument()257 bool Document::closeDocument() {
258   if(m_importer) {
259     m_importer->deleteLater();
260     m_importer = nullptr;
261   }
262   deleteContents();
263   return true;
264 }
265 
deleteContents()266 void Document::deleteContents() {
267   if(m_coll) {
268     emit signalCollectionDeleted(m_coll);
269   }
270   // don't delete the m_importer here, bad things will happen
271 
272   // since the collection holds a pointer to each entry and each entry
273   // hold a pointer to the collection, and they're both sharedptrs,
274   // neither will ever get deleted, unless the entries are removed from the collection
275   if(m_coll) {
276     m_coll->clear();
277   }
278   m_coll = nullptr; // old collection gets deleted as refcount goes to 0
279   m_cancelImageWriting = true;
280 }
281 
appendCollection(Tellico::Data::CollPtr coll_)282 void Document::appendCollection(Tellico::Data::CollPtr coll_) {
283   appendCollection(m_coll, coll_);
284 }
285 
appendCollection(Tellico::Data::CollPtr coll1_,Tellico::Data::CollPtr coll2_)286 void Document::appendCollection(Tellico::Data::CollPtr coll1_, Tellico::Data::CollPtr coll2_) {
287   if(!coll1_ || !coll2_) {
288     return;
289   }
290 
291   coll1_->blockSignals(true);
292 
293   foreach(FieldPtr field, coll2_->fields()) {
294     coll1_->mergeField(field);
295   }
296 
297   Data::EntryList newEntries;
298   foreach(EntryPtr entry, coll2_->entries()) {
299     Data::EntryPtr newEntry(new Data::Entry(*entry));
300     newEntry->setCollection(coll1_);
301     newEntries << newEntry;
302   }
303   coll1_->addEntries(newEntries);
304   // TODO: merge filters and loans
305   coll1_->blockSignals(false);
306 }
307 
mergeCollection(Tellico::Data::CollPtr coll_)308 Tellico::Data::MergePair Document::mergeCollection(Tellico::Data::CollPtr coll_) {
309   return mergeCollection(m_coll, coll_);
310 }
311 
mergeCollection(Tellico::Data::CollPtr coll1_,Tellico::Data::CollPtr coll2_)312 Tellico::Data::MergePair Document::mergeCollection(Tellico::Data::CollPtr coll1_, Tellico::Data::CollPtr coll2_) {
313   MergePair pair;
314   if(!coll1_ || !coll2_) {
315     return pair;
316   }
317 
318   coll1_->blockSignals(true);
319   Data::FieldList fields = coll2_->fields();
320   foreach(FieldPtr field, fields) {
321     coll1_->mergeField(field);
322   }
323 
324   EntryList currEntries = coll1_->entries();
325   EntryList newEntries = coll2_->entries();
326   std::sort(currEntries.begin(), currEntries.end(), Data::EntryCmp(QStringLiteral("title")));
327   std::sort(newEntries.begin(), newEntries.end(), Data::EntryCmp(QStringLiteral("title")));
328 
329   const int currTotal = currEntries.count();
330   int lastMatchId = 0;
331   bool checkSameId = false; // if the matching entries have the same id, then check that first for later comparisons
332   foreach(EntryPtr newEntry, newEntries) {
333     int bestMatch = 0;
334     Data::EntryPtr matchEntry, currEntry;
335     // first, if we're checking against same ID
336     if(checkSameId) {
337       currEntry = coll1_->entryById(newEntry->id());
338       if(currEntry && coll1_->sameEntry(currEntry, newEntry) >= EntryComparison::ENTRY_PERFECT_MATCH) {
339         // only have to compare against perfect match
340         matchEntry = currEntry;
341       }
342     }
343     if(!matchEntry) {
344       // alternative is to loop over them all
345       for(int i = 0; i < currTotal; ++i) {
346         // since we're sorted by title, track the index of the previous match and start comparison there
347         currEntry = currEntries.at((i+lastMatchId) % currTotal);
348         const int match = coll1_->sameEntry(currEntry, newEntry);
349         if(match >= EntryComparison::ENTRY_PERFECT_MATCH) {
350           matchEntry = currEntry;
351           lastMatchId = (i+lastMatchId) % currTotal;
352           break;
353         } else if(match >= EntryComparison::ENTRY_GOOD_MATCH && match > bestMatch) {
354           bestMatch = match;
355           matchEntry = currEntry;
356           lastMatchId = (i+lastMatchId) % currTotal;
357           // don't break, keep looking for better one
358         }
359       }
360     }
361     if(matchEntry) {
362       checkSameId = checkSameId || (matchEntry->id() == newEntry->id());
363       Merge::mergeEntry(matchEntry, newEntry);
364     } else {
365       Data::EntryPtr e(new Data::Entry(*newEntry));
366       e->setCollection(coll1_);
367       // keep track of which entries got added
368       pair.first.append(e);
369     }
370   }
371   coll1_->addEntries(pair.first);
372   // TODO: merge filters and loans
373   coll1_->blockSignals(false);
374   return pair;
375 }
376 
replaceCollection(Tellico::Data::CollPtr coll_)377 void Document::replaceCollection(Tellico::Data::CollPtr coll_) {
378   if(!coll_) {
379     return;
380   }
381 
382   QUrl url = QUrl::fromLocalFile(i18n(Tellico::untitledFilename));
383   setURL(url);
384   m_validFile = false;
385 
386   // the collection gets cleared by the CollectionCommand that called this function
387   // no need to do it here
388 
389   m_coll = coll_;
390   m_coll->setTrackGroups(true);
391   m_cancelImageWriting = true;
392   // CollectionCommand takes care of calling Controller signals
393 }
394 
unAppendCollection(Tellico::Data::FieldList origFields_,QList<int> addedEntries_)395 void Document::unAppendCollection(Tellico::Data::FieldList origFields_, QList<int> addedEntries_) {
396   m_coll->blockSignals(true);
397 
398   StringSet origFieldNames;
399   foreach(FieldPtr field, origFields_) {
400     m_coll->modifyField(field);
401     origFieldNames.add(field->name());
402   }
403 
404   EntryList entriesToRemove;
405   foreach(int id, addedEntries_) {
406     auto e = m_coll->entryById(id);
407     if(e) entriesToRemove << e;
408   }
409   m_coll->removeEntries(entriesToRemove);
410 
411   // since Collection::removeField() iterates over all entries to reset the value of the field
412   // don't removeField() until after removeEntry() is done
413   FieldList currFields = m_coll->fields();
414   foreach(FieldPtr field, currFields) {
415     if(!origFieldNames.has(field->name())) {
416       m_coll->removeField(field);
417     }
418   }
419   m_coll->blockSignals(false);
420 }
421 
unMergeCollection(Tellico::Data::FieldList origFields_,Tellico::Data::MergePair entryPair_)422 void Document::unMergeCollection(Tellico::Data::FieldList origFields_, Tellico::Data::MergePair entryPair_) {
423   m_coll->blockSignals(true);
424 
425   QStringList origFieldNames;
426   foreach(FieldPtr field, origFields_) {
427     m_coll->modifyField(field);
428     origFieldNames << field->name();
429   }
430 
431   // first item in pair are the entries added by the operation, remove them
432   EntryList entries = entryPair_.first;
433   m_coll->removeEntries(entries);
434 
435   // second item in pair are the entries which got modified by the original merge command
436   const QString track = QStringLiteral("track");
437   PairVector trackChanges = entryPair_.second;
438   // need to go through them in reverse since one entry may have been modified multiple times
439   // first item in the pair is the entry pointer
440   // second item is the old value of the track field
441   for(int i = trackChanges.count()-1; i >= 0; --i) {
442     trackChanges[i].first->setField(track, trackChanges[i].second);
443   }
444 
445   // since Collection::removeField() iterates over all entries to reset the value of the field
446   // don't removeField() until after removeEntry() is done
447   FieldList currFields = m_coll->fields();
448   foreach(FieldPtr field, currFields) {
449     if(origFieldNames.indexOf(field->name()) == -1) {
450       m_coll->removeField(field);
451     }
452   }
453   m_coll->blockSignals(false);
454 }
455 
isEmpty() const456 bool Document::isEmpty() const {
457   //an empty doc may contain a collection, but no entries
458   return (!m_coll || m_coll->entries().isEmpty());
459 }
460 
loadAllImagesNow() const461 bool Document::loadAllImagesNow() const {
462 //  DEBUG_LINE;
463   if(!m_coll || !m_validFile) {
464     return false;
465   }
466   if(m_loadAllImages) {
467     myDebug() << "Document::loadAllImagesNow() - all valid images should already be loaded!";
468     return false;
469   }
470   return Import::TellicoImporter::loadAllImages(m_url);
471 }
472 
filteredEntries(Tellico::FilterPtr filter_) const473 Tellico::Data::EntryList Document::filteredEntries(Tellico::FilterPtr filter_) const {
474   Data::EntryList matches;
475   Data::EntryList entries = m_coll->entries();
476   foreach(EntryPtr entry, entries) {
477     if(filter_->matches(entry)) {
478       matches.append(entry);
479     }
480   }
481   return matches;
482 }
483 
checkOutEntry(Tellico::Data::EntryPtr entry_)484 void Document::checkOutEntry(Tellico::Data::EntryPtr entry_) {
485   if(!entry_) {
486     return;
487   }
488 
489   const QString loaned = QStringLiteral("loaned");
490   if(!m_coll->hasField(loaned)) {
491     FieldPtr f(new Field(loaned, i18n("Loaned"), Field::Bool));
492     f->setFlags(Field::AllowGrouped);
493     f->setCategory(i18n("Personal"));
494     m_coll->addField(f);
495   }
496   entry_->setField(loaned, QStringLiteral("true"));
497   EntryList vec;
498   vec.append(entry_);
499   m_coll->updateDicts(vec, QStringList() << loaned);
500 }
501 
checkInEntry(Tellico::Data::EntryPtr entry_)502 void Document::checkInEntry(Tellico::Data::EntryPtr entry_) {
503   if(!entry_) {
504     return;
505   }
506 
507   const QString loaned = QStringLiteral("loaned");
508   if(!m_coll->hasField(loaned)) {
509     return;
510   }
511   entry_->setField(loaned, QString());
512   m_coll->updateDicts(EntryList() << entry_, QStringList() << loaned);
513 }
514 
renameCollection(const QString & newTitle_)515 void Document::renameCollection(const QString& newTitle_) {
516   m_coll->setTitle(newTitle_);
517 }
518 
519 // this only gets called when a zip file with images is opened
520 // by loading every image, it gets pulled out of the zip file and
521 // copied to disk. Then the zip file can be closed and not retained in memory
slotLoadAllImages()522 void Document::slotLoadAllImages() {
523   QString id;
524   StringSet images;
525   foreach(EntryPtr entry, m_coll->entries()) {
526     foreach(FieldPtr field, m_coll->imageFields()) {
527       id = entry->field(field);
528       if(id.isEmpty() || images.has(id)) {
529         continue;
530       }
531       // this is the early loading, so just by calling imageById()
532       // the image gets sucked from the zip file and written to disk
533       // by ImageFactory::imageById()
534       // TODO:: does this need to check against images with link only?
535       if(ImageFactory::imageById(id).isNull()) {
536         myDebug() << "Null image for entry:" << entry->title() << id;
537       }
538       images.add(id);
539       if(m_cancelImageWriting) {
540         break;
541       }
542     }
543     if(m_cancelImageWriting) {
544       break;
545     }
546     // stay responsive, do this in the background
547     qApp->processEvents();
548   }
549 
550   if(m_cancelImageWriting) {
551     myLog() << "slotLoadAllImages() - cancel image writing";
552   } else {
553     emit signalCollectionImagesLoaded(m_coll);
554   }
555 
556   m_cancelImageWriting = false;
557   if(m_importer) {
558     m_importer->deleteLater();
559     m_importer = nullptr;
560   }
561 }
562 
563 // cacheDir_ is the location dir to write the images
564 // localDir_ provide the new file location which is only needed if cacheDir == LocalDir
writeAllImages(int cacheDir_,const QUrl & localDir_)565 void Document::writeAllImages(int cacheDir_, const QUrl& localDir_) {
566   // images get 80 steps in saveDocument()
567   const uint stepSize = 1 + qMax(1, m_coll->entryCount()/80); // add 1 since it could round off
568   uint j = 1;
569 
570   ImageFactory::CacheDir cacheDir = static_cast<ImageFactory::CacheDir>(cacheDir_);
571   QScopedPointer<ImageDirectory> imgDir;
572   if(cacheDir == ImageFactory::LocalDir) {
573     imgDir.reset(new ImageDirectory(ImageFactory::localDirectory(localDir_)));
574   }
575 
576   QString id;
577   StringSet images;
578   EntryList entries = m_coll->entries();
579   FieldList imageFields = m_coll->imageFields();
580   foreach(EntryPtr entry, entries) {
581     foreach(FieldPtr field, imageFields) {
582       id = entry->field(field);
583       if(id.isEmpty() || images.has(id)) {
584         continue;
585       }
586       images.add(id);
587       if(ImageFactory::imageInfo(id).linkOnly) {
588         continue;
589       }
590       // careful here, if we're writing to LocalDir, need to read from the old LocalDir and write to new
591       bool success;
592       if(cacheDir == ImageFactory::LocalDir) {
593         success = ImageFactory::writeCachedImage(id, imgDir.data());
594       } else {
595         success = ImageFactory::writeCachedImage(id, cacheDir);
596       }
597       if(!success) {
598         myDebug() << "did not write image for entry title:" << entry->title();
599       }
600       if(m_cancelImageWriting) {
601         break;
602       }
603     }
604     if(j%stepSize == 0) {
605       ProgressManager::self()->setProgress(this, j/stepSize);
606     }
607     ++j;
608     if(m_cancelImageWriting) {
609       break;
610     }
611   }
612 
613   if(m_cancelImageWriting) {
614     myDebug() << "Document::writeAllImages() - cancel image writing";
615   }
616 
617   m_cancelImageWriting = false;
618 }
619 
pruneImages()620 bool Document::pruneImages() {
621   bool found = false;
622   QString id;
623   StringSet images;
624   Data::EntryList entries = m_coll->entries();
625   Data::FieldList imageFields = m_coll->imageFields();
626   foreach(EntryPtr entry, entries) {
627     foreach(FieldPtr field, imageFields) {
628       id = entry->field(field);
629       if(id.isEmpty() || images.has(id)) {
630         continue;
631       }
632       const Data::Image& img = ImageFactory::imageById(id);
633       if(img.isNull()) {
634         entry->setField(field, QString());
635         found = true;
636         myDebug() << "removing null image for" << entry->title() << ":" << id;
637       } else {
638         images.add(id);
639       }
640     }
641   }
642   return found;
643 }
644 
imageCount() const645 int Document::imageCount() const {
646   if(!m_coll) {
647     return 0;
648   }
649   StringSet images;
650   FieldList fields = m_coll->imageFields();
651   EntryList entries = m_coll->entries();
652   foreach(FieldPtr field, fields) {
653     foreach(EntryPtr entry, entries) {
654       images.add(entry->field(field));
655     }
656   }
657   return images.count();
658 }
659 
removeImagesNotInCollection(Tellico::Data::EntryList entries_,Tellico::Data::EntryList entriesToKeep_)660 void Document::removeImagesNotInCollection(Tellico::Data::EntryList entries_, Tellico::Data::EntryList entriesToKeep_) {
661   // first get list of all images in collection
662   StringSet images;
663   FieldList fields = m_coll->imageFields();
664   EntryList allEntries = m_coll->entries();
665   foreach(FieldPtr field, fields) {
666     foreach(EntryPtr entry, allEntries) {
667       images.add(entry->field(field));
668     }
669     foreach(EntryPtr entry, entriesToKeep_) {
670       images.add(entry->field(field));
671     }
672   }
673 
674   // now for all images not in the cache, we can clear them
675   StringSet imagesToCheck = ImageFactory::imagesNotInCache();
676 
677   // if entries_ is not empty, that means we want to limit the images removed
678   // to those that are referenced in those entries
679   StringSet imagesToRemove;
680   foreach(FieldPtr field, fields) {
681     foreach(EntryPtr entry, entries_) {
682       QString id = entry->field(field);
683       if(!id.isEmpty() && imagesToCheck.has(id) && !images.has(id)) {
684         imagesToRemove.add(id);
685       }
686     }
687   }
688 
689   const QStringList realImagesToRemove = imagesToRemove.values();
690   for(QStringList::ConstIterator it = realImagesToRemove.begin(); it != realImagesToRemove.end(); ++it) {
691     ImageFactory::removeImage(*it, false); // doesn't delete, just remove link
692   }
693 }
694