File indexing completed on 2024-04-21 03:55:22

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"