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"