File indexing completed on 2024-11-10 05:02:44

0001 /*
0002     SPDX-FileCopyrightText: 2022 Fushan Wen <qydwhotmail@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "abstractimagelistmodel.h"
0008 
0009 #include <QPainter>
0010 #include <QThreadPool>
0011 
0012 #include <KFileItem>
0013 #include <KIO/PreviewJob>
0014 
0015 #include "../finder/mediametadatafinder.h"
0016 #include "config-KExiv2.h"
0017 
0018 AbstractImageListModel::AbstractImageListModel(const QBindable<QSize> &bindableTargetSize, const QBindable<bool> &bindableUsedInConfig, QObject *parent)
0019     : QAbstractListModel(parent)
0020 {
0021     m_targetSize.setBinding(bindableTargetSize.makeBinding());
0022     m_screenshotSize.setBinding([this] {
0023         return m_targetSize.value() / 8;
0024     });
0025     m_targetSizeChangeNotifier = m_screenshotSize.addNotifier([this] {
0026         reload();
0027     });
0028     m_usedInConfig.setBinding(bindableUsedInConfig.makeBinding());
0029 
0030     constexpr int maxCacheSize = 10;
0031     m_imageCache.setMaxCost(maxCacheSize);
0032     m_backgroundTitleCache.setMaxCost(maxCacheSize);
0033     m_backgroundAuthorCache.setMaxCost(maxCacheSize);
0034     m_imageSizeCache.setMaxCost(maxCacheSize);
0035 
0036     connect(this, &QAbstractListModel::rowsInserted, this, &AbstractImageListModel::countChanged);
0037     connect(this, &QAbstractListModel::rowsRemoved, this, &AbstractImageListModel::countChanged);
0038     connect(this, &QAbstractListModel::modelReset, this, &AbstractImageListModel::countChanged);
0039 }
0040 
0041 QHash<int, QByteArray> AbstractImageListModel::roleNames() const
0042 {
0043     return {
0044         {Qt::DisplayRole, "display"},
0045         {Qt::DecorationRole, "decoration"},
0046         {AuthorRole, "author"},
0047         {ScreenshotRole, "screenshot"},
0048         {PathRole, "path"},
0049         {PackageNameRole, "packageName"},
0050         {RemovableRole, "removable"},
0051         {PendingDeletionRole, "pendingDeletion"},
0052         {ToggleRole, "checked"},
0053     };
0054 }
0055 
0056 int AbstractImageListModel::count() const
0057 {
0058     return rowCount();
0059 }
0060 
0061 void AbstractImageListModel::load(const QStringList &customPaths)
0062 {
0063     Q_ASSERT(!m_loading && !customPaths.empty());
0064     m_customPaths = customPaths;
0065     m_customPaths.removeDuplicates();
0066     m_loading = true;
0067 }
0068 
0069 void AbstractImageListModel::reload()
0070 {
0071     if (m_loading || m_customPaths.empty()) {
0072         return;
0073     }
0074 
0075     load(m_customPaths);
0076 }
0077 
0078 void AbstractImageListModel::slotHandlePreview(const KFileItem &item, const QPixmap &preview)
0079 {
0080     auto job = qobject_cast<KIO::PreviewJob *>(sender());
0081     if (!job) {
0082         return;
0083     }
0084 
0085     const QString urlString = item.url().toLocalFile();
0086     const QPersistentModelIndex idx = job->property("index").toPersistentModelIndex();
0087 
0088     auto it = m_previewJobsUrls.find(idx);
0089     Q_ASSERT(it != m_previewJobsUrls.end());
0090     it->removeOne(urlString);
0091 
0092     const QStringList paths = job->property("paths").toStringList();
0093     auto cachedPreviewIt = m_imageTempCache.find(paths);
0094 
0095     if (cachedPreviewIt == m_imageTempCache.end() && !it->empty()) {
0096         m_imageTempCache.insert(paths, preview);
0097         // it->empty() is handled in the end
0098         return;
0099     } else if (cachedPreviewIt != m_imageTempCache.end()) {
0100         // Show multiple images side by side
0101         QPainter p(&*cachedPreviewIt);
0102 
0103         const int i = paths.indexOf(urlString);
0104         const double start = i / static_cast<double>(paths.size());
0105         const double end = (i + 1) / static_cast<double>(paths.size());
0106         // Cropped area
0107         const QPoint topLeft(start * preview.width(), 0);
0108         const QPoint bottomRight(end * preview.width(), preview.height());
0109         // Inserted area
0110         const QPoint topLeft2(start * cachedPreviewIt->width(), 0);
0111         const QPoint bottomRight2(end * cachedPreviewIt->width(), cachedPreviewIt->height());
0112 
0113         p.drawPixmap(QRect(topLeft2, bottomRight2), preview.copy(QRect(topLeft, bottomRight)));
0114     }
0115 
0116     if (it->empty()) {
0117         // All images in the list have been loaded
0118         m_previewJobsUrls.erase(it);
0119 
0120         QPixmap *finalPreview = nullptr;
0121         if (cachedPreviewIt == m_imageTempCache.end()) {
0122             // Single image
0123             finalPreview = new QPixmap(preview);
0124         } else {
0125             // Side-by-side image
0126             finalPreview = new QPixmap(*cachedPreviewIt);
0127             m_imageTempCache.erase(cachedPreviewIt);
0128         }
0129 
0130         if (m_imageCache.insert(paths, finalPreview, 1)) {
0131             Q_EMIT dataChanged(idx, idx, {ScreenshotRole});
0132         } else {
0133             delete finalPreview;
0134         }
0135     }
0136 }
0137 
0138 void AbstractImageListModel::slotHandlePreviewFailed(const KFileItem &item)
0139 {
0140     auto job = qobject_cast<KIO::PreviewJob *>(sender());
0141     if (!job) {
0142         return;
0143     }
0144 
0145     auto it = m_previewJobsUrls.find(job->property("index").toPersistentModelIndex());
0146     Q_ASSERT(it != m_previewJobsUrls.end());
0147 
0148     it->removeOne(item.url().toLocalFile());
0149     if (it->empty()) {
0150         m_previewJobsUrls.erase(it);
0151     }
0152 }
0153 
0154 void AbstractImageListModel::asyncGetPreview(const QStringList &paths, const QPersistentModelIndex &index) const
0155 {
0156     if (m_previewJobsUrls.contains(index) || paths.isEmpty()) {
0157         return;
0158     }
0159 
0160     const QStringList availablePlugins = KIO::PreviewJob::availablePlugins();
0161     KFileItemList list;
0162 
0163     for (const QString &path : paths) {
0164         list.append(KFileItem(QUrl::fromLocalFile(path), QString(), 0));
0165     }
0166 
0167     KIO::PreviewJob *const job = KIO::filePreview(list, m_screenshotSize, &availablePlugins);
0168     job->setIgnoreMaximumSize(true);
0169 
0170     job->setProperty("paths", paths);
0171     job->setProperty("index", index);
0172 
0173     connect(job, &KIO::PreviewJob::gotPreview, this, &AbstractImageListModel::slotHandlePreview);
0174     connect(job, &KIO::PreviewJob::failed, this, &AbstractImageListModel::slotHandlePreviewFailed);
0175 
0176     m_previewJobsUrls.insert(index, paths);
0177 }
0178 
0179 void AbstractImageListModel::asyncGetMediaMetadata(const QString &path, const QPersistentModelIndex &index) const
0180 {
0181     if (m_sizeJobsUrls.contains(path) || path.isEmpty()) {
0182         return;
0183     }
0184 
0185     MediaMetadataFinder *finder = new MediaMetadataFinder(path);
0186     connect(finder, &MediaMetadataFinder::metadataFound, this, &AbstractImageListModel::slotMediaMetadataFound);
0187     QThreadPool::globalInstance()->start(finder);
0188 
0189     m_sizeJobsUrls.insert(path, index);
0190 }
0191 
0192 void AbstractImageListModel::clearCache()
0193 {
0194     m_imageCache.clear();
0195     m_backgroundTitleCache.clear();
0196     m_backgroundAuthorCache.clear();
0197     m_imageSizeCache.clear();
0198 }
0199 
0200 void AbstractImageListModel::slotMediaMetadataFound(const QString &path, const MediaMetadata &metadata)
0201 {
0202     const QPersistentModelIndex index = m_sizeJobsUrls.take(path);
0203 
0204 #if HAVE_KExiv2
0205     if (!metadata.title.isEmpty()) {
0206         auto title = new QString(metadata.title);
0207         if (m_backgroundTitleCache.insert(path, title, 1)) {
0208             Q_EMIT dataChanged(index, index, {Qt::DisplayRole});
0209         } else {
0210             delete title;
0211         }
0212     }
0213 
0214     if (!metadata.author.isEmpty()) {
0215         auto author = new QString(metadata.author);
0216         if (m_backgroundAuthorCache.insert(path, author, 1)) {
0217             Q_EMIT dataChanged(index, index, {AuthorRole});
0218         } else {
0219             delete author;
0220         }
0221     }
0222 #endif
0223 
0224     auto resolution = new QSize(metadata.resolution);
0225     if (m_imageSizeCache.insert(path, resolution, 1)) {
0226         Q_EMIT dataChanged(index, index, {ResolutionRole});
0227     } else {
0228         delete resolution;
0229     }
0230 }