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"