File indexing completed on 2024-09-15 03:38:42
0001 /* 0002 SPDX-FileCopyrightText: 2008-2009 Peter Penz <peter.penz@gmx.at> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "kfilepreviewgenerator.h" 0008 0009 #include "defaultviewadapter_p.h" 0010 #include <KConfigGroup> 0011 #include <KIconEffect> 0012 #include <KIconLoader> 0013 #include <KSharedConfig> 0014 #include <KUrlMimeData> 0015 #include <imagefilter_p.h> // from kiowidgets 0016 #include <kdirlister.h> 0017 #include <kdirmodel.h> 0018 #include <kfileitem.h> 0019 #include <kio/paste.h> 0020 #include <kio/previewjob.h> 0021 0022 #include <QAbstractItemView> 0023 #include <QAbstractProxyModel> 0024 #include <QApplication> 0025 #include <QClipboard> 0026 #include <QHash> 0027 #include <QIcon> 0028 #include <QList> 0029 #include <QListView> 0030 #include <QMimeData> 0031 #include <QPainter> 0032 #include <QPixmap> 0033 #include <QPointer> 0034 #include <QTimer> 0035 0036 class KFilePreviewGeneratorPrivate 0037 { 0038 class TileSet; 0039 class LayoutBlocker; 0040 0041 public: 0042 KFilePreviewGeneratorPrivate(KFilePreviewGenerator *qq, KAbstractViewAdapter *viewAdapter, QAbstractItemModel *model); 0043 0044 ~KFilePreviewGeneratorPrivate(); 0045 /** 0046 * Requests a new icon for the item \a index. 0047 * @param sequenceIndex If this is zero, the standard icon is requested, else another one. 0048 */ 0049 void requestSequenceIcon(const QModelIndex &index, int sequenceIndex); 0050 0051 /** 0052 * Generates previews for the items \a items asynchronously. 0053 */ 0054 void updateIcons(const KFileItemList &items); 0055 0056 /** 0057 * Generates previews for the indices within \a topLeft 0058 * and \a bottomRight asynchronously. 0059 */ 0060 void updateIcons(const QModelIndex &topLeft, const QModelIndex &bottomRight); 0061 0062 /** 0063 * Adds the preview \a pixmap for the item \a item to the preview 0064 * queue and starts a timer which will dispatch the preview queue 0065 * later. 0066 */ 0067 void addToPreviewQueue(const KFileItem &item, const QPixmap &pixmap, KIO::PreviewJob *job); 0068 0069 /** 0070 * Is invoked when the preview job has been finished and 0071 * removes the job from the m_previewJobs list. 0072 */ 0073 void slotPreviewJobFinished(KJob *job); 0074 0075 /** Synchronizes the icon of all items with the clipboard of cut items. */ 0076 void updateCutItems(); 0077 0078 /** 0079 * Reset all icons of the items from m_cutItemsCache and clear 0080 * the cache. 0081 */ 0082 void clearCutItemsCache(); 0083 0084 /** 0085 * Dispatches the preview queue block by block within 0086 * time slices. 0087 */ 0088 void dispatchIconUpdateQueue(); 0089 0090 /** 0091 * Pauses all icon updates and invokes KFilePreviewGenerator::resumeIconUpdates() 0092 * after a short delay. Is invoked as soon as the user has moved 0093 * a scrollbar. 0094 */ 0095 void pauseIconUpdates(); 0096 0097 /** 0098 * Resumes the icons updates that have been paused after moving the 0099 * scrollbar. The previews for the current visible area are 0100 * generated first. 0101 */ 0102 void resumeIconUpdates(); 0103 0104 /** 0105 * Starts the resolving of the MIME types from 0106 * the m_pendingItems queue. 0107 */ 0108 void startMimeTypeResolving(); 0109 0110 /** 0111 * Resolves the MIME type for exactly one item of the 0112 * m_pendingItems queue. 0113 */ 0114 void resolveMimeType(); 0115 0116 /** 0117 * Returns true, if the item \a item has been cut into 0118 * the clipboard. 0119 */ 0120 bool isCutItem(const KFileItem &item) const; 0121 0122 /** 0123 * Applies a cut-item effect to all given \a items, if they 0124 * are marked as cut in the clipboard. 0125 */ 0126 void applyCutItemEffect(const KFileItemList &items); 0127 0128 /** 0129 * Applies a frame around the icon. False is returned if 0130 * no frame has been added because the icon is too small. 0131 */ 0132 bool applyImageFrame(QPixmap &icon); 0133 0134 /** 0135 * Resizes the icon to \a maxSize if the icon size does not 0136 * fit into the maximum size. The aspect ratio of the icon 0137 * is kept. 0138 */ 0139 void limitToSize(QPixmap &icon, const QSize &maxSize); 0140 0141 /** 0142 * Creates previews by starting new preview jobs for the items 0143 * and triggers the preview timer. 0144 */ 0145 void createPreviews(const KFileItemList &items); 0146 0147 /** 0148 * Helper method for createPreviews(): Starts a preview job for the given 0149 * items. For each returned preview addToPreviewQueue() will get invoked. 0150 */ 0151 void startPreviewJob(const KFileItemList &items, int width, int height); 0152 0153 /** Kills all ongoing preview jobs. */ 0154 void killPreviewJobs(); 0155 0156 /** 0157 * Orders the items \a items in a way that the visible items 0158 * are moved to the front of the list. When passing this 0159 * list to a preview job, the visible items will get generated 0160 * first. 0161 */ 0162 void orderItems(KFileItemList &items); 0163 0164 /** 0165 * Helper method for KFilePreviewGenerator::updateIcons(). Adds 0166 * recursively all items from the model to the list \a list. 0167 */ 0168 void addItemsToList(const QModelIndex &index, KFileItemList &list); 0169 0170 /** 0171 * Updates the icons of files that are constantly changed due to a copy 0172 * operation. See m_changedItems and m_changedItemsTimer for details. 0173 */ 0174 void delayedIconUpdate(); 0175 0176 /** 0177 * Any items that are removed from the model are also removed from m_changedItems. 0178 */ 0179 void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); 0180 0181 /** Remembers the pixmap for an item specified by an URL. */ 0182 struct ItemInfo { 0183 QUrl url; 0184 QPixmap pixmap; 0185 }; 0186 0187 /** 0188 * During the lifetime of a DataChangeObtainer instance changing 0189 * the data of the model won't trigger generating a preview. 0190 */ 0191 class DataChangeObtainer 0192 { 0193 public: 0194 explicit DataChangeObtainer(KFilePreviewGeneratorPrivate *generator) 0195 : m_gen(generator) 0196 { 0197 ++m_gen->m_internalDataChange; 0198 } 0199 0200 ~DataChangeObtainer() 0201 { 0202 --m_gen->m_internalDataChange; 0203 } 0204 0205 private: 0206 KFilePreviewGeneratorPrivate *m_gen; 0207 }; 0208 0209 KFilePreviewGenerator *const q; 0210 0211 bool m_previewShown = true; 0212 0213 /** 0214 * True, if m_pendingItems and m_dispatchedItems should be 0215 * cleared when the preview jobs have been finished. 0216 */ 0217 bool m_clearItemQueues = true; 0218 0219 /** 0220 * True if a selection has been done which should cut items. 0221 */ 0222 bool m_hasCutSelection = false; 0223 0224 /** 0225 * True if the updates of icons has been paused by pauseIconUpdates(). 0226 * The value is reset by resumeIconUpdates(). 0227 */ 0228 bool m_iconUpdatesPaused = false; 0229 0230 /** 0231 * If the value is 0, the slot 0232 * updateIcons(const QModelIndex&, const QModelIndex&) has 0233 * been triggered by an external data change. 0234 */ 0235 int m_internalDataChange = 0; 0236 0237 int m_pendingVisibleIconUpdates = 0; 0238 0239 KAbstractViewAdapter *m_viewAdapter = nullptr; 0240 QAbstractItemView *m_itemView = nullptr; 0241 QTimer *m_iconUpdateTimer = nullptr; 0242 QTimer *m_scrollAreaTimer = nullptr; 0243 QList<KJob *> m_previewJobs; 0244 QPointer<KDirModel> m_dirModel; 0245 QAbstractProxyModel *m_proxyModel = nullptr; 0246 0247 /** 0248 * Set of all items that already have the 'cut' effect applied, together with the pixmap it was applied to 0249 * This is used to make sure that the 'cut' effect is applied max. once for each pixmap 0250 * 0251 * Referencing the pixmaps here imposes no overhead, as they were also given to KDirModel::setData(), 0252 * and thus are held anyway. 0253 */ 0254 QHash<QUrl, QPixmap> m_cutItemsCache; 0255 QList<ItemInfo> m_previews; 0256 QMap<QUrl, int> m_sequenceIndices; 0257 0258 /** 0259 * When huge items are copied, it must be prevented that a preview gets generated 0260 * for each item size change. m_changedItems keeps track of the changed items and it 0261 * is assured that a final preview is only done if an item does not change within 0262 * at least 5 seconds. 0263 */ 0264 QHash<QUrl, bool> m_changedItems; 0265 QTimer *m_changedItemsTimer = nullptr; 0266 0267 /** 0268 * Contains all items where a preview must be generated, but 0269 * where the preview job has not dispatched the items yet. 0270 */ 0271 KFileItemList m_pendingItems; 0272 0273 /** 0274 * Contains all items, where a preview has already been 0275 * generated by the preview jobs. 0276 */ 0277 KFileItemList m_dispatchedItems; 0278 0279 KFileItemList m_resolvedMimeTypes; 0280 0281 QStringList m_enabledPlugins; 0282 0283 std::unique_ptr<TileSet> m_tileSet; 0284 }; 0285 0286 /** 0287 * If the passed item view is an instance of QListView, expensive 0288 * layout operations are blocked in the constructor and are unblocked 0289 * again in the destructor. 0290 * 0291 * This helper class is a workaround for the following huge performance 0292 * problem when having directories with several 1000 items: 0293 * - each change of an icon emits a dataChanged() signal from the model 0294 * - QListView iterates through all items on each dataChanged() signal 0295 * and invokes QItemDelegate::sizeHint() 0296 * - the sizeHint() implementation of KFileItemDelegate is quite complex, 0297 * invoking it 1000 times for each icon change might block the UI 0298 * 0299 * QListView does not invoke QItemDelegate::sizeHint() when the 0300 * uniformItemSize property has been set to true, so this property is 0301 * set before exchanging a block of icons. 0302 */ 0303 class KFilePreviewGeneratorPrivate::LayoutBlocker 0304 { 0305 public: 0306 explicit LayoutBlocker(QAbstractItemView *view) 0307 : m_uniformSizes(false) 0308 , m_view(qobject_cast<QListView *>(view)) 0309 { 0310 if (m_view) { 0311 m_uniformSizes = m_view->uniformItemSizes(); 0312 m_view->setUniformItemSizes(true); 0313 } 0314 } 0315 0316 ~LayoutBlocker() 0317 { 0318 if (m_view) { 0319 m_view->setUniformItemSizes(m_uniformSizes); 0320 /* The QListView did the layout with uniform item 0321 * sizes, so trigger a relayout with the expected sizes. */ 0322 if (!m_uniformSizes) { 0323 m_view->setGridSize(m_view->gridSize()); 0324 } 0325 } 0326 } 0327 0328 private: 0329 bool m_uniformSizes = false; 0330 QListView *m_view = nullptr; 0331 }; 0332 0333 /** Helper class for drawing frames for image previews. */ 0334 class KFilePreviewGeneratorPrivate::TileSet 0335 { 0336 public: 0337 enum { LeftMargin = 3, TopMargin = 2, RightMargin = 3, BottomMargin = 4 }; 0338 0339 enum Tile { 0340 TopLeftCorner = 0, 0341 TopSide, 0342 TopRightCorner, 0343 LeftSide, 0344 RightSide, 0345 BottomLeftCorner, 0346 BottomSide, 0347 BottomRightCorner, 0348 NumTiles, 0349 }; 0350 0351 explicit TileSet() 0352 { 0353 QImage image(8 * 3, 8 * 3, QImage::Format_ARGB32_Premultiplied); 0354 0355 QPainter p(&image); 0356 p.setCompositionMode(QPainter::CompositionMode_Source); 0357 p.fillRect(image.rect(), Qt::transparent); 0358 p.fillRect(image.rect().adjusted(3, 3, -3, -3), Qt::black); 0359 p.end(); 0360 0361 KIO::ImageFilter::shadowBlur(image, 3, Qt::black); 0362 0363 QPixmap pixmap = QPixmap::fromImage(image); 0364 m_tiles[TopLeftCorner] = pixmap.copy(0, 0, 8, 8); 0365 m_tiles[TopSide] = pixmap.copy(8, 0, 8, 8); 0366 m_tiles[TopRightCorner] = pixmap.copy(16, 0, 8, 8); 0367 m_tiles[LeftSide] = pixmap.copy(0, 8, 8, 8); 0368 m_tiles[RightSide] = pixmap.copy(16, 8, 8, 8); 0369 m_tiles[BottomLeftCorner] = pixmap.copy(0, 16, 8, 8); 0370 m_tiles[BottomSide] = pixmap.copy(8, 16, 8, 8); 0371 m_tiles[BottomRightCorner] = pixmap.copy(16, 16, 8, 8); 0372 } 0373 0374 void paint(QPainter *p, const QRect &r) 0375 { 0376 p->drawPixmap(r.topLeft(), m_tiles[TopLeftCorner]); 0377 if (r.width() - 16 > 0) { 0378 p->drawTiledPixmap(r.x() + 8, r.y(), r.width() - 16, 8, m_tiles[TopSide]); 0379 } 0380 p->drawPixmap(r.right() - 8 + 1, r.y(), m_tiles[TopRightCorner]); 0381 if (r.height() - 16 > 0) { 0382 p->drawTiledPixmap(r.x(), r.y() + 8, 8, r.height() - 16, m_tiles[LeftSide]); 0383 p->drawTiledPixmap(r.right() - 8 + 1, r.y() + 8, 8, r.height() - 16, m_tiles[RightSide]); 0384 } 0385 p->drawPixmap(r.x(), r.bottom() - 8 + 1, m_tiles[BottomLeftCorner]); 0386 if (r.width() - 16 > 0) { 0387 p->drawTiledPixmap(r.x() + 8, r.bottom() - 8 + 1, r.width() - 16, 8, m_tiles[BottomSide]); 0388 } 0389 p->drawPixmap(r.right() - 8 + 1, r.bottom() - 8 + 1, m_tiles[BottomRightCorner]); 0390 0391 const QRect contentRect = r.adjusted(LeftMargin + 1, TopMargin + 1, -(RightMargin + 1), -(BottomMargin + 1)); 0392 p->fillRect(contentRect, Qt::transparent); 0393 } 0394 0395 private: 0396 QPixmap m_tiles[NumTiles]; 0397 }; 0398 0399 KFilePreviewGeneratorPrivate::KFilePreviewGeneratorPrivate(KFilePreviewGenerator *qq, KAbstractViewAdapter *viewAdapter, QAbstractItemModel *model) 0400 : q(qq) 0401 , m_viewAdapter(viewAdapter) 0402 { 0403 if (!m_viewAdapter->iconSize().isValid()) { 0404 m_previewShown = false; 0405 } 0406 0407 m_proxyModel = qobject_cast<QAbstractProxyModel *>(model); 0408 m_dirModel = (m_proxyModel == nullptr) ? qobject_cast<KDirModel *>(model) : qobject_cast<KDirModel *>(m_proxyModel->sourceModel()); 0409 if (!m_dirModel) { 0410 // previews can only get generated for directory models 0411 m_previewShown = false; 0412 } else { 0413 KDirModel *dirModel = m_dirModel.data(); 0414 q->connect(dirModel->dirLister(), &KCoreDirLister::newItems, q, [this](const KFileItemList &items) { 0415 updateIcons(items); 0416 }); 0417 0418 q->connect(dirModel, &KDirModel::dataChanged, q, [this](const QModelIndex &topLeft, const QModelIndex &bottomRight) { 0419 updateIcons(topLeft, bottomRight); 0420 }); 0421 0422 q->connect(dirModel, &KDirModel::needSequenceIcon, q, [this](const QModelIndex &index, int sequenceIndex) { 0423 requestSequenceIcon(index, sequenceIndex); 0424 }); 0425 0426 q->connect(dirModel, &KDirModel::rowsAboutToBeRemoved, q, [this](const QModelIndex &parent, int first, int last) { 0427 rowsAboutToBeRemoved(parent, first, last); 0428 }); 0429 } 0430 0431 QClipboard *clipboard = QApplication::clipboard(); 0432 q->connect(clipboard, &QClipboard::dataChanged, q, [this]() { 0433 updateCutItems(); 0434 }); 0435 0436 m_iconUpdateTimer = new QTimer(q); 0437 m_iconUpdateTimer->setSingleShot(true); 0438 m_iconUpdateTimer->setInterval(200); 0439 q->connect(m_iconUpdateTimer, &QTimer::timeout, q, [this]() { 0440 dispatchIconUpdateQueue(); 0441 }); 0442 0443 // Whenever the scrollbar values have been changed, the pending previews should 0444 // be reordered in a way that the previews for the visible items are generated 0445 // first. The reordering is done with a small delay, so that during moving the 0446 // scrollbars the CPU load is kept low. 0447 m_scrollAreaTimer = new QTimer(q); 0448 m_scrollAreaTimer->setSingleShot(true); 0449 m_scrollAreaTimer->setInterval(200); 0450 q->connect(m_scrollAreaTimer, &QTimer::timeout, q, [this]() { 0451 resumeIconUpdates(); 0452 }); 0453 m_viewAdapter->connect(KAbstractViewAdapter::IconSizeChanged, q, SLOT(updateIcons())); 0454 m_viewAdapter->connect(KAbstractViewAdapter::ScrollBarValueChanged, q, SLOT(pauseIconUpdates())); 0455 0456 m_changedItemsTimer = new QTimer(q); 0457 m_changedItemsTimer->setSingleShot(true); 0458 m_changedItemsTimer->setInterval(5000); 0459 q->connect(m_changedItemsTimer, &QTimer::timeout, q, [this]() { 0460 delayedIconUpdate(); 0461 }); 0462 0463 KConfigGroup globalConfig(KSharedConfig::openConfig(QStringLiteral("dolphinrc")), QStringLiteral("PreviewSettings")); 0464 m_enabledPlugins = 0465 globalConfig.readEntry("Plugins", QStringList{QStringLiteral("directorythumbnail"), QStringLiteral("imagethumbnail"), QStringLiteral("jpegthumbnail")}); 0466 0467 // Compatibility update: in 4.7, jpegrotatedthumbnail was merged into (or 0468 // replaced with?) jpegthumbnail 0469 if (m_enabledPlugins.contains(QLatin1String("jpegrotatedthumbnail"))) { 0470 m_enabledPlugins.removeAll(QStringLiteral("jpegrotatedthumbnail")); 0471 m_enabledPlugins.append(QStringLiteral("jpegthumbnail")); 0472 globalConfig.writeEntry("Plugins", m_enabledPlugins); 0473 globalConfig.sync(); 0474 } 0475 } 0476 0477 KFilePreviewGeneratorPrivate::~KFilePreviewGeneratorPrivate() 0478 { 0479 killPreviewJobs(); 0480 m_pendingItems.clear(); 0481 m_dispatchedItems.clear(); 0482 } 0483 0484 void KFilePreviewGeneratorPrivate::requestSequenceIcon(const QModelIndex &index, int sequenceIndex) 0485 { 0486 if (m_pendingItems.isEmpty() || (sequenceIndex == 0)) { 0487 KDirModel *dirModel = m_dirModel.data(); 0488 if (!dirModel) { 0489 return; 0490 } 0491 0492 KFileItem item = dirModel->itemForIndex(index); 0493 if (sequenceIndex == 0) { 0494 m_sequenceIndices.remove(item.url()); 0495 } else { 0496 m_sequenceIndices.insert(item.url(), sequenceIndex); 0497 } 0498 0499 ///@todo Update directly, without using m_sequenceIndices 0500 updateIcons(KFileItemList{item}); 0501 } 0502 } 0503 0504 void KFilePreviewGeneratorPrivate::updateIcons(const KFileItemList &items) 0505 { 0506 if (items.isEmpty()) { 0507 return; 0508 } 0509 0510 applyCutItemEffect(items); 0511 0512 KFileItemList orderedItems = items; 0513 orderItems(orderedItems); 0514 0515 m_pendingItems.reserve(m_pendingItems.size() + orderedItems.size()); 0516 for (const KFileItem &item : std::as_const(orderedItems)) { 0517 m_pendingItems.append(item); 0518 } 0519 0520 if (m_previewShown) { 0521 createPreviews(orderedItems); 0522 } else { 0523 startMimeTypeResolving(); 0524 } 0525 } 0526 0527 void KFilePreviewGeneratorPrivate::updateIcons(const QModelIndex &topLeft, const QModelIndex &bottomRight) 0528 { 0529 if (m_internalDataChange > 0) { 0530 // QAbstractItemModel::setData() has been invoked internally by the KFilePreviewGenerator. 0531 // The signal dataChanged() is connected with this method, but previews only need 0532 // to be generated when an external data change has occurred. 0533 return; 0534 } 0535 0536 // dataChanged emitted for the root dir (e.g. permission changes) 0537 if (!topLeft.isValid() || !bottomRight.isValid()) { 0538 return; 0539 } 0540 0541 KDirModel *dirModel = m_dirModel.data(); 0542 if (!dirModel) { 0543 return; 0544 } 0545 0546 KFileItemList itemList; 0547 for (int row = topLeft.row(); row <= bottomRight.row(); ++row) { 0548 const QModelIndex index = dirModel->index(row, 0); 0549 if (!index.isValid()) { 0550 continue; 0551 } 0552 const KFileItem item = dirModel->itemForIndex(index); 0553 Q_ASSERT(!item.isNull()); 0554 0555 if (m_previewShown) { 0556 const QUrl url = item.url(); 0557 const bool hasChanged = m_changedItems.contains(url); // O(1) 0558 m_changedItems.insert(url, hasChanged); 0559 if (!hasChanged) { 0560 // only update the icon if it has not been already updated within 0561 // the last 5 seconds (the other icons will be updated later with 0562 // the help of m_changedItemsTimer) 0563 itemList.append(item); 0564 } 0565 } else { 0566 itemList.append(item); 0567 } 0568 } 0569 0570 updateIcons(itemList); 0571 m_changedItemsTimer->start(); 0572 } 0573 0574 void KFilePreviewGeneratorPrivate::addToPreviewQueue(const KFileItem &item, const QPixmap &pixmap, KIO::PreviewJob *job) 0575 { 0576 Q_ASSERT(job); 0577 if (job) { 0578 QMap<QUrl, int>::iterator it = m_sequenceIndices.find(item.url()); 0579 if (job->sequenceIndex() && (it == m_sequenceIndices.end() || *it != job->sequenceIndex())) { 0580 return; // the sequence index does not match the one we want 0581 } 0582 if (!job->sequenceIndex() && it != m_sequenceIndices.end()) { 0583 return; // the sequence index does not match the one we want 0584 } 0585 0586 if (it != m_sequenceIndices.end()) { 0587 m_sequenceIndices.erase(it); 0588 } 0589 } 0590 0591 if (!m_previewShown) { 0592 // the preview has been canceled in the meantime 0593 return; 0594 } 0595 0596 KDirModel *dirModel = m_dirModel.data(); 0597 if (!dirModel) { 0598 return; 0599 } 0600 0601 const QUrl itemParentDir = item.url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 0602 0603 const QList<QUrl> dirs = dirModel->dirLister()->directories(); 0604 0605 // check whether the item is part of the directory lister (it is possible 0606 // that a preview from an old directory lister is received) 0607 const bool isOldPreview = std::none_of(dirs.cbegin(), dirs.cend(), [&itemParentDir](const QUrl &dir) { 0608 return dir == itemParentDir || dir.path().isEmpty(); 0609 }); 0610 if (isOldPreview) { 0611 return; 0612 } 0613 0614 QPixmap icon = pixmap; 0615 0616 const QString mimeType = item.mimetype(); 0617 const int slashIndex = mimeType.indexOf(QLatin1Char('/')); 0618 const auto mimeTypeGroup = QStringView(mimeType).left(slashIndex); 0619 if (mimeTypeGroup != QLatin1String("image") || !applyImageFrame(icon)) { 0620 limitToSize(icon, m_viewAdapter->iconSize()); 0621 } 0622 0623 if (m_hasCutSelection && isCutItem(item)) { 0624 // apply the disabled effect to the icon for marking it as "cut item" 0625 // and apply the icon to the item 0626 KIconEffect *iconEffect = KIconLoader::global()->iconEffect(); 0627 icon = iconEffect->apply(icon, KIconLoader::Desktop, KIconLoader::DisabledState); 0628 } 0629 0630 KIconLoader::global()->drawOverlays(item.overlays(), icon, KIconLoader::Desktop); 0631 0632 // remember the preview and URL, so that it can be applied to the model 0633 // in KFilePreviewGenerator::dispatchIconUpdateQueue() 0634 ItemInfo preview; 0635 preview.url = item.url(); 0636 preview.pixmap = icon; 0637 m_previews.append(preview); 0638 0639 m_pendingItems.removeOne(item); 0640 0641 m_dispatchedItems.append(item); 0642 } 0643 0644 void KFilePreviewGeneratorPrivate::slotPreviewJobFinished(KJob *job) 0645 { 0646 const int index = m_previewJobs.indexOf(job); 0647 m_previewJobs.removeAt(index); 0648 0649 if (m_previewJobs.isEmpty()) { 0650 for (const KFileItem &item : std::as_const(m_pendingItems)) { 0651 if (item.isMimeTypeKnown()) { 0652 m_resolvedMimeTypes.append(item); 0653 } 0654 } 0655 0656 if (m_clearItemQueues) { 0657 m_pendingItems.clear(); 0658 m_dispatchedItems.clear(); 0659 m_pendingVisibleIconUpdates = 0; 0660 auto dispatchFunc = [this]() { 0661 dispatchIconUpdateQueue(); 0662 }; 0663 QMetaObject::invokeMethod(q, dispatchFunc, Qt::QueuedConnection); 0664 } 0665 m_sequenceIndices.clear(); // just to be sure that we don't leak anything 0666 } 0667 } 0668 0669 void KFilePreviewGeneratorPrivate::updateCutItems() 0670 { 0671 KDirModel *dirModel = m_dirModel.data(); 0672 if (!dirModel) { 0673 return; 0674 } 0675 0676 DataChangeObtainer obt(this); 0677 clearCutItemsCache(); 0678 0679 KFileItemList items; 0680 KDirLister *dirLister = dirModel->dirLister(); 0681 const QList<QUrl> dirs = dirLister->directories(); 0682 items.reserve(dirs.size()); 0683 for (const QUrl &url : dirs) { 0684 items << dirLister->itemsForDir(url); 0685 } 0686 applyCutItemEffect(items); 0687 } 0688 0689 void KFilePreviewGeneratorPrivate::clearCutItemsCache() 0690 { 0691 KDirModel *dirModel = m_dirModel.data(); 0692 if (!dirModel) { 0693 return; 0694 } 0695 0696 DataChangeObtainer obt(this); 0697 KFileItemList previews; 0698 // Reset the icons of all items that are stored in the cache 0699 // to use their default MIME type icon. 0700 for (auto it = m_cutItemsCache.cbegin(); it != m_cutItemsCache.cend(); ++it) { 0701 const QModelIndex index = dirModel->indexForUrl(it.key()); 0702 if (index.isValid()) { 0703 dirModel->setData(index, QIcon(), Qt::DecorationRole); 0704 if (m_previewShown) { 0705 previews.append(dirModel->itemForIndex(index)); 0706 } 0707 } 0708 } 0709 m_cutItemsCache.clear(); 0710 0711 if (!previews.isEmpty()) { 0712 // assure that the previews gets restored 0713 Q_ASSERT(m_previewShown); 0714 orderItems(previews); 0715 updateIcons(previews); 0716 } 0717 } 0718 0719 void KFilePreviewGeneratorPrivate::dispatchIconUpdateQueue() 0720 { 0721 KDirModel *dirModel = m_dirModel.data(); 0722 if (!dirModel) { 0723 return; 0724 } 0725 0726 const int count = m_previews.count() + m_resolvedMimeTypes.count(); 0727 if (count > 0) { 0728 LayoutBlocker blocker(m_itemView); 0729 DataChangeObtainer obt(this); 0730 0731 if (m_previewShown) { 0732 // dispatch preview queue 0733 for (const ItemInfo &preview : std::as_const(m_previews)) { 0734 const QModelIndex idx = dirModel->indexForUrl(preview.url); 0735 if (idx.isValid() && (idx.column() == 0)) { 0736 dirModel->setData(idx, QIcon(preview.pixmap), Qt::DecorationRole); 0737 } 0738 } 0739 m_previews.clear(); 0740 } 0741 0742 // dispatch MIME type queue 0743 for (const KFileItem &item : std::as_const(m_resolvedMimeTypes)) { 0744 const QModelIndex idx = dirModel->indexForItem(item); 0745 dirModel->itemChanged(idx); 0746 } 0747 m_resolvedMimeTypes.clear(); 0748 0749 m_pendingVisibleIconUpdates -= count; 0750 if (m_pendingVisibleIconUpdates < 0) { 0751 m_pendingVisibleIconUpdates = 0; 0752 } 0753 } 0754 0755 if (m_pendingVisibleIconUpdates > 0) { 0756 // As long as there are pending previews for visible items, poll 0757 // the preview queue periodically. If there are no pending previews, 0758 // the queue is dispatched in slotPreviewJobFinished(). 0759 m_iconUpdateTimer->start(); 0760 } 0761 } 0762 0763 void KFilePreviewGeneratorPrivate::pauseIconUpdates() 0764 { 0765 m_iconUpdatesPaused = true; 0766 for (KJob *job : std::as_const(m_previewJobs)) { 0767 Q_ASSERT(job); 0768 job->suspend(); 0769 } 0770 m_scrollAreaTimer->start(); 0771 } 0772 0773 void KFilePreviewGeneratorPrivate::resumeIconUpdates() 0774 { 0775 m_iconUpdatesPaused = false; 0776 0777 // Before creating new preview jobs the m_pendingItems queue must be 0778 // cleaned up by removing the already dispatched items. Implementation 0779 // note: The order of the m_dispatchedItems queue and the m_pendingItems 0780 // queue is usually equal. So even when having a lot of elements the 0781 // nested loop is no performance bottle neck, as the inner loop is only 0782 // entered once in most cases. 0783 for (const KFileItem &item : std::as_const(m_dispatchedItems)) { 0784 auto it = std::remove_if(m_pendingItems.begin(), m_pendingItems.end(), [&item](const KFileItem &pending) { 0785 return pending.url() == item.url(); 0786 }); 0787 m_pendingItems.erase(it, m_pendingItems.end()); 0788 } 0789 0790 m_dispatchedItems.clear(); 0791 0792 m_pendingVisibleIconUpdates = 0; 0793 dispatchIconUpdateQueue(); 0794 0795 if (m_previewShown) { 0796 KFileItemList orderedItems = m_pendingItems; 0797 orderItems(orderedItems); 0798 0799 // Kill all suspended preview jobs. Usually when a preview job 0800 // has been finished, slotPreviewJobFinished() clears all item queues. 0801 // This is not wanted in this case, as a new job is created afterwards 0802 // for m_pendingItems. 0803 m_clearItemQueues = false; 0804 killPreviewJobs(); 0805 m_clearItemQueues = true; 0806 0807 createPreviews(orderedItems); 0808 } else { 0809 orderItems(m_pendingItems); 0810 startMimeTypeResolving(); 0811 } 0812 } 0813 0814 void KFilePreviewGeneratorPrivate::startMimeTypeResolving() 0815 { 0816 resolveMimeType(); 0817 m_iconUpdateTimer->start(); 0818 } 0819 0820 void KFilePreviewGeneratorPrivate::resolveMimeType() 0821 { 0822 if (m_pendingItems.isEmpty()) { 0823 return; 0824 } 0825 0826 // resolve at least one MIME type 0827 bool resolved = false; 0828 do { 0829 KFileItem item = m_pendingItems.takeFirst(); 0830 if (item.isMimeTypeKnown()) { 0831 if (m_pendingVisibleIconUpdates > 0) { 0832 // The item is visible and the MIME type already known. 0833 // Decrease the update counter for dispatchIconUpdateQueue(): 0834 --m_pendingVisibleIconUpdates; 0835 } 0836 } else { 0837 // The MIME type is unknown and must get resolved. The 0838 // directory model is not informed yet, as a single update 0839 // would be very expensive. Instead the item is remembered in 0840 // m_resolvedMimeTypes and will be dispatched later 0841 // by dispatchIconUpdateQueue(). 0842 item.determineMimeType(); 0843 m_resolvedMimeTypes.append(item); 0844 resolved = true; 0845 } 0846 } while (!resolved && !m_pendingItems.isEmpty()); 0847 0848 if (m_pendingItems.isEmpty()) { 0849 // All MIME types have been resolved now. Assure 0850 // that the directory model gets informed about 0851 // this, so that an update of the icons is done. 0852 dispatchIconUpdateQueue(); 0853 } else if (!m_iconUpdatesPaused) { 0854 // assure that the MIME type of the next 0855 // item will be resolved asynchronously 0856 auto mimeFunc = [this]() { 0857 resolveMimeType(); 0858 }; 0859 QMetaObject::invokeMethod(q, mimeFunc, Qt::QueuedConnection); 0860 } 0861 } 0862 0863 bool KFilePreviewGeneratorPrivate::isCutItem(const KFileItem &item) const 0864 { 0865 const QMimeData *mimeData = QApplication::clipboard()->mimeData(); 0866 const QList<QUrl> cutUrls = KUrlMimeData::urlsFromMimeData(mimeData); 0867 return cutUrls.contains(item.url()); 0868 } 0869 0870 void KFilePreviewGeneratorPrivate::applyCutItemEffect(const KFileItemList &items) 0871 { 0872 const QMimeData *mimeData = QApplication::clipboard()->mimeData(); 0873 m_hasCutSelection = mimeData && KIO::isClipboardDataCut(mimeData); 0874 if (!m_hasCutSelection) { 0875 return; 0876 } 0877 0878 KDirModel *dirModel = m_dirModel.data(); 0879 if (!dirModel) { 0880 return; 0881 } 0882 0883 const QList<QUrl> urlsList = KUrlMimeData::urlsFromMimeData(mimeData); 0884 const QSet<QUrl> cutUrls(urlsList.begin(), urlsList.end()); 0885 0886 DataChangeObtainer obt(this); 0887 KIconEffect *iconEffect = KIconLoader::global()->iconEffect(); 0888 for (const KFileItem &item : items) { 0889 if (cutUrls.contains(item.url())) { 0890 const QModelIndex index = dirModel->indexForItem(item); 0891 const QVariant value = dirModel->data(index, Qt::DecorationRole); 0892 if (value.typeId() == QMetaType::QIcon) { 0893 const QIcon icon(qvariant_cast<QIcon>(value)); 0894 const QSize actualSize = icon.actualSize(m_viewAdapter->iconSize()); 0895 QPixmap pixmap = icon.pixmap(actualSize); 0896 0897 const auto cacheIt = m_cutItemsCache.constFind(item.url()); 0898 if ((cacheIt == m_cutItemsCache.constEnd()) || (cacheIt->cacheKey() != pixmap.cacheKey())) { 0899 pixmap = iconEffect->apply(pixmap, KIconLoader::Desktop, KIconLoader::DisabledState); 0900 dirModel->setData(index, QIcon(pixmap), Qt::DecorationRole); 0901 0902 m_cutItemsCache.insert(item.url(), pixmap); 0903 } 0904 } 0905 } 0906 } 0907 } 0908 0909 bool KFilePreviewGeneratorPrivate::applyImageFrame(QPixmap &icon) 0910 { 0911 const QSize maxSize = m_viewAdapter->iconSize(); 0912 const bool applyFrame = (maxSize.width() > KIconLoader::SizeSmallMedium) && (maxSize.height() > KIconLoader::SizeSmallMedium) && !icon.hasAlpha(); 0913 if (!applyFrame) { 0914 // the maximum size or the image itself is too small for a frame 0915 return false; 0916 } 0917 0918 // resize the icon to the maximum size minus the space required for the frame 0919 const QSize size(maxSize.width() - TileSet::LeftMargin - TileSet::RightMargin, maxSize.height() - TileSet::TopMargin - TileSet::BottomMargin); 0920 limitToSize(icon, size); 0921 0922 if (!m_tileSet) { 0923 m_tileSet.reset(new TileSet{}); 0924 } 0925 0926 QPixmap framedIcon(icon.size().width() + TileSet::LeftMargin + TileSet::RightMargin, icon.size().height() + TileSet::TopMargin + TileSet::BottomMargin); 0927 framedIcon.fill(Qt::transparent); 0928 0929 QPainter painter; 0930 painter.begin(&framedIcon); 0931 painter.setCompositionMode(QPainter::CompositionMode_Source); 0932 m_tileSet->paint(&painter, framedIcon.rect()); 0933 painter.setCompositionMode(QPainter::CompositionMode_SourceOver); 0934 painter.drawPixmap(TileSet::LeftMargin, TileSet::TopMargin, icon); 0935 painter.end(); 0936 0937 icon = framedIcon; 0938 return true; 0939 } 0940 0941 void KFilePreviewGeneratorPrivate::limitToSize(QPixmap &icon, const QSize &maxSize) 0942 { 0943 if ((icon.width() > maxSize.width()) || (icon.height() > maxSize.height())) { 0944 icon = icon.scaled(maxSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); 0945 } 0946 } 0947 0948 void KFilePreviewGeneratorPrivate::createPreviews(const KFileItemList &items) 0949 { 0950 if (items.isEmpty()) { 0951 return; 0952 } 0953 0954 const QMimeData *mimeData = QApplication::clipboard()->mimeData(); 0955 m_hasCutSelection = mimeData && KIO::isClipboardDataCut(mimeData); 0956 0957 // PreviewJob internally caches items always with the size of 0958 // 128 x 128 pixels or 256 x 256 pixels. A downscaling is done 0959 // by PreviewJob if a smaller size is requested. For images KFilePreviewGenerator must 0960 // do a downscaling anyhow because of the frame, so in this case only the provided 0961 // cache sizes are requested. 0962 KFileItemList imageItems; 0963 KFileItemList otherItems; 0964 QString mimeType; 0965 for (const KFileItem &item : items) { 0966 mimeType = item.mimetype(); 0967 const int slashIndex = mimeType.indexOf(QLatin1Char('/')); 0968 const auto mimeTypeGroup = QStringView(mimeType).left(slashIndex); 0969 if (mimeTypeGroup == QLatin1String("image")) { 0970 imageItems.append(item); 0971 } else { 0972 otherItems.append(item); 0973 } 0974 } 0975 const QSize size = m_viewAdapter->iconSize(); 0976 const int width = size.width(); 0977 const int height = size.height(); 0978 startPreviewJob(otherItems, width, height); 0979 0980 const int longer = std::max(width, height); 0981 int cacheSize = 128; 0982 if (longer > 512) { 0983 cacheSize = 1024; 0984 } else if (longer > 256) { 0985 cacheSize = 512; 0986 } else if (longer > 128) { 0987 cacheSize = 256; 0988 } 0989 startPreviewJob(imageItems, cacheSize, cacheSize); 0990 0991 m_iconUpdateTimer->start(); 0992 } 0993 0994 void KFilePreviewGeneratorPrivate::startPreviewJob(const KFileItemList &items, int width, int height) 0995 { 0996 if (items.isEmpty()) { 0997 return; 0998 } 0999 1000 KIO::PreviewJob *job = KIO::filePreview(items, QSize(width, height), &m_enabledPlugins); 1001 1002 // Set the sequence index to the target. We only need to check if items.count() == 1, 1003 // because requestSequenceIcon(..) creates exactly such a request. 1004 if (!m_sequenceIndices.isEmpty() && (items.count() == 1)) { 1005 const auto it = m_sequenceIndices.constFind(items[0].url()); 1006 if (it != m_sequenceIndices.cend()) { 1007 job->setSequenceIndex(*it); 1008 } 1009 } 1010 1011 q->connect(job, &KIO::PreviewJob::gotPreview, q, [this, job](const KFileItem &item, const QPixmap &pixmap) { 1012 addToPreviewQueue(item, pixmap, job); 1013 }); 1014 1015 q->connect(job, &KIO::PreviewJob::finished, q, [this, job]() { 1016 slotPreviewJobFinished(job); 1017 }); 1018 m_previewJobs.append(job); 1019 } 1020 1021 void KFilePreviewGeneratorPrivate::killPreviewJobs() 1022 { 1023 for (KJob *job : std::as_const(m_previewJobs)) { 1024 Q_ASSERT(job); 1025 job->kill(); 1026 } 1027 m_previewJobs.clear(); 1028 m_sequenceIndices.clear(); 1029 1030 m_iconUpdateTimer->stop(); 1031 m_scrollAreaTimer->stop(); 1032 m_changedItemsTimer->stop(); 1033 } 1034 1035 void KFilePreviewGeneratorPrivate::orderItems(KFileItemList &items) 1036 { 1037 KDirModel *dirModel = m_dirModel.data(); 1038 if (!dirModel) { 1039 return; 1040 } 1041 1042 // Order the items in a way that the preview for the visible items 1043 // is generated first, as this improves the felt performance a lot. 1044 const bool hasProxy = m_proxyModel != nullptr; 1045 const int itemCount = items.count(); 1046 const QRect visibleArea = m_viewAdapter->visibleArea(); 1047 1048 QModelIndex dirIndex; 1049 QRect itemRect; 1050 int insertPos = 0; 1051 for (int i = 0; i < itemCount; ++i) { 1052 dirIndex = dirModel->indexForItem(items.at(i)); // O(n) (n = number of rows) 1053 if (hasProxy) { 1054 const QModelIndex proxyIndex = m_proxyModel->mapFromSource(dirIndex); 1055 itemRect = m_viewAdapter->visualRect(proxyIndex); 1056 } else { 1057 itemRect = m_viewAdapter->visualRect(dirIndex); 1058 } 1059 1060 if (itemRect.intersects(visibleArea)) { 1061 // The current item is (at least partly) visible. Move it 1062 // to the front of the list, so that the preview is 1063 // generated earlier. 1064 items.insert(insertPos, items.at(i)); 1065 items.removeAt(i + 1); 1066 ++insertPos; 1067 ++m_pendingVisibleIconUpdates; 1068 } 1069 } 1070 } 1071 1072 void KFilePreviewGeneratorPrivate::addItemsToList(const QModelIndex &index, KFileItemList &list) 1073 { 1074 KDirModel *dirModel = m_dirModel.data(); 1075 if (!dirModel) { 1076 return; 1077 } 1078 1079 const int rowCount = dirModel->rowCount(index); 1080 for (int row = 0; row < rowCount; ++row) { 1081 const QModelIndex subIndex = dirModel->index(row, 0, index); 1082 KFileItem item = dirModel->itemForIndex(subIndex); 1083 list.append(item); 1084 1085 if (dirModel->rowCount(subIndex) > 0) { 1086 // the model is hierarchical (treeview) 1087 addItemsToList(subIndex, list); 1088 } 1089 } 1090 } 1091 1092 void KFilePreviewGeneratorPrivate::delayedIconUpdate() 1093 { 1094 KDirModel *dirModel = m_dirModel.data(); 1095 if (!dirModel) { 1096 return; 1097 } 1098 1099 // Precondition: No items have been changed within the last 1100 // 5 seconds. This means that items that have been changed constantly 1101 // due to a copy operation should be updated now. 1102 1103 KFileItemList itemList; 1104 1105 for (auto it = m_changedItems.cbegin(); it != m_changedItems.cend(); ++it) { 1106 const bool hasChanged = it.value(); 1107 if (hasChanged) { 1108 const QModelIndex index = dirModel->indexForUrl(it.key()); 1109 const KFileItem item = dirModel->itemForIndex(index); 1110 itemList.append(item); 1111 } 1112 } 1113 m_changedItems.clear(); 1114 1115 updateIcons(itemList); 1116 } 1117 1118 void KFilePreviewGeneratorPrivate::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) 1119 { 1120 if (m_changedItems.isEmpty()) { 1121 return; 1122 } 1123 1124 KDirModel *dirModel = m_dirModel.data(); 1125 if (!dirModel) { 1126 return; 1127 } 1128 1129 for (int row = start; row <= end; row++) { 1130 const QModelIndex index = dirModel->index(row, 0, parent); 1131 1132 const KFileItem item = dirModel->itemForIndex(index); 1133 if (!item.isNull()) { 1134 m_changedItems.remove(item.url()); 1135 } 1136 1137 if (dirModel->hasChildren(index)) { 1138 rowsAboutToBeRemoved(index, 0, dirModel->rowCount(index) - 1); 1139 } 1140 } 1141 } 1142 1143 KFilePreviewGenerator::KFilePreviewGenerator(QAbstractItemView *parent) 1144 : QObject(parent) 1145 , d(new KFilePreviewGeneratorPrivate(this, new KIO::DefaultViewAdapter(parent, this), parent->model())) 1146 { 1147 d->m_itemView = parent; 1148 } 1149 1150 KFilePreviewGenerator::KFilePreviewGenerator(KAbstractViewAdapter *parent, QAbstractProxyModel *model) 1151 : QObject(parent) 1152 , d(new KFilePreviewGeneratorPrivate(this, parent, model)) 1153 { 1154 } 1155 1156 KFilePreviewGenerator::~KFilePreviewGenerator() = default; 1157 1158 void KFilePreviewGenerator::setPreviewShown(bool show) 1159 { 1160 if (d->m_previewShown == show) { 1161 return; 1162 } 1163 1164 KDirModel *dirModel = d->m_dirModel.data(); 1165 if (show && (!d->m_viewAdapter->iconSize().isValid() || !dirModel)) { 1166 // The view must provide an icon size and a directory model, 1167 // otherwise the showing the previews will get ignored 1168 return; 1169 } 1170 1171 d->m_previewShown = show; 1172 if (!show) { 1173 dirModel->clearAllPreviews(); 1174 } 1175 updateIcons(); 1176 } 1177 1178 bool KFilePreviewGenerator::isPreviewShown() const 1179 { 1180 return d->m_previewShown; 1181 } 1182 1183 void KFilePreviewGenerator::updateIcons() 1184 { 1185 d->killPreviewJobs(); 1186 1187 d->clearCutItemsCache(); 1188 d->m_pendingItems.clear(); 1189 d->m_dispatchedItems.clear(); 1190 1191 KFileItemList itemList; 1192 d->addItemsToList(QModelIndex(), itemList); 1193 1194 d->updateIcons(itemList); 1195 } 1196 1197 void KFilePreviewGenerator::cancelPreviews() 1198 { 1199 d->killPreviewJobs(); 1200 d->m_pendingItems.clear(); 1201 d->m_dispatchedItems.clear(); 1202 updateIcons(); 1203 } 1204 1205 void KFilePreviewGenerator::setEnabledPlugins(const QStringList &plugins) 1206 { 1207 d->m_enabledPlugins = plugins; 1208 } 1209 1210 QStringList KFilePreviewGenerator::enabledPlugins() const 1211 { 1212 return d->m_enabledPlugins; 1213 } 1214 1215 #include "moc_kfilepreviewgenerator.cpp"