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 }