File indexing completed on 2024-04-28 04:21:23

0001 // SPDX-FileCopyrightText: 2009-2022 Jesper K. Pedersen <jesper.pedersen@kdab.com>
0002 // SPDX-FileCopyrightText: 2010 Jan Kundrát <jkt@flaska.net>
0003 // SPDX-FileCopyrightText: 2010 Tuomas Suutari <tuomas@nepnep.net>
0004 // SPDX-FileCopyrightText: 2012 Miika Turkia <miika.turkia@gmail.com>
0005 // SPDX-FileCopyrightText: 2013-2023 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0006 // SPDX-FileCopyrightText: 2015 Andreas Neustifter <andreas.neustifter@gmail.com>
0007 // SPDX-FileCopyrightText: 2015-2022 Tobias Leupold <tl@stonemx.de>
0008 //
0009 // SPDX-License-Identifier: GPL-2.0-or-later
0010 
0011 #include "ThumbnailModel.h"
0012 
0013 #include "CellGeometry.h"
0014 #include "FilterWidget.h"
0015 #include "Logging.h"
0016 #include "SelectionMaintainer.h"
0017 #include "ThumbnailRequest.h"
0018 #include "ThumbnailWidget.h"
0019 
0020 #include <DB/ImageDB.h>
0021 #include <ImageManager/AsyncLoader.h>
0022 #include <Utilities/FileUtil.h>
0023 #include <kpabase/FileName.h>
0024 #include <kpabase/Logging.h>
0025 #include <kpabase/SettingsData.h>
0026 #include <kpathumbnails/ThumbnailCache.h>
0027 
0028 #include <KLocalizedString>
0029 #include <QElapsedTimer>
0030 #include <QIcon>
0031 #include <QLoggingCategory>
0032 
0033 ThumbnailView::ThumbnailModel::ThumbnailModel(ThumbnailFactory *factory, const ImageManager::ThumbnailCache *thumbnailCache)
0034     : ThumbnailComponent(factory)
0035     , m_sortDirection(Settings::SettingsData::instance()->showNewestThumbnailFirst() ? NewestFirst : OldestFirst)
0036     , m_firstVisibleRow(-1)
0037     , m_lastVisibleRow(-1)
0038     , m_thumbnailCache(thumbnailCache)
0039 {
0040     connect(DB::ImageDB::instance(), SIGNAL(imagesDeleted(DB::FileNameList)), this, SLOT(imagesDeletedFromDB(DB::FileNameList)));
0041     m_ImagePlaceholder = QIcon::fromTheme(QLatin1String("image-x-generic")).pixmap(cellGeometryInfo()->preferredIconSize());
0042     m_VideoPlaceholder = QIcon::fromTheme(QLatin1String("video-x-generic")).pixmap(cellGeometryInfo()->preferredIconSize());
0043 
0044     m_filter.setSearchMode(0);
0045     connect(this, &ThumbnailModel::filterChanged, this, &ThumbnailModel::updateDisplayModel);
0046     connect(m_thumbnailCache, &ImageManager::ThumbnailCache::thumbnailUpdated, this, qOverload<const DB::FileName &>(&ThumbnailModel::updateCell));
0047 }
0048 
0049 static bool stackOrderComparator(const DB::FileName &a, const DB::FileName &b)
0050 {
0051     return DB::ImageDB::instance()->info(a)->stackOrder() < DB::ImageDB::instance()->info(b)->stackOrder();
0052 }
0053 
0054 void ThumbnailView::ThumbnailModel::updateDisplayModel()
0055 {
0056     QElapsedTimer timer;
0057     timer.start();
0058     beginResetModel();
0059     ImageManager::AsyncLoader::instance()->stop(model(), ImageManager::StopOnlyNonPriorityLoads);
0060 
0061     // Note, this can be simplified, if we make the database backend already
0062     // return things in the right order. Then we only need one pass while now
0063     // we need to go through the list two times.
0064 
0065     /* Extract all stacks we have first. Different stackid's might be
0066      * intermingled in the result so we need to know this ahead before
0067      * creating the display list.
0068      */
0069     m_allStacks.clear();
0070     typedef QList<DB::FileName> StackList;
0071     typedef QMap<DB::StackID, StackList> StackMap;
0072     StackMap stackContents;
0073     for (const DB::FileName &fileName : qAsConst(m_imageList)) {
0074         const DB::ImageInfoPtr imageInfo = DB::ImageDB::instance()->info(fileName);
0075         if (imageInfo && imageInfo->isStacked()) {
0076             DB::StackID stackid = imageInfo->stackId();
0077             m_allStacks << stackid;
0078             stackContents[stackid].append(fileName);
0079         }
0080     }
0081 
0082     /*
0083      * All stacks need to be ordered in their stack order. We don't rely that
0084      * the images actually came in the order necessary.
0085      */
0086     for (StackMap::iterator it = stackContents.begin(); it != stackContents.end(); ++it) {
0087         std::stable_sort(it->begin(), it->end(), stackOrderComparator);
0088     }
0089 
0090     /* Build the final list to be displayed. That is basically the sequence
0091      * we got from the original, but the stacks shown with all images together
0092      * in the right sequence or collapsed showing only the top image.
0093      */
0094     m_displayList = DB::FileNameList();
0095     QSet<DB::StackID> alreadyShownStacks;
0096     for (const DB::FileName &fileName : qAsConst(m_imageList)) {
0097         const DB::ImageInfoPtr imageInfo = DB::ImageDB::instance()->info(fileName);
0098         if (!m_filter.match(imageInfo))
0099             continue;
0100         if (imageInfo && imageInfo->isStacked()) {
0101             DB::StackID stackid = imageInfo->stackId();
0102             if (alreadyShownStacks.contains(stackid))
0103                 continue;
0104             StackMap::iterator found = stackContents.find(stackid);
0105             Q_ASSERT(found != stackContents.end());
0106             const StackList &orderedStack = *found;
0107             if (m_expandedStacks.contains(stackid)) {
0108                 for (const DB::FileName &fileName : orderedStack) {
0109                     m_displayList.append(fileName);
0110                 }
0111             } else {
0112                 m_displayList.append(orderedStack.at(0));
0113             }
0114             alreadyShownStacks.insert(stackid);
0115         } else {
0116             m_displayList.append(fileName);
0117         }
0118     }
0119 
0120     if (m_sortDirection != OldestFirst)
0121         m_displayList = m_displayList.reversed();
0122 
0123     updateIndexCache();
0124 
0125     Q_EMIT collapseAllStacksEnabled(m_expandedStacks.size() > 0);
0126     Q_EMIT expandAllStacksEnabled(m_allStacks.size() != m_expandedStacks.size());
0127     endResetModel();
0128     qCInfo(TimingLog) << "ThumbnailModel::updateDisplayModel(): " << timer.restart() << "ms.";
0129 }
0130 
0131 void ThumbnailView::ThumbnailModel::toggleStackExpansion(const DB::FileName &fileName)
0132 {
0133     const DB::ImageInfoPtr imageInfo = DB::ImageDB::instance()->info(fileName);
0134     if (imageInfo) {
0135         DB::StackID stackid = imageInfo->stackId();
0136         beginResetModel();
0137         if (m_expandedStacks.contains(stackid))
0138             m_expandedStacks.remove(stackid);
0139         else
0140             m_expandedStacks.insert(stackid);
0141         endResetModel();
0142         updateDisplayModel();
0143     }
0144 }
0145 
0146 void ThumbnailView::ThumbnailModel::collapseAllStacks()
0147 {
0148     m_expandedStacks.clear();
0149     updateDisplayModel();
0150 }
0151 
0152 void ThumbnailView::ThumbnailModel::expandAllStacks()
0153 {
0154     m_expandedStacks = m_allStacks;
0155     updateDisplayModel();
0156 }
0157 
0158 void ThumbnailView::ThumbnailModel::setImageList(const DB::FileNameList &items)
0159 {
0160     m_imageList = items;
0161     updateDisplayModel();
0162     preloadThumbnails();
0163 }
0164 
0165 DB::FileNameList ThumbnailView::ThumbnailModel::imageList(Order order) const
0166 {
0167     if (order == SortedOrder && m_sortDirection == NewestFirst)
0168         return m_displayList.reversed();
0169     else
0170         return m_displayList;
0171 }
0172 
0173 void ThumbnailView::ThumbnailModel::imagesDeletedFromDB(const DB::FileNameList &list)
0174 {
0175     SelectionMaintainer dummy(widget(), model());
0176 
0177     for (const DB::FileName &fileName : list) {
0178         m_displayList.removeAll(fileName);
0179         m_imageList.removeAll(fileName);
0180     }
0181     updateDisplayModel();
0182 }
0183 
0184 int ThumbnailView::ThumbnailModel::indexOf(const DB::FileName &fileName)
0185 {
0186     Q_ASSERT(!fileName.isNull());
0187     if (!m_fileNameToIndex.contains(fileName))
0188         m_fileNameToIndex.insert(fileName, m_displayList.indexOf(fileName));
0189 
0190     return m_fileNameToIndex[fileName];
0191 }
0192 
0193 int ThumbnailView::ThumbnailModel::indexOf(const DB::FileName &fileName) const
0194 {
0195     Q_ASSERT(!fileName.isNull());
0196     if (!m_fileNameToIndex.contains(fileName))
0197         return -1;
0198 
0199     return m_fileNameToIndex[fileName];
0200 }
0201 
0202 void ThumbnailView::ThumbnailModel::updateIndexCache()
0203 {
0204     m_fileNameToIndex.clear();
0205     int index = 0;
0206     for (const DB::FileName &fileName : qAsConst(m_displayList)) {
0207         m_fileNameToIndex[fileName] = index;
0208         ++index;
0209     }
0210 }
0211 
0212 DB::FileName ThumbnailView::ThumbnailModel::rightDropItem() const
0213 {
0214     return m_rightDrop;
0215 }
0216 
0217 void ThumbnailView::ThumbnailModel::setRightDropItem(const DB::FileName &item)
0218 {
0219     m_rightDrop = item;
0220 }
0221 
0222 DB::FileName ThumbnailView::ThumbnailModel::leftDropItem() const
0223 {
0224     return m_leftDrop;
0225 }
0226 
0227 void ThumbnailView::ThumbnailModel::setLeftDropItem(const DB::FileName &item)
0228 {
0229     m_leftDrop = item;
0230 }
0231 
0232 void ThumbnailView::ThumbnailModel::setSortDirection(SortDirection direction)
0233 {
0234     if (direction == m_sortDirection)
0235         return;
0236 
0237     Settings::SettingsData::instance()->setShowNewestFirst(direction == NewestFirst);
0238     m_displayList = m_displayList.reversed();
0239     updateIndexCache();
0240 
0241     m_sortDirection = direction;
0242 }
0243 
0244 bool ThumbnailView::ThumbnailModel::isItemInExpandedStack(const DB::StackID &id) const
0245 {
0246     return m_expandedStacks.contains(id);
0247 }
0248 
0249 int ThumbnailView::ThumbnailModel::imageCount() const
0250 {
0251     return m_displayList.size();
0252 }
0253 
0254 void ThumbnailView::ThumbnailModel::setOverrideImage(const DB::FileName &fileName, const QPixmap &pixmap)
0255 {
0256     if (pixmap.isNull())
0257         m_overrideFileName = DB::FileName();
0258     else {
0259         m_overrideFileName = fileName;
0260         m_overrideImage = pixmap;
0261     }
0262     Q_EMIT dataChanged(fileNameToIndex(fileName), fileNameToIndex(fileName));
0263 }
0264 
0265 DB::FileName ThumbnailView::ThumbnailModel::imageAt(int index) const
0266 {
0267     Q_ASSERT(index >= 0 && index < imageCount());
0268     return m_displayList.at(index);
0269 }
0270 
0271 int ThumbnailView::ThumbnailModel::rowCount(const QModelIndex &) const
0272 {
0273     return imageCount();
0274 }
0275 
0276 QVariant ThumbnailView::ThumbnailModel::data(const QModelIndex &index, int role) const
0277 {
0278     if (!index.isValid() || index.row() >= m_displayList.size())
0279         return QVariant();
0280 
0281     if (role == Qt::DecorationRole) {
0282         const DB::FileName fileName = m_displayList.at(index.row());
0283         return pixmap(fileName);
0284     }
0285 
0286     if (role == Qt::DisplayRole)
0287         return thumbnailText(index);
0288 
0289     return QVariant();
0290 }
0291 
0292 void ThumbnailView::ThumbnailModel::requestThumbnail(const DB::FileName &fileName, const ImageManager::Priority priority)
0293 {
0294     const DB::ImageInfoPtr imageInfo = DB::ImageDB::instance()->info(fileName);
0295     if (!imageInfo)
0296         return;
0297     // request the thumbnail in the size that is set in the settings, not in the current grid size:
0298     const QSize cellSize = cellGeometryInfo()->baseIconSize();
0299     const int angle = imageInfo->angle();
0300     const int row = indexOf(fileName);
0301     ThumbnailRequest *request
0302         = new ThumbnailRequest(row, fileName, cellSize, angle, this);
0303     request->setPriority(priority);
0304     ImageManager::AsyncLoader::instance()->load(request);
0305 }
0306 
0307 void ThumbnailView::ThumbnailModel::pixmapLoaded(ImageManager::ImageRequest *request, const QImage & /*image*/)
0308 {
0309     const DB::FileName fileName = request->databaseFileName();
0310     const QSize fullSize = request->fullSize();
0311 
0312     // As a result of the image being loaded, we Q_EMIT the dataChanged signal, which in turn asks the delegate to paint the cell
0313     // The delegate now fetches the newly loaded image from the cache.
0314 
0315     DB::ImageInfoPtr imageInfo = DB::ImageDB::instance()->info(fileName);
0316     // (hzeller): figure out, why the size is set here. We do an implicit
0317     // write here to the database.
0318     // (jzarl 2020-07-25): when loading a fullsize pixmap, we get the size "for free";
0319     // calculating it separately would cost us more than writing to the database from here.
0320     if (fullSize.isValid() && imageInfo) {
0321         imageInfo->setSize(fullSize);
0322     }
0323 
0324     Q_EMIT dataChanged(fileNameToIndex(fileName), fileNameToIndex(fileName));
0325 }
0326 
0327 QString ThumbnailView::ThumbnailModel::thumbnailText(const QModelIndex &index) const
0328 {
0329     const DB::FileName fileName = imageAt(index.row());
0330     const auto info = DB::ImageDB::instance()->info(fileName);
0331 
0332     QString text;
0333 
0334     const QSize cellSize = cellGeometryInfo()->preferredIconSize();
0335     const int thumbnailHeight = cellSize.height() - 2 * Settings::SettingsData::instance()->thumbnailSpace();
0336     const int thumbnailWidth = cellSize.width(); // no subtracting here
0337     const int maxCharacters = thumbnailHeight / QFontMetrics(widget()->font()).maxWidth() * 2;
0338 
0339     if (Settings::SettingsData::instance()->displayLabels()) {
0340         QString line = info->label();
0341         if (widget()->fontMetrics().horizontalAdvance(line) > thumbnailWidth) {
0342             line = line.left(maxCharacters);
0343             line += QLatin1String(" ...");
0344         }
0345         text += line + QLatin1String("\n");
0346     }
0347 
0348     if (Settings::SettingsData::instance()->displayCategories()) {
0349         QStringList grps = info->availableCategories();
0350         grps.sort();
0351         for (QStringList::const_iterator it = grps.constBegin(); it != grps.constEnd(); ++it) {
0352             QString category = *it;
0353             if (category != i18n("Folder") && category != i18n("Media Type")) {
0354                 Utilities::StringSet items = info->itemsOfCategory(category);
0355 
0356                 if (DB::ImageDB::instance()->untaggedCategoryFeatureConfigured()
0357                     && !Settings::SettingsData::instance()->untaggedImagesTagVisible()) {
0358 
0359                     if (category == Settings::SettingsData::instance()->untaggedCategory()) {
0360                         if (items.contains(Settings::SettingsData::instance()->untaggedTag())) {
0361                             items.remove(Settings::SettingsData::instance()->untaggedTag());
0362                         }
0363                     }
0364                 }
0365 
0366                 if (!items.empty()) {
0367                     auto itemList = items.values();
0368                     itemList.sort();
0369                     QString line = itemList.join(QLatin1String(", "));
0370 
0371                     if (widget()->fontMetrics().horizontalAdvance(line) > thumbnailWidth) {
0372                         line = line.left(maxCharacters);
0373                         line += QLatin1String(" ...");
0374                     }
0375                     text += line + QLatin1String("\n");
0376                 }
0377             }
0378         }
0379     }
0380 
0381     return text.trimmed();
0382 }
0383 
0384 void ThumbnailView::ThumbnailModel::updateCell(int row)
0385 {
0386     updateCell(index(row, 0));
0387 }
0388 
0389 void ThumbnailView::ThumbnailModel::updateCell(const QModelIndex &index)
0390 {
0391     Q_EMIT dataChanged(index, index);
0392 }
0393 
0394 void ThumbnailView::ThumbnailModel::updateCell(const DB::FileName &fileName)
0395 {
0396     if (fileName.isNull())
0397         return;
0398     updateCell(indexOf(fileName));
0399 }
0400 
0401 QModelIndex ThumbnailView::ThumbnailModel::fileNameToIndex(const DB::FileName &fileName) const
0402 {
0403     if (fileName.isNull())
0404         return QModelIndex();
0405     else
0406         return index(indexOf(fileName), 0);
0407 }
0408 
0409 QPixmap ThumbnailView::ThumbnailModel::pixmap(const DB::FileName &fileName) const
0410 {
0411     if (m_overrideFileName == fileName)
0412         return m_overrideImage;
0413 
0414     const DB::ImageInfoPtr imageInfo = DB::ImageDB::instance()->info(fileName);
0415     if (imageInfo == DB::ImageInfoPtr(nullptr))
0416         return QPixmap();
0417 
0418     if (m_thumbnailCache->contains(fileName)) {
0419         // the cached thumbnail needs to be scaled to the actual thumbnail size:
0420         return m_thumbnailCache->lookup(fileName).scaled(cellGeometryInfo()->preferredIconSize(), Qt::KeepAspectRatio);
0421     }
0422 
0423     const_cast<ThumbnailView::ThumbnailModel *>(this)->requestThumbnail(fileName, ImageManager::ThumbnailVisible);
0424     if (imageInfo->isVideo())
0425         return m_VideoPlaceholder;
0426     else
0427         return m_ImagePlaceholder;
0428 }
0429 
0430 bool ThumbnailView::ThumbnailModel::isFiltered() const
0431 {
0432     return !m_filter.isNull();
0433 }
0434 
0435 ThumbnailView::FilterWidget *ThumbnailView::ThumbnailModel::createFilterWidget(QWidget *parent)
0436 {
0437 
0438     auto filterWidget = new FilterWidget(parent);
0439     connect(this, &ThumbnailModel::filterChanged, filterWidget, &FilterWidget::setFilter);
0440     connect(filterWidget, &FilterWidget::ratingChanged, this, &ThumbnailModel::filterByRating);
0441     connect(filterWidget, &FilterWidget::filterToggled, this, &ThumbnailModel::toggleFilter);
0442     return filterWidget;
0443 }
0444 
0445 bool ThumbnailView::ThumbnailModel::thumbnailStillNeeded(int row) const
0446 {
0447     return (row >= m_firstVisibleRow && row <= m_lastVisibleRow);
0448 }
0449 
0450 void ThumbnailView::ThumbnailModel::updateVisibleRowInfo()
0451 {
0452     m_firstVisibleRow = widget()->indexAt(QPoint(0, 0)).row();
0453     const int columns = widget()->width() / cellGeometryInfo()->cellSize().width();
0454     const int rows = widget()->height() / cellGeometryInfo()->cellSize().height();
0455     m_lastVisibleRow = qMin(m_firstVisibleRow + columns * (rows + 1), rowCount(QModelIndex()));
0456 
0457     // the cellGeometry has changed -> update placeholders
0458     m_ImagePlaceholder = QIcon::fromTheme(QLatin1String("image-x-generic")).pixmap(cellGeometryInfo()->preferredIconSize());
0459     m_VideoPlaceholder = QIcon::fromTheme(QLatin1String("video-x-generic")).pixmap(cellGeometryInfo()->preferredIconSize());
0460 }
0461 
0462 void ThumbnailView::ThumbnailModel::toggleFilter(bool enable)
0463 {
0464     if (!enable)
0465         clearFilter();
0466     else if (m_filter.isNull()) {
0467         std::swap(m_filter, m_previousFilter);
0468         Q_EMIT filterChanged(m_filter);
0469     }
0470 }
0471 
0472 void ThumbnailView::ThumbnailModel::clearFilter()
0473 {
0474     if (!m_filter.isNull()) {
0475         qCDebug(ThumbnailViewLog) << "Filter cleared.";
0476         m_previousFilter = m_filter;
0477         m_filter = DB::ImageSearchInfo();
0478         Q_EMIT filterChanged(m_filter);
0479     }
0480 }
0481 
0482 void ThumbnailView::ThumbnailModel::filterByRating(short rating)
0483 {
0484     Q_ASSERT(-1 <= rating && rating <= 10);
0485     qCDebug(ThumbnailViewLog) << "Filter set: rating(" << rating << ")";
0486     m_filter.setRating(rating);
0487     Q_EMIT filterChanged(m_filter);
0488 }
0489 
0490 void ThumbnailView::ThumbnailModel::toggleRatingFilter(short rating)
0491 {
0492     if (m_filter.rating() == rating) {
0493         filterByRating(rating);
0494     } else {
0495         filterByRating(-1);
0496         qCDebug(ThumbnailViewLog) << "Filter removed: rating";
0497         m_filter.setRating(-1);
0498         m_filter.checkIfNull();
0499         Q_EMIT filterChanged(m_filter);
0500     }
0501 }
0502 
0503 void ThumbnailView::ThumbnailModel::filterByCategory(const QString &category, const QString &tag)
0504 {
0505     qCDebug(ThumbnailViewLog) << "Filter added: category(" << category << "," << tag << ")";
0506 
0507     m_filter.addAnd(category, tag);
0508     Q_EMIT filterChanged(m_filter);
0509 }
0510 
0511 void ThumbnailView::ThumbnailModel::toggleCategoryFilter(const QString &category, const QString &tag)
0512 {
0513     auto tags = m_filter.categoryMatchText(category).split(QLatin1String("&"), Qt::SkipEmptyParts);
0514     for (const auto &existingTag : qAsConst(tags)) {
0515         if (tag == existingTag.trimmed()) {
0516             qCDebug(ThumbnailViewLog) << "Filter removed: category(" << category << "," << tag << ")";
0517             tags.removeAll(existingTag);
0518             m_filter.setCategoryMatchText(category, tags.join(QLatin1String(" & ")));
0519             m_filter.checkIfNull();
0520             Q_EMIT filterChanged(m_filter);
0521             return;
0522         }
0523     }
0524     filterByCategory(category, tag);
0525 }
0526 
0527 void ThumbnailView::ThumbnailModel::filterByFreeformText(const QString &text)
0528 {
0529     qCDebug(ThumbnailViewLog) << "Filter added: freeform_match(" << text << ")";
0530     m_filter.setFreeformMatchText(text);
0531     Q_EMIT filterChanged(m_filter);
0532 }
0533 
0534 void ThumbnailView::ThumbnailModel::preloadThumbnails()
0535 {
0536     // FIXME: it would make a lot of sense to merge preloadThumbnails() with pixmap()
0537     // and maybe also move the caching stuff into the ImageManager
0538     for (const DB::FileName &fileName : qAsConst(m_displayList)) {
0539         if (fileName.isNull())
0540             continue;
0541 
0542         if (m_thumbnailCache->contains(fileName))
0543             continue;
0544         const_cast<ThumbnailView::ThumbnailModel *>(this)->requestThumbnail(fileName, ImageManager::ThumbnailInvisible);
0545     }
0546 }
0547 
0548 // vi:expandtab:tabstop=4 shiftwidth=4:
0549 
0550 #include "moc_ThumbnailModel.cpp"