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