File indexing completed on 2025-01-19 03:56:07
0001 /* ============================================================ 0002 * 0003 * This file is a part of digiKam project 0004 * https://www.digikam.org 0005 * 0006 * Date : 2009-03-08 0007 * Description : Qt item model for database entries, listing done with database job 0008 * 0009 * SPDX-FileCopyrightText: 2009-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de> 0010 * SPDX-FileCopyrightText: 2015 by Mohamed_Anwer <m_dot_anwer at gmx dot com> 0011 * 0012 * SPDX-License-Identifier: GPL-2.0-or-later 0013 * 0014 * ============================================================ */ 0015 0016 #include "itemalbummodel.h" 0017 0018 // Qt includes 0019 0020 #include <QTimer> 0021 0022 // Local includes 0023 0024 #include "digikam_debug.h" 0025 #include "albummanager.h" 0026 #include "coredbaccess.h" 0027 #include "coredbchangesets.h" 0028 #include "facetagsiface.h" 0029 #include "coredbwatch.h" 0030 #include "coredburl.h" 0031 #include "iteminfo.h" 0032 #include "iteminfolist.h" 0033 #include "itemlister.h" 0034 #include "dnotificationwrapper.h" 0035 #include "digikamapp.h" 0036 #include "dbjobsmanager.h" 0037 #include "dbjobsthread.h" 0038 0039 namespace Digikam 0040 { 0041 0042 class Q_DECL_HIDDEN ItemAlbumModel::Private 0043 { 0044 public: 0045 0046 explicit Private() 0047 : jobThread (nullptr), 0048 incrementalTimer (nullptr), 0049 recurseAlbums (false), 0050 recurseTags (false), 0051 listOnlyAvailableImages (false), 0052 extraValueJob (false) 0053 { 0054 } 0055 0056 QList<Album*> currentAlbums; 0057 DBJobsThread* jobThread; 0058 QTimer* incrementalTimer; 0059 0060 bool recurseAlbums; 0061 bool recurseTags; 0062 bool listOnlyAvailableImages; 0063 QString specialListing; 0064 0065 bool extraValueJob; 0066 }; 0067 0068 ItemAlbumModel::ItemAlbumModel(QWidget* const parent) 0069 : ItemThumbnailModel(parent), 0070 d(new Private) 0071 { 0072 qRegisterMetaType<QList<ItemListerRecord>>("QList<ItemListerRecord>"); 0073 0074 d->incrementalTimer = new QTimer(this); 0075 d->incrementalTimer->setSingleShot(true); 0076 0077 connect(d->incrementalTimer, SIGNAL(timeout()), 0078 this, SLOT(slotNextIncrementalRefresh())); 0079 0080 connect(this, SIGNAL(readyForIncrementalRefresh()), 0081 this, SLOT(incrementalRefresh())); 0082 0083 connect(CoreDbAccess::databaseWatch(), SIGNAL(collectionImageChange(CollectionImageChangeset)), 0084 this, SLOT(slotCollectionImageChange(CollectionImageChangeset))); 0085 0086 connect(CoreDbAccess::databaseWatch(), SIGNAL(searchChange(SearchChangeset)), 0087 this, SLOT(slotSearchChange(SearchChangeset))); 0088 0089 connect(AlbumManager::instance(), SIGNAL(signalAlbumAdded(Album*)), 0090 this, SLOT(slotAlbumAdded(Album*))); 0091 0092 connect(AlbumManager::instance(), SIGNAL(signalAlbumDeleted(Album*)), 0093 this, SLOT(slotAlbumDeleted(Album*))); 0094 0095 connect(AlbumManager::instance(), SIGNAL(signalAlbumRenamed(Album*)), 0096 this, SLOT(slotAlbumRenamed(Album*))); 0097 0098 connect(AlbumManager::instance(), SIGNAL(signalAlbumsCleared()), 0099 this, SLOT(slotAlbumsCleared())); 0100 0101 connect(AlbumManager::instance(), SIGNAL(signalShowOnlyAvailableAlbumsChanged(bool)), 0102 this, SLOT(setListOnlyAvailableImages(bool))); 0103 } 0104 0105 ItemAlbumModel::~ItemAlbumModel() 0106 { 0107 if (d->jobThread) 0108 { 0109 d->jobThread->cancel(); 0110 d->jobThread = nullptr; 0111 } 0112 0113 delete d; 0114 } 0115 0116 QList<Album*> ItemAlbumModel::currentAlbums() const 0117 { 0118 return d->currentAlbums; 0119 } 0120 0121 void ItemAlbumModel::setRecurseAlbums(bool recursiveListing) 0122 { 0123 if (d->recurseAlbums != recursiveListing) 0124 { 0125 d->recurseAlbums = recursiveListing; 0126 refresh(); 0127 } 0128 } 0129 0130 void ItemAlbumModel::setRecurseTags(bool recursiveListing) 0131 { 0132 if (d->recurseTags != recursiveListing) 0133 { 0134 d->recurseTags = recursiveListing; 0135 refresh(); 0136 } 0137 } 0138 0139 void ItemAlbumModel::setListOnlyAvailableImages(bool onlyAvailable) 0140 { 0141 if (d->listOnlyAvailableImages!= onlyAvailable) 0142 { 0143 d->listOnlyAvailableImages = onlyAvailable; 0144 refresh(); 0145 } 0146 } 0147 0148 bool ItemAlbumModel::isRecursingAlbums() const 0149 { 0150 return d->recurseAlbums; 0151 } 0152 0153 bool ItemAlbumModel::isRecursingTags() const 0154 { 0155 return d->recurseTags; 0156 } 0157 0158 bool ItemAlbumModel::isListingOnlyAvailableImages() const 0159 { 0160 return d->listOnlyAvailableImages; 0161 } 0162 0163 void ItemAlbumModel::setSpecialTagListing(const QString& specialListing) 0164 { 0165 if (d->specialListing != specialListing) 0166 { 0167 d->specialListing = specialListing; 0168 refresh(); 0169 } 0170 } 0171 0172 void ItemAlbumModel::openAlbum(const QList<Album*>& albums) 0173 { 0174 if (d->currentAlbums == albums) 0175 { 0176 return; 0177 } 0178 0179 d->currentAlbums.clear(); 0180 0181 /** 0182 * Extra safety, ensure that no null pointers are added 0183 */ 0184 Q_FOREACH (Album* const a, albums) 0185 { 0186 if (a) 0187 { 0188 d->currentAlbums << a; 0189 } 0190 } 0191 /* 0192 Q_EMIT listedAlbumChanged(d->currentAlbums); 0193 */ 0194 refresh(); 0195 } 0196 0197 void ItemAlbumModel::refresh() 0198 { 0199 if (d->jobThread) 0200 { 0201 d->jobThread->cancel(); 0202 d->jobThread = nullptr; 0203 } 0204 0205 clearItemInfos(); 0206 0207 if (d->currentAlbums.isEmpty()) 0208 { 0209 return; 0210 } 0211 0212 /* 0213 // TODO: Figure out how to deal with root album 0214 0215 if (d->currentAlbum->isRoot()) 0216 { 0217 return; 0218 } 0219 */ 0220 0221 startRefresh(); 0222 0223 startListJob(d->currentAlbums); 0224 } 0225 0226 void ItemAlbumModel::incrementalRefresh() 0227 { 0228 // The path to this method is: 0229 // scheduleIncrementalRefresh -> incrementalTimer waits 100ms -> slotNextIncrementalRefresh 0230 // -> ItemModel::requestIncrementalRefresh -> waits until model is ready, maybe immediately 0231 // -> to this method via SIGNAL(readyForIncrementalRefresh()) 0232 0233 if (d->currentAlbums.isEmpty()) 0234 { 0235 return; 0236 } 0237 0238 if (d->jobThread) 0239 { 0240 d->jobThread->cancel(); 0241 d->jobThread = nullptr; 0242 } 0243 0244 startIncrementalRefresh(); 0245 0246 startListJob(d->currentAlbums); 0247 } 0248 0249 bool ItemAlbumModel::hasScheduledRefresh() const 0250 { 0251 return (d->incrementalTimer->isActive() || hasIncrementalRefreshPending()); 0252 } 0253 0254 void ItemAlbumModel::scheduleIncrementalRefresh() 0255 { 0256 if (!hasScheduledRefresh()) 0257 { 0258 d->incrementalTimer->start(250); 0259 } 0260 } 0261 0262 void ItemAlbumModel::slotNextIncrementalRefresh() 0263 { 0264 if (d->jobThread) 0265 { 0266 d->incrementalTimer->start(100); 0267 } 0268 else 0269 { 0270 requestIncrementalRefresh(); 0271 } 0272 } 0273 0274 void ItemAlbumModel::startListJob(const QList<Album*>& albums) 0275 { 0276 if (albums.isEmpty()) 0277 { 0278 return; 0279 } 0280 0281 if (d->jobThread) 0282 { 0283 d->jobThread->cancel(); 0284 d->jobThread = nullptr; 0285 } 0286 0287 // stop preloading Thumbnails 0288 0289 imageInfosCleared(); 0290 0291 if (albums.first()->isTrashAlbum()) 0292 { 0293 return; 0294 } 0295 0296 CoreDbUrl url; 0297 QList<int> ids; 0298 0299 if ((albums.first()->type() == Album::TAG) || (albums.first()->type() == Album::SEARCH)) 0300 { 0301 for (QList<Album*>::const_iterator it = albums.constBegin() ; it != albums.constEnd() ; ++it) 0302 { 0303 ids << (*it)->id(); 0304 } 0305 0306 if (albums.first()->type() == Album::TAG) 0307 { 0308 url = CoreDbUrl::fromTagIds(ids); 0309 } 0310 } 0311 else 0312 { 0313 url = albums.first()->databaseUrl(); 0314 } 0315 0316 if (albums.first()->type() == Album::DATE) 0317 { 0318 d->extraValueJob = false; 0319 0320 DatesDBJobInfo jobInfo; 0321 0322 jobInfo.setStartDate(url.startDate()); 0323 jobInfo.setEndDate(url.endDate()); 0324 0325 if (d->recurseAlbums) 0326 { 0327 jobInfo.setRecursive(); 0328 } 0329 0330 if (d->listOnlyAvailableImages) 0331 { 0332 jobInfo.setListAvailableImagesOnly(); 0333 } 0334 0335 d->jobThread = DBJobsManager::instance()->startDatesJobThread(jobInfo); 0336 } 0337 else if (albums.first()->type() == Album::TAG) 0338 { 0339 d->extraValueJob = false; 0340 0341 TagsDBJobInfo jobInfo; 0342 0343 if (d->listOnlyAvailableImages) 0344 { 0345 jobInfo.setListAvailableImagesOnly(); 0346 } 0347 0348 if (d->recurseTags) 0349 { 0350 jobInfo.setRecursive(); 0351 } 0352 0353 jobInfo.setTagsIds(ids); 0354 0355 if (!d->specialListing.isNull()) 0356 { 0357 jobInfo.setSpecialTag(d->specialListing); 0358 d->extraValueJob = true; 0359 } 0360 0361 d->jobThread = DBJobsManager::instance()->startTagsJobThread(jobInfo); 0362 } 0363 else if (albums.first()->type() == Album::PHYSICAL) 0364 { 0365 d->extraValueJob = false; 0366 AlbumsDBJobInfo jobInfo; 0367 0368 if (d->recurseAlbums) 0369 { 0370 jobInfo.setRecursive(); 0371 } 0372 0373 if (d->listOnlyAvailableImages) 0374 { 0375 jobInfo.setListAvailableImagesOnly(); 0376 } 0377 0378 jobInfo.setAlbumRootId(url.albumRootId()); 0379 jobInfo.setAlbum( url.album() ); 0380 0381 d->jobThread = DBJobsManager::instance()->startAlbumsJobThread(jobInfo); 0382 } 0383 else if (albums.first()->type() == Album::SEARCH) 0384 { 0385 d->extraValueJob = false; 0386 0387 SearchesDBJobInfo jobInfo(std::move(ids)); 0388 0389 if (d->listOnlyAvailableImages) 0390 { 0391 jobInfo.setListAvailableImagesOnly(); 0392 } 0393 0394 d->jobThread = DBJobsManager::instance()->startSearchesJobThread(jobInfo); 0395 } 0396 0397 connect(d->jobThread, SIGNAL(finished()), 0398 this, SLOT(slotResult())); 0399 0400 connect(d->jobThread, SIGNAL(data(QList<ItemListerRecord>)), 0401 this, SLOT(slotData(QList<ItemListerRecord>))); 0402 } 0403 0404 void ItemAlbumModel::slotResult() 0405 { 0406 if (d->jobThread != sender()) 0407 { 0408 return; 0409 } 0410 0411 if (d->jobThread->hasErrors()) 0412 { 0413 qCWarning(DIGIKAM_GENERAL_LOG) << "Failed to list url: " 0414 << d->jobThread->errorsList().first(); 0415 0416 // Pop-up a message about the error. 0417 0418 DNotificationWrapper(QString(), d->jobThread->errorsList().first(), 0419 DigikamApp::instance(), DigikamApp::instance()->windowTitle()); 0420 } 0421 0422 d->jobThread->cancel(); 0423 d->jobThread = nullptr; 0424 0425 // either of the two 0426 0427 finishRefresh(); 0428 finishIncrementalRefresh(); 0429 } 0430 0431 void ItemAlbumModel::slotData(const QList<ItemListerRecord>& records) 0432 { 0433 if (d->jobThread != sender()) 0434 { 0435 return; 0436 } 0437 0438 if (records.isEmpty()) 0439 { 0440 qCDebug(DIGIKAM_GENERAL_LOG) << "Data From DBJobsThread is null: " << records.isEmpty(); 0441 return; 0442 } 0443 0444 ItemInfoList newItemsList; 0445 0446 if (d->extraValueJob) 0447 { 0448 QList<QVariant> extraValues; 0449 0450 Q_FOREACH (const ItemListerRecord& record, records) 0451 { 0452 ItemInfo info(record); 0453 newItemsList << info; 0454 0455 if (d->specialListing == QLatin1String("faces")) 0456 { 0457 FaceTagsIface face = FaceTagsIface::fromListing(info.id(), record.extraValues); 0458 extraValues << face.toVariant(); 0459 } 0460 else 0461 { 0462 // default handling: just pass extraValue 0463 0464 if (record.extraValues.isEmpty()) 0465 { 0466 extraValues << QVariant(); 0467 } 0468 else if (record.extraValues.size() == 1) 0469 { 0470 extraValues << record.extraValues.first(); 0471 } 0472 else 0473 { 0474 extraValues << QVariant(record.extraValues); // uh-uh. List in List. 0475 } 0476 } 0477 } 0478 0479 addItemInfos(newItemsList, extraValues); 0480 } 0481 else 0482 { 0483 Q_FOREACH (const ItemListerRecord& record, records) 0484 { 0485 ItemInfo info(record); 0486 newItemsList << info; 0487 } 0488 0489 addItemInfos(newItemsList); 0490 } 0491 0492 // A refresh has just been completed, but another one has already 0493 // started, then slow down the refresh significantly. 0494 0495 if (hasScheduledRefresh()) 0496 { 0497 d->incrementalTimer->start(1000); 0498 } 0499 } 0500 0501 void ItemAlbumModel::slotImageChange(const ImageChangeset& changeset) 0502 { 0503 if (d->currentAlbums.isEmpty()) 0504 { 0505 return; 0506 } 0507 0508 ItemModel::slotImageChange(changeset); 0509 0510 // already scheduled to refresh? 0511 0512 if (hasScheduledRefresh()) 0513 { 0514 return; 0515 } 0516 0517 // this is for the case that _only_ the status changes, i.e., explicit setVisible() 0518 0519 if ((DatabaseFields::Images)changeset.changes() == DatabaseFields::Status) 0520 { 0521 scheduleIncrementalRefresh(); 0522 0523 return; 0524 } 0525 0526 // If we list a search, a change to a property may alter the search result 0527 0528 QList<Album*>::iterator it; 0529 0530 /** 0531 * QList is designed for multiple selection, for now, only tags are supported 0532 * for SAlbum it will be a list with one element 0533 */ 0534 0535 for (it = d->currentAlbums.begin() ; it != d->currentAlbums.end() ; ++it) 0536 { 0537 if ((*it)->type() == Album::SEARCH) 0538 { 0539 SAlbum* const salbum = static_cast<SAlbum*>(*it); 0540 bool needCheckRefresh = false; 0541 0542 if (salbum->isNormalSearch()) 0543 { 0544 // For searches any touched field can require a refresh. 0545 // We cannot easily find out which fields are searched for, so we refresh for any change. 0546 0547 needCheckRefresh = true; 0548 } 0549 else if (salbum->isTimelineSearch()) 0550 { 0551 if (changeset.changes() & DatabaseFields::CreationDate) 0552 { 0553 needCheckRefresh = true; 0554 } 0555 } 0556 else if (salbum->isMapSearch()) 0557 { 0558 if (changeset.changes() & DatabaseFields::ItemPositionsAll) 0559 { 0560 needCheckRefresh = true; 0561 } 0562 } 0563 0564 if (needCheckRefresh) 0565 { 0566 Q_FOREACH (const qlonglong& id, changeset.ids()) 0567 { 0568 // if one matching image id is found, trigger a refresh 0569 0570 if (hasImage(id)) 0571 { // cppcheck-suppress useStlAlgorithm 0572 scheduleIncrementalRefresh(); 0573 break; 0574 } 0575 } 0576 } 0577 } 0578 } 0579 } 0580 0581 void ItemAlbumModel::slotImageTagChange(const ImageTagChangeset& changeset) 0582 { 0583 if (d->currentAlbums.isEmpty()) 0584 { 0585 return; 0586 } 0587 0588 ItemModel::slotImageTagChange(changeset); 0589 0590 // already scheduled to refresh? 0591 0592 if (hasScheduledRefresh()) 0593 { 0594 return; 0595 } 0596 0597 bool doRefresh = false; 0598 QList<Album*>::iterator it; 0599 0600 for (it = d->currentAlbums.begin() ; it != d->currentAlbums.end() ; ++it) 0601 { 0602 if ((*it)->type() == Album::TAG) 0603 { 0604 doRefresh = changeset.containsTag((*it)->id()); 0605 0606 if (!doRefresh && d->recurseTags) 0607 { 0608 Q_FOREACH (int tagId, changeset.tags()) 0609 { 0610 Album* const a = AlbumManager::instance()->findTAlbum(tagId); 0611 0612 if (a && (*it)->isAncestorOf(a)) 0613 { 0614 doRefresh = true; 0615 break; 0616 } 0617 } 0618 } 0619 } 0620 0621 if (doRefresh) 0622 { 0623 break; 0624 } 0625 } 0626 0627 if (doRefresh) 0628 { 0629 scheduleIncrementalRefresh(); 0630 } 0631 } 0632 0633 void ItemAlbumModel::slotCollectionImageChange(const CollectionImageChangeset& changeset) 0634 { 0635 if (d->currentAlbums.isEmpty()) 0636 { 0637 return; 0638 } 0639 0640 // already scheduled to refresh? 0641 0642 if (hasScheduledRefresh()) 0643 { 0644 return; 0645 } 0646 0647 bool doRefresh = false; 0648 0649 QList<Album*>::iterator it; 0650 0651 for (it = d->currentAlbums.begin() ; it != d->currentAlbums.end() ; ++it) 0652 { 0653 switch (changeset.operation()) 0654 { 0655 case CollectionImageChangeset::Added: 0656 { 0657 switch ((*it)->type()) 0658 { 0659 case Album::PHYSICAL: 0660 0661 // that's easy: try if our album is affected 0662 0663 doRefresh = changeset.containsAlbum((*it)->id()); 0664 0665 if (!doRefresh && d->recurseAlbums) 0666 { 0667 Q_FOREACH (int albumId, changeset.albums()) 0668 { 0669 Album* const a = AlbumManager::instance()->findPAlbum(albumId); 0670 0671 if (a && (*it)->isAncestorOf(a)) 0672 { 0673 doRefresh = true; 0674 break; 0675 } 0676 } 0677 } 0678 0679 break; 0680 0681 default: 0682 { 0683 // we cannot easily know if we are affected 0684 0685 doRefresh = true; 0686 break; 0687 } 0688 } 0689 0690 break; 0691 } 0692 0693 case CollectionImageChangeset::Deleted: 0694 case CollectionImageChangeset::Removed: 0695 case CollectionImageChangeset::RemovedAll: 0696 0697 // is one of our images affected? 0698 0699 Q_FOREACH (const qlonglong& id, changeset.ids()) 0700 { 0701 // if one matching image id is found, trigger a refresh 0702 0703 if (hasImage(id)) 0704 { // cppcheck-suppress useStlAlgorithm 0705 doRefresh = true; 0706 break; 0707 } 0708 } 0709 0710 break; 0711 0712 default: 0713 { 0714 break; 0715 } 0716 } 0717 0718 if (doRefresh) 0719 { 0720 break; 0721 } 0722 } 0723 0724 if (doRefresh) 0725 { 0726 scheduleIncrementalRefresh(); 0727 } 0728 } 0729 0730 void ItemAlbumModel::slotSearchChange(const SearchChangeset& changeset) 0731 { 0732 if (d->currentAlbums.isEmpty()) 0733 { 0734 return; 0735 } 0736 0737 if (changeset.operation() != SearchChangeset::Changed) 0738 { 0739 return; 0740 } 0741 0742 SAlbum* const album = AlbumManager::instance()->findSAlbum(changeset.searchId()); 0743 0744 QList<Album*>::iterator it; 0745 0746 for (it = d->currentAlbums.begin() ; it != d->currentAlbums.end() ; ++it) 0747 { 0748 if (album && (*it) == album) 0749 { 0750 scheduleIncrementalRefresh(); 0751 } 0752 } 0753 } 0754 0755 void ItemAlbumModel::slotAlbumAdded(Album* /*album*/) 0756 { 0757 } 0758 0759 void ItemAlbumModel::slotAlbumDeleted(Album* album) 0760 { 0761 if (d->currentAlbums.contains(album)) 0762 { 0763 d->currentAlbums.removeOne(album); 0764 clearItemInfos(); 0765 return; 0766 } 0767 0768 // display changed tags 0769 0770 if (album->type() == Album::TAG) 0771 { 0772 emitDataChangedForAll(); 0773 } 0774 } 0775 0776 void ItemAlbumModel::slotAlbumRenamed(Album* album) 0777 { 0778 // display changed names 0779 0780 if (d->currentAlbums.contains(album)) 0781 { 0782 emitDataChangedForAll(); 0783 } 0784 } 0785 0786 void ItemAlbumModel::slotAlbumsCleared() 0787 { 0788 d->currentAlbums.clear(); 0789 clearItemInfos(); 0790 } 0791 0792 } // namespace Digikam 0793 0794 #include "moc_itemalbummodel.cpp"