X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/7c0b5c08cc97eded7755ca5cef9c7e5ba5c15537..cd2e64154fd5446a7e19aff4cb147efe2f2ba31e:/src/kitemviews/kfileitemmodelrolesupdater.cpp diff --git a/src/kitemviews/kfileitemmodelrolesupdater.cpp b/src/kitemviews/kfileitemmodelrolesupdater.cpp index d18387f51..09dd2eba1 100644 --- a/src/kitemviews/kfileitemmodelrolesupdater.cpp +++ b/src/kitemviews/kfileitemmodelrolesupdater.cpp @@ -6,79 +6,89 @@ #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 -#ifdef HAVE_BALOO +#include "dolphin_contentdisplaysettings.h" + +#if HAVE_BALOO #include "private/kbaloorolesprovider.h" #include #include #endif #include -#include -#include #include +#include +#include +#include #include +#include + +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; +// If the number of items is smaller than ResolveAllItemsLimit, +// the roles of all items will be resolved. +const int ResolveAllItemsLimit = 500; - // Not only the visible area, but up to ReadAheadPages before and after - // this area will be resolved. - const int ReadAheadPages = 5; +// 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_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_firstVisibleIndex(0), - m_lastVisibleIndex(-1), - m_maximumVisibleItems(50), - m_roles(), - m_resolvableRoles(), - m_enabledPlugins(), - m_localFileSizePreviewLimit(0), - m_scanDirectories(true), - m_pendingSortRoleItems(), - m_pendingIndexes(), - m_pendingPreviewItems(), - m_previewJob(), - m_recentlyChangedItemsTimer(nullptr), - m_recentlyChangedItems(), - m_changedItems(), - m_directoryContentsCounter(nullptr) - #ifdef HAVE_BALOO - , m_balooFileMonitor(nullptr) - #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_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); @@ -86,44 +96,40 @@ KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel* model, QO 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); + 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 100 ms. m_recentlyChangedItemsTimer = new QTimer(this); - m_recentlyChangedItemsTimer->setInterval(100); + 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_BALOO +#if HAVE_BALOO m_resolvableRoles += KBalooRolesProvider::instance().roles(); #endif m_directoryContentsCounter = new KDirectoryContentsCounter(m_model, this); - connect(m_directoryContentsCounter, &KDirectoryContentsCounter::result, - this, &KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived); + connect(m_directoryContentsCounter, &KDirectoryContentsCounter::result, this, &KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived); - const auto plugins = KPluginLoader::instantiatePlugins(QStringLiteral("kf5/overlayicon"), nullptr, qApp); - for (QObject *it : plugins) { - auto plugin = qobject_cast(it); + const QString pluginNamespace = QStringLiteral("kf" QT_STRINGIFY(QT_MAJOR_VERSION)) + QStringLiteral("/overlayicon"); + const auto plugins = KPluginMetaData::findPlugins(pluginNamespace, {}, 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 - it->deleteLater(); + delete instance; } } } @@ -133,7 +139,7 @@ KFileItemModelRolesUpdater::~KFileItemModelRolesUpdater() killPreviewJob(); } -void KFileItemModelRolesUpdater::setIconSize(const QSize& size) +void KFileItemModelRolesUpdater::setIconSize(const QSize &size) { if (size != m_iconSize) { m_iconSize = size; @@ -212,7 +218,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; @@ -232,8 +238,7 @@ void KFileItemModelRolesUpdater::setPaused(bool paused) m_state = Paused; killPreviewJob(); } else { - const bool updatePreviews = (m_iconSizeChangedDuringPausing && m_previewShown) || - m_previewChangedDuringPausing; + const bool updatePreviews = (m_iconSizeChangedDuringPausing && m_previewShown) || m_previewChangedDuringPausing; const bool resolveAll = updatePreviews || m_rolesChangedDuringPausing; if (resolveAll) { m_finishedItems.clear(); @@ -254,21 +259,21 @@ void KFileItemModelRolesUpdater::setPaused(bool paused) } } -void KFileItemModelRolesUpdater::setRoles(const QSet& roles) +void KFileItemModelRolesUpdater::setRoles(const QSet &roles) { if (m_roles != roles) { m_roles = roles; -#ifdef HAVE_BALOO +#if HAVE_BALOO // Check whether there is at least one role that must be resolved // 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 KBalooRolesProvider& rolesProvider = KBalooRolesProvider::instance(); + 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)) { hasBalooRole = true; break; @@ -277,8 +282,7 @@ void KFileItemModelRolesUpdater::setRoles(const QSet& roles) if (hasBalooRole && m_balooConfig.fileIndexingEnabled() && !m_balooFileMonitor) { m_balooFileMonitor = new Baloo::FileMonitor(this); - connect(m_balooFileMonitor, &Baloo::FileMonitor::fileMetaDataChanged, - this, &KFileItemModelRolesUpdater::applyChangedBalooRoles); + connect(m_balooFileMonitor, &Baloo::FileMonitor::fileMetaDataChanged, this, &KFileItemModelRolesUpdater::applyChangedBalooRoles); } else if (!hasBalooRole && m_balooFileMonitor) { delete m_balooFileMonitor; m_balooFileMonitor = nullptr; @@ -318,17 +322,27 @@ qlonglong KFileItemModelRolesUpdater::localFileSizePreviewLimit() const return m_localFileSizePreviewLimit; } -void KFileItemModelRolesUpdater::setScanDirectories(bool enabled) +void KFileItemModelRolesUpdater::setHoverSequenceState(const QUrl &itemUrl, int seqIdx) { - m_scanDirectories = enabled; -} + const KFileItem item = m_model->fileItem(itemUrl); -bool KFileItemModelRolesUpdater::scanDirectories() const -{ - return m_scanDirectories; + 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) +void KFileItemModelRolesUpdater::slotItemsInserted(const KItemRangeList &itemRanges) { QElapsedTimer timer; timer.start(); @@ -336,7 +350,7 @@ void KFileItemModelRolesUpdater::slotItemsInserted(const KItemRangeList& itemRan // 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) { + 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) { @@ -363,13 +377,13 @@ void KFileItemModelRolesUpdater::slotItemsInserted(const KItemRangeList& itemRan startUpdating(); } -void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList& itemRanges) +void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList &itemRanges) { Q_UNUSED(itemRanges) const bool allItemsRemoved = (m_model->count() == 0); -#ifdef HAVE_BALOO +#if HAVE_BALOO if (m_balooFileMonitor) { // Don't let the FileWatcher watch for removed items if (allItemsRemoved) { @@ -377,7 +391,7 @@ void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList& itemRang } else { QStringList newFileList; const QStringList oldFileList = m_balooFileMonitor->files(); - for (const QString& file : oldFileList) { + for (const QString &file : oldFileList) { if (m_model->index(QUrl::fromLocalFile(file)) >= 0) { newFileList.append(file); } @@ -397,8 +411,12 @@ void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList& itemRang m_recentlyChangedItems.clear(); m_recentlyChangedItemsTimer->stop(); m_changedItems.clear(); + m_hoverSequenceLoadedItems.clear(); killPreviewJob(); + if (!m_model->showDirectoriesOnly()) { + m_directoryContentsCounter->stopWorker(); + } } else { // Only remove the items from m_finishedItems. They will be removed // from the other sets later on. @@ -411,12 +429,22 @@ void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList& itemRang } } + // 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) { + const KFileItem item = m_model->fileItem(index); + m_hoverSequenceLoadedItems.remove(item); + ++index; + } + } + // The visible items might have changed. startUpdating(); } } -void KFileItemModelRolesUpdater::slotItemsMoved(const KItemRange& itemRange, const QList &movedToIndexes) +void KFileItemModelRolesUpdater::slotItemsMoved(KItemRange itemRange, const QList &movedToIndexes) { Q_UNUSED(itemRange) Q_UNUSED(movedToIndexes) @@ -425,8 +453,7 @@ void KFileItemModelRolesUpdater::slotItemsMoved(const KItemRange& itemRange, con startUpdating(); } -void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList& itemRanges, - const QSet& roles) +void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList &itemRanges, const QSet &roles) { Q_UNUSED(roles) @@ -435,9 +462,9 @@ void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList& itemRang // to prevent expensive repeated updates if files are updated frequently. const bool itemsChangedRecently = m_recentlyChangedItemsTimer->isActive(); - QSet& targetSet = itemsChangedRecently ? m_recentlyChangedItems : m_changedItems; + QSet &targetSet = itemsChangedRecently ? m_recentlyChangedItems : m_changedItems; - for (const KItemRange& itemRange : itemRanges) { + for (const KItemRange &itemRange : itemRanges) { int index = itemRange.index; for (int count = itemRange.count; count > 0; --count) { const KFileItem item = m_model->fileItem(index); @@ -453,8 +480,7 @@ void KFileItemModelRolesUpdater::slotItemsChanged(const KItemRangeList& itemRang } } -void KFileItemModelRolesUpdater::slotSortRoleChanged(const QByteArray& current, - const QByteArray& previous) +void KFileItemModelRolesUpdater::slotSortRoleChanged(const QByteArray ¤t, const QByteArray &previous) { Q_UNUSED(current) Q_UNUSED(previous) @@ -491,7 +517,7 @@ void KFileItemModelRolesUpdater::slotSortRoleChanged(const QByteArray& current, } } -void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem& item, const QPixmap& pixmap) +void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem &item, const QPixmap &pixmap) { if (m_state != PreviewJobRunning) { return; @@ -504,45 +530,9 @@ void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem& item, const QPi return; } - QPixmap scaledPixmap = pixmap; + QPixmap scaledPixmap = transformPreviewPixmap(pixmap); - 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); - - 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 * qApp->devicePixelRatio()); - scaledPixmap.setDevicePixelRatio(qApp->devicePixelRatio()); - } - - QHash data = rolesData(item); + QHash data = rolesData(item, index); const QStringList overlays = data["iconOverlays"].toStringList(); // Strangely KFileItem::overlays() returns empty string-values, so @@ -551,7 +541,7 @@ void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem& item, const QPi // assumes that an overlay will be drawn and has some additional // setup time. if (!scaledPixmap.isNull()) { - for (const QString& overlay : overlays) { + for (const QString &overlay : overlays) { if (!overlay.isEmpty()) { // There is at least one overlay, draw all overlays above m_pixmap // and cancel the check @@ -563,16 +553,14 @@ void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem& item, const QPi data.insert("iconPixmap", scaledPixmap); - disconnect(m_model, &KFileItemModel::itemsChanged, - this, &KFileItemModelRolesUpdater::slotItemsChanged); + disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); m_model->setData(index, data); - connect(m_model, &KFileItemModel::itemsChanged, - this, &KFileItemModelRolesUpdater::slotItemsChanged); + connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); m_finishedItems.insert(item); } -void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem& item) +void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem &item) { if (m_state != PreviewJobRunning) { return; @@ -585,11 +573,9 @@ void KFileItemModelRolesUpdater::slotPreviewFailed(const KFileItem& item) QHash data; data.insert("iconPixmap", QPixmap()); - disconnect(m_model, &KFileItemModel::itemsChanged, - this, &KFileItemModelRolesUpdater::slotItemsChanged); + disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); m_model->setData(index, data); - connect(m_model, &KFileItemModel::itemsChanged, - this, &KFileItemModelRolesUpdater::slotItemsChanged); + connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); applyResolvedRoles(index, ResolveAll); m_finishedItems.insert(item); @@ -615,6 +601,108 @@ void KFileItemModelRolesUpdater::slotPreviewJobFinished() } } +void KFileItemModelRolesUpdater::slotHoverSequenceGotPreview(const KFileItem &item, const QPixmap &pixmap) +{ + const int index = m_model->index(item); + if (index < 0) { + 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); + } + + // 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(); + } + } + + m_hoverSequenceNumSuccessiveFailures = 0; +} + +void KFileItemModelRolesUpdater::slotHoverSequencePreviewFailed(const KFileItem &item) +{ + const int index = m_model->index(item); + if (index < 0) { + return; + } + + static const int numRetries = 2; + + QHash data = m_model->data(index); + QVector pixmaps = data["hoverSequencePixmaps"].value>(); + + 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++; + } + + // 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::slotHoverSequencePreviewJobFinished() +{ + const int index = m_model->index(m_hoverSequenceItem); + if (index < 0) { + m_hoverSequencePreviewJob = nullptr; + return; + } + + // 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. + + // Load the next image in the sequence + m_hoverSequencePreviewJob = nullptr; + loadNextHoverSequencePreview(); +} + void KFileItemModelRolesUpdater::resolveNextSortRole() { if (m_state != ResolvingSortRole) { @@ -645,11 +733,9 @@ void KFileItemModelRolesUpdater::resolveNextSortRole() m_state = Idle; // Prevent that we try to update the items twice. - disconnect(m_model, &KFileItemModel::itemsMoved, - this, &KFileItemModelRolesUpdater::slotItemsMoved); + disconnect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved); applySortProgressToModel(); - connect(m_model, &KFileItemModel::itemsMoved, - this, &KFileItemModelRolesUpdater::slotItemsMoved); + connect(m_model, &KFileItemModel::itemsMoved, this, &KFileItemModelRolesUpdater::slotItemsMoved); startUpdating(); } } @@ -684,17 +770,15 @@ void KFileItemModelRolesUpdater::resolveNextPendingRoles() 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); + disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); for (int index = 0; index <= m_model->count(); ++index) { - if (m_model->data(index).contains("iconPixmap")) { + 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); - + connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); } m_clearPreviews = false; } @@ -712,9 +796,9 @@ void KFileItemModelRolesUpdater::resolveRecentlyChangedItems() updateChangedItems(); } -void KFileItemModelRolesUpdater::applyChangedBalooRoles(const QString& file) +void KFileItemModelRolesUpdater::applyChangedBalooRoles(const QString &file) { -#ifdef HAVE_BALOO +#if HAVE_BALOO const KFileItem item = m_model->fileItem(QUrl::fromLocalFile(file)); if (item.isNull()) { @@ -730,15 +814,15 @@ void KFileItemModelRolesUpdater::applyChangedBalooRoles(const QString& file) void KFileItemModelRolesUpdater::applyChangedBalooRolesForItem(const KFileItem &item) { -#ifdef HAVE_BALOO +#if HAVE_BALOO Baloo::File file(item.localPath()); file.load(); - const KBalooRolesProvider& rolesProvider = KBalooRolesProvider::instance(); + const KBalooRolesProvider &rolesProvider = KBalooRolesProvider::instance(); QHash data; const auto roles = rolesProvider.roles(); - for (const QByteArray& role : 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 @@ -751,12 +835,10 @@ void KFileItemModelRolesUpdater::applyChangedBalooRolesForItem(const KFileItem & data.insert(it.key(), it.value()); } - disconnect(m_model, &KFileItemModel::itemsChanged, - this, &KFileItemModelRolesUpdater::slotItemsChanged); + disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); const int index = m_model->index(item); m_model->setData(index, data); - connect(m_model, &KFileItemModel::itemsChanged, - this, &KFileItemModelRolesUpdater::slotItemsChanged); + connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); #else #ifndef Q_CC_MSVC Q_UNUSED(item) @@ -764,10 +846,10 @@ void KFileItemModelRolesUpdater::applyChangedBalooRolesForItem(const KFileItem & #endif } -void KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived(const QString& path, int count, long size) +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(QUrl::fromLocalFile(path)); @@ -782,7 +864,9 @@ void KFileItemModelRolesUpdater::slotDirectoryContentsCountReceived(const QStrin 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); } } } @@ -878,8 +962,7 @@ void KFileItemModelRolesUpdater::startPreviewJob() // 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 @@ -909,23 +992,133 @@ void KFileItemModelRolesUpdater::startPreviewJob() } while (!m_pendingPreviewItems.isEmpty() && timer.elapsed() < MaxBlockTimeout); } - KIO::PreviewJob* job = new KIO::PreviewJob(itemSubSet, cacheSize, &m_enabledPlugins); + KIO::PreviewJob *job = new KIO::PreviewJob(itemSubSet, cacheSize, &m_enabledPlugins); - job->setIgnoreMaximumSize(itemSubSet.first().isLocalFile() && m_localFileSizePreviewLimit <= 0); + job->setIgnoreMaximumSize(itemSubSet.first().isLocalFile() && !itemSubSet.first().isSlow() && m_localFileSizePreviewLimit <= 0); if (job->uiDelegate()) { KJobWidgets::setWindow(job, qApp->activeWindow()); } - connect(job, &KIO::PreviewJob::gotPreview, - this, &KFileItemModelRolesUpdater::slotGotPreview); - connect(job, &KIO::PreviewJob::failed, - this, &KFileItemModelRolesUpdater::slotPreviewFailed); - connect(job, &KIO::PreviewJob::finished, - this, &KFileItemModelRolesUpdater::slotPreviewJobFinished); + 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_previewJob = job; } +QPixmap KFileItemModelRolesUpdater::transformPreviewPixmap(const QPixmap &pixmap) +{ + QPixmap scaledPixmap = pixmap; + + 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); + + 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 * qApp->devicePixelRatio()); + scaledPixmap.setDevicePixelRatio(qApp->devicePixelRatio()); + } + + return scaledPixmap; +} + +void KFileItemModelRolesUpdater::loadNextHoverSequencePreview() +{ + if (m_hoverSequenceItem.isNull() || m_hoverSequencePreviewJob) { + return; + } + + const int index = m_model->index(m_hoverSequenceItem); + if (index < 0) { + return; + } + + // 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); + } + + const QVector pixmaps = data["hoverSequencePixmaps"].value>(); + + 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 (loadSeqIdx > maxSeqIdx) { + // Wait until setHoverSequenceState() is called with a higher sequence index. + return; + } + + // 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()); + } + + 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::killHoverSequencePreviewJob() +{ + 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; + } +} + void KFileItemModelRolesUpdater::updateChangedItems() { if (m_state == Paused) { @@ -957,15 +1150,16 @@ void KFileItemModelRolesUpdater::updateChangedItems() visibleChangedIndexes.reserve(m_changedItems.size()); invisibleChangedIndexes.reserve(m_changedItems.size()); - // Iterate over a const copy because items are deleted within the loop - const auto changedItems = m_changedItems; - for (const KFileItem &item : changedItems) { + auto changedItemsIt = m_changedItems.begin(); + while (changedItemsIt != m_changedItems.end()) { + const auto &item = *changedItemsIt; const int index = m_model->index(item); if (index < 0) { - m_changedItems.remove(item); + changedItemsIt = m_changedItems.erase(changedItemsIt); continue; } + ++changedItemsIt; if (index >= m_firstVisibleIndex && index <= m_lastVisibleIndex) { visibleChangedIndexes.append(index); @@ -1011,20 +1205,16 @@ void KFileItemModelRolesUpdater::applySortRole(int index) data.insert("type", item.mimeComment()); } else if (m_model->sortRole() == "size" && item.isLocalFile() && item.isDir()) { - const QString path = item.localPath(); - if (m_scanDirectories) { - m_directoryContentsCounter->scanDirectory(path); - } + startDirectorySizeCounting(item, index); + return; } else { // Probably the sort role is a baloo role - just determine all roles. - data = rolesData(item); + data = rolesData(item, index); } - disconnect(m_model, &KFileItemModel::itemsChanged, - this, &KFileItemModelRolesUpdater::slotItemsChanged); + disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); m_model->setData(index, data); - connect(m_model, &KFileItemModel::itemsChanged, - this, &KFileItemModelRolesUpdater::slotItemsChanged); + connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); } void KFileItemModelRolesUpdater::applySortProgressToModel() @@ -1055,7 +1245,7 @@ bool KFileItemModelRolesUpdater::applyResolvedRoles(int index, ResolveHint hint) QHash data; if (resolveAll) { - data = rolesData(item); + data = rolesData(item, index); } if (!item.iconName().isEmpty()) { @@ -1064,20 +1254,88 @@ bool KFileItemModelRolesUpdater::applyResolvedRoles(int index, ResolveHint hint) if (m_clearPreviews) { data.insert("iconPixmap", QPixmap()); + data.insert("hoverSequencePixmaps", QVariant::fromValue(QVector())); } - disconnect(m_model, &KFileItemModel::itemsChanged, - this, &KFileItemModelRolesUpdater::slotItemsChanged); + disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); m_model->setData(index, data); - connect(m_model, &KFileItemModel::itemsChanged, - this, &KFileItemModelRolesUpdater::slotItemsChanged); + connect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); return true; } return false; } -QHash KFileItemModelRolesUpdater::rolesData(const KFileItem& item) +void KFileItemModelRolesUpdater::startDirectorySizeCounting(const KFileItem &item, int index) +{ + if (ContentDisplaySettings::directorySizeCount() || item.isSlow() || !item.isLocalFile()) { + // 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("isExpandable", false); + data.insert("count", 0); + 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); + QObject::connect(listJob, &KIO::ListJob::entries, this, [this, index](const KJob * /*job*/, const KIO::UDSEntryList &list) { + 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; + } + + // count has changed + if (origCount < entryCount) { + QHash data; + data.insert("isExpandable", entryCount > 0); + data.insert("count", entryCount); + + disconnect(m_model, &KFileItemModel::itemsChanged, this, &KFileItemModelRolesUpdater::slotItemsChanged); + m_model->setData(index, data); + 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; @@ -1085,16 +1343,12 @@ QHash KFileItemModelRolesUpdater::rolesData(const KFileIte const bool getIsExpandableRole = m_roles.contains("isExpandable"); if ((getSizeRole || getIsExpandableRole) && item.isDir()) { - if (item.isLocalFile()) { - // Tell m_directoryContentsCounter that we want to count the items - // inside the directory. The result will be received in slotDirectoryContentsCountReceived. - if (m_scanDirectories) { - const QString path = item.localPath(); - m_directoryContentsCounter->scanDirectory(path); - } - } else if (getSizeRole) { - data.insert("size", -1); // -1 indicates an unknown number of items - } + startDirectorySizeCounting(item, index); + } + + if (m_roles.contains("extension")) { + // TODO KF6 use KFileItem::suffix 464722 + data.insert("extension", QFileInfo(item.name()).suffix()); } if (m_roles.contains("type")) { @@ -1105,9 +1359,11 @@ QHash KFileItemModelRolesUpdater::rolesData(const KFileIte for (KOverlayIconPlugin *it : qAsConst(m_overlayIconsPlugin)) { overlays.append(it->getOverlays(item.url())); } - data.insert("iconOverlays", overlays); + if (!overlays.isEmpty()) { + data.insert("iconOverlays", overlays); + } -#ifdef HAVE_BALOO +#if HAVE_BALOO if (m_balooFileMonitor) { m_balooFileMonitor->addFile(item.localPath()); applyChangedBalooRolesForItem(item); @@ -1116,14 +1372,14 @@ QHash KFileItemModelRolesUpdater::rolesData(const KFileIte return data; } -void KFileItemModelRolesUpdater::slotOverlaysChanged(const QUrl& url, const QStringList &) +void KFileItemModelRolesUpdater::slotOverlaysChanged(const QUrl &url, const QStringList &) { const KFileItem item = m_model->fileItem(url); if (item.isNull()) { return; } const int index = m_model->index(item); - QHash data = m_model->data(index); + QHash data = m_model->data(index); QStringList overlays = item.overlays(); for (KOverlayIconPlugin *it : qAsConst(m_overlayIconsPlugin)) { overlays.append(it->getOverlays(url)); @@ -1145,12 +1401,9 @@ void KFileItemModelRolesUpdater::updateAllPreviews() 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); + 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(); @@ -1162,15 +1415,22 @@ 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))); + 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) { - result.append(i); + const KFileItem item = m_model->fileItem(i); + if (item.isDir()) { + visibleDirs.append(i); + } else { + result.append(i); + } } + result.append(visibleDirs); + // 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. @@ -1216,3 +1476,22 @@ QList KFileItemModelRolesUpdater::indexesToResolve() const return result; } +void KFileItemModelRolesUpdater::trimHoverSequenceLoadedItems() +{ + 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); + } + } +}