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"