File indexing completed on 2024-04-28 05:45:05

0001 /*
0002  * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-or-later
0005  */
0006 
0007 #include "kfileitemmodelrolesupdater.h"
0008 
0009 #include "dolphindebug.h"
0010 #include "kfileitemmodel.h"
0011 #include "private/kdirectorycontentscounter.h"
0012 #include "private/kpixmapmodifier.h"
0013 
0014 #include <KConfig>
0015 #include <KConfigGroup>
0016 #include <KIO/ListJob>
0017 #include <KIO/PreviewJob>
0018 #include <KIconLoader>
0019 #include <KJobWidgets>
0020 #include <KOverlayIconPlugin>
0021 #include <KPluginMetaData>
0022 #include <KSharedConfig>
0023 
0024 #include "dolphin_contentdisplaysettings.h"
0025 
0026 #if HAVE_BALOO
0027 #include "private/kbaloorolesprovider.h"
0028 #include <Baloo/File>
0029 #include <Baloo/FileMonitor>
0030 #endif
0031 
0032 #include <QApplication>
0033 #include <QElapsedTimer>
0034 #include <QFileInfo>
0035 #include <QPainter>
0036 #include <QPluginLoader>
0037 #include <QTimer>
0038 #include <chrono>
0039 
0040 using namespace std::chrono_literals;
0041 
0042 // #define KFILEITEMMODELROLESUPDATER_DEBUG
0043 
0044 namespace
0045 {
0046 // Maximum time in ms that the KFileItemModelRolesUpdater
0047 // may perform a blocking operation
0048 const int MaxBlockTimeout = 200;
0049 
0050 // If the number of items is smaller than ResolveAllItemsLimit,
0051 // the roles of all items will be resolved.
0052 const int ResolveAllItemsLimit = 500;
0053 
0054 // Not only the visible area, but up to ReadAheadPages before and after
0055 // this area will be resolved.
0056 const int ReadAheadPages = 5;
0057 }
0058 
0059 KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel *model, QObject *parent)
0060     : QObject(parent)
0061     , m_state(Idle)
0062     , m_previewChangedDuringPausing(false)
0063     , m_iconSizeChangedDuringPausing(false)
0064     , m_rolesChangedDuringPausing(false)
0065     , m_previewShown(false)
0066     , m_enlargeSmallPreviews(true)
0067     , m_clearPreviews(false)
0068     , m_finishedItems()
0069     , m_model(model)
0070     , m_iconSize()
0071     , m_devicePixelRatio(1.0)
0072     , m_firstVisibleIndex(0)
0073     , m_lastVisibleIndex(-1)
0074     , m_maximumVisibleItems(50)
0075     , m_roles()
0076     , m_resolvableRoles()
0077     , m_enabledPlugins()
0078     , m_localFileSizePreviewLimit(0)
0079     , m_pendingSortRoleItems()
0080     , m_pendingIndexes()
0081     , m_pendingPreviewItems()
0082     , m_previewJob()
0083     , m_hoverSequenceItem()
0084     , m_hoverSequenceIndex(0)
0085     , m_hoverSequencePreviewJob(nullptr)
0086     , m_hoverSequenceNumSuccessiveFailures(0)
0087     , m_recentlyChangedItemsTimer(nullptr)
0088     , m_recentlyChangedItems()
0089     , m_changedItems()
0090     , m_directoryContentsCounter(nullptr)
0091 #if HAVE_BALOO
0092     , m_balooFileMonitor(nullptr)
0093 #endif
0094 {
0095     Q_ASSERT(model);
0096 
0097     const KConfigGroup globalConfig(KSharedConfig::openConfig(), QStringLiteral("PreviewSettings"));
0098     m_enabledPlugins = globalConfig.readEntry("Plugins", KIO::PreviewJob::defaultPlugins());
0099     m_localFileSizePreviewLimit = static_cast<qulonglong>(globalConfig.readEntry("MaximumSize", 0));
0100 
0101     connect(m_model, &KFileItemModel::itemsInserted, this, &KFileItemModelRolesUpdater::slotItemsInserted);
0102     connect(m_model, &KFileItemModel::itemsRemoved, this, &KFileItemModelRolesUpdater::slotItemsRemoved);
0103     connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
0104     connect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved);
0105     connect(m_model, &KFileItemModel::sortRoleChanged, this, &KFileItemModelRolesUpdater::slotSortRoleChanged);
0106 
0107     // Use a timer to prevent that each call of slotItemsChanged() results in a synchronous
0108     // resolving of the roles. Postpone the resolving until no update has been done for 100 ms.
0109     m_recentlyChangedItemsTimer = new QTimer(this);
0110     m_recentlyChangedItemsTimer->setInterval(100ms);
0111     m_recentlyChangedItemsTimer->setSingleShot(true);
0112     connect(m_recentlyChangedItemsTimer, &QTimer::timeout, this, &KFileItemModelRolesUpdater::resolveRecentlyChangedItems);
0113 
0114     m_resolvableRoles.insert("size");
0115     m_resolvableRoles.insert("type");
0116     m_resolvableRoles.insert("isExpandable");
0117 #if HAVE_BALOO
0118     m_resolvableRoles += KBalooRolesProvider::instance().roles();
0119 #endif
0120 
0121     m_directoryContentsCounter = new KDirectoryContentsCounter(m_model, this);
0122     connect(m_directoryContentsCounter, &KDirectoryContentsCounter::result, this, &KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived);
0123 
0124     const auto plugins = KPluginMetaData::findPlugins(QStringLiteral("kf6/overlayicon"), {}, KPluginMetaData::AllowEmptyMetaData);
0125     for (const KPluginMetaData &data : plugins) {
0126         auto instance = QPluginLoader(data.fileName()).instance();
0127         auto plugin = qobject_cast<KOverlayIconPlugin *>(instance);
0128         if (plugin) {
0129             m_overlayIconsPlugin.append(plugin);
0130             connect(plugin, &KOverlayIconPlugin::overlaysChanged, this, &KFileItemModelRolesUpdater::slotOverlaysChanged);
0131         } else {
0132             // not our/valid plugin, so delete the created object
0133             delete instance;
0134         }
0135     }
0136 }
0137 
0138 KFileItemModelRolesUpdater::~KFileItemModelRolesUpdater()
0139 {
0140     killPreviewJob();
0141 }
0142 
0143 void KFileItemModelRolesUpdater::setIconSize(const QSize &size)
0144 {
0145     if (size != m_iconSize) {
0146         m_iconSize = size;
0147         if (m_state == Paused) {
0148             m_iconSizeChangedDuringPausing = true;
0149         } else if (m_previewShown) {
0150             // An icon size change requires the regenerating of
0151             // all previews
0152             m_finishedItems.clear();
0153             startUpdating();
0154         }
0155     }
0156 }
0157 
0158 QSize KFileItemModelRolesUpdater::iconSize() const
0159 {
0160     return m_iconSize;
0161 }
0162 
0163 void KFileItemModelRolesUpdater::setDevicePixelRatio(qreal devicePixelRatio)
0164 {
0165     if (m_devicePixelRatio != devicePixelRatio) {
0166         m_devicePixelRatio = devicePixelRatio;
0167         if (m_state == Paused) {
0168             m_iconSizeChangedDuringPausing = true;
0169         } else if (m_previewShown) {
0170             // A dpr change requires the regenerating of all previews.
0171             m_finishedItems.clear();
0172             startUpdating();
0173         }
0174     }
0175 }
0176 
0177 qreal KFileItemModelRolesUpdater::devicePixelRatio() const
0178 {
0179     return m_devicePixelRatio;
0180 }
0181 
0182 void KFileItemModelRolesUpdater::setVisibleIndexRange(int index, int count)
0183 {
0184     if (index < 0) {
0185         index = 0;
0186     }
0187     if (count < 0) {
0188         count = 0;
0189     }
0190 
0191     if (index == m_firstVisibleIndex && count == m_lastVisibleIndex - m_firstVisibleIndex + 1) {
0192         // The range has not been changed
0193         return;
0194     }
0195 
0196     m_firstVisibleIndex = index;
0197     m_lastVisibleIndex = qMin(index + count - 1, m_model->count() - 1);
0198 
0199     startUpdating();
0200 }
0201 
0202 void KFileItemModelRolesUpdater::setMaximumVisibleItems(int count)
0203 {
0204     m_maximumVisibleItems = count;
0205 }
0206 
0207 void KFileItemModelRolesUpdater::setPreviewsShown(bool show)
0208 {
0209     if (show == m_previewShown) {
0210         return;
0211     }
0212 
0213     m_previewShown = show;
0214     if (!show) {
0215         m_clearPreviews = true;
0216     }
0217 
0218     updateAllPreviews();
0219 }
0220 
0221 bool KFileItemModelRolesUpdater::previewsShown() const
0222 {
0223     return m_previewShown;
0224 }
0225 
0226 void KFileItemModelRolesUpdater::setEnlargeSmallPreviews(bool enlarge)
0227 {
0228     if (enlarge != m_enlargeSmallPreviews) {
0229         m_enlargeSmallPreviews = enlarge;
0230         if (m_previewShown) {
0231             updateAllPreviews();
0232         }
0233     }
0234 }
0235 
0236 bool KFileItemModelRolesUpdater::enlargeSmallPreviews() const
0237 {
0238     return m_enlargeSmallPreviews;
0239 }
0240 
0241 void KFileItemModelRolesUpdater::setEnabledPlugins(const QStringList &list)
0242 {
0243     if (m_enabledPlugins != list) {
0244         m_enabledPlugins = list;
0245         if (m_previewShown) {
0246             updateAllPreviews();
0247         }
0248     }
0249 }
0250 
0251 void KFileItemModelRolesUpdater::setPaused(bool paused)
0252 {
0253     if (paused == (m_state == Paused)) {
0254         return;
0255     }
0256 
0257     if (paused) {
0258         m_state = Paused;
0259         killPreviewJob();
0260     } else {
0261         const bool updatePreviews = (m_iconSizeChangedDuringPausing && m_previewShown) || m_previewChangedDuringPausing;
0262         const bool resolveAll = updatePreviews || m_rolesChangedDuringPausing;
0263         if (resolveAll) {
0264             m_finishedItems.clear();
0265         }
0266 
0267         m_iconSizeChangedDuringPausing = false;
0268         m_previewChangedDuringPausing = false;
0269         m_rolesChangedDuringPausing = false;
0270 
0271         if (!m_pendingSortRoleItems.isEmpty()) {
0272             m_state = ResolvingSortRole;
0273             resolveNextSortRole();
0274         } else {
0275             m_state = Idle;
0276         }
0277 
0278         startUpdating();
0279     }
0280 }
0281 
0282 void KFileItemModelRolesUpdater::setRoles(const QSet<QByteArray> &roles)
0283 {
0284     if (m_roles != roles) {
0285         m_roles = roles;
0286 
0287 #if HAVE_BALOO
0288         // Check whether there is at least one role that must be resolved
0289         // with the help of Baloo. If this is the case, a (quite expensive)
0290         // resolving will be done in KFileItemModelRolesUpdater::rolesData() and
0291         // the role gets watched for changes.
0292         const KBalooRolesProvider &rolesProvider = KBalooRolesProvider::instance();
0293         bool hasBalooRole = false;
0294         QSetIterator<QByteArray> it(roles);
0295         while (it.hasNext()) {
0296             const QByteArray &role = it.next();
0297             if (rolesProvider.roles().contains(role)) {
0298                 hasBalooRole = true;
0299                 break;
0300             }
0301         }
0302 
0303         if (hasBalooRole && m_balooConfig.fileIndexingEnabled() && !m_balooFileMonitor) {
0304             m_balooFileMonitor = new Baloo::FileMonitor(this);
0305             connect(m_balooFileMonitor, &Baloo::FileMonitor::fileMetaDataChanged, this, &KFileItemModelRolesUpdater::applyChangedBalooRoles);
0306         } else if (!hasBalooRole && m_balooFileMonitor) {
0307             delete m_balooFileMonitor;
0308             m_balooFileMonitor = nullptr;
0309         }
0310 #endif
0311 
0312         if (m_state == Paused) {
0313             m_rolesChangedDuringPausing = true;
0314         } else {
0315             startUpdating();
0316         }
0317     }
0318 }
0319 
0320 QSet<QByteArray> KFileItemModelRolesUpdater::roles() const
0321 {
0322     return m_roles;
0323 }
0324 
0325 bool KFileItemModelRolesUpdater::isPaused() const
0326 {
0327     return m_state == Paused;
0328 }
0329 
0330 QStringList KFileItemModelRolesUpdater::enabledPlugins() const
0331 {
0332     return m_enabledPlugins;
0333 }
0334 
0335 void KFileItemModelRolesUpdater::setLocalFileSizePreviewLimit(const qlonglong size)
0336 {
0337     m_localFileSizePreviewLimit = size;
0338 }
0339 
0340 qlonglong KFileItemModelRolesUpdater::localFileSizePreviewLimit() const
0341 {
0342     return m_localFileSizePreviewLimit;
0343 }
0344 
0345 void KFileItemModelRolesUpdater::setHoverSequenceState(const QUrl &itemUrl, int seqIdx)
0346 {
0347     const KFileItem item = m_model->fileItem(itemUrl);
0348 
0349     if (item != m_hoverSequenceItem) {
0350         killHoverSequencePreviewJob();
0351     }
0352 
0353     m_hoverSequenceItem = item;
0354     m_hoverSequenceIndex = seqIdx;
0355 
0356     if (!m_previewShown) {
0357         return;
0358     }
0359 
0360     m_hoverSequenceNumSuccessiveFailures = 0;
0361 
0362     loadNextHoverSequencePreview();
0363 }
0364 
0365 void KFileItemModelRolesUpdater::slotItemsInserted(const KItemRangeList &itemRanges)
0366 {
0367     QElapsedTimer timer;
0368     timer.start();
0369 
0370     // Determine the sort role synchronously for as many items as possible.
0371     if (m_resolvableRoles.contains(m_model->sortRole())) {
0372         int insertedCount = 0;
0373         for (const KItemRange &range : itemRanges) {
0374             const int lastIndex = insertedCount + range.index + range.count - 1;
0375             for (int i = insertedCount + range.index; i <= lastIndex; ++i) {
0376                 if (timer.elapsed() < MaxBlockTimeout) {
0377                     applySortRole(i);
0378                 } else {
0379                     m_pendingSortRoleItems.insert(m_model->fileItem(i));
0380                 }
0381             }
0382             insertedCount += range.count;
0383         }
0384 
0385         applySortProgressToModel();
0386 
0387         // If there are still items whose sort role is unknown, check if the
0388         // asynchronous determination of the sort role is already in progress,
0389         // and start it if that is not the case.
0390         if (!m_pendingSortRoleItems.isEmpty() && m_state != ResolvingSortRole) {
0391             killPreviewJob();
0392             m_state = ResolvingSortRole;
0393             resolveNextSortRole();
0394         }
0395     }
0396 
0397     startUpdating();
0398 }
0399 
0400 void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList &itemRanges)
0401 {
0402     Q_UNUSED(itemRanges)
0403 
0404     const bool allItemsRemoved = (m_model->count() == 0);
0405 
0406 #if HAVE_BALOO
0407     if (m_balooFileMonitor) {
0408         // Don't let the FileWatcher watch for removed items
0409         if (allItemsRemoved) {
0410             m_balooFileMonitor->clear();
0411         } else {
0412             QStringList newFileList;
0413             const QStringList oldFileList = m_balooFileMonitor->files();
0414             for (const QString &file : oldFileList) {
0415                 if (m_model->index(QUrl::fromLocalFile(file)) >= 0) {
0416                     newFileList.append(file);
0417                 }
0418             }
0419             m_balooFileMonitor->setFiles(newFileList);
0420         }
0421     }
0422 #endif
0423 
0424     if (allItemsRemoved) {
0425         m_state = Idle;
0426 
0427         m_finishedItems.clear();
0428         m_pendingSortRoleItems.clear();
0429         m_pendingIndexes.clear();
0430         m_pendingPreviewItems.clear();
0431         m_recentlyChangedItems.clear();
0432         m_recentlyChangedItemsTimer->stop();
0433         m_changedItems.clear();
0434         m_hoverSequenceLoadedItems.clear();
0435 
0436         killPreviewJob();
0437         if (!m_model->showDirectoriesOnly()) {
0438             m_directoryContentsCounter->stopWorker();
0439         }
0440     } else {
0441         // Only remove the items from m_finishedItems. They will be removed
0442         // from the other sets later on.
0443         QSet<KFileItem>::iterator it = m_finishedItems.begin();
0444         while (it != m_finishedItems.end()) {
0445             if (m_model->index(*it) < 0) {
0446                 it = m_finishedItems.erase(it);
0447             } else {
0448                 ++it;
0449             }
0450         }
0451 
0452         // Removed items won't have hover previews loaded anymore.
0453         for (const KItemRange &itemRange : itemRanges) {
0454             int index = itemRange.index;
0455             for (int count = itemRange.count; count > 0; --count) {
0456                 const KFileItem item = m_model->fileItem(index);
0457                 m_hoverSequenceLoadedItems.remove(item);
0458                 ++index;
0459             }
0460         }
0461 
0462         // The visible items might have changed.
0463         startUpdating();
0464     }
0465 }
0466 
0467 void KFileItemModelRolesUpdater::slotItemsMoved(KItemRange itemRange, const QList<int> &movedToIndexes)
0468 {
0469     Q_UNUSED(itemRange)
0470     Q_UNUSED(movedToIndexes)
0471 
0472     // The visible items might have changed.
0473     startUpdating();
0474 }
0475 
0476 void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList &itemRanges, const QSet<QByteArray> &roles)
0477 {
0478     Q_UNUSED(roles)
0479 
0480     // Find out if slotItemsChanged() has been done recently. If that is the
0481     // case, resolving the roles is postponed until a timer has exceeded
0482     // to prevent expensive repeated updates if files are updated frequently.
0483     const bool itemsChangedRecently = m_recentlyChangedItemsTimer->isActive();
0484 
0485     QSet<KFileItem> &targetSet = itemsChangedRecently ? m_recentlyChangedItems : m_changedItems;
0486 
0487     for (const KItemRange &itemRange : itemRanges) {
0488         int index = itemRange.index;
0489         for (int count = itemRange.count; count > 0; --count) {
0490             const KFileItem item = m_model->fileItem(index);
0491             targetSet.insert(item);
0492             ++index;
0493         }
0494     }
0495 
0496     m_recentlyChangedItemsTimer->start();
0497 
0498     if (!itemsChangedRecently) {
0499         updateChangedItems();
0500     }
0501 }
0502 
0503 void KFileItemModelRolesUpdater::slotSortRoleChanged(const QByteArray &current, const QByteArray &previous)
0504 {
0505     Q_UNUSED(current)
0506     Q_UNUSED(previous)
0507 
0508     if (m_resolvableRoles.contains(current)) {
0509         m_pendingSortRoleItems.clear();
0510         m_finishedItems.clear();
0511 
0512         const int count = m_model->count();
0513         QElapsedTimer timer;
0514         timer.start();
0515 
0516         // Determine the sort role synchronously for as many items as possible.
0517         for (int index = 0; index < count; ++index) {
0518             if (timer.elapsed() < MaxBlockTimeout) {
0519                 applySortRole(index);
0520             } else {
0521                 m_pendingSortRoleItems.insert(m_model->fileItem(index));
0522             }
0523         }
0524 
0525         applySortProgressToModel();
0526 
0527         if (!m_pendingSortRoleItems.isEmpty()) {
0528             // Trigger the asynchronous determination of the sort role.
0529             killPreviewJob();
0530             m_state = ResolvingSortRole;
0531             resolveNextSortRole();
0532         }
0533     } else {
0534         m_state = Idle;
0535         m_pendingSortRoleItems.clear();
0536         applySortProgressToModel();
0537     }
0538 }
0539 
0540 void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem &item, const QPixmap &pixmap)
0541 {
0542     if (m_state != PreviewJobRunning) {
0543         return;
0544     }
0545 
0546     m_changedItems.remove(item);
0547 
0548     const int index = m_model->index(item);
0549     if (index < 0) {
0550         return;
0551     }
0552 
0553     QPixmap scaledPixmap = transformPreviewPixmap(pixmap);
0554 
0555     QHash<QByteArray, QVariant> data = rolesData(item, index);
0556 
0557     const QStringList overlays = data["iconOverlays"].toStringList();
0558     // Strangely KFileItem::overlays() returns empty string-values, so
0559     // we need to check first whether an overlay must be drawn at all.
0560     // It is more efficient to do it here, as KIconLoader::drawOverlays()
0561     // assumes that an overlay will be drawn and has some additional
0562     // setup time.
0563     if (!scaledPixmap.isNull()) {
0564         for (const QString &overlay : overlays) {
0565             if (!overlay.isEmpty()) {
0566                 // There is at least one overlay, draw all overlays above m_pixmap
0567                 // and cancel the check
0568                 KIconLoader::global()->drawOverlays(overlays, scaledPixmap, KIconLoader::Desktop);
0569                 break;
0570             }
0571         }
0572     }
0573 
0574     data.insert("iconPixmap", scaledPixmap);
0575 
0576     disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
0577     m_model->setData(index, data);
0578     connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
0579 
0580     m_finishedItems.insert(item);
0581 }
0582 
0583 void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem &item)
0584 {
0585     if (m_state != PreviewJobRunning) {
0586         return;
0587     }
0588 
0589     m_changedItems.remove(item);
0590 
0591     const int index = m_model->index(item);
0592     if (index >= 0) {
0593         QHash<QByteArray, QVariant> data;
0594         data.insert("iconPixmap", QPixmap());
0595 
0596         disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
0597         m_model->setData(index, data);
0598         connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
0599 
0600         applyResolvedRoles(index, ResolveAll);
0601         m_finishedItems.insert(item);
0602     }
0603 }
0604 
0605 void KFileItemModelRolesUpdater::slotPreviewJobFinished()
0606 {
0607     m_previewJob = nullptr;
0608 
0609     if (m_state != PreviewJobRunning) {
0610         return;
0611     }
0612 
0613     m_state = Idle;
0614 
0615     if (!m_pendingPreviewItems.isEmpty()) {
0616         startPreviewJob();
0617     } else {
0618         if (!m_changedItems.isEmpty()) {
0619             updateChangedItems();
0620         }
0621     }
0622 }
0623 
0624 void KFileItemModelRolesUpdater::slotHoverSequenceGotPreview(const KFileItem &item, const QPixmap &pixmap)
0625 {
0626     const int index = m_model->index(item);
0627     if (index < 0) {
0628         return;
0629     }
0630 
0631     QHash<QByteArray, QVariant> data = m_model->data(index);
0632     QVector<QPixmap> pixmaps = data["hoverSequencePixmaps"].value<QVector<QPixmap>>();
0633     const int loadedIndex = pixmaps.size();
0634 
0635     float wap = m_hoverSequencePreviewJob->sequenceIndexWraparoundPoint();
0636     if (!m_hoverSequencePreviewJob->handlesSequences()) {
0637         wap = 1.0f;
0638     }
0639     if (wap >= 0.0f) {
0640         data["hoverSequenceWraparoundPoint"] = wap;
0641         m_model->setData(index, data);
0642     }
0643 
0644     // For hover sequence previews we never load index 0, because that's just the regular preview
0645     // in "iconPixmap". But that means we'll load index 1 even for thumbnailers that don't support
0646     // sequences, in which case we can just throw away the preview because it's the same as for
0647     // index 0. Unfortunately we can't find it out earlier :(
0648     if (wap < 0.0f || loadedIndex < static_cast<int>(wap)) {
0649         // Add the preview to the model data
0650 
0651         const QPixmap scaledPixmap = transformPreviewPixmap(pixmap);
0652 
0653         pixmaps.append(scaledPixmap);
0654         data["hoverSequencePixmaps"] = QVariant::fromValue(pixmaps);
0655 
0656         m_model->setData(index, data);
0657 
0658         const auto loadedIt = std::find(m_hoverSequenceLoadedItems.begin(), m_hoverSequenceLoadedItems.end(), item);
0659         if (loadedIt == m_hoverSequenceLoadedItems.end()) {
0660             m_hoverSequenceLoadedItems.push_back(item);
0661             trimHoverSequenceLoadedItems();
0662         }
0663     }
0664 
0665     m_hoverSequenceNumSuccessiveFailures = 0;
0666 }
0667 
0668 void KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed(const KFileItem &item)
0669 {
0670     const int index = m_model->index(item);
0671     if (index < 0) {
0672         return;
0673     }
0674 
0675     static const int numRetries = 2;
0676 
0677     QHash<QByteArray, QVariant> data = m_model->data(index);
0678     QVector<QPixmap> pixmaps = data["hoverSequencePixmaps"].value<QVector<QPixmap>>();
0679 
0680     qCDebug(DolphinDebug).nospace() << "Failed to generate hover sequence preview #" << pixmaps.size() << " for file " << item.url().toString() << " (attempt "
0681                                     << (m_hoverSequenceNumSuccessiveFailures + 1) << "/" << (numRetries + 1) << ")";
0682 
0683     if (m_hoverSequenceNumSuccessiveFailures >= numRetries) {
0684         // Give up and simply duplicate the previous sequence image (if any)
0685 
0686         pixmaps.append(pixmaps.empty() ? QPixmap() : pixmaps.last());
0687         data["hoverSequencePixmaps"] = QVariant::fromValue(pixmaps);
0688 
0689         if (!data.contains("hoverSequenceWraparoundPoint")) {
0690             // hoverSequenceWraparoundPoint is only available when PreviewJob succeeds, so unless
0691             // it has previously succeeded, it's best to assume that it just doesn't handle
0692             // sequences instead of trying to load the next image indefinitely.
0693             data["hoverSequenceWraparoundPoint"] = 1.0f;
0694         }
0695 
0696         m_model->setData(index, data);
0697 
0698         m_hoverSequenceNumSuccessiveFailures = 0;
0699     } else {
0700         // Retry
0701 
0702         m_hoverSequenceNumSuccessiveFailures++;
0703     }
0704 
0705     // Next image in the sequence (or same one if the retry limit wasn't reached yet) will be
0706     // loaded automatically, because slotHoverSequencePreviewJobFinished() will be triggered
0707     // even when PreviewJob fails.
0708 }
0709 
0710 void KFileItemModelRolesUpdater::slotHoverSequencePreviewJobFinished()
0711 {
0712     const int index = m_model->index(m_hoverSequenceItem);
0713     if (index < 0) {
0714         m_hoverSequencePreviewJob = nullptr;
0715         return;
0716     }
0717 
0718     // Since a PreviewJob can only have one associated sequence index, we can only generate
0719     // one sequence image per job, so we have to start another one for the next index.
0720 
0721     // Load the next image in the sequence
0722     m_hoverSequencePreviewJob = nullptr;
0723     loadNextHoverSequencePreview();
0724 }
0725 
0726 void KFileItemModelRolesUpdater::resolveNextSortRole()
0727 {
0728     if (m_state != ResolvingSortRole) {
0729         return;
0730     }
0731 
0732     QSet<KFileItem>::iterator it = m_pendingSortRoleItems.begin();
0733     while (it != m_pendingSortRoleItems.end()) {
0734         const KFileItem item = *it;
0735         const int index = m_model->index(item);
0736 
0737         // Continue if the sort role has already been determined for the
0738         // item, and the item has not been changed recently.
0739         if (!m_changedItems.contains(item) && m_model->data(index).contains(m_model->sortRole())) {
0740             it = m_pendingSortRoleItems.erase(it);
0741             continue;
0742         }
0743 
0744         applySortRole(index);
0745         m_pendingSortRoleItems.erase(it);
0746         break;
0747     }
0748 
0749     if (!m_pendingSortRoleItems.isEmpty()) {
0750         applySortProgressToModel();
0751         QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextSortRole);
0752     } else {
0753         m_state = Idle;
0754 
0755         // Prevent that we try to update the items twice.
0756         disconnect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved);
0757         applySortProgressToModel();
0758         connect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved);
0759         startUpdating();
0760     }
0761 }
0762 
0763 void KFileItemModelRolesUpdater::resolveNextPendingRoles()
0764 {
0765     if (m_state != ResolvingAllRoles) {
0766         return;
0767     }
0768 
0769     while (!m_pendingIndexes.isEmpty()) {
0770         const int index = m_pendingIndexes.takeFirst();
0771         const KFileItem item = m_model->fileItem(index);
0772 
0773         if (m_finishedItems.contains(item)) {
0774             continue;
0775         }
0776 
0777         applyResolvedRoles(index, ResolveAll);
0778         m_finishedItems.insert(item);
0779         m_changedItems.remove(item);
0780         break;
0781     }
0782 
0783     if (!m_pendingIndexes.isEmpty()) {
0784         QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles);
0785     } else {
0786         m_state = Idle;
0787 
0788         if (m_clearPreviews) {
0789             // Only go through the list if there are items which might still have previews.
0790             if (m_finishedItems.count() != m_model->count()) {
0791                 QHash<QByteArray, QVariant> data;
0792                 data.insert("iconPixmap", QPixmap());
0793                 data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector<QPixmap>()));
0794 
0795                 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
0796                 for (int index = 0; index <= m_model->count(); ++index) {
0797                     if (m_model->data(index).contains("iconPixmap") || m_model->data(index).contains("hoverSequencePixmaps")) {
0798                         m_model->setData(index, data);
0799                     }
0800                 }
0801                 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
0802             }
0803             m_clearPreviews = false;
0804         }
0805 
0806         if (!m_changedItems.isEmpty()) {
0807             updateChangedItems();
0808         }
0809     }
0810 }
0811 
0812 void KFileItemModelRolesUpdater::resolveRecentlyChangedItems()
0813 {
0814     m_changedItems += m_recentlyChangedItems;
0815     m_recentlyChangedItems.clear();
0816     updateChangedItems();
0817 }
0818 
0819 void KFileItemModelRolesUpdater::applyChangedBalooRoles(const QString &file)
0820 {
0821 #if HAVE_BALOO
0822     const KFileItem item = m_model->fileItem(QUrl::fromLocalFile(file));
0823 
0824     if (item.isNull()) {
0825         // itemUrl is not in the model anymore, probably because
0826         // the corresponding file has been deleted in the meantime.
0827         return;
0828     }
0829     applyChangedBalooRolesForItem(item);
0830 #else
0831     Q_UNUSED(file)
0832 #endif
0833 }
0834 
0835 void KFileItemModelRolesUpdater::applyChangedBalooRolesForItem(const KFileItem &item)
0836 {
0837 #if HAVE_BALOO
0838     Baloo::File file(item.localPath());
0839     file.load();
0840 
0841     const KBalooRolesProvider &rolesProvider = KBalooRolesProvider::instance();
0842     QHash<QByteArray, QVariant> data;
0843 
0844     const auto roles = rolesProvider.roles();
0845     for (const QByteArray &role : roles) {
0846         // Overwrite all the role values with an empty QVariant, because the roles
0847         // provider doesn't overwrite it when the property value list is empty.
0848         // See bug 322348
0849         data.insert(role, QVariant());
0850     }
0851 
0852     QHashIterator<QByteArray, QVariant> it(rolesProvider.roleValues(file, m_roles));
0853     while (it.hasNext()) {
0854         it.next();
0855         data.insert(it.key(), it.value());
0856     }
0857 
0858     disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
0859     const int index = m_model->index(item);
0860     m_model->setData(index, data);
0861     connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
0862 #else
0863 #ifndef Q_CC_MSVC
0864     Q_UNUSED(item)
0865 #endif
0866 #endif
0867 }
0868 
0869 void KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived(const QString &path, int count, long long size)
0870 {
0871     const bool getIsExpandableRole = m_roles.contains("isExpandable");
0872     const bool getSizeRole = m_roles.contains("size");
0873 
0874     if (getSizeRole || getIsExpandableRole) {
0875         const int index = m_model->index(QUrl::fromLocalFile(path));
0876         if (index >= 0) {
0877             QHash<QByteArray, QVariant> data;
0878 
0879             if (getSizeRole) {
0880                 data.insert("count", count);
0881                 data.insert("size", QVariant::fromValue(size));
0882             }
0883             if (getIsExpandableRole) {
0884                 data.insert("isExpandable", count > 0);
0885             }
0886 
0887             disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
0888             m_model->setData(index, data);
0889             connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
0890         }
0891     }
0892 }
0893 
0894 void KFileItemModelRolesUpdater::startUpdating()
0895 {
0896     if (m_state == Paused) {
0897         return;
0898     }
0899 
0900     if (m_finishedItems.count() == m_model->count()) {
0901         // All roles have been resolved already.
0902         m_state = Idle;
0903         return;
0904     }
0905 
0906     // Terminate all updates that are currently active.
0907     killPreviewJob();
0908     m_pendingIndexes.clear();
0909 
0910     QElapsedTimer timer;
0911     timer.start();
0912 
0913     // Determine the icons for the visible items synchronously.
0914     updateVisibleIcons();
0915 
0916     // A detailed update of the items in and near the visible area
0917     // only makes sense if sorting is finished.
0918     if (m_state == ResolvingSortRole) {
0919         return;
0920     }
0921 
0922     // Start the preview job or the asynchronous resolving of all roles.
0923     QList<int> indexes = indexesToResolve();
0924 
0925     if (m_previewShown) {
0926         m_pendingPreviewItems.clear();
0927         m_pendingPreviewItems.reserve(indexes.count());
0928 
0929         for (int index : std::as_const(indexes)) {
0930             const KFileItem item = m_model->fileItem(index);
0931             if (!m_finishedItems.contains(item)) {
0932                 m_pendingPreviewItems.append(item);
0933             }
0934         }
0935 
0936         startPreviewJob();
0937     } else {
0938         m_pendingIndexes = indexes;
0939         // Trigger the asynchronous resolving of all roles.
0940         m_state = ResolvingAllRoles;
0941         QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles);
0942     }
0943 }
0944 
0945 void KFileItemModelRolesUpdater::updateVisibleIcons()
0946 {
0947     int lastVisibleIndex = m_lastVisibleIndex;
0948     if (lastVisibleIndex <= 0) {
0949         // Guess a reasonable value for the last visible index if the view
0950         // has not told us about the real value yet.
0951         lastVisibleIndex = qMin(m_firstVisibleIndex + m_maximumVisibleItems, m_model->count() - 1);
0952         if (lastVisibleIndex <= 0) {
0953             lastVisibleIndex = qMin(200, m_model->count() - 1);
0954         }
0955     }
0956 
0957     QElapsedTimer timer;
0958     timer.start();
0959 
0960     // Try to determine the final icons for all visible items.
0961     int index;
0962     for (index = m_firstVisibleIndex; index <= lastVisibleIndex && timer.elapsed() < MaxBlockTimeout; ++index) {
0963         applyResolvedRoles(index, ResolveFast);
0964     }
0965 
0966     // KFileItemListView::initializeItemListWidget(KItemListWidget*) will load
0967     // preliminary icons (i.e., without mime type determination) for the
0968     // remaining items.
0969 }
0970 
0971 void KFileItemModelRolesUpdater::startPreviewJob()
0972 {
0973     m_state = PreviewJobRunning;
0974 
0975     if (m_pendingPreviewItems.isEmpty()) {
0976         QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished);
0977         return;
0978     }
0979 
0980     // PreviewJob internally caches items always with the size of
0981     // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done
0982     // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must
0983     // do a downscaling anyhow because of the frame, so in this case only the provided
0984     // cache sizes are requested.
0985     const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128) ? QSize(256, 256) : QSize(128, 128);
0986 
0987     // KIO::filePreview() will request the MIME-type of all passed items, which (in the
0988     // worst case) might block the application for several seconds. To prevent such
0989     // a blocking, we only pass items with known mime type to the preview job.
0990     const int count = m_pendingPreviewItems.count();
0991     KFileItemList itemSubSet;
0992     itemSubSet.reserve(count);
0993 
0994     if (m_pendingPreviewItems.first().isMimeTypeKnown()) {
0995         // Some mime types are known already, probably because they were
0996         // determined when loading the icons for the visible items. Start
0997         // a preview job for all items at the beginning of the list which
0998         // have a known mime type.
0999         do {
1000             itemSubSet.append(m_pendingPreviewItems.takeFirst());
1001         } while (!m_pendingPreviewItems.isEmpty() && m_pendingPreviewItems.first().isMimeTypeKnown());
1002     } else {
1003         // Determine mime types for MaxBlockTimeout ms, and start a preview
1004         // job for the corresponding items.
1005         QElapsedTimer timer;
1006         timer.start();
1007 
1008         do {
1009             const KFileItem item = m_pendingPreviewItems.takeFirst();
1010             item.determineMimeType();
1011             itemSubSet.append(item);
1012         } while (!m_pendingPreviewItems.isEmpty() && timer.elapsed() < MaxBlockTimeout);
1013     }
1014 
1015     KIO::PreviewJob *job = new KIO::PreviewJob(itemSubSet, cacheSize, &m_enabledPlugins);
1016     job->setDevicePixelRatio(m_devicePixelRatio);
1017     job->setIgnoreMaximumSize(itemSubSet.first().isLocalFile() && !itemSubSet.first().isSlow() && m_localFileSizePreviewLimit <= 0);
1018     if (job->uiDelegate()) {
1019         KJobWidgets::setWindow(job, qApp->activeWindow());
1020     }
1021 
1022     connect(job, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotGotPreview);
1023     connect(job, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotPreviewFailed);
1024     connect(job, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished);
1025 
1026     m_previewJob = job;
1027 }
1028 
1029 QPixmap KFileItemModelRolesUpdater::transformPreviewPixmap(const QPixmap &pixmap)
1030 {
1031     QPixmap scaledPixmap = pixmap;
1032 
1033     if (!pixmap.hasAlpha() && !pixmap.isNull() && m_iconSize.width() > KIconLoader::SizeSmallMedium && m_iconSize.height() > KIconLoader::SizeSmallMedium) {
1034         if (m_enlargeSmallPreviews) {
1035             KPixmapModifier::applyFrame(scaledPixmap, m_iconSize);
1036         } else {
1037             // Assure that small previews don't get enlarged. Instead they
1038             // should be shown centered within the frame.
1039             const QSize contentSize = KPixmapModifier::sizeInsideFrame(m_iconSize);
1040             const bool enlargingRequired = scaledPixmap.width() < contentSize.width() && scaledPixmap.height() < contentSize.height();
1041             if (enlargingRequired) {
1042                 QSize frameSize = scaledPixmap.size() / scaledPixmap.devicePixelRatio();
1043                 frameSize.scale(m_iconSize, Qt::KeepAspectRatio);
1044 
1045                 QPixmap largeFrame(frameSize);
1046                 largeFrame.fill(Qt::transparent);
1047 
1048                 KPixmapModifier::applyFrame(largeFrame, frameSize);
1049 
1050                 QPainter painter(&largeFrame);
1051                 painter.drawPixmap((largeFrame.width() - scaledPixmap.width() / scaledPixmap.devicePixelRatio()) / 2,
1052                                    (largeFrame.height() - scaledPixmap.height() / scaledPixmap.devicePixelRatio()) / 2,
1053                                    scaledPixmap);
1054                 scaledPixmap = largeFrame;
1055             } else {
1056                 // The image must be shrunk as it is too large to fit into
1057                 // the available icon size
1058                 KPixmapModifier::applyFrame(scaledPixmap, m_iconSize);
1059             }
1060         }
1061     } else if (!pixmap.isNull()) {
1062         KPixmapModifier::scale(scaledPixmap, m_iconSize * m_devicePixelRatio);
1063         scaledPixmap.setDevicePixelRatio(m_devicePixelRatio);
1064     }
1065 
1066     return scaledPixmap;
1067 }
1068 
1069 void KFileItemModelRolesUpdater::loadNextHoverSequencePreview()
1070 {
1071     if (m_hoverSequenceItem.isNull() || m_hoverSequencePreviewJob) {
1072         return;
1073     }
1074 
1075     const int index = m_model->index(m_hoverSequenceItem);
1076     if (index < 0) {
1077         return;
1078     }
1079 
1080     // We generate the next few sequence indices in advance (buffering)
1081     const int maxSeqIdx = m_hoverSequenceIndex + 5;
1082 
1083     QHash<QByteArray, QVariant> data = m_model->data(index);
1084 
1085     if (!data.contains("hoverSequencePixmaps")) {
1086         // The pixmap at index 0 isn't used ("iconPixmap" will be used instead)
1087         data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector<QPixmap>() << QPixmap()));
1088         m_model->setData(index, data);
1089     }
1090 
1091     const QVector<QPixmap> pixmaps = data["hoverSequencePixmaps"].value<QVector<QPixmap>>();
1092 
1093     const int loadSeqIdx = pixmaps.size();
1094 
1095     float wap = -1.0f;
1096     if (data.contains("hoverSequenceWraparoundPoint")) {
1097         wap = data["hoverSequenceWraparoundPoint"].toFloat();
1098     }
1099     if (wap >= 1.0f && loadSeqIdx >= static_cast<int>(wap)) {
1100         // Reached the wraparound point -> no more previews to load.
1101         return;
1102     }
1103 
1104     if (loadSeqIdx > maxSeqIdx) {
1105         // Wait until setHoverSequenceState() is called with a higher sequence index.
1106         return;
1107     }
1108 
1109     // PreviewJob internally caches items always with the size of
1110     // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done
1111     // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must
1112     // do a downscaling anyhow because of the frame, so in this case only the provided
1113     // cache sizes are requested.
1114     const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128) ? QSize(256, 256) : QSize(128, 128);
1115 
1116     KIO::PreviewJob *job = new KIO::PreviewJob({m_hoverSequenceItem}, cacheSize, &m_enabledPlugins);
1117 
1118     job->setSequenceIndex(loadSeqIdx);
1119     job->setIgnoreMaximumSize(m_hoverSequenceItem.isLocalFile() && !m_hoverSequenceItem.isSlow() && m_localFileSizePreviewLimit <= 0);
1120     if (job->uiDelegate()) {
1121         KJobWidgets::setWindow(job, qApp->activeWindow());
1122     }
1123 
1124     connect(job, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotHoverSequenceGotPreview);
1125     connect(job, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed);
1126     connect(job, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewJobFinished);
1127 
1128     m_hoverSequencePreviewJob = job;
1129 }
1130 
1131 void KFileItemModelRolesUpdater::killHoverSequencePreviewJob()
1132 {
1133     if (m_hoverSequencePreviewJob) {
1134         disconnect(m_hoverSequencePreviewJob, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotHoverSequenceGotPreview);
1135         disconnect(m_hoverSequencePreviewJob, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed);
1136         disconnect(m_hoverSequencePreviewJob, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewJobFinished);
1137         m_hoverSequencePreviewJob->kill();
1138         m_hoverSequencePreviewJob = nullptr;
1139     }
1140 }
1141 
1142 void KFileItemModelRolesUpdater::updateChangedItems()
1143 {
1144     if (m_state == Paused) {
1145         return;
1146     }
1147 
1148     if (m_changedItems.isEmpty()) {
1149         return;
1150     }
1151 
1152     m_finishedItems -= m_changedItems;
1153 
1154     if (m_resolvableRoles.contains(m_model->sortRole())) {
1155         m_pendingSortRoleItems += m_changedItems;
1156 
1157         if (m_state != ResolvingSortRole) {
1158             // Stop the preview job if necessary, and trigger the
1159             // asynchronous determination of the sort role.
1160             killPreviewJob();
1161             m_state = ResolvingSortRole;
1162             QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextSortRole);
1163         }
1164 
1165         return;
1166     }
1167 
1168     QList<int> visibleChangedIndexes;
1169     QList<int> invisibleChangedIndexes;
1170     visibleChangedIndexes.reserve(m_changedItems.size());
1171     invisibleChangedIndexes.reserve(m_changedItems.size());
1172 
1173     auto changedItemsIt = m_changedItems.begin();
1174     while (changedItemsIt != m_changedItems.end()) {
1175         const auto &item = *changedItemsIt;
1176         const int index = m_model->index(item);
1177 
1178         if (index < 0) {
1179             changedItemsIt = m_changedItems.erase(changedItemsIt);
1180             continue;
1181         }
1182         ++changedItemsIt;
1183 
1184         if (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex) {
1185             visibleChangedIndexes.append(index);
1186         } else {
1187             invisibleChangedIndexes.append(index);
1188         }
1189     }
1190 
1191     std::sort(visibleChangedIndexes.begin(), visibleChangedIndexes.end());
1192 
1193     if (m_previewShown) {
1194         for (int index : std::as_const(visibleChangedIndexes)) {
1195             m_pendingPreviewItems.append(m_model->fileItem(index));
1196         }
1197 
1198         for (int index : std::as_const(invisibleChangedIndexes)) {
1199             m_pendingPreviewItems.append(m_model->fileItem(index));
1200         }
1201 
1202         if (!m_previewJob) {
1203             startPreviewJob();
1204         }
1205     } else {
1206         const bool resolvingInProgress = !m_pendingIndexes.isEmpty();
1207         m_pendingIndexes = visibleChangedIndexes + m_pendingIndexes + invisibleChangedIndexes;
1208         if (!resolvingInProgress) {
1209             // Trigger the asynchronous resolving of the changed roles.
1210             m_state = ResolvingAllRoles;
1211             QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles);
1212         }
1213     }
1214 }
1215 
1216 void KFileItemModelRolesUpdater::applySortRole(int index)
1217 {
1218     QHash<QByteArray, QVariant> data;
1219     const KFileItem item = m_model->fileItem(index);
1220 
1221     if (m_model->sortRole() == "type") {
1222         if (!item.isMimeTypeKnown()) {
1223             item.determineMimeType();
1224         }
1225 
1226         data.insert("type", item.mimeComment());
1227     } else if (m_model->sortRole() == "size" && item.isLocalFile() && item.isDir()) {
1228         startDirectorySizeCounting(item, index);
1229         return;
1230     } else {
1231         // Probably the sort role is a baloo role - just determine all roles.
1232         data = rolesData(item, index);
1233     }
1234 
1235     disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1236     m_model->setData(index, data);
1237     connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1238 }
1239 
1240 void KFileItemModelRolesUpdater::applySortProgressToModel()
1241 {
1242     // Inform the model about the progress of the resolved items,
1243     // so that it can give an indication when the sorting has been finished.
1244     const int resolvedCount = m_model->count() - m_pendingSortRoleItems.count();
1245     m_model->emitSortProgress(resolvedCount);
1246 }
1247 
1248 bool KFileItemModelRolesUpdater::applyResolvedRoles(int index, ResolveHint hint)
1249 {
1250     const KFileItem item = m_model->fileItem(index);
1251     const bool resolveAll = (hint == ResolveAll);
1252 
1253     bool iconChanged = false;
1254     if (!item.isMimeTypeKnown() || !item.isFinalIconKnown()) {
1255         item.determineMimeType();
1256         iconChanged = true;
1257     } else if (!m_model->data(index).contains("iconName")) {
1258         iconChanged = true;
1259     }
1260 
1261     if (iconChanged || resolveAll || m_clearPreviews) {
1262         if (index < 0) {
1263             return false;
1264         }
1265 
1266         QHash<QByteArray, QVariant> data;
1267         if (resolveAll) {
1268             data = rolesData(item, index);
1269         }
1270 
1271         if (!item.iconName().isEmpty()) {
1272             data.insert("iconName", item.iconName());
1273         }
1274 
1275         if (m_clearPreviews) {
1276             data.insert("iconPixmap", QPixmap());
1277             data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector<QPixmap>()));
1278         }
1279 
1280         disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1281         m_model->setData(index, data);
1282         connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1283         return true;
1284     }
1285 
1286     return false;
1287 }
1288 
1289 void KFileItemModelRolesUpdater::startDirectorySizeCounting(const KFileItem &item, int index)
1290 {
1291     if (!item.isLocalFile()) {
1292         return;
1293     }
1294 
1295     if (ContentDisplaySettings::directorySizeCount() || item.isSlow()) {
1296         // fastpath no recursion necessary
1297 
1298         auto data = m_model->data(index);
1299         if (data.value("size") == -2) {
1300             // means job already started
1301             return;
1302         }
1303 
1304         auto url = item.url();
1305         if (!item.localPath().isEmpty()) {
1306             // optimization for desktop:/, trash:/
1307             url = QUrl::fromLocalFile(item.localPath());
1308         }
1309 
1310         data.insert("size", -2); // invalid size, -1 means size unknown
1311 
1312         disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1313         m_model->setData(index, data);
1314         connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1315 
1316         auto listJob = KIO::listDir(url, KIO::HideProgressInfo);
1317         QObject::connect(listJob, &KIO::ListJob::entries, this, [this, item](const KJob * /*job*/, const KIO::UDSEntryList &list) {
1318             int index = m_model->index(item);
1319             if (index < 0) {
1320                 return;
1321             }
1322             auto data = m_model->data(index);
1323             int origCount = data.value("count").toInt();
1324             int entryCount = origCount;
1325 
1326             for (const KIO::UDSEntry &entry : list) {
1327                 const auto name = entry.stringValue(KIO::UDSEntry::UDS_NAME);
1328 
1329                 if (name == QStringLiteral("..") || name == QStringLiteral(".")) {
1330                     continue;
1331                 }
1332                 if (!m_model->showHiddenFiles() && name.startsWith(QLatin1Char('.'))) {
1333                     continue;
1334                 }
1335                 if (m_model->showDirectoriesOnly() && !entry.isDir()) {
1336                     continue;
1337                 }
1338                 ++entryCount;
1339             }
1340 
1341             QHash<QByteArray, QVariant> newData;
1342             QVariant expandable = data.value("isExpandable");
1343             if (expandable.isNull() || expandable.toBool() != (entryCount > 0)) {
1344                 // if expandable has changed
1345                 newData.insert("isExpandable", entryCount > 0);
1346             }
1347 
1348             if (origCount != entryCount) {
1349                 // count has changed
1350                 newData.insert("count", entryCount);
1351             }
1352 
1353             if (!newData.isEmpty()) {
1354                 disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1355                 m_model->setData(index, newData);
1356                 connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged);
1357             }
1358         });
1359         return;
1360     }
1361 
1362     // Tell m_directoryContentsCounter that we want to count the items
1363     // inside the directory. The result will be received in slotDirectoryContentsCountReceived.
1364     const QString path = item.localPath();
1365     const auto priority = index >= m_firstVisibleIndex && index <= m_lastVisibleIndex ? KDirectoryContentsCounter::PathCountPriority::High
1366                                                                                       : KDirectoryContentsCounter::PathCountPriority::Normal;
1367 
1368     m_directoryContentsCounter->scanDirectory(path, priority);
1369 }
1370 
1371 QHash<QByteArray, QVariant> KFileItemModelRolesUpdater::rolesData(const KFileItem &item, int index)
1372 {
1373     QHash<QByteArray, QVariant> data;
1374 
1375     const bool getSizeRole = m_roles.contains("size");
1376     const bool getIsExpandableRole = m_roles.contains("isExpandable");
1377 
1378     if ((getSizeRole || getIsExpandableRole) && item.isDir()) {
1379         startDirectorySizeCounting(item, index);
1380     }
1381 
1382     if (m_roles.contains("extension")) {
1383         // TODO KF6 use KFileItem::suffix 464722
1384         data.insert("extension", QFileInfo(item.name()).suffix());
1385     }
1386 
1387     if (m_roles.contains("type")) {
1388         data.insert("type", item.mimeComment());
1389     }
1390 
1391     QStringList overlays = item.overlays();
1392     for (KOverlayIconPlugin *it : std::as_const(m_overlayIconsPlugin)) {
1393         overlays.append(it->getOverlays(item.url()));
1394     }
1395     if (!overlays.isEmpty()) {
1396         data.insert("iconOverlays", overlays);
1397     }
1398 
1399 #if HAVE_BALOO
1400     if (m_balooFileMonitor) {
1401         m_balooFileMonitor->addFile(item.localPath());
1402         applyChangedBalooRolesForItem(item);
1403     }
1404 #endif
1405     return data;
1406 }
1407 
1408 void KFileItemModelRolesUpdater::slotOverlaysChanged(const QUrl &url, const QStringList &)
1409 {
1410     const KFileItem item = m_model->fileItem(url);
1411     if (item.isNull()) {
1412         return;
1413     }
1414     const int index = m_model->index(item);
1415     QHash<QByteArray, QVariant> data = m_model->data(index);
1416     QStringList overlays = item.overlays();
1417     for (KOverlayIconPlugin *it : std::as_const(m_overlayIconsPlugin)) {
1418         overlays.append(it->getOverlays(url));
1419     }
1420     data.insert("iconOverlays", overlays);
1421     m_model->setData(index, data);
1422 }
1423 
1424 void KFileItemModelRolesUpdater::updateAllPreviews()
1425 {
1426     if (m_state == Paused) {
1427         m_previewChangedDuringPausing = true;
1428     } else {
1429         m_finishedItems.clear();
1430         startUpdating();
1431     }
1432 }
1433 
1434 void KFileItemModelRolesUpdater::killPreviewJob()
1435 {
1436     if (m_previewJob) {
1437         disconnect(m_previewJob, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotGotPreview);
1438         disconnect(m_previewJob, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotPreviewFailed);
1439         disconnect(m_previewJob, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished);
1440         m_previewJob->kill();
1441         m_previewJob = nullptr;
1442         m_pendingPreviewItems.clear();
1443     }
1444 }
1445 
1446 QList<int> KFileItemModelRolesUpdater::indexesToResolve() const
1447 {
1448     const int count = m_model->count();
1449 
1450     QList<int> result;
1451     result.reserve(qMin(count, (m_lastVisibleIndex - m_firstVisibleIndex + 1) + ResolveAllItemsLimit + (2 * m_maximumVisibleItems)));
1452 
1453     // Add visible items.
1454     // Resolve files first, their previews are quicker.
1455     QList<int> visibleDirs;
1456     for (int i = m_firstVisibleIndex; i <= m_lastVisibleIndex; ++i) {
1457         const KFileItem item = m_model->fileItem(i);
1458         if (item.isDir()) {
1459             visibleDirs.append(i);
1460         } else {
1461             result.append(i);
1462         }
1463     }
1464 
1465     result.append(visibleDirs);
1466 
1467     // We need a reasonable upper limit for number of items to resolve after
1468     // and before the visible range. m_maximumVisibleItems can be quite large
1469     // when using Compact View.
1470     const int readAheadItems = qMin(ReadAheadPages * m_maximumVisibleItems, ResolveAllItemsLimit / 2);
1471 
1472     // Add items after the visible range.
1473     const int endExtendedVisibleRange = qMin(m_lastVisibleIndex + readAheadItems, count - 1);
1474     for (int i = m_lastVisibleIndex + 1; i <= endExtendedVisibleRange; ++i) {
1475         result.append(i);
1476     }
1477 
1478     // Add items before the visible range in reverse order.
1479     const int beginExtendedVisibleRange = qMax(0, m_firstVisibleIndex - readAheadItems);
1480     for (int i = m_firstVisibleIndex - 1; i >= beginExtendedVisibleRange; --i) {
1481         result.append(i);
1482     }
1483 
1484     // Add items on the last page.
1485     const int beginLastPage = qMax(endExtendedVisibleRange + 1, count - m_maximumVisibleItems);
1486     for (int i = beginLastPage; i < count; ++i) {
1487         result.append(i);
1488     }
1489 
1490     // Add items on the first page.
1491     const int endFirstPage = qMin(beginExtendedVisibleRange, m_maximumVisibleItems);
1492     for (int i = 0; i < endFirstPage; ++i) {
1493         result.append(i);
1494     }
1495 
1496     // Continue adding items until ResolveAllItemsLimit is reached.
1497     int remainingItems = ResolveAllItemsLimit - result.count();
1498 
1499     for (int i = endExtendedVisibleRange + 1; i < beginLastPage && remainingItems > 0; ++i) {
1500         result.append(i);
1501         --remainingItems;
1502     }
1503 
1504     for (int i = beginExtendedVisibleRange - 1; i >= endFirstPage && remainingItems > 0; --i) {
1505         result.append(i);
1506         --remainingItems;
1507     }
1508 
1509     return result;
1510 }
1511 
1512 void KFileItemModelRolesUpdater::trimHoverSequenceLoadedItems()
1513 {
1514     static const size_t maxLoadedItems = 20;
1515 
1516     size_t loadedItems = m_hoverSequenceLoadedItems.size();
1517     while (loadedItems > maxLoadedItems) {
1518         const KFileItem item = m_hoverSequenceLoadedItems.front();
1519 
1520         m_hoverSequenceLoadedItems.pop_front();
1521         loadedItems--;
1522 
1523         const int index = m_model->index(item);
1524         if (index >= 0) {
1525             QHash<QByteArray, QVariant> data = m_model->data(index);
1526             data["hoverSequencePixmaps"] = QVariant::fromValue(QVector<QPixmap>() << QPixmap());
1527             m_model->setData(index, data);
1528         }
1529     }
1530 }
1531 
1532 #include "moc_kfileitemmodelrolesupdater.cpp"