X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/bc662543042a7677538ecb95a16f31b460b2b661..cebcf8db:/src/kitemviews/kfileitemmodelrolesupdater.cpp diff --git a/src/kitemviews/kfileitemmodelrolesupdater.cpp b/src/kitemviews/kfileitemmodelrolesupdater.cpp index 5d6bdda9c..ac14ed795 100644 --- a/src/kitemviews/kfileitemmodelrolesupdater.cpp +++ b/src/kitemviews/kfileitemmodelrolesupdater.cpp @@ -1,152 +1,157 @@ -/*************************************************************************** - * Copyright (C) 2011 by Peter Penz * - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - * This program is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU General Public License for more details. * - * * - * You should have received a copy of the GNU General Public License * - * along with this program; if not, write to the * - * Free Software Foundation, Inc., * - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * - ***************************************************************************/ +/* + * SPDX-FileCopyrightText: 2011 Peter Penz + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ #include "kfileitemmodelrolesupdater.h" +#include "dolphindebug.h" #include "kfileitemmodel.h" +#include "private/kdirectorycontentscounter.h" +#include "private/kpixmapmodifier.h" #include #include -#include -#include -#include -#include -#include +#include #include - -#include "private/kpixmapmodifier.h" +#include +#include +#include +#include +#include +#include + +#include "dolphin_contentdisplaysettings.h" + +#if HAVE_BALOO +#include "private/kbaloorolesprovider.h" +#include +#include +#endif #include -#include -#include #include +#include +#include +#include #include +#include -#ifdef HAVE_NEPOMUK - #include "private/knepomukrolesprovider.h" - #include "private/nepomuk/resourcewatcher.h" -#endif - -// Required includes for subItemsCount(): -#ifdef Q_WS_WIN - #include -#else - #include - #include -#endif +using namespace std::chrono_literals; // #define KFILEITEMMODELROLESUPDATER_DEBUG -namespace { - // Maximum time in ms that the KFileItemModelRolesUpdater - // may perform a blocking operation - const int MaxBlockTimeout = 200; +namespace +{ +// Maximum time in ms that the KFileItemModelRolesUpdater +// may perform a blocking operation +const int MaxBlockTimeout = 200; + +// If the number of items is smaller than ResolveAllItemsLimit, +// the roles of all items will be resolved. +const int ResolveAllItemsLimit = 500; - // Maximum number of items that will get resolved synchronously. - // The value should roughly represent the number of maximum visible - // items, as it does not make sense to resolve more items synchronously - // and probably reach the MaxBlockTimeout because of invisible items. - const int MaxResolveItemsCount = 100; +// Not only the visible area, but up to ReadAheadPages before and after +// this area will be resolved. +const int ReadAheadPages = 5; } -KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel* model, QObject* parent) : - QObject(parent), - m_paused(false), - m_previewChangedDuringPausing(false), - m_iconSizeChangedDuringPausing(false), - m_rolesChangedDuringPausing(false), - m_previewShown(false), - m_enlargeSmallPreviews(true), - m_clearPreviews(false), - m_sortingProgress(-1), - m_model(model), - m_iconSize(), - m_firstVisibleIndex(0), - m_lastVisibleIndex(-1), - m_roles(), - m_enabledPlugins(), - m_pendingVisibleItems(), - m_pendingInvisibleItems(), - m_previewJobs(), - m_changedItemsTimer(0), - m_changedItems(), - m_dirWatcher(0), - m_watchedDirs() - #ifdef HAVE_NEPOMUK - , m_nepomukResourceWatcher(0), - m_nepomukUriItems() - #endif +KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel *model, QObject *parent) + : QObject(parent) + , m_state(Idle) + , m_previewChangedDuringPausing(false) + , m_iconSizeChangedDuringPausing(false) + , m_rolesChangedDuringPausing(false) + , m_previewShown(false) + , m_enlargeSmallPreviews(true) + , m_clearPreviews(false) + , m_finishedItems() + , m_model(model) + , m_iconSize() + , m_devicePixelRatio(1.0) + , m_firstVisibleIndex(0) + , m_lastVisibleIndex(-1) + , m_maximumVisibleItems(50) + , m_roles() + , m_resolvableRoles() + , m_enabledPlugins() + , m_localFileSizePreviewLimit(0) + , m_pendingSortRoleItems() + , m_pendingIndexes() + , m_pendingPreviewItems() + , m_previewJob() + , m_hoverSequenceItem() + , m_hoverSequenceIndex(0) + , m_hoverSequencePreviewJob(nullptr) + , m_hoverSequenceNumSuccessiveFailures(0) + , m_recentlyChangedItemsTimer(nullptr) + , m_recentlyChangedItems() + , m_changedItems() + , m_directoryContentsCounter(nullptr) +#if HAVE_BALOO + , m_balooFileMonitor(nullptr) +#endif { Q_ASSERT(model); - const KConfigGroup globalConfig(KGlobal::config(), "PreviewSettings"); - m_enabledPlugins = globalConfig.readEntry("Plugins", QStringList() - << "directorythumbnail" - << "imagethumbnail" - << "jpegthumbnail"); - - connect(m_model, SIGNAL(itemsInserted(KItemRangeList)), - this, SLOT(slotItemsInserted(KItemRangeList))); - connect(m_model, SIGNAL(itemsRemoved(KItemRangeList)), - this, SLOT(slotItemsRemoved(KItemRangeList))); - connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet)), - this, SLOT(slotItemsChanged(KItemRangeList,QSet))); - connect(m_model, SIGNAL(sortRoleChanged(QByteArray,QByteArray)), - this, SLOT(slotSortRoleChanged(QByteArray,QByteArray))); + const KConfigGroup globalConfig(KSharedConfig::openConfig(), QStringLiteral("PreviewSettings")); + m_enabledPlugins = globalConfig.readEntry("Plugins", KIO::PreviewJob::defaultPlugins()); + m_localFileSizePreviewLimit = static_cast(globalConfig.readEntry("MaximumSize", 0)); + + connect(m_model, &KFileItemModel::itemsInserted, this, &KFileItemModelRolesUpdater::slotItemsInserted); + connect(m_model, &KFileItemModel::itemsRemoved, this, &KFileItemModelRolesUpdater::slotItemsRemoved); + connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); + connect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved); + connect(m_model, &KFileItemModel::sortRoleChanged, this, &KFileItemModelRolesUpdater::slotSortRoleChanged); // Use a timer to prevent that each call of slotItemsChanged() results in a synchronous - // resolving of the roles. Postpone the resolving until no update has been done for 1 second. - m_changedItemsTimer = new QTimer(this); - m_changedItemsTimer->setInterval(1000); - m_changedItemsTimer->setSingleShot(true); - connect(m_changedItemsTimer, SIGNAL(timeout()), this, SLOT(resolveChangedItems())); + // resolving of the roles. Postpone the resolving until no update has been done for 100 ms. + m_recentlyChangedItemsTimer = new QTimer(this); + m_recentlyChangedItemsTimer->setInterval(100ms); + m_recentlyChangedItemsTimer->setSingleShot(true); + connect(m_recentlyChangedItemsTimer, &QTimer::timeout, this, &KFileItemModelRolesUpdater::resolveRecentlyChangedItems); m_resolvableRoles.insert("size"); m_resolvableRoles.insert("type"); m_resolvableRoles.insert("isExpandable"); -#ifdef HAVE_NEPOMUK - m_resolvableRoles += KNepomukRolesProvider::instance().roles(); +#if HAVE_BALOO + m_resolvableRoles += KBalooRolesProvider::instance().roles(); #endif - // When folders are expandable or the item-count is shown for folders, it is necessary - // to watch the number of items of the sub-folder to be able to react on changes. - m_dirWatcher = new KDirWatch(this); - connect(m_dirWatcher, SIGNAL(dirty(QString)), this, SLOT(slotDirWatchDirty(QString))); + m_directoryContentsCounter = new KDirectoryContentsCounter(m_model, this); + connect(m_directoryContentsCounter, &KDirectoryContentsCounter::result, this, &KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived); + + const auto plugins = KPluginMetaData::findPlugins(QStringLiteral("kf6/overlayicon"), {}, KPluginMetaData::AllowEmptyMetaData); + for (const KPluginMetaData &data : plugins) { + auto instance = QPluginLoader(data.fileName()).instance(); + auto plugin = qobject_cast(instance); + if (plugin) { + m_overlayIconsPlugin.append(plugin); + connect(plugin, &KOverlayIconPlugin::overlaysChanged, this, &KFileItemModelRolesUpdater::slotOverlaysChanged); + } else { + // not our/valid plugin, so delete the created object + delete instance; + } + } } KFileItemModelRolesUpdater::~KFileItemModelRolesUpdater() { - resetPendingRoles(); + killPreviewJob(); } -void KFileItemModelRolesUpdater::setIconSize(const QSize& size) +void KFileItemModelRolesUpdater::setIconSize(const QSize &size) { if (size != m_iconSize) { m_iconSize = size; - if (m_paused) { + if (m_state == Paused) { m_iconSizeChangedDuringPausing = true; } else if (m_previewShown) { // An icon size change requires the regenerating of // all previews - sortAndResolveAllRoles(); - } else { - sortAndResolvePendingRoles(); + m_finishedItems.clear(); + startUpdating(); } } } @@ -156,6 +161,25 @@ QSize KFileItemModelRolesUpdater::iconSize() const return m_iconSize; } +void KFileItemModelRolesUpdater::setDevicePixelRatio(qreal devicePixelRatio) +{ + if (m_devicePixelRatio != devicePixelRatio) { + m_devicePixelRatio = devicePixelRatio; + if (m_state == Paused) { + m_iconSizeChangedDuringPausing = true; + } else if (m_previewShown) { + // A dpr change requires the regenerating of all previews. + m_finishedItems.clear(); + startUpdating(); + } + } +} + +qreal KFileItemModelRolesUpdater::devicePixelRatio() const +{ + return m_devicePixelRatio; +} + void KFileItemModelRolesUpdater::setVisibleIndexRange(int index, int count) { if (index < 0) { @@ -173,9 +197,12 @@ void KFileItemModelRolesUpdater::setVisibleIndexRange(int index, int count) m_firstVisibleIndex = index; m_lastVisibleIndex = qMin(index + count - 1, m_model->count() - 1); - if (hasPendingRoles() && !m_paused) { - sortAndResolvePendingRoles(); - } + startUpdating(); +} + +void KFileItemModelRolesUpdater::setMaximumVisibleItems(int count) +{ + m_maximumVisibleItems = count; } void KFileItemModelRolesUpdater::setPreviewsShown(bool show) @@ -212,7 +239,7 @@ bool KFileItemModelRolesUpdater::enlargeSmallPreviews() const return m_enlargeSmallPreviews; } -void KFileItemModelRolesUpdater::setEnabledPlugins(const QStringList& list) +void KFileItemModelRolesUpdater::setEnabledPlugins(const QStringList &list) { if (m_enabledPlugins != list) { m_enabledPlugins = list; @@ -224,80 +251,69 @@ void KFileItemModelRolesUpdater::setEnabledPlugins(const QStringList& list) void KFileItemModelRolesUpdater::setPaused(bool paused) { - if (paused == m_paused) { + if (paused == (m_state == Paused)) { return; } - m_paused = paused; if (paused) { - if (hasPendingRoles()) { - foreach (KJob* job, m_previewJobs) { - job->kill(); - } - Q_ASSERT(m_previewJobs.isEmpty()); - } + m_state = Paused; + killPreviewJob(); } else { - const bool resolveAll = (m_iconSizeChangedDuringPausing && m_previewShown) || - m_previewChangedDuringPausing || - m_rolesChangedDuringPausing; + const bool updatePreviews = (m_iconSizeChangedDuringPausing && m_previewShown) || m_previewChangedDuringPausing; + const bool resolveAll = updatePreviews || m_rolesChangedDuringPausing; if (resolveAll) { - sortAndResolveAllRoles(); - } else { - sortAndResolvePendingRoles(); + m_finishedItems.clear(); } m_iconSizeChangedDuringPausing = false; m_previewChangedDuringPausing = false; m_rolesChangedDuringPausing = false; + + if (!m_pendingSortRoleItems.isEmpty()) { + m_state = ResolvingSortRole; + resolveNextSortRole(); + } else { + m_state = Idle; + } + + startUpdating(); } } -void KFileItemModelRolesUpdater::setRoles(const QSet& roles) +void KFileItemModelRolesUpdater::setRoles(const QSet &roles) { if (m_roles != roles) { m_roles = roles; -#ifdef HAVE_NEPOMUK +#if HAVE_BALOO // Check whether there is at least one role that must be resolved - // with the help of Nepomuk. If this is the case, a (quite expensive) + // with the help of Baloo. If this is the case, a (quite expensive) // resolving will be done in KFileItemModelRolesUpdater::rolesData() and // the role gets watched for changes. - const KNepomukRolesProvider& rolesProvider = KNepomukRolesProvider::instance(); - bool hasNepomukRole = false; + const KBalooRolesProvider &rolesProvider = KBalooRolesProvider::instance(); + bool hasBalooRole = false; QSetIterator it(roles); while (it.hasNext()) { - const QByteArray& role = it.next(); + const QByteArray &role = it.next(); if (rolesProvider.roles().contains(role)) { - hasNepomukRole = true; + hasBalooRole = true; break; } } - if (hasNepomukRole && !m_nepomukResourceWatcher) { - Q_ASSERT(m_nepomukUriItems.isEmpty()); - - m_nepomukResourceWatcher = new Nepomuk::ResourceWatcher(this); - connect(m_nepomukResourceWatcher, SIGNAL(propertyChanged(Nepomuk::Resource,Nepomuk::Types::Property,QVariantList,QVariantList)), - this, SLOT(applyChangedNepomukRoles(Nepomuk::Resource))); - connect(m_nepomukResourceWatcher, SIGNAL(propertyRemoved(Nepomuk::Resource,Nepomuk::Types::Property,QVariant)), - this, SLOT(applyChangedNepomukRoles(Nepomuk::Resource))); - connect(m_nepomukResourceWatcher, SIGNAL(propertyAdded(Nepomuk::Resource,Nepomuk::Types::Property,QVariant)), - this, SLOT(applyChangedNepomukRoles(Nepomuk::Resource))); - connect(m_nepomukResourceWatcher, SIGNAL(resourceCreated(Nepomuk::Resource,QList)), - this, SLOT(applyChangedNepomukRoles(Nepomuk::Resource))); - } else if (!hasNepomukRole && m_nepomukResourceWatcher) { - delete m_nepomukResourceWatcher; - m_nepomukResourceWatcher = 0; - m_nepomukUriItems.clear(); + if (hasBalooRole && m_balooConfig.fileIndexingEnabled() && !m_balooFileMonitor) { + m_balooFileMonitor = new Baloo::FileMonitor(this); + connect(m_balooFileMonitor, &Baloo::FileMonitor::fileMetaDataChanged, this, &KFileItemModelRolesUpdater::applyChangedBalooRoles); + } else if (!hasBalooRole && m_balooFileMonitor) { + delete m_balooFileMonitor; + m_balooFileMonitor = nullptr; } #endif - updateSortProgress(); - - if (m_paused) { + if (m_state == Paused) { m_rolesChangedDuringPausing = true; } else { - sortAndResolveAllRoles(); + startUpdating(); } } } @@ -309,7 +325,7 @@ QSet KFileItemModelRolesUpdater::roles() const bool KFileItemModelRolesUpdater::isPaused() const { - return m_paused; + return m_state == Paused; } QStringList KFileItemModelRolesUpdater::enabledPlugins() const @@ -317,379 +333,646 @@ QStringList KFileItemModelRolesUpdater::enabledPlugins() const return m_enabledPlugins; } -void KFileItemModelRolesUpdater::slotItemsInserted(const KItemRangeList& itemRanges) +void KFileItemModelRolesUpdater::setLocalFileSizePreviewLimit(const qlonglong size) { - startUpdating(itemRanges); + m_localFileSizePreviewLimit = size; } -void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList& itemRanges) +qlonglong KFileItemModelRolesUpdater::localFileSizePreviewLimit() const { - Q_UNUSED(itemRanges); + return m_localFileSizePreviewLimit; +} - const bool allItemsRemoved = (m_model->count() == 0); +void KFileItemModelRolesUpdater::setHoverSequenceState(const QUrl &itemUrl, int seqIdx) +{ + const KFileItem item = m_model->fileItem(itemUrl); - if (!m_watchedDirs.isEmpty()) { - // Don't let KDirWatch watch for removed items - if (allItemsRemoved) { - foreach (const QString& path, m_watchedDirs) { - m_dirWatcher->removeDir(path); - } - m_watchedDirs.clear(); - } else { - QMutableSetIterator it(m_watchedDirs); - while (it.hasNext()) { - const QString& path = it.next(); - if (m_model->index(KUrl(path)) < 0) { - m_dirWatcher->removeDir(path); - it.remove(); + if (item != m_hoverSequenceItem) { + killHoverSequencePreviewJob(); + } + + m_hoverSequenceItem = item; + m_hoverSequenceIndex = seqIdx; + + if (!m_previewShown) { + return; + } + + m_hoverSequenceNumSuccessiveFailures = 0; + + loadNextHoverSequencePreview(); +} + +void KFileItemModelRolesUpdater::slotItemsInserted(const KItemRangeList &itemRanges) +{ + QElapsedTimer timer; + timer.start(); + + // Determine the sort role synchronously for as many items as possible. + if (m_resolvableRoles.contains(m_model->sortRole())) { + int insertedCount = 0; + for (const KItemRange &range : itemRanges) { + const int lastIndex = insertedCount + range.index + range.count - 1; + for (int i = insertedCount + range.index; i <= lastIndex; ++i) { + if (timer.elapsed() < MaxBlockTimeout) { + applySortRole(i); + } else { + m_pendingSortRoleItems.insert(m_model->fileItem(i)); } } + insertedCount += range.count; + } + + applySortProgressToModel(); + + // If there are still items whose sort role is unknown, check if the + // asynchronous determination of the sort role is already in progress, + // and start it if that is not the case. + if (!m_pendingSortRoleItems.isEmpty() && m_state != ResolvingSortRole) { + killPreviewJob(); + m_state = ResolvingSortRole; + resolveNextSortRole(); } } -#ifdef HAVE_NEPOMUK - if (m_nepomukResourceWatcher) { - // Don't let the ResourceWatcher watch for removed items + startUpdating(); +} + +void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList &itemRanges) +{ + Q_UNUSED(itemRanges) + + const bool allItemsRemoved = (m_model->count() == 0); + +#if HAVE_BALOO + if (m_balooFileMonitor) { + // Don't let the FileWatcher watch for removed items if (allItemsRemoved) { - m_nepomukResourceWatcher->setResources(QList()); - m_nepomukResourceWatcher->stop(); - m_nepomukUriItems.clear(); + m_balooFileMonitor->clear(); } else { - QList newResources; - const QList oldResources = m_nepomukResourceWatcher->resources(); - foreach (const Nepomuk::Resource& resource, oldResources) { - const QUrl uri = resource.resourceUri(); - const KUrl itemUrl = m_nepomukUriItems.value(uri); - if (m_model->index(itemUrl) >= 0) { - newResources.append(resource); - } else { - m_nepomukUriItems.remove(uri); + QStringList newFileList; + const QStringList oldFileList = m_balooFileMonitor->files(); + for (const QString &file : oldFileList) { + if (m_model->index(QUrl::fromLocalFile(file)) >= 0) { + newFileList.append(file); } } - m_nepomukResourceWatcher->setResources(newResources); - if (newResources.isEmpty()) { - Q_ASSERT(m_nepomukUriItems.isEmpty()); - m_nepomukResourceWatcher->stop(); - } + m_balooFileMonitor->setFiles(newFileList); } } #endif - m_firstVisibleIndex = 0; - m_lastVisibleIndex = -1; - if (!hasPendingRoles()) { - return; - } - if (allItemsRemoved) { - // Most probably a directory change is done. Clear all pending items - // and also kill all ongoing preview-jobs. - resetPendingRoles(); - + m_state = Idle; + + m_finishedItems.clear(); + m_pendingSortRoleItems.clear(); + m_pendingIndexes.clear(); + m_pendingPreviewItems.clear(); + m_recentlyChangedItems.clear(); + m_recentlyChangedItemsTimer->stop(); m_changedItems.clear(); - m_changedItemsTimer->stop(); + m_hoverSequenceLoadedItems.clear(); + + killPreviewJob(); + if (!m_model->showDirectoriesOnly()) { + m_directoryContentsCounter->stopWorker(); + } } else { - // Remove all items from m_pendingVisibleItems and m_pendingInvisibleItems - // that are not part of the model anymore. The items from m_changedItems - // don't need to be handled here, removed items are just skipped in - // resolveChangedItems(). - for (int i = 0; i <= 1; ++i) { - QSet& pendingItems = (i == 0) ? m_pendingVisibleItems : m_pendingInvisibleItems; - QMutableSetIterator it(pendingItems); - while (it.hasNext()) { - const KFileItem item = it.next(); - if (m_model->index(item) < 0) { - pendingItems.remove(item); - } + // Only remove the items from m_finishedItems. They will be removed + // from the other sets later on. + QSet::iterator it = m_finishedItems.begin(); + while (it != m_finishedItems.end()) { + if (m_model->index(*it) < 0) { + it = m_finishedItems.erase(it); + } else { + ++it; } } - } -} -void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList& itemRanges, - const QSet& roles) -{ - Q_UNUSED(roles); - - if (m_changedItemsTimer->isActive()) { - // A call of slotItemsChanged() has been done recently. Postpone the resolving - // of the roles until the timer has exceeded. - foreach (const KItemRange& itemRange, itemRanges) { + // Removed items won't have hover previews loaded anymore. + for (const KItemRange &itemRange : itemRanges) { int index = itemRange.index; for (int count = itemRange.count; count > 0; --count) { - m_changedItems.insert(m_model->fileItem(index)); + const KFileItem item = m_model->fileItem(index); + m_hoverSequenceLoadedItems.remove(item); ++index; } } - } else { - // No call of slotItemsChanged() has been done recently, resolve the roles now. - startUpdating(itemRanges); + + // The visible items might have changed. + startUpdating(); } - m_changedItemsTimer->start(); } -void KFileItemModelRolesUpdater::slotSortRoleChanged(const QByteArray& current, - const QByteArray& previous) +void KFileItemModelRolesUpdater::slotItemsMoved(KItemRange itemRange, const QList &movedToIndexes) { - Q_UNUSED(current); - Q_UNUSED(previous); - updateSortProgress(); + Q_UNUSED(itemRange) + Q_UNUSED(movedToIndexes) + + // The visible items might have changed. + startUpdating(); } -void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem& item, const QPixmap& pixmap) +void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList &itemRanges, const QSet &roles) { - m_pendingVisibleItems.remove(item); - m_pendingInvisibleItems.remove(item); + Q_UNUSED(roles) - const int index = m_model->index(item); - if (index < 0) { - return; + // Find out if slotItemsChanged() has been done recently. If that is the + // case, resolving the roles is postponed until a timer has exceeded + // to prevent expensive repeated updates if files are updated frequently. + const bool itemsChangedRecently = m_recentlyChangedItemsTimer->isActive(); + + QSet &targetSet = itemsChangedRecently ? m_recentlyChangedItems : m_changedItems; + + for (const KItemRange &itemRange : itemRanges) { + int index = itemRange.index; + for (int count = itemRange.count; count > 0; --count) { + const KFileItem item = m_model->fileItem(index); + targetSet.insert(item); + ++index; + } } - QPixmap scaledPixmap = pixmap; + m_recentlyChangedItemsTimer->start(); - const QString mimeType = item.mimetype(); - const int slashIndex = mimeType.indexOf(QLatin1Char('/')); - const QString mimeTypeGroup = mimeType.left(slashIndex); - if (mimeTypeGroup == QLatin1String("image")) { - if (m_enlargeSmallPreviews) { - KPixmapModifier::applyFrame(scaledPixmap, m_iconSize); - } else { - // Assure that small previews don't get enlarged. Instead they - // should be shown centered within the frame. - const QSize contentSize = KPixmapModifier::sizeInsideFrame(m_iconSize); - const bool enlargingRequired = scaledPixmap.width() < contentSize.width() && - scaledPixmap.height() < contentSize.height(); - if (enlargingRequired) { - QSize frameSize = scaledPixmap.size(); - frameSize.scale(m_iconSize, Qt::KeepAspectRatio); + if (!itemsChangedRecently) { + updateChangedItems(); + } +} - QPixmap largeFrame(frameSize); - largeFrame.fill(Qt::transparent); +void KFileItemModelRolesUpdater::slotSortRoleChanged(const QByteArray ¤t, const QByteArray &previous) +{ + Q_UNUSED(current) + Q_UNUSED(previous) - KPixmapModifier::applyFrame(largeFrame, frameSize); + if (m_resolvableRoles.contains(current)) { + m_pendingSortRoleItems.clear(); + m_finishedItems.clear(); - QPainter painter(&largeFrame); - painter.drawPixmap((largeFrame.width() - scaledPixmap.width()) / 2, - (largeFrame.height() - scaledPixmap.height()) / 2, - scaledPixmap); - scaledPixmap = largeFrame; + const int count = m_model->count(); + QElapsedTimer timer; + timer.start(); + + // Determine the sort role synchronously for as many items as possible. + for (int index = 0; index < count; ++index) { + if (timer.elapsed() < MaxBlockTimeout) { + applySortRole(index); } else { - // The image must be shrinked as it is too large to fit into - // the available icon size - KPixmapModifier::applyFrame(scaledPixmap, m_iconSize); + m_pendingSortRoleItems.insert(m_model->fileItem(index)); } } + + applySortProgressToModel(); + + if (!m_pendingSortRoleItems.isEmpty()) { + // Trigger the asynchronous determination of the sort role. + killPreviewJob(); + m_state = ResolvingSortRole; + resolveNextSortRole(); + } } else { - KPixmapModifier::scale(scaledPixmap, m_iconSize); + m_state = Idle; + m_pendingSortRoleItems.clear(); + applySortProgressToModel(); + } +} + +void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem &item, const QPixmap &pixmap) +{ + if (m_state != PreviewJobRunning) { + return; + } + + m_changedItems.remove(item); + + const int index = m_model->index(item); + if (index < 0) { + return; + } + + QPixmap scaledPixmap = transformPreviewPixmap(pixmap); + + QHash data = rolesData(item, index); + + const QStringList overlays = data["iconOverlays"].toStringList(); + // Strangely KFileItem::overlays() returns empty string-values, so + // we need to check first whether an overlay must be drawn at all. + if (!scaledPixmap.isNull()) { + for (const QString &overlay : overlays) { + if (!overlay.isEmpty()) { + // There is at least one overlay, draw all overlays above m_pixmap + // and cancel the check + const QSize size = scaledPixmap.size(); + scaledPixmap = KIconUtils::addOverlays(scaledPixmap, overlays).pixmap(size); + break; + } + } } - QHash data = rolesData(item); data.insert("iconPixmap", scaledPixmap); - disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet)), - this, SLOT(slotItemsChanged(KItemRangeList,QSet))); + disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); m_model->setData(index, data); - connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet)), - this, SLOT(slotItemsChanged(KItemRangeList,QSet))); + connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); - applySortProgressToModel(); + m_finishedItems.insert(item); } -void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem& item) +void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem &item) { - m_pendingVisibleItems.remove(item); - m_pendingInvisibleItems.remove(item); + if (m_state != PreviewJobRunning) { + return; + } + + m_changedItems.remove(item); + + const int index = m_model->index(item); + if (index >= 0) { + QHash data; + data.insert("iconPixmap", QPixmap()); - const bool clearPreviews = m_clearPreviews; - m_clearPreviews = true; - applyResolvedRoles(item, ResolveAll); - m_clearPreviews = clearPreviews; + disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); + m_model->setData(index, data); + connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); - applySortProgressToModel(); + applyResolvedRoles(index, ResolveAll); + m_finishedItems.insert(item); + } } -void KFileItemModelRolesUpdater::slotPreviewJobFinished(KJob* job) +void KFileItemModelRolesUpdater::slotPreviewJobFinished() { -#ifdef KFILEITEMMODELROLESUPDATER_DEBUG - kDebug() << "Preview job finished. Pending visible:" << m_pendingVisibleItems.count() << "invisible:" << m_pendingInvisibleItems.count(); -#endif + m_previewJob = nullptr; - m_previewJobs.removeOne(job); - if (!m_previewJobs.isEmpty() || !hasPendingRoles()) { + if (m_state != PreviewJobRunning) { return; } - const KFileItemList visibleItems = sortedItems(m_pendingVisibleItems); - startPreviewJob(visibleItems + m_pendingInvisibleItems.toList()); + m_state = Idle; + + if (!m_pendingPreviewItems.isEmpty()) { + startPreviewJob(); + } else { + if (!m_changedItems.isEmpty()) { + updateChangedItems(); + } + } } -void KFileItemModelRolesUpdater::resolveNextPendingRoles() +void KFileItemModelRolesUpdater::slotHoverSequenceGotPreview(const KFileItem &item, const QPixmap &pixmap) { - if (m_paused) { + const int index = m_model->index(item); + if (index < 0) { return; } - if (m_previewShown) { - // The preview has been turned on since the last run. Skip - // resolving further pending roles as this is done as soon - // as a preview has been received. - return; + QHash data = m_model->data(index); + QVector pixmaps = data["hoverSequencePixmaps"].value>(); + const int loadedIndex = pixmaps.size(); + + float wap = m_hoverSequencePreviewJob->sequenceIndexWraparoundPoint(); + if (!m_hoverSequencePreviewJob->handlesSequences()) { + wap = 1.0f; + } + if (wap >= 0.0f) { + data["hoverSequenceWraparoundPoint"] = wap; + m_model->setData(index, data); } - int resolvedCount = 0; - bool changed = false; - for (int i = 0; i <= 1; ++i) { - QSet& pendingItems = (i == 0) ? m_pendingVisibleItems : m_pendingInvisibleItems; - QSet::iterator it = pendingItems.begin(); - while (it != pendingItems.end() && !changed && resolvedCount < MaxResolveItemsCount) { - changed = applyResolvedRoles(*it, ResolveAll); - it = pendingItems.erase(it); - ++resolvedCount; + // For hover sequence previews we never load index 0, because that's just the regular preview + // in "iconPixmap". But that means we'll load index 1 even for thumbnailers that don't support + // sequences, in which case we can just throw away the preview because it's the same as for + // index 0. Unfortunately we can't find it out earlier :( + if (wap < 0.0f || loadedIndex < static_cast(wap)) { + // Add the preview to the model data + + const QPixmap scaledPixmap = transformPreviewPixmap(pixmap); + + pixmaps.append(scaledPixmap); + data["hoverSequencePixmaps"] = QVariant::fromValue(pixmaps); + + m_model->setData(index, data); + + const auto loadedIt = std::find(m_hoverSequenceLoadedItems.begin(), m_hoverSequenceLoadedItems.end(), item); + if (loadedIt == m_hoverSequenceLoadedItems.end()) { + m_hoverSequenceLoadedItems.push_back(item); + trimHoverSequenceLoadedItems(); } } - if (hasPendingRoles()) { - QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles())); - } else { - m_clearPreviews = false; + m_hoverSequenceNumSuccessiveFailures = 0; +} + +void KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed(const KFileItem &item) +{ + const int index = m_model->index(item); + if (index < 0) { + return; } - applySortProgressToModel(); + static const int numRetries = 2; + + QHash data = m_model->data(index); + QVector pixmaps = data["hoverSequencePixmaps"].value>(); -#ifdef KFILEITEMMODELROLESUPDATER_DEBUG - static int callCount = 0; - ++callCount; - if (callCount % 100 == 0) { - kDebug() << "Remaining visible roles to resolve:" << m_pendingVisibleItems.count() - << "invisible:" << m_pendingInvisibleItems.count(); + qCDebug(DolphinDebug).nospace() << "Failed to generate hover sequence preview #" << pixmaps.size() << " for file " << item.url().toString() << " (attempt " + << (m_hoverSequenceNumSuccessiveFailures + 1) << "/" << (numRetries + 1) << ")"; + + if (m_hoverSequenceNumSuccessiveFailures >= numRetries) { + // Give up and simply duplicate the previous sequence image (if any) + + pixmaps.append(pixmaps.empty() ? QPixmap() : pixmaps.last()); + data["hoverSequencePixmaps"] = QVariant::fromValue(pixmaps); + + if (!data.contains("hoverSequenceWraparoundPoint")) { + // hoverSequenceWraparoundPoint is only available when PreviewJob succeeds, so unless + // it has previously succeeded, it's best to assume that it just doesn't handle + // sequences instead of trying to load the next image indefinitely. + data["hoverSequenceWraparoundPoint"] = 1.0f; + } + + m_model->setData(index, data); + + m_hoverSequenceNumSuccessiveFailures = 0; + } else { + // Retry + + m_hoverSequenceNumSuccessiveFailures++; } -#endif + + // Next image in the sequence (or same one if the retry limit wasn't reached yet) will be + // loaded automatically, because slotHoverSequencePreviewJobFinished() will be triggered + // even when PreviewJob fails. } -void KFileItemModelRolesUpdater::resolveChangedItems() +void KFileItemModelRolesUpdater::slotHoverSequencePreviewJobFinished() { - if (m_changedItems.isEmpty()) { + const int index = m_model->index(m_hoverSequenceItem); + if (index < 0) { + m_hoverSequencePreviewJob = nullptr; return; } - KItemRangeList itemRanges; + // Since a PreviewJob can only have one associated sequence index, we can only generate + // one sequence image per job, so we have to start another one for the next index. - QSetIterator it(m_changedItems); - while (it.hasNext()) { - const KFileItem& item = it.next(); + // Load the next image in the sequence + m_hoverSequencePreviewJob = nullptr; + loadNextHoverSequencePreview(); +} + +void KFileItemModelRolesUpdater::resolveNextSortRole() +{ + if (m_state != ResolvingSortRole) { + return; + } + + QSet::iterator it = m_pendingSortRoleItems.begin(); + while (it != m_pendingSortRoleItems.end()) { + const KFileItem item = *it; const int index = m_model->index(item); - if (index >= 0) { - itemRanges.append(KItemRange(index, 1)); + + // Continue if the sort role has already been determined for the + // item, and the item has not been changed recently. + if (!m_changedItems.contains(item) && m_model->data(index).contains(m_model->sortRole())) { + it = m_pendingSortRoleItems.erase(it); + continue; } + + applySortRole(index); + m_pendingSortRoleItems.erase(it); + break; } - m_changedItems.clear(); - startUpdating(itemRanges); + if (!m_pendingSortRoleItems.isEmpty()) { + applySortProgressToModel(); + QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextSortRole); + } else { + m_state = Idle; + + // Prevent that we try to update the items twice. + disconnect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved); + applySortProgressToModel(); + connect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved); + startUpdating(); + } } -void KFileItemModelRolesUpdater::applyChangedNepomukRoles(const Nepomuk::Resource& resource) +void KFileItemModelRolesUpdater::resolveNextPendingRoles() { -#ifdef HAVE_NEPOMUK - const KUrl itemUrl = m_nepomukUriItems.value(resource.resourceUri()); - const KFileItem item = m_model->fileItem(itemUrl); + if (m_state != ResolvingAllRoles) { + return; + } + + while (!m_pendingIndexes.isEmpty()) { + const int index = m_pendingIndexes.takeFirst(); + const KFileItem item = m_model->fileItem(index); + + if (m_finishedItems.contains(item)) { + continue; + } + + applyResolvedRoles(index, ResolveAll); + m_finishedItems.insert(item); + m_changedItems.remove(item); + break; + } + + if (!m_pendingIndexes.isEmpty()) { + QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles); + } else { + m_state = Idle; + + if (m_clearPreviews) { + // Only go through the list if there are items which might still have previews. + if (m_finishedItems.count() != m_model->count()) { + QHash data; + data.insert("iconPixmap", QPixmap()); + data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector())); + + disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); + for (int index = 0; index <= m_model->count(); ++index) { + if (m_model->data(index).contains("iconPixmap") || m_model->data(index).contains("hoverSequencePixmaps")) { + m_model->setData(index, data); + } + } + connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); + } + m_clearPreviews = false; + } + + if (!m_changedItems.isEmpty()) { + updateChangedItems(); + } + } +} + +void KFileItemModelRolesUpdater::resolveRecentlyChangedItems() +{ + m_changedItems += m_recentlyChangedItems; + m_recentlyChangedItems.clear(); + updateChangedItems(); +} + +void KFileItemModelRolesUpdater::applyChangedBalooRoles(const QString &file) +{ +#if HAVE_BALOO + const KFileItem item = m_model->fileItem(QUrl::fromLocalFile(file)); if (item.isNull()) { // itemUrl is not in the model anymore, probably because // the corresponding file has been deleted in the meantime. return; } + applyChangedBalooRolesForItem(item); +#else + Q_UNUSED(file) +#endif +} + +void KFileItemModelRolesUpdater::applyChangedBalooRolesForItem(const KFileItem &item) +{ +#if HAVE_BALOO + Baloo::File file(item.localPath()); + file.load(); + + const KBalooRolesProvider &rolesProvider = KBalooRolesProvider::instance(); + QHash data; - QHash data = rolesData(item); + const auto roles = rolesProvider.roles(); + for (const QByteArray &role : roles) { + // Overwrite all the role values with an empty QVariant, because the roles + // provider doesn't overwrite it when the property value list is empty. + // See bug 322348 + data.insert(role, QVariant()); + } - const KNepomukRolesProvider& rolesProvider = KNepomukRolesProvider::instance(); - QHashIterator it(rolesProvider.roleValues(resource, m_roles)); + QHashIterator it(rolesProvider.roleValues(file, m_roles)); while (it.hasNext()) { it.next(); data.insert(it.key(), it.value()); } - disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet)), - this, SLOT(slotItemsChanged(KItemRangeList,QSet))); + disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); const int index = m_model->index(item); m_model->setData(index, data); - connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet)), - this, SLOT(slotItemsChanged(KItemRangeList,QSet))); + connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); #else #ifndef Q_CC_MSVC - Q_UNUSED(resource); + Q_UNUSED(item) #endif #endif } -void KFileItemModelRolesUpdater::slotDirWatchDirty(const QString& path) +void KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived(const QString &path, int count, long long size) { - const bool getSizeRole = m_roles.contains("size"); const bool getIsExpandableRole = m_roles.contains("isExpandable"); + const bool getSizeRole = m_roles.contains("size"); if (getSizeRole || getIsExpandableRole) { - const int index = m_model->index(KUrl(path)); + const int index = m_model->index(QUrl::fromLocalFile(path)); if (index >= 0) { QHash data; - const int count = subItemsCount(path); if (getSizeRole) { - data.insert("size", count); + data.insert("count", count); + data.insert("size", QVariant::fromValue(size)); } if (getIsExpandableRole) { data.insert("isExpandable", count > 0); } + disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); m_model->setData(index, data); + connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); } } } -void KFileItemModelRolesUpdater::startUpdating(const KItemRangeList& itemRanges) +void KFileItemModelRolesUpdater::startUpdating() { - // If no valid index range is given assume that all items are visible. - // A cleanup will be done later as soon as the index range has been set. - const bool hasValidIndexRange = (m_lastVisibleIndex >= 0); - - if (hasValidIndexRange) { - // Move all current pending visible items that are not visible anymore - // to the pending invisible items. - QSet::iterator it = m_pendingVisibleItems.begin(); - while (it != m_pendingVisibleItems.end()) { - const KFileItem item = *it; - const int index = m_model->index(item); - if (index < m_firstVisibleIndex || index > m_lastVisibleIndex) { - it = m_pendingVisibleItems.erase(it); - m_pendingInvisibleItems.insert(item); - } else { - ++it; - } - } + if (m_state == Paused) { + return; } - int rangesCount = 0; + if (m_finishedItems.count() == m_model->count()) { + // All roles have been resolved already. + m_state = Idle; + return; + } - foreach (const KItemRange& range, itemRanges) { - rangesCount += range.count; + // Terminate all updates that are currently active. + killPreviewJob(); + m_pendingIndexes.clear(); - // Add the inserted items to the pending visible and invisible items - const int lastIndex = range.index + range.count - 1; - for (int i = range.index; i <= lastIndex; ++i) { - const KFileItem item = m_model->fileItem(i); - if (!hasValidIndexRange || (i >= m_firstVisibleIndex && i <= m_lastVisibleIndex)) { - m_pendingVisibleItems.insert(item); - } else { - m_pendingInvisibleItems.insert(item); + QElapsedTimer timer; + timer.start(); + + // Determine the icons for the visible items synchronously. + updateVisibleIcons(); + + // A detailed update of the items in and near the visible area + // only makes sense if sorting is finished. + if (m_state == ResolvingSortRole) { + return; + } + + // Start the preview job or the asynchronous resolving of all roles. + QList indexes = indexesToResolve(); + + if (m_previewShown) { + m_pendingPreviewItems.clear(); + m_pendingPreviewItems.reserve(indexes.count()); + + for (int index : std::as_const(indexes)) { + const KFileItem item = m_model->fileItem(index); + if (!m_finishedItems.contains(item)) { + m_pendingPreviewItems.append(item); } } + + startPreviewJob(); + } else { + m_pendingIndexes = indexes; + // Trigger the asynchronous resolving of all roles. + m_state = ResolvingAllRoles; + QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles); + } +} + +void KFileItemModelRolesUpdater::updateVisibleIcons() +{ + int lastVisibleIndex = m_lastVisibleIndex; + if (lastVisibleIndex <= 0) { + // Guess a reasonable value for the last visible index if the view + // has not told us about the real value yet. + lastVisibleIndex = qMin(m_firstVisibleIndex + m_maximumVisibleItems, m_model->count() - 1); + if (lastVisibleIndex <= 0) { + lastVisibleIndex = qMin(200, m_model->count() - 1); + } + } + + QElapsedTimer timer; + timer.start(); + + // Try to determine the final icons for all visible items. + int index; + for (index = m_firstVisibleIndex; index <= lastVisibleIndex && timer.elapsed() < MaxBlockTimeout; ++index) { + applyResolvedRoles(index, ResolveFast); } - resolvePendingRoles(); + // KFileItemListView::initializeItemListWidget(KItemListWidget*) will load + // preliminary icons (i.e., without mime type determination) for the + // remaining items. } -void KFileItemModelRolesUpdater::startPreviewJob(const KFileItemList& items) +void KFileItemModelRolesUpdater::startPreviewJob() { - if (items.isEmpty() || m_paused) { + m_state = PreviewJobRunning; + + if (m_pendingPreviewItems.isEmpty()) { + QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished); return; } @@ -698,344 +981,393 @@ void KFileItemModelRolesUpdater::startPreviewJob(const KFileItemList& items) // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must // do a downscaling anyhow because of the frame, so in this case only the provided // cache sizes are requested. - const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128) - ? QSize(256, 256) : QSize(128, 128); + const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128) ? QSize(256, 256) : QSize(128, 128); // KIO::filePreview() will request the MIME-type of all passed items, which (in the // worst case) might block the application for several seconds. To prevent such - // a blocking the MIME-type of the items will determined until the MaxBlockTimeout - // has been reached and only those items will get passed. As soon as the MIME-type - // has been resolved once KIO::PreviewJob() can already access the resolved - // MIME-type in a fast way. - QElapsedTimer timer; - timer.start(); - + // a blocking, we only pass items with known mime type to the preview job. + const int count = m_pendingPreviewItems.count(); KFileItemList itemSubSet; - const int count = items.count(); itemSubSet.reserve(count); - for (int i = 0; i < count; ++i) { - KFileItem item = items.at(i); - item.determineMimeType(); - itemSubSet.append(item); - if (timer.elapsed() > MaxBlockTimeout) { -#ifdef KFILEITEMMODELROLESUPDATER_DEBUG - kDebug() << "Maximum time of" << MaxBlockTimeout << "ms exceeded, creating only previews for" - << (i + 1) << "items," << (items.count() - (i + 1)) << "will be resolved later"; -#endif - break; - } + + if (m_pendingPreviewItems.first().isMimeTypeKnown()) { + // Some mime types are known already, probably because they were + // determined when loading the icons for the visible items. Start + // a preview job for all items at the beginning of the list which + // have a known mime type. + do { + itemSubSet.append(m_pendingPreviewItems.takeFirst()); + } while (!m_pendingPreviewItems.isEmpty() && m_pendingPreviewItems.first().isMimeTypeKnown()); + } else { + // Determine mime types for MaxBlockTimeout ms, and start a preview + // job for the corresponding items. + QElapsedTimer timer; + timer.start(); + + do { + const KFileItem item = m_pendingPreviewItems.takeFirst(); + item.determineMimeType(); + itemSubSet.append(item); + } while (!m_pendingPreviewItems.isEmpty() && timer.elapsed() < MaxBlockTimeout); } - KIO::PreviewJob* job = new KIO::PreviewJob(itemSubSet, cacheSize, &m_enabledPlugins); - job->setIgnoreMaximumSize(items.first().isLocalFile()); - if (job->ui()) { - job->ui()->setWindow(qApp->activeWindow()); + + KIO::PreviewJob *job = new KIO::PreviewJob(itemSubSet, cacheSize, &m_enabledPlugins); + job->setDevicePixelRatio(m_devicePixelRatio); + job->setIgnoreMaximumSize(itemSubSet.first().isLocalFile() && !itemSubSet.first().isSlow() && m_localFileSizePreviewLimit <= 0); + if (job->uiDelegate()) { + KJobWidgets::setWindow(job, qApp->activeWindow()); } - connect(job, SIGNAL(gotPreview(KFileItem,QPixmap)), - this, SLOT(slotGotPreview(KFileItem,QPixmap))); - connect(job, SIGNAL(failed(KFileItem)), - this, SLOT(slotPreviewFailed(KFileItem))); - connect(job, SIGNAL(finished(KJob*)), - this, SLOT(slotPreviewJobFinished(KJob*))); + connect(job, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotGotPreview); + connect(job, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotPreviewFailed); + connect(job, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished); - m_previewJobs.append(job); + m_previewJob = job; } - -bool KFileItemModelRolesUpdater::hasPendingRoles() const +QPixmap KFileItemModelRolesUpdater::transformPreviewPixmap(const QPixmap &pixmap) { - return !m_pendingVisibleItems.isEmpty() || !m_pendingInvisibleItems.isEmpty(); -} + QPixmap scaledPixmap = pixmap; -void KFileItemModelRolesUpdater::resolvePendingRoles() -{ - int resolvedCount = 0; + if (!pixmap.hasAlpha() && !pixmap.isNull() && m_iconSize.width() > KIconLoader::SizeSmallMedium && m_iconSize.height() > KIconLoader::SizeSmallMedium) { + if (m_enlargeSmallPreviews) { + KPixmapModifier::applyFrame(scaledPixmap, m_iconSize); + } else { + // Assure that small previews don't get enlarged. Instead they + // should be shown centered within the frame. + const QSize contentSize = KPixmapModifier::sizeInsideFrame(m_iconSize); + const bool enlargingRequired = scaledPixmap.width() < contentSize.width() && scaledPixmap.height() < contentSize.height(); + if (enlargingRequired) { + QSize frameSize = scaledPixmap.size() / scaledPixmap.devicePixelRatio(); + frameSize.scale(m_iconSize, Qt::KeepAspectRatio); - bool hasSlowRoles = m_previewShown; - if (!hasSlowRoles) { - QSetIterator it(m_roles); - while (it.hasNext()) { - if (m_resolvableRoles.contains(it.next())) { - hasSlowRoles = true; - break; + QPixmap largeFrame(frameSize); + largeFrame.fill(Qt::transparent); + + KPixmapModifier::applyFrame(largeFrame, frameSize); + + QPainter painter(&largeFrame); + painter.drawPixmap((largeFrame.width() - scaledPixmap.width() / scaledPixmap.devicePixelRatio()) / 2, + (largeFrame.height() - scaledPixmap.height() / scaledPixmap.devicePixelRatio()) / 2, + scaledPixmap); + scaledPixmap = largeFrame; + } else { + // The image must be shrunk as it is too large to fit into + // the available icon size + KPixmapModifier::applyFrame(scaledPixmap, m_iconSize); } } + } else if (!pixmap.isNull()) { + KPixmapModifier::scale(scaledPixmap, m_iconSize * m_devicePixelRatio); + scaledPixmap.setDevicePixelRatio(m_devicePixelRatio); } - const ResolveHint resolveHint = hasSlowRoles ? ResolveFast : ResolveAll; - - // Resolving the MIME type can be expensive. Assure that not more than MaxBlockTimeout ms are - // spend for resolving them synchronously. Usually this is more than enough to determine - // all visible items, but there are corner cases where this limit gets easily exceeded. - QElapsedTimer timer; - timer.start(); + return scaledPixmap; +} - // Resolve the MIME type of all visible items - QSet::iterator visibleIt = m_pendingVisibleItems.begin(); - while (visibleIt != m_pendingVisibleItems.end()) { - const KFileItem item = *visibleIt; - if (!hasSlowRoles) { - Q_ASSERT(!m_pendingInvisibleItems.contains(item)); - // All roles will be resolved by applyResolvedRoles() - visibleIt = m_pendingVisibleItems.erase(visibleIt); - } else { - ++visibleIt; - } - applyResolvedRoles(item, resolveHint); - ++resolvedCount; +void KFileItemModelRolesUpdater::loadNextHoverSequencePreview() +{ + if (m_hoverSequenceItem.isNull() || m_hoverSequencePreviewJob) { + return; + } - if (timer.elapsed() > MaxBlockTimeout) { - break; - } + const int index = m_model->index(m_hoverSequenceItem); + if (index < 0) { + return; } - // Resolve the MIME type of the invisible items at least until the timeout - // has been exceeded or the maximum number of items has been reached - KFileItemList invisibleItems; - if (m_lastVisibleIndex >= 0) { - // The visible range is valid, don't care about the order how the MIME - // type of invisible items get resolved - invisibleItems = m_pendingInvisibleItems.toList(); - } else { - // The visible range is temporary invalid (e.g. happens when loading - // a directory) so take care to sort the currently invisible items where - // a part will get visible later - invisibleItems = sortedItems(m_pendingInvisibleItems); + // We generate the next few sequence indices in advance (buffering) + const int maxSeqIdx = m_hoverSequenceIndex + 5; + + QHash data = m_model->data(index); + + if (!data.contains("hoverSequencePixmaps")) { + // The pixmap at index 0 isn't used ("iconPixmap" will be used instead) + data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector() << QPixmap())); + m_model->setData(index, data); } - int index = 0; - while (resolvedCount < MaxResolveItemsCount && index < invisibleItems.count() && timer.elapsed() <= MaxBlockTimeout) { - const KFileItem item = invisibleItems.at(index); - applyResolvedRoles(item, resolveHint); + const QVector pixmaps = data["hoverSequencePixmaps"].value>(); - if (!hasSlowRoles) { - // All roles have been resolved already by applyResolvedRoles() - m_pendingInvisibleItems.remove(item); - } - ++index; - ++resolvedCount; + const int loadSeqIdx = pixmaps.size(); + + float wap = -1.0f; + if (data.contains("hoverSequenceWraparoundPoint")) { + wap = data["hoverSequenceWraparoundPoint"].toFloat(); + } + if (wap >= 1.0f && loadSeqIdx >= static_cast(wap)) { + // Reached the wraparound point -> no more previews to load. + return; } - if (m_previewShown) { - KFileItemList items = sortedItems(m_pendingVisibleItems); - items += invisibleItems; - startPreviewJob(items); - } else { - QTimer::singleShot(0, this, SLOT(resolveNextPendingRoles())); + if (loadSeqIdx > maxSeqIdx) { + // Wait until setHoverSequenceState() is called with a higher sequence index. + return; } -#ifdef KFILEITEMMODELROLESUPDATER_DEBUG - if (timer.elapsed() > MaxBlockTimeout) { - kDebug() << "Maximum time of" << MaxBlockTimeout - << "ms exceeded, skipping items... Remaining visible:" << m_pendingVisibleItems.count() - << "invisible:" << m_pendingInvisibleItems.count(); + // PreviewJob internally caches items always with the size of + // 128 x 128 pixels or 256 x 256 pixels. A (slow) downscaling is done + // by PreviewJob if a smaller size is requested. For images KFileItemModelRolesUpdater must + // do a downscaling anyhow because of the frame, so in this case only the provided + // cache sizes are requested. + const QSize cacheSize = (m_iconSize.width() > 128) || (m_iconSize.height() > 128) ? QSize(256, 256) : QSize(128, 128); + + KIO::PreviewJob *job = new KIO::PreviewJob({m_hoverSequenceItem}, cacheSize, &m_enabledPlugins); + + job->setSequenceIndex(loadSeqIdx); + job->setIgnoreMaximumSize(m_hoverSequenceItem.isLocalFile() && !m_hoverSequenceItem.isSlow() && m_localFileSizePreviewLimit <= 0); + if (job->uiDelegate()) { + KJobWidgets::setWindow(job, qApp->activeWindow()); } - kDebug() << "[TIME] Resolved pending roles:" << timer.elapsed(); -#endif - applySortProgressToModel(); + connect(job, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotHoverSequenceGotPreview); + connect(job, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed); + connect(job, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewJobFinished); + + m_hoverSequencePreviewJob = job; } -void KFileItemModelRolesUpdater::resetPendingRoles() +void KFileItemModelRolesUpdater::killHoverSequencePreviewJob() { - m_pendingVisibleItems.clear(); - m_pendingInvisibleItems.clear(); - - foreach (KJob* job, m_previewJobs) { - job->kill(); + if (m_hoverSequencePreviewJob) { + disconnect(m_hoverSequencePreviewJob, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotHoverSequenceGotPreview); + disconnect(m_hoverSequencePreviewJob, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed); + disconnect(m_hoverSequencePreviewJob, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotHoverSequencePreviewJobFinished); + m_hoverSequencePreviewJob->kill(); + m_hoverSequencePreviewJob = nullptr; } - Q_ASSERT(m_previewJobs.isEmpty()); } -void KFileItemModelRolesUpdater::sortAndResolveAllRoles() +void KFileItemModelRolesUpdater::updateChangedItems() { - if (m_paused) { + if (m_state == Paused) { return; } - resetPendingRoles(); - Q_ASSERT(m_pendingVisibleItems.isEmpty()); - Q_ASSERT(m_pendingInvisibleItems.isEmpty()); - - if (m_model->count() == 0) { + if (m_changedItems.isEmpty()) { return; } - // Determine all visible items - Q_ASSERT(m_firstVisibleIndex >= 0); - for (int i = m_firstVisibleIndex; i <= m_lastVisibleIndex; ++i) { - const KFileItem item = m_model->fileItem(i); - if (!item.isNull()) { - m_pendingVisibleItems.insert(item); - } - } + m_finishedItems -= m_changedItems; - // Determine all invisible items - for (int i = 0; i < m_firstVisibleIndex; ++i) { - const KFileItem item = m_model->fileItem(i); - if (!item.isNull()) { - m_pendingInvisibleItems.insert(item); - } - } - const int count = m_model->count(); - for (int i = m_lastVisibleIndex + 1; i < count; ++i) { - const KFileItem item = m_model->fileItem(i); - if (!item.isNull()) { - m_pendingInvisibleItems.insert(item); - } - } + if (m_resolvableRoles.contains(m_model->sortRole())) { + m_pendingSortRoleItems += m_changedItems; - resolvePendingRoles(); -} + if (m_state != ResolvingSortRole) { + // Stop the preview job if necessary, and trigger the + // asynchronous determination of the sort role. + killPreviewJob(); + m_state = ResolvingSortRole; + QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextSortRole); + } -void KFileItemModelRolesUpdater::sortAndResolvePendingRoles() -{ - Q_ASSERT(!m_paused); - if (m_model->count() == 0) { return; } - // If no valid index range is given assume that all items are visible. - // A cleanup will be done later as soon as the index range has been set. - const bool hasValidIndexRange = (m_lastVisibleIndex >= 0); + QList visibleChangedIndexes; + QList invisibleChangedIndexes; + visibleChangedIndexes.reserve(m_changedItems.size()); + invisibleChangedIndexes.reserve(m_changedItems.size()); - // Trigger a preview generation of all pending items. Assure that the visible - // pending items get generated first. + auto changedItemsIt = m_changedItems.begin(); + while (changedItemsIt != m_changedItems.end()) { + const auto &item = *changedItemsIt; + const int index = m_model->index(item); - // Step 1: Check if any items in m_pendingVisibleItems are not visible any more - // and move them to m_pendingInvisibleItems. - QSet::iterator itVisible = m_pendingVisibleItems.begin(); - while (itVisible != m_pendingVisibleItems.end()) { - const KFileItem item = *itVisible; - if (item.isNull()) { - itVisible = m_pendingVisibleItems.erase(itVisible); + if (index < 0) { + changedItemsIt = m_changedItems.erase(changedItemsIt); continue; } + ++changedItemsIt; - const int index = m_model->index(item); - if (!hasValidIndexRange || (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex)) { - ++itVisible; + if (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex) { + visibleChangedIndexes.append(index); } else { - itVisible = m_pendingVisibleItems.erase(itVisible); - m_pendingInvisibleItems.insert(item); + invisibleChangedIndexes.append(index); } } - // Step 2: Check if any items in m_pendingInvisibleItems have become visible - // and move them to m_pendingVisibleItems. - QSet::iterator itInvisible = m_pendingInvisibleItems.begin(); - while (itInvisible != m_pendingInvisibleItems.end()) { - const KFileItem item = *itInvisible; - if (item.isNull()) { - itInvisible = m_pendingInvisibleItems.erase(itInvisible); - continue; - } + std::sort(visibleChangedIndexes.begin(), visibleChangedIndexes.end()); - const int index = m_model->index(item); - if (!hasValidIndexRange || (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex)) { - itInvisible = m_pendingInvisibleItems.erase(itInvisible); - m_pendingVisibleItems.insert(item); - } else { - ++itInvisible; + if (m_previewShown) { + for (int index : std::as_const(visibleChangedIndexes)) { + m_pendingPreviewItems.append(m_model->fileItem(index)); } - } - - resolvePendingRoles(); -} -void KFileItemModelRolesUpdater::applySortProgressToModel() -{ - if (m_sortingProgress < 0) { - return; - } + for (int index : std::as_const(invisibleChangedIndexes)) { + m_pendingPreviewItems.append(m_model->fileItem(index)); + } - // Inform the model about the progress of the resolved items, - // so that it can give an indication when the sorting has been finished. - const int resolvedCount = m_model->count() - - m_pendingVisibleItems.count() - - m_pendingInvisibleItems.count(); - if (resolvedCount > 0) { - m_model->emitSortProgress(resolvedCount); - if (resolvedCount == m_model->count()) { - m_sortingProgress = -1; + if (!m_previewJob) { + startPreviewJob(); + } + } else { + const bool resolvingInProgress = !m_pendingIndexes.isEmpty(); + m_pendingIndexes = visibleChangedIndexes + m_pendingIndexes + invisibleChangedIndexes; + if (!resolvingInProgress) { + // Trigger the asynchronous resolving of the changed roles. + m_state = ResolvingAllRoles; + QTimer::singleShot(0, this, &KFileItemModelRolesUpdater::resolveNextPendingRoles); } } } -void KFileItemModelRolesUpdater::updateSortProgress() +void KFileItemModelRolesUpdater::applySortRole(int index) { - const QByteArray sortRole = m_model->sortRole(); - - // Optimization if the sorting is done by type: In case if all MIME-types - // are known, the types have been resolved already by KFileItemModel and - // no sort-progress feedback is required. - const bool showProgress = (sortRole == "type") - ? hasUnknownMimeTypes() - : m_resolvableRoles.contains(sortRole); - - if (m_sortingProgress >= 0) { - // Mark the current sorting as finished - m_model->emitSortProgress(m_model->count()); - } - m_sortingProgress = showProgress ? 0 : -1; -} + QHash data; + const KFileItem item = m_model->fileItem(index); -bool KFileItemModelRolesUpdater::hasUnknownMimeTypes() const -{ - const int count = m_model->count(); - for (int i = 0; i < count; ++i) { - const KFileItem item = m_model->fileItem(i); + if (m_model->sortRole() == "type") { if (!item.isMimeTypeKnown()) { - return true; + item.determineMimeType(); } + + data.insert("type", item.mimeComment()); + } else if (m_model->sortRole() == "size" && item.isLocalFile() && item.isDir()) { + startDirectorySizeCounting(item, index); + return; + } else { + // Probably the sort role is a baloo role - just determine all roles. + data = rolesData(item, index); } - return false; + disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); + m_model->setData(index, data); + connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); } -bool KFileItemModelRolesUpdater::applyResolvedRoles(const KFileItem& item, ResolveHint hint) +void KFileItemModelRolesUpdater::applySortProgressToModel() { - if (item.isNull()) { - return false; - } + // Inform the model about the progress of the resolved items, + // so that it can give an indication when the sorting has been finished. + const int resolvedCount = m_model->count() - m_pendingSortRoleItems.count(); + m_model->emitSortProgress(resolvedCount); +} +bool KFileItemModelRolesUpdater::applyResolvedRoles(int index, ResolveHint hint) +{ + const KFileItem item = m_model->fileItem(index); const bool resolveAll = (hint == ResolveAll); - bool mimeTypeChanged = false; - if (!item.isMimeTypeKnown()) { + bool iconChanged = false; + if (!item.isMimeTypeKnown() || !item.isFinalIconKnown()) { item.determineMimeType(); - mimeTypeChanged = true; + iconChanged = true; + } else if (!m_model->data(index).contains("iconName")) { + iconChanged = true; } - if (mimeTypeChanged || resolveAll || m_clearPreviews) { - const int index = m_model->index(item); + if (iconChanged || resolveAll || m_clearPreviews) { if (index < 0) { return false; } QHash data; if (resolveAll) { - data = rolesData(item); + data = rolesData(item, index); } - data.insert("iconName", item.iconName()); + if (!item.iconName().isEmpty()) { + data.insert("iconName", item.iconName()); + } if (m_clearPreviews) { data.insert("iconPixmap", QPixmap()); + data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector())); } - disconnect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet)), - this, SLOT(slotItemsChanged(KItemRangeList,QSet))); + disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); m_model->setData(index, data); - connect(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet)), - this, SLOT(slotItemsChanged(KItemRangeList,QSet))); + connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); return true; } return false; } -QHash KFileItemModelRolesUpdater::rolesData(const KFileItem& item) const +void KFileItemModelRolesUpdater::startDirectorySizeCounting(const KFileItem &item, int index) +{ + if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::None || !item.isLocalFile()) { + return; + } + + if (ContentDisplaySettings::directorySizeMode() == ContentDisplaySettings::EnumDirectorySizeMode::ContentCount || item.isSlow()) { + // fastpath no recursion necessary + + auto data = m_model->data(index); + if (data.value("size") == -2) { + // means job already started + return; + } + + auto url = item.url(); + if (!item.localPath().isEmpty()) { + // optimization for desktop:/, trash:/ + url = QUrl::fromLocalFile(item.localPath()); + } + + data.insert("size", -2); // invalid size, -1 means size unknown + + disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); + m_model->setData(index, data); + connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); + + auto listJob = KIO::listDir(url, KIO::HideProgressInfo); + QObject::connect(listJob, &KIO::ListJob::entries, this, [this, item](const KJob * /*job*/, const KIO::UDSEntryList &list) { + int index = m_model->index(item); + if (index < 0) { + return; + } + auto data = m_model->data(index); + int origCount = data.value("count").toInt(); + int entryCount = origCount; + + for (const KIO::UDSEntry &entry : list) { + const auto name = entry.stringValue(KIO::UDSEntry::UDS_NAME); + + if (name == QStringLiteral("..") || name == QStringLiteral(".")) { + continue; + } + if (!m_model->showHiddenFiles() && name.startsWith(QLatin1Char('.'))) { + continue; + } + if (m_model->showDirectoriesOnly() && !entry.isDir()) { + continue; + } + ++entryCount; + } + + QHash newData; + QVariant expandable = data.value("isExpandable"); + if (expandable.isNull() || expandable.toBool() != (entryCount > 0)) { + // if expandable has changed + newData.insert("isExpandable", entryCount > 0); + } + + if (origCount != entryCount) { + // count has changed + newData.insert("count", entryCount); + } + + if (!newData.isEmpty()) { + disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); + m_model->setData(index, newData); + connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); + } + }); + return; + } + + // Tell m_directoryContentsCounter that we want to count the items + // inside the directory. The result will be received in slotDirectoryContentsCountReceived. + const QString path = item.localPath(); + const auto priority = index >= m_firstVisibleIndex && index <= m_lastVisibleIndex ? KDirectoryContentsCounter::PathCountPriority::High + : KDirectoryContentsCounter::PathCountPriority::Normal; + + m_directoryContentsCounter->scanDirectory(path, priority); +} + +QHash KFileItemModelRolesUpdater::rolesData(const KFileItem &item, int index) { QHash data; @@ -1043,161 +1375,157 @@ QHash KFileItemModelRolesUpdater::rolesData(const KFileIte const bool getIsExpandableRole = m_roles.contains("isExpandable"); if ((getSizeRole || getIsExpandableRole) && item.isDir()) { - if (item.isLocalFile()) { - const QString path = item.localPath(); - const int count = subItemsCount(path); - if (getSizeRole) { - data.insert("size", count); - } - if (getIsExpandableRole) { - data.insert("isExpandable", count > 0); - } + startDirectorySizeCounting(item, index); + } - if (!m_dirWatcher->contains(path)) { - m_dirWatcher->addDir(path); - m_watchedDirs.insert(path); - } - } else if (getSizeRole) { - data.insert("size", -1); // -1 indicates an unknown number of items - } + if (m_roles.contains("extension")) { + // TODO KF6 use KFileItem::suffix 464722 + data.insert("extension", QFileInfo(item.name()).suffix()); } if (m_roles.contains("type")) { data.insert("type", item.mimeComment()); } - data.insert("iconOverlays", item.overlays()); - -#ifdef HAVE_NEPOMUK - if (m_nepomukResourceWatcher) { - const KNepomukRolesProvider& rolesProvider = KNepomukRolesProvider::instance(); - Nepomuk::Resource resource(item.nepomukUri()); - QHashIterator it(rolesProvider.roleValues(resource, m_roles)); - while (it.hasNext()) { - it.next(); - data.insert(it.key(), it.value()); - } - - QUrl uri = resource.resourceUri(); - if (uri.isEmpty()) { - // TODO: Is there another way to explicitly create a resource? - // We need a resource to be able to track it for changes. - resource.setRating(0); - uri = resource.resourceUri(); - } - if (!uri.isEmpty() && !m_nepomukUriItems.contains(uri)) { - m_nepomukResourceWatcher->addResource(resource); - - if (m_nepomukUriItems.isEmpty()) { - m_nepomukResourceWatcher->start(); - } + QStringList overlays = item.overlays(); + for (KOverlayIconPlugin *it : std::as_const(m_overlayIconsPlugin)) { + overlays.append(it->getOverlays(item.url())); + } + if (!overlays.isEmpty()) { + data.insert("iconOverlays", overlays); + } - m_nepomukUriItems.insert(uri, item.url()); - } +#if HAVE_BALOO + if (m_balooFileMonitor) { + m_balooFileMonitor->addFile(item.localPath()); + applyChangedBalooRolesForItem(item); } #endif - return data; } -KFileItemList KFileItemModelRolesUpdater::sortedItems(const QSet& items) const +void KFileItemModelRolesUpdater::slotOverlaysChanged(const QUrl &url, const QStringList &) { - KFileItemList itemList; - if (items.isEmpty()) { - return itemList; + const KFileItem item = m_model->fileItem(url); + if (item.isNull()) { + return; + } + const int index = m_model->index(item); + QHash data = m_model->data(index); + QStringList overlays = item.overlays(); + for (KOverlayIconPlugin *it : std::as_const(m_overlayIconsPlugin)) { + overlays.append(it->getOverlays(url)); } + data.insert("iconOverlays", overlays); + m_model->setData(index, data); +} -#ifdef KFILEITEMMODELROLESUPDATER_DEBUG - QElapsedTimer timer; - timer.start(); -#endif +void KFileItemModelRolesUpdater::updateAllPreviews() +{ + if (m_state == Paused) { + m_previewChangedDuringPausing = true; + } else { + m_finishedItems.clear(); + startUpdating(); + } +} - QList indexes; - indexes.reserve(items.count()); +void KFileItemModelRolesUpdater::killPreviewJob() +{ + if (m_previewJob) { + disconnect(m_previewJob, &KIO::PreviewJob::gotPreview, this, &KFileItemModelRolesUpdater::slotGotPreview); + disconnect(m_previewJob, &KIO::PreviewJob::failed, this, &KFileItemModelRolesUpdater::slotPreviewFailed); + disconnect(m_previewJob, &KIO::PreviewJob::finished, this, &KFileItemModelRolesUpdater::slotPreviewJobFinished); + m_previewJob->kill(); + m_previewJob = nullptr; + m_pendingPreviewItems.clear(); + } +} - QSetIterator it(items); - while (it.hasNext()) { - const KFileItem item = it.next(); - const int index = m_model->index(item); - if (index >= 0) { - indexes.append(index); +QList KFileItemModelRolesUpdater::indexesToResolve() const +{ + const int count = m_model->count(); + + QList result; + result.reserve(qMin(count, (m_lastVisibleIndex - m_firstVisibleIndex + 1) + ResolveAllItemsLimit + (2 * m_maximumVisibleItems))); + + // Add visible items. + // Resolve files first, their previews are quicker. + QList visibleDirs; + for (int i = m_firstVisibleIndex; i <= m_lastVisibleIndex; ++i) { + const KFileItem item = m_model->fileItem(i); + if (item.isDir()) { + visibleDirs.append(i); + } else { + result.append(i); } } - qSort(indexes); - itemList.reserve(items.count()); - foreach (int index, indexes) { - itemList.append(m_model->fileItem(index)); - } + result.append(visibleDirs); -#ifdef KFILEITEMMODELROLESUPDATER_DEBUG - kDebug() << "[TIME] Sorting of items:" << timer.elapsed(); -#endif - return itemList; -} + // We need a reasonable upper limit for number of items to resolve after + // and before the visible range. m_maximumVisibleItems can be quite large + // when using Compact View. + const int readAheadItems = qMin(ReadAheadPages * m_maximumVisibleItems, ResolveAllItemsLimit / 2); -int KFileItemModelRolesUpdater::subItemsCount(const QString& path) const -{ - const bool countHiddenFiles = m_model->showHiddenFiles(); - const bool showFoldersOnly = m_model->showDirectoriesOnly(); + // Add items after the visible range. + const int endExtendedVisibleRange = qMin(m_lastVisibleIndex + readAheadItems, count - 1); + for (int i = m_lastVisibleIndex + 1; i <= endExtendedVisibleRange; ++i) { + result.append(i); + } -#ifdef Q_WS_WIN - QDir dir(path); - QDir::Filters filters = QDir::NoDotAndDotDot | QDir::System; - if (countHiddenFiles) { - filters |= QDir::Hidden; + // Add items before the visible range in reverse order. + const int beginExtendedVisibleRange = qMax(0, m_firstVisibleIndex - readAheadItems); + for (int i = m_firstVisibleIndex - 1; i >= beginExtendedVisibleRange; --i) { + result.append(i); } - if (showFoldersOnly) { - filters |= QDir::Dirs; - } else { - filters |= QDir::AllEntries; + + // Add items on the last page. + const int beginLastPage = qMax(endExtendedVisibleRange + 1, count - m_maximumVisibleItems); + for (int i = beginLastPage; i < count; ++i) { + result.append(i); } - return dir.entryList(filters).count(); -#else - // Taken from kdelibs/kio/kio/kdirmodel.cpp - // Copyright (C) 2006 David Faure - int count = -1; - DIR* dir = ::opendir(QFile::encodeName(path)); - if (dir) { // krazy:exclude=syscalls - count = 0; - struct dirent *dirEntry = 0; - while ((dirEntry = ::readdir(dir))) { - if (dirEntry->d_name[0] == '.') { - if (dirEntry->d_name[1] == '\0' || !countHiddenFiles) { - // Skip "." or hidden files - continue; - } - if (dirEntry->d_name[1] == '.' && dirEntry->d_name[2] == '\0') { - // Skip ".." - continue; - } - } + // Add items on the first page. + const int endFirstPage = qMin(beginExtendedVisibleRange, m_maximumVisibleItems); + for (int i = 0; i < endFirstPage; ++i) { + result.append(i); + } - // If only directories are counted, consider an unknown file type and links also - // as directory instead of trying to do an expensive stat() - // (see bugs 292642 and 299997). - const bool countEntry = !showFoldersOnly || - dirEntry->d_type == DT_DIR || - dirEntry->d_type == DT_LNK || - dirEntry->d_type == DT_UNKNOWN; - if (countEntry) { - ++count; - } - } - ::closedir(dir); + // Continue adding items until ResolveAllItemsLimit is reached. + int remainingItems = ResolveAllItemsLimit - result.count(); + + for (int i = endExtendedVisibleRange + 1; i < beginLastPage && remainingItems > 0; ++i) { + result.append(i); + --remainingItems; } - return count; -#endif + + for (int i = beginExtendedVisibleRange - 1; i >= endFirstPage && remainingItems > 0; --i) { + result.append(i); + --remainingItems; + } + + return result; } -void KFileItemModelRolesUpdater::updateAllPreviews() +void KFileItemModelRolesUpdater::trimHoverSequenceLoadedItems() { - if (m_paused) { - m_previewChangedDuringPausing = true; - } else { - sortAndResolveAllRoles(); + static const size_t maxLoadedItems = 20; + + size_t loadedItems = m_hoverSequenceLoadedItems.size(); + while (loadedItems > maxLoadedItems) { + const KFileItem item = m_hoverSequenceLoadedItems.front(); + + m_hoverSequenceLoadedItems.pop_front(); + loadedItems--; + + const int index = m_model->index(item); + if (index >= 0) { + QHash data = m_model->data(index); + data["hoverSequencePixmaps"] = QVariant::fromValue(QVector() << QPixmap()); + m_model->setData(index, data); + } } } -#include "kfileitemmodelrolesupdater.moc" +#include "moc_kfileitemmodelrolesupdater.cpp"