File indexing completed on 2024-04-28 15:26:44
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")), "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 m_sequenceIndices.erase(it); 0587 } 0588 0589 if (!m_previewShown) { 0590 // the preview has been canceled in the meantime 0591 return; 0592 } 0593 0594 KDirModel *dirModel = m_dirModel.data(); 0595 if (!dirModel) { 0596 return; 0597 } 0598 0599 const QUrl itemParentDir = item.url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash); 0600 0601 const QList<QUrl> dirs = dirModel->dirLister()->directories(); 0602 0603 // check whether the item is part of the directory lister (it is possible 0604 // that a preview from an old directory lister is received) 0605 const bool isOldPreview = std::none_of(dirs.cbegin(), dirs.cend(), [&itemParentDir](const QUrl &dir) { 0606 return dir == itemParentDir || dir.path().isEmpty(); 0607 }); 0608 if (isOldPreview) { 0609 return; 0610 } 0611 0612 QPixmap icon = pixmap; 0613 0614 const QString mimeType = item.mimetype(); 0615 const int slashIndex = mimeType.indexOf(QLatin1Char('/')); 0616 const auto mimeTypeGroup = QStringView(mimeType).left(slashIndex); 0617 if (mimeTypeGroup != QLatin1String("image") || !applyImageFrame(icon)) { 0618 limitToSize(icon, m_viewAdapter->iconSize()); 0619 } 0620 0621 if (m_hasCutSelection && isCutItem(item)) { 0622 // apply the disabled effect to the icon for marking it as "cut item" 0623 // and apply the icon to the item 0624 KIconEffect *iconEffect = KIconLoader::global()->iconEffect(); 0625 icon = iconEffect->apply(icon, KIconLoader::Desktop, KIconLoader::DisabledState); 0626 } 0627 0628 KIconLoader::global()->drawOverlays(item.overlays(), icon, KIconLoader::Desktop); 0629 0630 // remember the preview and URL, so that it can be applied to the model 0631 // in KFilePreviewGenerator::dispatchIconUpdateQueue() 0632 ItemInfo preview; 0633 preview.url = item.url(); 0634 preview.pixmap = icon; 0635 m_previews.append(preview); 0636 0637 m_pendingItems.removeOne(item); 0638 0639 m_dispatchedItems.append(item); 0640 } 0641 0642 void KFilePreviewGeneratorPrivate::slotPreviewJobFinished(KJob *job) 0643 { 0644 const int index = m_previewJobs.indexOf(job); 0645 m_previewJobs.removeAt(index); 0646 0647 if (m_previewJobs.isEmpty()) { 0648 for (const KFileItem &item : std::as_const(m_pendingItems)) { 0649 if (item.isMimeTypeKnown()) { 0650 m_resolvedMimeTypes.append(item); 0651 } 0652 } 0653 0654 if (m_clearItemQueues) { 0655 m_pendingItems.clear(); 0656 m_dispatchedItems.clear(); 0657 m_pendingVisibleIconUpdates = 0; 0658 auto dispatchFunc = [this]() { 0659 dispatchIconUpdateQueue(); 0660 }; 0661 QMetaObject::invokeMethod(q, dispatchFunc, Qt::QueuedConnection); 0662 } 0663 m_sequenceIndices.clear(); // just to be sure that we don't leak anything 0664 } 0665 } 0666 0667 void KFilePreviewGeneratorPrivate::updateCutItems() 0668 { 0669 KDirModel *dirModel = m_dirModel.data(); 0670 if (!dirModel) { 0671 return; 0672 } 0673 0674 DataChangeObtainer obt(this); 0675 clearCutItemsCache(); 0676 0677 KFileItemList items; 0678 KDirLister *dirLister = dirModel->dirLister(); 0679 const QList<QUrl> dirs = dirLister->directories(); 0680 items.reserve(dirs.size()); 0681 for (const QUrl &url : dirs) { 0682 items << dirLister->itemsForDir(url); 0683 } 0684 applyCutItemEffect(items); 0685 } 0686 0687 void KFilePreviewGeneratorPrivate::clearCutItemsCache() 0688 { 0689 KDirModel *dirModel = m_dirModel.data(); 0690 if (!dirModel) { 0691 return; 0692 } 0693 0694 DataChangeObtainer obt(this); 0695 KFileItemList previews; 0696 // Reset the icons of all items that are stored in the cache 0697 // to use their default MIME type icon. 0698 for (auto it = m_cutItemsCache.cbegin(); it != m_cutItemsCache.cend(); ++it) { 0699 const QModelIndex index = dirModel->indexForUrl(it.key()); 0700 if (index.isValid()) { 0701 dirModel->setData(index, QIcon(), Qt::DecorationRole); 0702 if (m_previewShown) { 0703 previews.append(dirModel->itemForIndex(index)); 0704 } 0705 } 0706 } 0707 m_cutItemsCache.clear(); 0708 0709 if (!previews.isEmpty()) { 0710 // assure that the previews gets restored 0711 Q_ASSERT(m_previewShown); 0712 orderItems(previews); 0713 updateIcons(previews); 0714 } 0715 } 0716 0717 void KFilePreviewGeneratorPrivate::dispatchIconUpdateQueue() 0718 { 0719 KDirModel *dirModel = m_dirModel.data(); 0720 if (!dirModel) { 0721 return; 0722 } 0723 0724 const int count = m_previews.count() + m_resolvedMimeTypes.count(); 0725 if (count > 0) { 0726 LayoutBlocker blocker(m_itemView); 0727 DataChangeObtainer obt(this); 0728 0729 if (m_previewShown) { 0730 // dispatch preview queue 0731 for (const ItemInfo &preview : std::as_const(m_previews)) { 0732 const QModelIndex idx = dirModel->indexForUrl(preview.url); 0733 if (idx.isValid() && (idx.column() == 0)) { 0734 dirModel->setData(idx, QIcon(preview.pixmap), Qt::DecorationRole); 0735 } 0736 } 0737 m_previews.clear(); 0738 } 0739 0740 // dispatch MIME type queue 0741 for (const KFileItem &item : std::as_const(m_resolvedMimeTypes)) { 0742 const QModelIndex idx = dirModel->indexForItem(item); 0743 dirModel->itemChanged(idx); 0744 } 0745 m_resolvedMimeTypes.clear(); 0746 0747 m_pendingVisibleIconUpdates -= count; 0748 if (m_pendingVisibleIconUpdates < 0) { 0749 m_pendingVisibleIconUpdates = 0; 0750 } 0751 } 0752 0753 if (m_pendingVisibleIconUpdates > 0) { 0754 // As long as there are pending previews for visible items, poll 0755 // the preview queue periodically. If there are no pending previews, 0756 // the queue is dispatched in slotPreviewJobFinished(). 0757 m_iconUpdateTimer->start(); 0758 } 0759 } 0760 0761 void KFilePreviewGeneratorPrivate::pauseIconUpdates() 0762 { 0763 m_iconUpdatesPaused = true; 0764 for (KJob *job : std::as_const(m_previewJobs)) { 0765 Q_ASSERT(job); 0766 job->suspend(); 0767 } 0768 m_scrollAreaTimer->start(); 0769 } 0770 0771 void KFilePreviewGeneratorPrivate::resumeIconUpdates() 0772 { 0773 m_iconUpdatesPaused = false; 0774 0775 // Before creating new preview jobs the m_pendingItems queue must be 0776 // cleaned up by removing the already dispatched items. Implementation 0777 // note: The order of the m_dispatchedItems queue and the m_pendingItems 0778 // queue is usually equal. So even when having a lot of elements the 0779 // nested loop is no performance bottle neck, as the inner loop is only 0780 // entered once in most cases. 0781 for (const KFileItem &item : std::as_const(m_dispatchedItems)) { 0782 auto it = std::remove_if(m_pendingItems.begin(), m_pendingItems.end(), [&item](const KFileItem &pending) { 0783 return pending.url() == item.url(); 0784 }); 0785 m_pendingItems.erase(it, m_pendingItems.end()); 0786 } 0787 0788 m_dispatchedItems.clear(); 0789 0790 m_pendingVisibleIconUpdates = 0; 0791 dispatchIconUpdateQueue(); 0792 0793 if (m_previewShown) { 0794 KFileItemList orderedItems = m_pendingItems; 0795 orderItems(orderedItems); 0796 0797 // Kill all suspended preview jobs. Usually when a preview job 0798 // has been finished, slotPreviewJobFinished() clears all item queues. 0799 // This is not wanted in this case, as a new job is created afterwards 0800 // for m_pendingItems. 0801 m_clearItemQueues = false; 0802 killPreviewJobs(); 0803 m_clearItemQueues = true; 0804 0805 createPreviews(orderedItems); 0806 } else { 0807 orderItems(m_pendingItems); 0808 startMimeTypeResolving(); 0809 } 0810 } 0811 0812 void KFilePreviewGeneratorPrivate::startMimeTypeResolving() 0813 { 0814 resolveMimeType(); 0815 m_iconUpdateTimer->start(); 0816 } 0817 0818 void KFilePreviewGeneratorPrivate::resolveMimeType() 0819 { 0820 if (m_pendingItems.isEmpty()) { 0821 return; 0822 } 0823 0824 // resolve at least one MIME type 0825 bool resolved = false; 0826 do { 0827 KFileItem item = m_pendingItems.takeFirst(); 0828 if (item.isMimeTypeKnown()) { 0829 if (m_pendingVisibleIconUpdates > 0) { 0830 // The item is visible and the MIME type already known. 0831 // Decrease the update counter for dispatchIconUpdateQueue(): 0832 --m_pendingVisibleIconUpdates; 0833 } 0834 } else { 0835 // The MIME type is unknown and must get resolved. The 0836 // directory model is not informed yet, as a single update 0837 // would be very expensive. Instead the item is remembered in 0838 // m_resolvedMimeTypes and will be dispatched later 0839 // by dispatchIconUpdateQueue(). 0840 item.determineMimeType(); 0841 m_resolvedMimeTypes.append(item); 0842 resolved = true; 0843 } 0844 } while (!resolved && !m_pendingItems.isEmpty()); 0845 0846 if (m_pendingItems.isEmpty()) { 0847 // All MIME types have been resolved now. Assure 0848 // that the directory model gets informed about 0849 // this, so that an update of the icons is done. 0850 dispatchIconUpdateQueue(); 0851 } else if (!m_iconUpdatesPaused) { 0852 // assure that the MIME type of the next 0853 // item will be resolved asynchronously 0854 auto mimeFunc = [this]() { 0855 resolveMimeType(); 0856 }; 0857 QMetaObject::invokeMethod(q, mimeFunc, Qt::QueuedConnection); 0858 } 0859 } 0860 0861 bool KFilePreviewGeneratorPrivate::isCutItem(const KFileItem &item) const 0862 { 0863 const QMimeData *mimeData = QApplication::clipboard()->mimeData(); 0864 const QList<QUrl> cutUrls = KUrlMimeData::urlsFromMimeData(mimeData); 0865 return cutUrls.contains(item.url()); 0866 } 0867 0868 void KFilePreviewGeneratorPrivate::applyCutItemEffect(const KFileItemList &items) 0869 { 0870 const QMimeData *mimeData = QApplication::clipboard()->mimeData(); 0871 m_hasCutSelection = mimeData && KIO::isClipboardDataCut(mimeData); 0872 if (!m_hasCutSelection) { 0873 return; 0874 } 0875 0876 KDirModel *dirModel = m_dirModel.data(); 0877 if (!dirModel) { 0878 return; 0879 } 0880 0881 const QList<QUrl> urlsList = KUrlMimeData::urlsFromMimeData(mimeData); 0882 const QSet<QUrl> cutUrls(urlsList.begin(), urlsList.end()); 0883 0884 DataChangeObtainer obt(this); 0885 KIconEffect *iconEffect = KIconLoader::global()->iconEffect(); 0886 for (const KFileItem &item : items) { 0887 if (cutUrls.contains(item.url())) { 0888 const QModelIndex index = dirModel->indexForItem(item); 0889 const QVariant value = dirModel->data(index, Qt::DecorationRole); 0890 if (value.type() == QVariant::Icon) { 0891 const QIcon icon(qvariant_cast<QIcon>(value)); 0892 const QSize actualSize = icon.actualSize(m_viewAdapter->iconSize()); 0893 QPixmap pixmap = icon.pixmap(actualSize); 0894 0895 const auto cacheIt = m_cutItemsCache.constFind(item.url()); 0896 if ((cacheIt == m_cutItemsCache.constEnd()) || (cacheIt->cacheKey() != pixmap.cacheKey())) { 0897 pixmap = iconEffect->apply(pixmap, KIconLoader::Desktop, KIconLoader::DisabledState); 0898 dirModel->setData(index, QIcon(pixmap), Qt::DecorationRole); 0899 0900 m_cutItemsCache.insert(item.url(), pixmap); 0901 } 0902 } 0903 } 0904 } 0905 } 0906 0907 bool KFilePreviewGeneratorPrivate::applyImageFrame(QPixmap &icon) 0908 { 0909 const QSize maxSize = m_viewAdapter->iconSize(); 0910 const bool applyFrame = (maxSize.width() > KIconLoader::SizeSmallMedium) && (maxSize.height() > KIconLoader::SizeSmallMedium) && !icon.hasAlpha(); 0911 if (!applyFrame) { 0912 // the maximum size or the image itself is too small for a frame 0913 return false; 0914 } 0915 0916 // resize the icon to the maximum size minus the space required for the frame 0917 const QSize size(maxSize.width() - TileSet::LeftMargin - TileSet::RightMargin, maxSize.height() - TileSet::TopMargin - TileSet::BottomMargin); 0918 limitToSize(icon, size); 0919 0920 if (!m_tileSet) { 0921 m_tileSet.reset(new TileSet{}); 0922 } 0923 0924 QPixmap framedIcon(icon.size().width() + TileSet::LeftMargin + TileSet::RightMargin, icon.size().height() + TileSet::TopMargin + TileSet::BottomMargin); 0925 framedIcon.fill(Qt::transparent); 0926 0927 QPainter painter; 0928 painter.begin(&framedIcon); 0929 painter.setCompositionMode(QPainter::CompositionMode_Source); 0930 m_tileSet->paint(&painter, framedIcon.rect()); 0931 painter.setCompositionMode(QPainter::CompositionMode_SourceOver); 0932 painter.drawPixmap(TileSet::LeftMargin, TileSet::TopMargin, icon); 0933 painter.end(); 0934 0935 icon = framedIcon; 0936 return true; 0937 } 0938 0939 void KFilePreviewGeneratorPrivate::limitToSize(QPixmap &icon, const QSize &maxSize) 0940 { 0941 if ((icon.width() > maxSize.width()) || (icon.height() > maxSize.height())) { 0942 icon = icon.scaled(maxSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); 0943 } 0944 } 0945 0946 void KFilePreviewGeneratorPrivate::createPreviews(const KFileItemList &items) 0947 { 0948 if (items.isEmpty()) { 0949 return; 0950 } 0951 0952 const QMimeData *mimeData = QApplication::clipboard()->mimeData(); 0953 m_hasCutSelection = mimeData && KIO::isClipboardDataCut(mimeData); 0954 0955 // PreviewJob internally caches items always with the size of 0956 // 128 x 128 pixels or 256 x 256 pixels. A downscaling is done 0957 // by PreviewJob if a smaller size is requested. For images KFilePreviewGenerator must 0958 // do a downscaling anyhow because of the frame, so in this case only the provided 0959 // cache sizes are requested. 0960 KFileItemList imageItems; 0961 KFileItemList otherItems; 0962 QString mimeType; 0963 for (const KFileItem &item : items) { 0964 mimeType = item.mimetype(); 0965 const int slashIndex = mimeType.indexOf(QLatin1Char('/')); 0966 const auto mimeTypeGroup = QStringView(mimeType).left(slashIndex); 0967 if (mimeTypeGroup == QLatin1String("image")) { 0968 imageItems.append(item); 0969 } else { 0970 otherItems.append(item); 0971 } 0972 } 0973 const QSize size = m_viewAdapter->iconSize(); 0974 const int width = size.width(); 0975 const int height = size.height(); 0976 startPreviewJob(otherItems, width, height); 0977 0978 const int longer = std::max(width, height); 0979 int cacheSize = 128; 0980 if (longer > 512) { 0981 cacheSize = 1024; 0982 } else if (longer > 256) { 0983 cacheSize = 512; 0984 } else if (longer > 128) { 0985 cacheSize = 256; 0986 } 0987 startPreviewJob(imageItems, cacheSize, cacheSize); 0988 0989 m_iconUpdateTimer->start(); 0990 } 0991 0992 void KFilePreviewGeneratorPrivate::startPreviewJob(const KFileItemList &items, int width, int height) 0993 { 0994 if (items.isEmpty()) { 0995 return; 0996 } 0997 0998 KIO::PreviewJob *job = KIO::filePreview(items, QSize(width, height), &m_enabledPlugins); 0999 1000 // Set the sequence index to the target. We only need to check if items.count() == 1, 1001 // because requestSequenceIcon(..) creates exactly such a request. 1002 if (!m_sequenceIndices.isEmpty() && (items.count() == 1)) { 1003 const auto it = m_sequenceIndices.constFind(items[0].url()); 1004 if (it != m_sequenceIndices.cend()) { 1005 job->setSequenceIndex(*it); 1006 } 1007 } 1008 1009 q->connect(job, &KIO::PreviewJob::gotPreview, q, [this, job](const KFileItem &item, const QPixmap &pixmap) { 1010 addToPreviewQueue(item, pixmap, job); 1011 }); 1012 1013 q->connect(job, &KIO::PreviewJob::finished, q, [this, job]() { 1014 slotPreviewJobFinished(job); 1015 }); 1016 m_previewJobs.append(job); 1017 } 1018 1019 void KFilePreviewGeneratorPrivate::killPreviewJobs() 1020 { 1021 for (KJob *job : std::as_const(m_previewJobs)) { 1022 Q_ASSERT(job); 1023 job->kill(); 1024 } 1025 m_previewJobs.clear(); 1026 m_sequenceIndices.clear(); 1027 1028 m_iconUpdateTimer->stop(); 1029 m_scrollAreaTimer->stop(); 1030 m_changedItemsTimer->stop(); 1031 } 1032 1033 void KFilePreviewGeneratorPrivate::orderItems(KFileItemList &items) 1034 { 1035 KDirModel *dirModel = m_dirModel.data(); 1036 if (!dirModel) { 1037 return; 1038 } 1039 1040 // Order the items in a way that the preview for the visible items 1041 // is generated first, as this improves the felt performance a lot. 1042 const bool hasProxy = m_proxyModel != nullptr; 1043 const int itemCount = items.count(); 1044 const QRect visibleArea = m_viewAdapter->visibleArea(); 1045 1046 QModelIndex dirIndex; 1047 QRect itemRect; 1048 int insertPos = 0; 1049 for (int i = 0; i < itemCount; ++i) { 1050 dirIndex = dirModel->indexForItem(items.at(i)); // O(n) (n = number of rows) 1051 if (hasProxy) { 1052 const QModelIndex proxyIndex = m_proxyModel->mapFromSource(dirIndex); 1053 itemRect = m_viewAdapter->visualRect(proxyIndex); 1054 } else { 1055 itemRect = m_viewAdapter->visualRect(dirIndex); 1056 } 1057 1058 if (itemRect.intersects(visibleArea)) { 1059 // The current item is (at least partly) visible. Move it 1060 // to the front of the list, so that the preview is 1061 // generated earlier. 1062 items.insert(insertPos, items.at(i)); 1063 items.removeAt(i + 1); 1064 ++insertPos; 1065 ++m_pendingVisibleIconUpdates; 1066 } 1067 } 1068 } 1069 1070 void KFilePreviewGeneratorPrivate::addItemsToList(const QModelIndex &index, KFileItemList &list) 1071 { 1072 KDirModel *dirModel = m_dirModel.data(); 1073 if (!dirModel) { 1074 return; 1075 } 1076 1077 const int rowCount = dirModel->rowCount(index); 1078 for (int row = 0; row < rowCount; ++row) { 1079 const QModelIndex subIndex = dirModel->index(row, 0, index); 1080 KFileItem item = dirModel->itemForIndex(subIndex); 1081 list.append(item); 1082 1083 if (dirModel->rowCount(subIndex) > 0) { 1084 // the model is hierarchical (treeview) 1085 addItemsToList(subIndex, list); 1086 } 1087 } 1088 } 1089 1090 void KFilePreviewGeneratorPrivate::delayedIconUpdate() 1091 { 1092 KDirModel *dirModel = m_dirModel.data(); 1093 if (!dirModel) { 1094 return; 1095 } 1096 1097 // Precondition: No items have been changed within the last 1098 // 5 seconds. This means that items that have been changed constantly 1099 // due to a copy operation should be updated now. 1100 1101 KFileItemList itemList; 1102 1103 for (auto it = m_changedItems.cbegin(); it != m_changedItems.cend(); ++it) { 1104 const bool hasChanged = it.value(); 1105 if (hasChanged) { 1106 const QModelIndex index = dirModel->indexForUrl(it.key()); 1107 const KFileItem item = dirModel->itemForIndex(index); 1108 itemList.append(item); 1109 } 1110 } 1111 m_changedItems.clear(); 1112 1113 updateIcons(itemList); 1114 } 1115 1116 void KFilePreviewGeneratorPrivate::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end) 1117 { 1118 if (m_changedItems.isEmpty()) { 1119 return; 1120 } 1121 1122 KDirModel *dirModel = m_dirModel.data(); 1123 if (!dirModel) { 1124 return; 1125 } 1126 1127 for (int row = start; row <= end; row++) { 1128 const QModelIndex index = dirModel->index(row, 0, parent); 1129 1130 const KFileItem item = dirModel->itemForIndex(index); 1131 if (!item.isNull()) { 1132 m_changedItems.remove(item.url()); 1133 } 1134 1135 if (dirModel->hasChildren(index)) { 1136 rowsAboutToBeRemoved(index, 0, dirModel->rowCount(index) - 1); 1137 } 1138 } 1139 } 1140 1141 KFilePreviewGenerator::KFilePreviewGenerator(QAbstractItemView *parent) 1142 : QObject(parent) 1143 , d(new KFilePreviewGeneratorPrivate(this, new KIO::DefaultViewAdapter(parent, this), parent->model())) 1144 { 1145 d->m_itemView = parent; 1146 } 1147 1148 KFilePreviewGenerator::KFilePreviewGenerator(KAbstractViewAdapter *parent, QAbstractProxyModel *model) 1149 : QObject(parent) 1150 , d(new KFilePreviewGeneratorPrivate(this, parent, model)) 1151 { 1152 } 1153 1154 KFilePreviewGenerator::~KFilePreviewGenerator() = default; 1155 1156 void KFilePreviewGenerator::setPreviewShown(bool show) 1157 { 1158 if (d->m_previewShown == show) { 1159 return; 1160 } 1161 1162 KDirModel *dirModel = d->m_dirModel.data(); 1163 if (show && (!d->m_viewAdapter->iconSize().isValid() || !dirModel)) { 1164 // The view must provide an icon size and a directory model, 1165 // otherwise the showing the previews will get ignored 1166 return; 1167 } 1168 1169 d->m_previewShown = show; 1170 if (!show) { 1171 dirModel->clearAllPreviews(); 1172 } 1173 updateIcons(); 1174 } 1175 1176 bool KFilePreviewGenerator::isPreviewShown() const 1177 { 1178 return d->m_previewShown; 1179 } 1180 1181 #if KIOFILEWIDGETS_BUILD_DEPRECATED_SINCE(4, 3) 1182 void KFilePreviewGenerator::updatePreviews() 1183 { 1184 updateIcons(); 1185 } 1186 #endif 1187 1188 void KFilePreviewGenerator::updateIcons() 1189 { 1190 d->killPreviewJobs(); 1191 1192 d->clearCutItemsCache(); 1193 d->m_pendingItems.clear(); 1194 d->m_dispatchedItems.clear(); 1195 1196 KFileItemList itemList; 1197 d->addItemsToList(QModelIndex(), itemList); 1198 1199 d->updateIcons(itemList); 1200 } 1201 1202 void KFilePreviewGenerator::cancelPreviews() 1203 { 1204 d->killPreviewJobs(); 1205 d->m_pendingItems.clear(); 1206 d->m_dispatchedItems.clear(); 1207 updateIcons(); 1208 } 1209 1210 void KFilePreviewGenerator::setEnabledPlugins(const QStringList &plugins) 1211 { 1212 d->m_enabledPlugins = plugins; 1213 } 1214 1215 QStringList KFilePreviewGenerator::enabledPlugins() const 1216 { 1217 return d->m_enabledPlugins; 1218 } 1219 1220 #include "moc_kfilepreviewgenerator.cpp"