From aba4462e0238d6075e8822d56a78372eacfa7d2e Mon Sep 17 00:00:00 2001 From: David Lerch Date: Sat, 30 Jan 2021 12:11:22 +0100 Subject: [PATCH] Add support for hover sequence thumbnails (via ThumbSequenceCreator) This shows a slideshow of thumbs when the user hovers a file item. --- src/kitemviews/kfileitemlistview.cpp | 7 + src/kitemviews/kfileitemlistview.h | 12 + src/kitemviews/kfileitemlistwidget.cpp | 49 +++ src/kitemviews/kfileitemlistwidget.h | 10 + src/kitemviews/kfileitemmodelrolesupdater.cpp | 329 ++++++++++++++++-- src/kitemviews/kfileitemmodelrolesupdater.h | 74 +++- src/kitemviews/kitemlistview.h | 1 + src/kitemviews/kitemlistwidget.cpp | 41 +++ src/kitemviews/kitemlistwidget.h | 24 ++ src/kitemviews/kstandarditemlistwidget.cpp | 37 +- src/kitemviews/kstandarditemlistwidget.h | 6 + src/settings/general/previewssettingspage.cpp | 1 + 12 files changed, 551 insertions(+), 40 deletions(-) diff --git a/src/kitemviews/kfileitemlistview.cpp b/src/kitemviews/kfileitemlistview.cpp index 9833b395f..adcc2d793 100644 --- a/src/kitemviews/kfileitemlistview.cpp +++ b/src/kitemviews/kfileitemlistview.cpp @@ -213,6 +213,13 @@ QPixmap KFileItemListView::createDragPixmap(const KItemSet& indexes) const return dragPixmap; } +void KFileItemListView::setHoverSequenceState(const QUrl& itemUrl, int seqIdx) +{ + if (m_modelRolesUpdater) { + m_modelRolesUpdater->setHoverSequenceState(itemUrl, seqIdx); + } +} + KItemListWidgetCreatorBase* KFileItemListView::defaultWidgetCreator() const { return new KItemListWidgetCreator(); diff --git a/src/kitemviews/kfileitemlistview.h b/src/kitemviews/kfileitemlistview.h index 8ef524a2d..bfbe85d12 100644 --- a/src/kitemviews/kfileitemlistview.h +++ b/src/kitemviews/kfileitemlistview.h @@ -10,6 +10,8 @@ #include "dolphin_export.h" #include "kitemviews/kstandarditemlistview.h" +#include + class KFileItemModelRolesUpdater; class QTimer; @@ -78,6 +80,16 @@ public: QPixmap createDragPixmap(const KItemSet& indexes) const override; + /** + * Notifies the view of a change in the hover state on an item. + * + * @param itemUrl URL of the item that is hovered, or an empty URL if no item is hovered. + * @param seqIdx The current hover sequence index. While an item is hovered, + * this method will be called repeatedly with increasing values + * for this parameter. + */ + void setHoverSequenceState(const QUrl& itemUrl, int seqIdx); + protected: KItemListWidgetCreatorBase* defaultWidgetCreator() const override; void initializeItemListWidget(KItemListWidget* item) override; diff --git a/src/kitemviews/kfileitemlistwidget.cpp b/src/kitemviews/kfileitemlistwidget.cpp index 1b38176cc..587603ab3 100644 --- a/src/kitemviews/kfileitemlistwidget.cpp +++ b/src/kitemviews/kfileitemlistwidget.cpp @@ -5,6 +5,7 @@ */ #include "kfileitemlistwidget.h" +#include "kfileitemlistview.h" #include "kfileitemmodel.h" #include "kitemlistview.h" @@ -13,6 +14,8 @@ #include #include +#include +#include #include KFileItemListWidgetInformant::KFileItemListWidgetInformant() : @@ -170,3 +173,49 @@ int KFileItemListWidget::selectionLength(const QString& text) const return selectionLength; } +void KFileItemListWidget::hoverSequenceStarted() +{ + KFileItemListView* view = listView(); + + if (!view) { + return; + } + + const QUrl itemUrl = data().value("url").toUrl(); + + view->setHoverSequenceState(itemUrl, 0); +} + +void KFileItemListWidget::hoverSequenceIndexChanged(int sequenceIndex) +{ + KFileItemListView* view = listView(); + + if (!view) { + return; + } + + const QUrl itemUrl = data().value("url").toUrl(); + + view->setHoverSequenceState(itemUrl, sequenceIndex); + + // Force-update the displayed icon + invalidateIconCache(); + update(); +} + +void KFileItemListWidget::hoverSequenceEnded() +{ + KFileItemListView* view = listView(); + + if (!view) { + return; + } + + view->setHoverSequenceState(QUrl(), 0); +} + +KFileItemListView* KFileItemListWidget::listView() +{ + return dynamic_cast(parentItem()); +} + diff --git a/src/kitemviews/kfileitemlistwidget.h b/src/kitemviews/kfileitemlistwidget.h index e09808024..094f6105f 100644 --- a/src/kitemviews/kfileitemlistwidget.h +++ b/src/kitemviews/kfileitemlistwidget.h @@ -10,6 +10,9 @@ #include "dolphin_export.h" #include "kitemviews/kstandarditemlistwidget.h" +class KFileItemListView; + + class DOLPHIN_EXPORT KFileItemListWidgetInformant : public KStandardItemListWidgetInformant { public: @@ -34,6 +37,10 @@ public: static KItemListWidgetInformant* createInformant(); protected: + virtual void hoverSequenceStarted() override; + virtual void hoverSequenceIndexChanged(int sequenceIndex) override; + virtual void hoverSequenceEnded() override; + bool isRoleRightAligned(const QByteArray& role) const override; bool isHidden() const override; QFont customizedFont(const QFont& baseFont) const override; @@ -42,6 +49,9 @@ protected: * @return Selection length without MIME-type extension */ int selectionLength(const QString& text) const override; + +private: + KFileItemListView* listView(); }; #endif diff --git a/src/kitemviews/kfileitemmodelrolesupdater.cpp b/src/kitemviews/kfileitemmodelrolesupdater.cpp index a603a94da..978f5df6e 100644 --- a/src/kitemviews/kfileitemmodelrolesupdater.cpp +++ b/src/kitemviews/kfileitemmodelrolesupdater.cpp @@ -6,6 +6,7 @@ #include "kfileitemmodelrolesupdater.h" +#include "dolphindebug.h" #include "kfileitemmodel.h" #include "private/kdirectorycontentscounter.h" #include "private/kpixmapmodifier.h" @@ -72,6 +73,10 @@ KFileItemModelRolesUpdater::KFileItemModelRolesUpdater(KFileItemModel* model, QO m_pendingIndexes(), m_pendingPreviewItems(), m_previewJob(), + m_hoverSequenceItem(), + m_hoverSequenceIndex(0), + m_hoverSequencePreviewJob(nullptr), + m_hoverSequenceNumSuccessiveFailures(0), m_recentlyChangedItemsTimer(nullptr), m_recentlyChangedItems(), m_changedItems(), @@ -328,6 +333,26 @@ bool KFileItemModelRolesUpdater::scanDirectories() const return m_scanDirectories; } +void KFileItemModelRolesUpdater::setHoverSequenceState(const QUrl& itemUrl, int seqIdx) +{ + const KFileItem item = m_model->fileItem(itemUrl); + + 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; @@ -397,6 +422,7 @@ void KFileItemModelRolesUpdater::slotItemsRemoved(const KItemRangeList& itemRang m_recentlyChangedItems.clear(); m_recentlyChangedItemsTimer->stop(); m_changedItems.clear(); + m_hoverSequenceLoadedItems.clear(); killPreviewJob(); } else { @@ -411,6 +437,16 @@ 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(); } @@ -504,43 +540,7 @@ void KFileItemModelRolesUpdater::slotGotPreview(const KFileItem& item, const QPi return; } - 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()); - } + QPixmap scaledPixmap = transformPreviewPixmap(pixmap); QHash data = rolesData(item); @@ -615,6 +615,112 @@ 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) { @@ -684,11 +790,14 @@ 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); 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); } } @@ -930,6 +1039,129 @@ void KFileItemModelRolesUpdater::startPreviewJob() 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_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) { @@ -1069,6 +1301,7 @@ 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, @@ -1221,3 +1454,23 @@ 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); + } + } +} + diff --git a/src/kitemviews/kfileitemmodelrolesupdater.h b/src/kitemviews/kfileitemmodelrolesupdater.h index 8e1e1fcbe..a03ab513a 100644 --- a/src/kitemviews/kfileitemmodelrolesupdater.h +++ b/src/kitemviews/kfileitemmodelrolesupdater.h @@ -10,6 +10,8 @@ #include "dolphin_export.h" #include "kitemviews/kitemmodelbase.h" +#include + #include #include @@ -159,6 +161,19 @@ public: void setScanDirectories(bool enabled); bool scanDirectories() const; + /** + * Notifies the updater of a change in the hover state on an item. + * + * This will trigger asynchronous loading of the next few thumb sequence images + * using a PreviewJob. + * + * @param item URL of the item that is hovered, or an empty URL if no item is hovered. + * @param seqIdx The current hover sequence index. While an item is hovered, + * this method will be called repeatedly with increasing values + * for this parameter. + */ + void setHoverSequenceState(const QUrl& itemUrl, int seqIdx); + private Q_SLOTS: void slotItemsInserted(const KItemRangeList& itemRanges); void slotItemsRemoved(const KItemRangeList& itemRanges); @@ -170,12 +185,18 @@ private Q_SLOTS: /** * Is invoked after a preview has been received successfully. + * + * Note that this is not called for hover sequence previews. + * * @see startPreviewJob() */ void slotGotPreview(const KFileItem& item, const QPixmap& pixmap); /** * Is invoked after generating a preview has failed. + * + * Note that this is not called for hover sequence previews. + * * @see startPreviewJob() */ void slotPreviewFailed(const KFileItem& item); @@ -183,11 +204,34 @@ private Q_SLOTS: /** * Is invoked when the preview job has been finished. Starts a new preview * job if there are any interesting items without previews left, or updates - * the changed items otherwise. * + * the changed items otherwise. + * + * Note that this is not called for hover sequence previews. + * * @see startPreviewJob() */ void slotPreviewJobFinished(); + /** + * Is invoked after a hover sequence preview has been received successfully. + */ + void slotHoverSequenceGotPreview(const KFileItem& item, const QPixmap& pixmap); + + /** + * Is invoked after generating a hover sequence preview has failed. + */ + void slotHoverSequencePreviewFailed(const KFileItem& item); + + /** + * Is invoked when a hover sequence preview job is finished. May start another + * job for the next sequence index right away by calling + * \a loadNextHoverSequencePreview(). + * + * Note that a PreviewJob will only ever generate a single sequence image, due + * to limitations of the PreviewJob API. + */ + void slotHoverSequencePreviewJobFinished(); + /** * Is invoked when one of the KOverlayIconPlugin emit the signal that an overlay has changed */ @@ -243,6 +287,24 @@ private: */ void startPreviewJob(); + /** + * Transforms a raw preview image, applying scale and frame. + * + * @param pixmap A raw preview image from a PreviewJob. + * @return The scaled and decorated preview image. + */ + QPixmap transformPreviewPixmap(const QPixmap& pixmap); + + /** + * Starts a PreviewJob for loading the next hover sequence image. + */ + void loadNextHoverSequencePreview(); + + /** + * Aborts the currently running hover sequence PreviewJob (if any). + */ + void killHoverSequencePreviewJob(); + /** * Ensures that icons, previews, and other roles are determined for any * items that have been changed. @@ -273,6 +335,8 @@ private: QList indexesToResolve() const; + void trimHoverSequenceLoadedItems(); + private: enum State { Idle, @@ -329,6 +393,14 @@ private: KIO::PreviewJob* m_previewJob; + // Info about the item that the user currently hovers, and the current sequence + // index for thumb generation. + KFileItem m_hoverSequenceItem; + int m_hoverSequenceIndex; + KIO::PreviewJob* m_hoverSequencePreviewJob; + int m_hoverSequenceNumSuccessiveFailures; + std::list m_hoverSequenceLoadedItems; + // When downloading or copying large files, the slot slotItemsChanged() // will be called periodically within a quite short delay. To prevent // a high CPU-load by generating e.g. previews for each notification, the update diff --git a/src/kitemviews/kitemlistview.h b/src/kitemviews/kitemlistview.h index 5453d851f..760e0a415 100644 --- a/src/kitemviews/kitemlistview.h +++ b/src/kitemviews/kitemlistview.h @@ -851,6 +851,7 @@ KItemListWidget* KItemListWidgetCreator::create(KItemListView* view) widget = new T(m_informant, view); addCreatedWidget(widget); } + widget->setParentItem(view); return widget; } diff --git a/src/kitemviews/kitemlistwidget.cpp b/src/kitemviews/kitemlistwidget.cpp index b4c14c1a7..69a38432a 100644 --- a/src/kitemviews/kitemlistwidget.cpp +++ b/src/kitemviews/kitemlistwidget.cpp @@ -11,6 +11,9 @@ #include "kitemlistview.h" #include "private/kitemlistselectiontoggle.h" +#include +#include + #include #include #include @@ -41,9 +44,11 @@ KItemListWidget::KItemListWidget(KItemListWidgetInformant* informant, QGraphicsI m_hoverOpacity(0), m_hoverCache(nullptr), m_hoverAnimation(nullptr), + m_hoverSequenceIndex(0), m_selectionToggle(nullptr), m_editedRole() { + connect(&m_hoverSequenceTimer, &QTimer::timeout, this, &KItemListWidget::slotHoverSequenceTimerTimeout); } KItemListWidget::~KItemListWidget() @@ -240,6 +245,8 @@ void KItemListWidget::setHovered(bool hovered) } m_hoverAnimation->stop(); + m_hoverSequenceIndex = 0; + if (hovered) { const qreal startValue = qMax(hoverOpacity(), qreal(0.1)); m_hoverAnimation->setStartValue(startValue); @@ -247,9 +254,19 @@ void KItemListWidget::setHovered(bool hovered) if (m_enabledSelectionToggle && !(QApplication::mouseButtons() & Qt::LeftButton)) { initializeSelectionToggle(); } + + hoverSequenceStarted(); + + const KConfigGroup globalConfig(KSharedConfig::openConfig(), "PreviewSettings"); + const int interval = globalConfig.readEntry("HoverSequenceInterval", 700); + + m_hoverSequenceTimer.start(interval); } else { m_hoverAnimation->setStartValue(hoverOpacity()); m_hoverAnimation->setEndValue(0.0); + + hoverSequenceEnded(); + m_hoverSequenceTimer.stop(); } m_hoverAnimation->start(); @@ -450,11 +467,29 @@ void KItemListWidget::resizeEvent(QGraphicsSceneResizeEvent* event) } } +void KItemListWidget::hoverSequenceStarted() +{ +} + +void KItemListWidget::hoverSequenceIndexChanged(int sequenceIndex) +{ + Q_UNUSED(sequenceIndex); +} + +void KItemListWidget::hoverSequenceEnded() +{ +} + qreal KItemListWidget::hoverOpacity() const { return m_hoverOpacity; } +int KItemListWidget::hoverSequenceIndex() const +{ + return m_hoverSequenceIndex; +} + void KItemListWidget::slotHoverAnimationFinished() { if (!m_hovered && m_selectionToggle) { @@ -463,6 +498,12 @@ void KItemListWidget::slotHoverAnimationFinished() } } +void KItemListWidget::slotHoverSequenceTimerTimeout() +{ + m_hoverSequenceIndex++; + hoverSequenceIndexChanged(m_hoverSequenceIndex); +} + void KItemListWidget::initializeSelectionToggle() { Q_ASSERT(m_enabledSelectionToggle); diff --git a/src/kitemviews/kitemlistwidget.h b/src/kitemviews/kitemlistwidget.h index 86bbcaf14..2de82c6fb 100644 --- a/src/kitemviews/kitemlistwidget.h +++ b/src/kitemviews/kitemlistwidget.h @@ -15,6 +15,7 @@ #include #include #include +#include class KItemListSelectionToggle; class KItemListView; @@ -190,16 +191,36 @@ protected: virtual void editedRoleChanged(const QByteArray& current, const QByteArray& previous); void resizeEvent(QGraphicsSceneResizeEvent* event) override; + /** + * Called when the user starts hovering this item. + */ + virtual void hoverSequenceStarted(); + + /** + * Called in regular intervals while the user is hovering this item. + * + * @param sequenceIndex An index that increases over time while the user hovers. + */ + virtual void hoverSequenceIndexChanged(int sequenceIndex); + + /** + * Called when the user stops hovering this item. + */ + virtual void hoverSequenceEnded(); + /** * @return The current opacity of the hover-animation. When implementing a custom painting-code for a hover-state * this opacity value should be respected. */ qreal hoverOpacity() const; + int hoverSequenceIndex() const; + const KItemListWidgetInformant* informant() const; private Q_SLOTS: void slotHoverAnimationFinished(); + void slotHoverSequenceTimerTimeout(); private: void initializeSelectionToggle(); @@ -227,6 +248,9 @@ private: mutable QPixmap* m_hoverCache; QPropertyAnimation* m_hoverAnimation; + int m_hoverSequenceIndex; + QTimer m_hoverSequenceTimer; + KItemListSelectionToggle* m_selectionToggle; QByteArray m_editedRole; diff --git a/src/kitemviews/kstandarditemlistwidget.cpp b/src/kitemviews/kstandarditemlistwidget.cpp index e58340fb8..9c527fa17 100644 --- a/src/kitemviews/kstandarditemlistwidget.cpp +++ b/src/kitemviews/kstandarditemlistwidget.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -581,6 +582,13 @@ void KStandardItemListWidget::invalidateCache() m_dirtyContent = true; } +void KStandardItemListWidget::invalidateIconCache() +{ + m_dirtyContent = true; + m_dirtyContentRoles.insert("iconPixmap"); + m_dirtyContentRoles.insert("iconOverlays"); +} + void KStandardItemListWidget::refreshCache() { } @@ -945,7 +953,34 @@ void KStandardItemListWidget::updatePixmapCache() } if (updatePixmap) { - m_pixmap = values["iconPixmap"].value(); + m_pixmap = QPixmap(); + + int sequenceIndex = hoverSequenceIndex(); + + if (values.contains("hoverSequencePixmaps")) { + // Use one of the hover sequence pixmaps instead of the default + // icon pixmap. + + const QVector pixmaps = values["hoverSequencePixmaps"].value>(); + + if (values.contains("hoverSequenceWraparoundPoint")) { + const float wap = values["hoverSequenceWraparoundPoint"].toFloat(); + if (wap >= 1.0f) { + sequenceIndex %= static_cast(wap); + } + } + + const int loadedIndex = qMax(qMin(sequenceIndex, pixmaps.size()-1), 0); + + if (loadedIndex != 0) { + m_pixmap = pixmaps[loadedIndex]; + } + } + + if (m_pixmap.isNull()) { + m_pixmap = values["iconPixmap"].value(); + } + if (m_pixmap.isNull()) { // Use the icon that fits to the MIME-type QString iconName = values["iconName"].toString(); diff --git a/src/kitemviews/kstandarditemlistwidget.h b/src/kitemviews/kstandarditemlistwidget.h index 1c3c61c38..9e57ecfe9 100644 --- a/src/kitemviews/kstandarditemlistwidget.h +++ b/src/kitemviews/kstandarditemlistwidget.h @@ -109,6 +109,12 @@ protected: */ void invalidateCache(); + /** + * Invalidates the icon cache which results in calling KStandardItemListWidget::refreshCache() as + * soon as the item needs to get repainted. + */ + void invalidateIconCache(); + /** * Is called if the cache got invalidated by KStandardItemListWidget::invalidateCache(). * The default implementation is empty. diff --git a/src/settings/general/previewssettingspage.cpp b/src/settings/general/previewssettingspage.cpp index 38caa6b70..564715ae3 100644 --- a/src/settings/general/previewssettingspage.cpp +++ b/src/settings/general/previewssettingspage.cpp @@ -134,6 +134,7 @@ void PreviewsSettingsPage::applySettings() globalConfig.writeEntry("MaximumRemoteSize", maximumRemoteSize, KConfigBase::Normal | KConfigBase::Global); + globalConfig.sync(); } -- 2.47.3