File indexing completed on 2024-05-12 04:19:51

0001 /*
0002 Gwenview: an image viewer
0003 Copyright 2007 Aurélien Gâteau <agateau@kde.org>
0004 
0005 This program is free software; you can redistribute it and/or
0006 modify it under the terms of the GNU General Public License
0007 as published by the Free Software Foundation; either version 2
0008 of the License, or (at your option) any later version.
0009 
0010 This program is distributed in the hope that it will be useful,
0011 but WITHOUT ANY WARRANTY; without even the implied warranty of
0012 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0013 GNU General Public License for more details.
0014 
0015 You should have received a copy of the GNU General Public License
0016 along with this program; if not, write to the Free Software
0017 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
0018 
0019 */
0020 #include "thumbnailview.h"
0021 
0022 // STL
0023 #include <cmath>
0024 
0025 // Qt
0026 #include <QApplication>
0027 #include <QDateTime>
0028 #include <QDrag>
0029 #include <QDragEnterEvent>
0030 #include <QDropEvent>
0031 #include <QMimeData>
0032 #include <QPainter>
0033 #include <QPointer>
0034 #include <QQueue>
0035 #include <QScrollBar>
0036 #include <QScroller>
0037 #include <QTimeLine>
0038 #include <QTimer>
0039 
0040 // KF
0041 #include <KDirModel>
0042 #include <KIconLoader>
0043 #include <KPixmapSequence>
0044 #include <KPixmapSequenceLoader>
0045 #include <KUrlMimeData>
0046 
0047 // Local
0048 #include "abstractdocumentinfoprovider.h"
0049 #include "abstractthumbnailviewhelper.h"
0050 #include "dragpixmapgenerator.h"
0051 #include "gwenview_lib_debug.h"
0052 #include "gwenviewconfig.h"
0053 #include "mimetypeutils.h"
0054 #include "urlutils.h"
0055 #include <lib/gvdebug.h>
0056 #include <lib/scrollerutils.h>
0057 #include <lib/thumbnailprovider/thumbnailprovider.h>
0058 #include <lib/touch/touch.h>
0059 
0060 namespace Gwenview
0061 {
0062 #undef ENABLE_LOG
0063 #undef LOG
0064 // #define ENABLE_LOG
0065 #ifdef ENABLE_LOG
0066 #define LOG(x) // qCDebug(GWENVIEW_LIB_LOG) << x
0067 #else
0068 #define LOG(x) ;
0069 #endif
0070 
0071 /** How many msec to wait before starting to smooth thumbnails */
0072 const int SMOOTH_DELAY = 500;
0073 
0074 const int WHEEL_ZOOM_MULTIPLIER = 4;
0075 
0076 static KFileItem fileItemForIndex(const QModelIndex &index)
0077 {
0078     if (!index.isValid()) {
0079         LOG("Invalid index");
0080         return {};
0081     }
0082     QVariant data = index.data(KDirModel::FileItemRole);
0083     return qvariant_cast<KFileItem>(data);
0084 }
0085 
0086 static QUrl urlForIndex(const QModelIndex &index)
0087 {
0088     KFileItem item = fileItemForIndex(index);
0089     return item.isNull() ? QUrl() : item.url();
0090 }
0091 
0092 struct Thumbnail {
0093     Thumbnail(const QPersistentModelIndex &index_, const QDateTime &mtime)
0094         : mIndex(index_)
0095         , mModificationTime(mtime)
0096         , mFileSize(0)
0097         , mRough(true)
0098         , mWaitingForThumbnail(true)
0099     {
0100     }
0101 
0102     Thumbnail()
0103         : mFileSize(0)
0104         , mRough(true)
0105         , mWaitingForThumbnail(true)
0106     {
0107     }
0108 
0109     /**
0110      * Init the thumbnail based on a icon
0111      */
0112     void initAsIcon(const QPixmap &pix)
0113     {
0114         mGroupPix = pix;
0115         int largeGroupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::Large);
0116         mFullSize = QSize(largeGroupSize, largeGroupSize);
0117     }
0118 
0119     bool isGroupPixAdaptedForSize(int size) const
0120     {
0121         if (mWaitingForThumbnail) {
0122             return false;
0123         }
0124         if (mGroupPix.isNull()) {
0125             return false;
0126         }
0127         const int groupSize = qMax(mGroupPix.width(), mGroupPix.height());
0128         if (groupSize >= size) {
0129             return true;
0130         }
0131 
0132         // groupSize is less than size, but this may be because the full image
0133         // is the same size as groupSize
0134         return groupSize == qMax(mFullSize.width(), mFullSize.height());
0135     }
0136 
0137     void prepareForRefresh(const QDateTime &mtime)
0138     {
0139         mModificationTime = mtime;
0140         mFileSize = 0;
0141         mGroupPix = QPixmap();
0142         mAdjustedPix = QPixmap();
0143         mFullSize = QSize();
0144         mRealFullSize = QSize();
0145         mRough = true;
0146         mWaitingForThumbnail = true;
0147     }
0148 
0149     QPersistentModelIndex mIndex;
0150     QDateTime mModificationTime;
0151     /// The pix loaded from .thumbnails/{large,normal}
0152     QPixmap mGroupPix;
0153     /// Scaled version of mGroupPix, adjusted to ThumbnailView::thumbnailSize
0154     QPixmap mAdjustedPix;
0155     /// Size of the full image
0156     QSize mFullSize;
0157     /// Real size of the full image, invalid unless the thumbnail
0158     /// represents a raster image (not an icon)
0159     QSize mRealFullSize;
0160     /// File size of the full image
0161     KIO::filesize_t mFileSize;
0162     /// Whether mAdjustedPix represents has been scaled using fast or smooth
0163     /// transformation
0164     bool mRough;
0165     /// Set to true if mGroupPix should be replaced with a real thumbnail
0166     bool mWaitingForThumbnail;
0167 };
0168 
0169 using ThumbnailForUrl = QHash<QUrl, Thumbnail>;
0170 using UrlQueue = QQueue<QUrl>;
0171 using PersistentModelIndexSet = QSet<QPersistentModelIndex>;
0172 
0173 struct ThumbnailViewPrivate {
0174     ThumbnailView *q;
0175     ThumbnailView::ThumbnailScaleMode mScaleMode;
0176     QSize mThumbnailSize;
0177     qreal mThumbnailAspectRatio;
0178     AbstractDocumentInfoProvider *mDocumentInfoProvider;
0179     AbstractThumbnailViewHelper *mThumbnailViewHelper;
0180     ThumbnailForUrl mThumbnailForUrl;
0181     QTimer mScheduledThumbnailGenerationTimer;
0182 
0183     UrlQueue mSmoothThumbnailQueue;
0184     QTimer mSmoothThumbnailTimer;
0185 
0186     QPixmap mWaitingThumbnail;
0187     QPointer<ThumbnailProvider> mThumbnailProvider;
0188 
0189     PersistentModelIndexSet mBusyIndexSet;
0190     KPixmapSequence mBusySequence;
0191     QTimeLine *mBusyAnimationTimeLine;
0192 
0193     bool mCreateThumbnailsForRemoteUrls;
0194 
0195     QScroller *mScroller;
0196     Touch *mTouch;
0197 
0198     void setupBusyAnimation()
0199     {
0200         mBusySequence = KPixmapSequenceLoader::load(QStringLiteral("process-working"), 22);
0201         mBusyAnimationTimeLine = new QTimeLine(100 * mBusySequence.frameCount(), q);
0202         mBusyAnimationTimeLine->setEasingCurve(QEasingCurve::Linear);
0203         mBusyAnimationTimeLine->setEndFrame(mBusySequence.frameCount() - 1);
0204         mBusyAnimationTimeLine->setLoopCount(0);
0205         QObject::connect(mBusyAnimationTimeLine, &QTimeLine::frameChanged, q, &ThumbnailView::updateBusyIndexes);
0206     }
0207 
0208     void scheduleThumbnailGeneration()
0209     {
0210         if (mThumbnailProvider) {
0211             mThumbnailProvider->removePendingItems();
0212         }
0213         mSmoothThumbnailQueue.clear();
0214         if (!mScheduledThumbnailGenerationTimer.isActive()) {
0215             mScheduledThumbnailGenerationTimer.start();
0216         }
0217     }
0218 
0219     void updateThumbnailForModifiedDocument(const QModelIndex &index)
0220     {
0221         Q_ASSERT(mDocumentInfoProvider);
0222         KFileItem item = fileItemForIndex(index);
0223         QUrl url = item.url();
0224         ThumbnailGroup::Enum group = ThumbnailGroup::fromPixelSize(mThumbnailSize.width());
0225         QPixmap pix;
0226         QSize fullSize;
0227         mDocumentInfoProvider->thumbnailForDocument(url, group, &pix, &fullSize);
0228         mThumbnailForUrl[url] = Thumbnail(QPersistentModelIndex(index), QDateTime::currentDateTime());
0229         q->setThumbnail(item, pix, fullSize, 0);
0230     }
0231 
0232     void appendItemsToThumbnailProvider(const KFileItemList &list)
0233     {
0234         if (mThumbnailProvider) {
0235             ThumbnailGroup::Enum group = ThumbnailGroup::fromPixelSize(mThumbnailSize.width());
0236             mThumbnailProvider->setThumbnailGroup(group);
0237             mThumbnailProvider->appendItems(list);
0238         }
0239     }
0240 
0241     void roughAdjustThumbnail(Thumbnail *thumbnail)
0242     {
0243         const QPixmap &mGroupPix = thumbnail->mGroupPix;
0244         const int groupSize = qMax(mGroupPix.width(), mGroupPix.height());
0245         const int fullSize = qMax(thumbnail->mFullSize.width(), thumbnail->mFullSize.height());
0246         if (fullSize == groupSize && mGroupPix.height() <= mThumbnailSize.height() && mGroupPix.width() <= mThumbnailSize.width()) {
0247             thumbnail->mAdjustedPix = mGroupPix;
0248             thumbnail->mRough = false;
0249         } else {
0250             thumbnail->mAdjustedPix = scale(mGroupPix, Qt::FastTransformation);
0251             thumbnail->mRough = true;
0252         }
0253     }
0254 
0255     void initDragPixmap(QDrag *drag, const QModelIndexList &indexes)
0256     {
0257         const int thumbCount = qMin(indexes.count(), int(DragPixmapGenerator::MaxCount));
0258         QList<QPixmap> lst;
0259         for (int row = 0; row < thumbCount; ++row) {
0260             const QUrl url = urlForIndex(indexes[row]);
0261             lst << mThumbnailForUrl.value(url).mAdjustedPix;
0262         }
0263         DragPixmapGenerator::DragPixmap dragPixmap = DragPixmapGenerator::generate(lst, indexes.count());
0264         drag->setPixmap(dragPixmap.pix);
0265         drag->setHotSpot(dragPixmap.hotSpot);
0266     }
0267 
0268     QPixmap scale(const QPixmap &pix, Qt::TransformationMode transformationMode)
0269     {
0270         switch (mScaleMode) {
0271         case ThumbnailView::ScaleToFit:
0272             return pix.scaled(mThumbnailSize.width(), mThumbnailSize.height(), Qt::KeepAspectRatio, transformationMode);
0273         case ThumbnailView::ScaleToSquare: {
0274             int minSize = qMin(pix.width(), pix.height());
0275             QPixmap pix2 = pix.copy((pix.width() - minSize) / 2, (pix.height() - minSize) / 2, minSize, minSize);
0276             return pix2.scaled(mThumbnailSize.width(), mThumbnailSize.height(), Qt::KeepAspectRatio, transformationMode);
0277         }
0278         case ThumbnailView::ScaleToHeight:
0279             return pix.scaledToHeight(mThumbnailSize.height(), transformationMode);
0280         case ThumbnailView::ScaleToWidth:
0281             return pix.scaledToWidth(mThumbnailSize.width(), transformationMode);
0282         }
0283         // Keep compiler happy
0284         Q_ASSERT(0);
0285         return {};
0286     }
0287 };
0288 
0289 ThumbnailView::ThumbnailView(QWidget *parent)
0290     : QListView(parent)
0291     , d(new ThumbnailViewPrivate)
0292 {
0293     d->q = this;
0294     d->mScaleMode = ScaleToFit;
0295     d->mThumbnailViewHelper = nullptr;
0296     d->mDocumentInfoProvider = nullptr;
0297     d->mThumbnailProvider = nullptr;
0298     // Init to some stupid value so that the first call to setThumbnailSize()
0299     // is not ignored (do not use 0 in case someone try to divide by
0300     // mThumbnailSize...)
0301     d->mThumbnailSize = QSize(1, 1);
0302     d->mThumbnailAspectRatio = 1;
0303     d->mCreateThumbnailsForRemoteUrls = true;
0304 
0305     setFrameShape(QFrame::NoFrame);
0306     setViewMode(QListView::IconMode);
0307     setResizeMode(QListView::Adjust);
0308     setDragEnabled(true);
0309     setAcceptDrops(true);
0310     setDropIndicatorShown(true);
0311     setUniformItemSizes(true);
0312     setEditTriggers(QAbstractItemView::EditKeyPressed);
0313 
0314     d->setupBusyAnimation();
0315 
0316     setVerticalScrollMode(ScrollPerPixel);
0317     setHorizontalScrollMode(ScrollPerPixel);
0318 
0319     d->mScheduledThumbnailGenerationTimer.setSingleShot(true);
0320     d->mScheduledThumbnailGenerationTimer.setInterval(500);
0321     connect(&d->mScheduledThumbnailGenerationTimer, &QTimer::timeout, this, &ThumbnailView::generateThumbnailsForItems);
0322 
0323     d->mSmoothThumbnailTimer.setSingleShot(true);
0324     connect(&d->mSmoothThumbnailTimer, &QTimer::timeout, this, &ThumbnailView::smoothNextThumbnail);
0325 
0326     setContextMenuPolicy(Qt::CustomContextMenu);
0327     connect(this, &ThumbnailView::customContextMenuRequested, this, &ThumbnailView::showContextMenu);
0328 
0329     connect(this, &ThumbnailView::activated, this, &ThumbnailView::emitIndexActivatedIfNoModifiers);
0330 
0331     d->mScroller = ScrollerUtils::setQScroller(this->viewport());
0332     d->mTouch = new Touch(viewport());
0333     connect(d->mTouch, &Touch::twoFingerTapTriggered, this, &ThumbnailView::showContextMenu);
0334     connect(d->mTouch, &Touch::pinchZoomTriggered, this, &ThumbnailView::zoomGesture);
0335     connect(d->mTouch, &Touch::pinchGestureStarted, this, &ThumbnailView::setZoomParameter);
0336     connect(d->mTouch, &Touch::tapTriggered, this, &ThumbnailView::tapGesture);
0337     connect(d->mTouch, &Touch::tapHoldAndMovingTriggered, this, &ThumbnailView::startDragFromTouch);
0338 
0339     const QFontMetrics metrics(viewport()->font());
0340     const int singleStep = metrics.height() * QApplication::wheelScrollLines();
0341 
0342     verticalScrollBar()->setSingleStep(singleStep);
0343     horizontalScrollBar()->setSingleStep(singleStep);
0344 }
0345 
0346 ThumbnailView::~ThumbnailView()
0347 {
0348     delete d->mTouch;
0349     delete d;
0350 }
0351 
0352 ThumbnailView::ThumbnailScaleMode ThumbnailView::thumbnailScaleMode() const
0353 {
0354     return d->mScaleMode;
0355 }
0356 
0357 void ThumbnailView::setThumbnailScaleMode(ThumbnailScaleMode mode)
0358 {
0359     d->mScaleMode = mode;
0360     setUniformItemSizes(mode == ScaleToFit || mode == ScaleToSquare);
0361 }
0362 
0363 void ThumbnailView::setModel(QAbstractItemModel *newModel)
0364 {
0365     if (model()) {
0366         disconnect(model(), nullptr, this, nullptr);
0367     }
0368     QListView::setModel(newModel);
0369 
0370     connect(model(), &QAbstractItemModel::rowsRemoved, this, [=](const QModelIndex &index, int first, int last) {
0371         // Avoid the delegate doing a ton of work if we're not visible
0372         if (isVisible()) {
0373             Q_EMIT rowsRemovedSignal(index, first, last);
0374         }
0375     });
0376 }
0377 
0378 void ThumbnailView::setThumbnailProvider(ThumbnailProvider *thumbnailProvider)
0379 {
0380     GV_RETURN_IF_FAIL(d->mThumbnailProvider != thumbnailProvider);
0381     if (thumbnailProvider) {
0382         connect(thumbnailProvider, &ThumbnailProvider::thumbnailLoaded, this, &ThumbnailView::setThumbnail);
0383         connect(thumbnailProvider, &ThumbnailProvider::thumbnailLoadingFailed, this, &ThumbnailView::setBrokenThumbnail);
0384     } else {
0385         disconnect(d->mThumbnailProvider, nullptr, this, nullptr);
0386     }
0387     d->mThumbnailProvider = thumbnailProvider;
0388 }
0389 
0390 void ThumbnailView::updateThumbnailSize()
0391 {
0392     QSize value = d->mThumbnailSize;
0393     // mWaitingThumbnail
0394     const auto dpr = devicePixelRatioF();
0395     int waitingThumbnailSize;
0396     if (value.width() > 64 * dpr) {
0397         waitingThumbnailSize = qRound(48 * dpr);
0398     } else {
0399         waitingThumbnailSize = qRound(32 * dpr);
0400     }
0401     QPixmap icon = QIcon::fromTheme(QStringLiteral("chronometer")).pixmap(waitingThumbnailSize);
0402     QPixmap pix(value);
0403     pix.fill(Qt::transparent);
0404     QPainter painter(&pix);
0405     painter.setOpacity(0.5);
0406     painter.drawPixmap((value.width() - icon.width()) / 2, (value.height() - icon.height()) / 2, icon);
0407     painter.end();
0408     d->mWaitingThumbnail = pix;
0409     d->mWaitingThumbnail.setDevicePixelRatio(dpr);
0410 
0411     // Stop smoothing
0412     d->mSmoothThumbnailTimer.stop();
0413     d->mSmoothThumbnailQueue.clear();
0414 
0415     // Clear adjustedPixes
0416     ThumbnailForUrl::iterator it = d->mThumbnailForUrl.begin(), end = d->mThumbnailForUrl.end();
0417     for (; it != end; ++it) {
0418         it.value().mAdjustedPix = QPixmap();
0419     }
0420 
0421     Q_EMIT thumbnailSizeChanged(value / dpr);
0422     Q_EMIT thumbnailWidthChanged(qRound(value.width() / dpr));
0423     if (d->mScaleMode != ScaleToFit) {
0424         scheduleDelayedItemsLayout();
0425     }
0426     d->scheduleThumbnailGeneration();
0427 }
0428 
0429 void ThumbnailView::setThumbnailWidth(int width)
0430 {
0431     const auto dpr = devicePixelRatioF();
0432     const qreal newWidthF = width * dpr;
0433     const int newWidth = qRound(newWidthF);
0434     if (d->mThumbnailSize.width() == newWidth) {
0435         return;
0436     }
0437     int height = qRound(newWidthF / d->mThumbnailAspectRatio);
0438     d->mThumbnailSize = QSize(newWidth, height);
0439     updateThumbnailSize();
0440 }
0441 
0442 void ThumbnailView::setThumbnailAspectRatio(qreal ratio)
0443 {
0444     if (d->mThumbnailAspectRatio == ratio) {
0445         return;
0446     }
0447     d->mThumbnailAspectRatio = ratio;
0448     int width = d->mThumbnailSize.width();
0449     int height = round((qreal)width / d->mThumbnailAspectRatio);
0450     d->mThumbnailSize = QSize(width, height);
0451     updateThumbnailSize();
0452 }
0453 
0454 qreal ThumbnailView::thumbnailAspectRatio() const
0455 {
0456     return d->mThumbnailAspectRatio;
0457 }
0458 
0459 QSize ThumbnailView::thumbnailSize() const
0460 {
0461     return d->mThumbnailSize / devicePixelRatioF();
0462 }
0463 
0464 void ThumbnailView::setThumbnailViewHelper(AbstractThumbnailViewHelper *helper)
0465 {
0466     d->mThumbnailViewHelper = helper;
0467 }
0468 
0469 AbstractThumbnailViewHelper *ThumbnailView::thumbnailViewHelper() const
0470 {
0471     return d->mThumbnailViewHelper;
0472 }
0473 
0474 void ThumbnailView::setDocumentInfoProvider(AbstractDocumentInfoProvider *provider)
0475 {
0476     d->mDocumentInfoProvider = provider;
0477     if (provider) {
0478         connect(provider, &AbstractDocumentInfoProvider::busyStateChanged, this, &ThumbnailView::updateThumbnailBusyState);
0479         connect(provider, &AbstractDocumentInfoProvider::documentChanged, this, &ThumbnailView::updateThumbnail);
0480     }
0481 }
0482 
0483 AbstractDocumentInfoProvider *ThumbnailView::documentInfoProvider() const
0484 {
0485     return d->mDocumentInfoProvider;
0486 }
0487 
0488 void ThumbnailView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
0489 {
0490     QListView::rowsAboutToBeRemoved(parent, start, end);
0491 
0492     // Remove references to removed items
0493     KFileItemList itemList;
0494     for (int pos = start; pos <= end; ++pos) {
0495         QModelIndex index = model()->index(pos, 0, parent);
0496         KFileItem item = fileItemForIndex(index);
0497         if (item.isNull()) {
0498             // qCDebug(GWENVIEW_LIB_LOG) << "Skipping invalid item!" << index.data().toString();
0499             continue;
0500         }
0501 
0502         QUrl url = item.url();
0503         d->mThumbnailForUrl.remove(url);
0504         d->mSmoothThumbnailQueue.removeAll(url);
0505 
0506         itemList.append(item);
0507     }
0508 
0509     if (d->mThumbnailProvider) {
0510         d->mThumbnailProvider->removeItems(itemList);
0511     }
0512 
0513     // Removing rows might make new images visible, make sure their thumbnail
0514     // is generated
0515     if (!d->mScheduledThumbnailGenerationTimer.isActive()) {
0516         d->mScheduledThumbnailGenerationTimer.start();
0517     }
0518 }
0519 
0520 void ThumbnailView::rowsInserted(const QModelIndex &parent, int start, int end)
0521 {
0522     QListView::rowsInserted(parent, start, end);
0523 
0524     if (!d->mScheduledThumbnailGenerationTimer.isActive()) {
0525         d->mScheduledThumbnailGenerationTimer.start();
0526     }
0527 
0528     if (isVisible()) {
0529         Q_EMIT rowsInsertedSignal(parent, start, end);
0530     }
0531 }
0532 
0533 void ThumbnailView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
0534 {
0535     QListView::dataChanged(topLeft, bottomRight, roles);
0536     bool thumbnailsNeedRefresh = false;
0537     for (int row = topLeft.row(); row <= bottomRight.row(); ++row) {
0538         QModelIndex index = model()->index(row, 0);
0539         KFileItem item = fileItemForIndex(index);
0540         if (item.isNull()) {
0541             qCWarning(GWENVIEW_LIB_LOG) << "Invalid item for index" << index << ". This should not happen!";
0542             GV_FATAL_FAILS;
0543             continue;
0544         }
0545 
0546         ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(item.url());
0547         if (it != d->mThumbnailForUrl.end()) {
0548             // All thumbnail views are connected to the model, so
0549             // ThumbnailView::dataChanged() is called for all of them. As a
0550             // result this method will also be called for views which are not
0551             // currently visible, and do not yet have a thumbnail for the
0552             // modified url.
0553             QDateTime mtime = item.time(KFileItem::ModificationTime);
0554             if (it->mModificationTime != mtime || it->mFileSize != item.size()) {
0555                 // dataChanged() is called when the file changes but also when
0556                 // the model fetched additional data such as semantic info. To
0557                 // avoid needless refreshes, we only trigger a refresh if the
0558                 // modification time changes.
0559                 thumbnailsNeedRefresh = true;
0560                 it->prepareForRefresh(mtime);
0561             }
0562         }
0563     }
0564     if (thumbnailsNeedRefresh && !d->mScheduledThumbnailGenerationTimer.isActive()) {
0565         d->mScheduledThumbnailGenerationTimer.start();
0566     }
0567 }
0568 
0569 void ThumbnailView::showContextMenu()
0570 {
0571     d->mThumbnailViewHelper->showContextMenu(this);
0572 }
0573 
0574 void ThumbnailView::emitIndexActivatedIfNoModifiers(const QModelIndex &index)
0575 {
0576     if (QApplication::keyboardModifiers() == Qt::NoModifier) {
0577         Q_EMIT indexActivated(index);
0578     }
0579 }
0580 
0581 void ThumbnailView::setThumbnail(const KFileItem &item, const QPixmap &pixmap, const QSize &size, qulonglong fileSize)
0582 {
0583     ThumbnailForUrl::iterator it = d->mThumbnailForUrl.find(item.url());
0584     if (it == d->mThumbnailForUrl.end()) {
0585         return;
0586     }
0587     Thumbnail &thumbnail = it.value();
0588     thumbnail.mGroupPix = pixmap;
0589     thumbnail.mAdjustedPix = QPixmap();
0590     int largeGroupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::XLarge);
0591     thumbnail.mFullSize = size.isValid() ? size : QSize(largeGroupSize, largeGroupSize);
0592     thumbnail.mRealFullSize = size;
0593     thumbnail.mWaitingForThumbnail = false;
0594     thumbnail.mFileSize = fileSize;
0595 
0596     update(thumbnail.mIndex);
0597     if (d->mScaleMode != ScaleToFit) {
0598         scheduleDelayedItemsLayout();
0599     }
0600 }
0601 
0602 void ThumbnailView::setBrokenThumbnail(const KFileItem &item)
0603 {
0604     ThumbnailForUrl::iterator it = d->mThumbnailForUrl.find(item.url());
0605     if (it == d->mThumbnailForUrl.end()) {
0606         return;
0607     }
0608     Thumbnail &thumbnail = it.value();
0609     MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item);
0610     if (kind == MimeTypeUtils::KIND_VIDEO) {
0611         // Special case for videos because our kde install may come without
0612         // support for video thumbnails so we show the mimetype icon instead of
0613         // a broken image icon
0614         const QPixmap pix = QIcon::fromTheme(item.iconName()).pixmap(d->mThumbnailSize.height());
0615         thumbnail.initAsIcon(pix);
0616     } else if (kind == MimeTypeUtils::KIND_DIR) {
0617         // Special case for folders because ThumbnailProvider does not return a
0618         // thumbnail if there is no images
0619         thumbnail.mWaitingForThumbnail = false;
0620         return;
0621     } else {
0622         thumbnail.initAsIcon(QIcon::fromTheme(QStringLiteral("image-missing")).pixmap(48));
0623         thumbnail.mFullSize = thumbnail.mGroupPix.size();
0624     }
0625     update(thumbnail.mIndex);
0626 }
0627 
0628 QPixmap ThumbnailView::thumbnailForIndex(const QModelIndex &index, QSize *fullSize)
0629 {
0630     KFileItem item = fileItemForIndex(index);
0631     if (item.isNull()) {
0632         LOG("Invalid item");
0633         if (fullSize) {
0634             *fullSize = QSize();
0635         }
0636         return {};
0637     }
0638     QUrl url = item.url();
0639 
0640     // Find or create Thumbnail instance
0641     ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url);
0642     if (it == d->mThumbnailForUrl.end()) {
0643         Thumbnail thumbnail = Thumbnail(QPersistentModelIndex(index), item.time(KFileItem::ModificationTime));
0644         it = d->mThumbnailForUrl.insert(url, thumbnail);
0645     }
0646     Thumbnail &thumbnail = it.value();
0647 
0648     // If dir or archive, generate a thumbnail from fileitem pixmap
0649     MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item);
0650     if (kind == MimeTypeUtils::KIND_ARCHIVE || kind == MimeTypeUtils::KIND_DIR) {
0651         int groupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::fromPixelSize(d->mThumbnailSize.height()));
0652         if (thumbnail.mGroupPix.isNull() || thumbnail.mGroupPix.height() < groupSize) {
0653             const QPixmap pix = QIcon::fromTheme(item.iconName()).pixmap(d->mThumbnailSize.height());
0654 
0655             thumbnail.initAsIcon(pix);
0656             if (kind == MimeTypeUtils::KIND_ARCHIVE) {
0657                 // No thumbnails for archives
0658                 thumbnail.mWaitingForThumbnail = false;
0659             } else if (!d->mCreateThumbnailsForRemoteUrls && !UrlUtils::urlIsFastLocalFile(url)) {
0660                 // If we don't want thumbnails for remote urls, use
0661                 // "folder-remote" icon for remote folders, so that they do
0662                 // not look like regular folders
0663                 thumbnail.mWaitingForThumbnail = false;
0664                 thumbnail.initAsIcon(QIcon::fromTheme(QStringLiteral("folder-remote")).pixmap(groupSize));
0665             } else {
0666                 // set mWaitingForThumbnail to true (necessary in the case
0667                 // 'thumbnail' already existed before, but with a too small
0668                 // mGroupPix)
0669                 thumbnail.mWaitingForThumbnail = true;
0670             }
0671         }
0672     }
0673 
0674     if (thumbnail.mGroupPix.isNull()) {
0675         if (fullSize) {
0676             *fullSize = QSize();
0677         }
0678         return d->mWaitingThumbnail;
0679     }
0680 
0681     // Adjust thumbnail
0682     if (thumbnail.mAdjustedPix.isNull()) {
0683         d->roughAdjustThumbnail(&thumbnail);
0684     }
0685     if (GwenviewConfig::lowResourceUsageMode() && thumbnail.mRough && !d->mSmoothThumbnailQueue.contains(url)) {
0686         d->mSmoothThumbnailQueue.enqueue(url);
0687         if (!d->mSmoothThumbnailTimer.isActive()) {
0688             d->mSmoothThumbnailTimer.start(SMOOTH_DELAY);
0689         }
0690     }
0691     if (fullSize) {
0692         *fullSize = thumbnail.mRealFullSize;
0693     }
0694     thumbnail.mAdjustedPix.setDevicePixelRatio(devicePixelRatioF());
0695     return thumbnail.mAdjustedPix;
0696 }
0697 
0698 bool ThumbnailView::isModified(const QModelIndex &index) const
0699 {
0700     if (!d->mDocumentInfoProvider) {
0701         return false;
0702     }
0703     QUrl url = urlForIndex(index);
0704     return d->mDocumentInfoProvider->isModified(url);
0705 }
0706 
0707 bool ThumbnailView::isBusy(const QModelIndex &index) const
0708 {
0709     if (!d->mDocumentInfoProvider) {
0710         return false;
0711     }
0712     QUrl url = urlForIndex(index);
0713     return d->mDocumentInfoProvider->isBusy(url);
0714 }
0715 
0716 void ThumbnailView::startDrag(Qt::DropActions)
0717 {
0718     const QModelIndexList indexes = selectionModel()->selectedIndexes();
0719     if (indexes.isEmpty()) {
0720         return;
0721     }
0722 
0723     KFileItemList selectedFiles;
0724     for (const auto &index : indexes) {
0725         selectedFiles << fileItemForIndex(index);
0726     }
0727 
0728     auto drag = new QDrag(this);
0729     auto *mimeData = MimeTypeUtils::selectionMimeData(selectedFiles, MimeTypeUtils::DropTarget);
0730     KUrlMimeData::exportUrlsToPortal(mimeData);
0731     drag->setMimeData(mimeData);
0732     d->initDragPixmap(drag, indexes);
0733     drag->exec(Qt::MoveAction | Qt::CopyAction | Qt::LinkAction, Qt::CopyAction);
0734 }
0735 
0736 void ThumbnailView::setZoomParameter()
0737 {
0738     const qreal sensitivityModifier = 0.25;
0739     d->mTouch->setZoomParameter(sensitivityModifier, thumbnailSize().width());
0740 }
0741 
0742 void ThumbnailView::zoomGesture(qreal newZoom, const QPoint &)
0743 {
0744     if (newZoom >= 0.0) {
0745         int width = qBound(int(MinThumbnailSize), static_cast<int>(newZoom), int(MaxThumbnailSize));
0746         setThumbnailWidth(width);
0747     }
0748 }
0749 
0750 void ThumbnailView::tapGesture(const QPoint &pos)
0751 {
0752     const QRect rect = QRect(pos, QSize(1, 1));
0753     setSelection(rect, QItemSelectionModel::ClearAndSelect);
0754     Q_EMIT activated(indexAt(pos));
0755 }
0756 
0757 void ThumbnailView::startDragFromTouch(const QPoint &pos)
0758 {
0759     QModelIndex index = indexAt(pos);
0760     if (index.isValid()) {
0761         setCurrentIndex(index);
0762         d->mScroller->stop();
0763         startDrag(Qt::CopyAction);
0764     }
0765 }
0766 
0767 void ThumbnailView::dragEnterEvent(QDragEnterEvent *event)
0768 {
0769     QAbstractItemView::dragEnterEvent(event);
0770     if (event->mimeData()->hasUrls()) {
0771         event->acceptProposedAction();
0772     }
0773 }
0774 
0775 void ThumbnailView::dragMoveEvent(QDragMoveEvent *event)
0776 {
0777     // Necessary, otherwise we don't reach dropEvent()
0778     QAbstractItemView::dragMoveEvent(event);
0779     event->acceptProposedAction();
0780 }
0781 
0782 void ThumbnailView::dropEvent(QDropEvent *event)
0783 {
0784     const QList<QUrl> urlList = KUrlMimeData::urlsFromMimeData(event->mimeData());
0785     if (urlList.isEmpty()) {
0786         return;
0787     }
0788 
0789     QModelIndex destIndex = indexAt(event->pos());
0790     if (destIndex.isValid()) {
0791         KFileItem item = fileItemForIndex(destIndex);
0792         if (item.isDir()) {
0793             QUrl destUrl = item.url();
0794             d->mThumbnailViewHelper->showMenuForUrlDroppedOnDir(this, urlList, destUrl);
0795             return;
0796         }
0797     }
0798 
0799     d->mThumbnailViewHelper->showMenuForUrlDroppedOnViewport(this, urlList);
0800 
0801     event->acceptProposedAction();
0802 }
0803 
0804 void ThumbnailView::keyPressEvent(QKeyEvent *event)
0805 {
0806     if (event->key() == Qt::Key_Return) {
0807         const QModelIndex index = selectionModel()->currentIndex();
0808         if (index.isValid() && selectionModel()->selectedIndexes().count() == 1) {
0809             Q_EMIT indexActivated(index);
0810         }
0811     } else if (event->key() == Qt::Key_Left && event->modifiers() == Qt::NoModifier) {
0812         if (flow() == LeftToRight && QApplication::isRightToLeft()) {
0813             setCurrentIndex(moveCursor(QAbstractItemView::MoveRight, Qt::NoModifier));
0814         } else {
0815             setCurrentIndex(moveCursor(QAbstractItemView::MoveLeft, Qt::NoModifier));
0816         }
0817         return;
0818     } else if (event->key() == Qt::Key_Right && event->modifiers() == Qt::NoModifier) {
0819         if (flow() == LeftToRight && QApplication::isRightToLeft()) {
0820             setCurrentIndex(moveCursor(QAbstractItemView::MoveLeft, Qt::NoModifier));
0821         } else {
0822             setCurrentIndex(moveCursor(QAbstractItemView::MoveRight, Qt::NoModifier));
0823         }
0824         return;
0825     }
0826 
0827     QListView::keyPressEvent(event);
0828 }
0829 
0830 void ThumbnailView::resizeEvent(QResizeEvent *event)
0831 {
0832     QListView::resizeEvent(event);
0833     d->scheduleThumbnailGeneration();
0834 }
0835 
0836 void ThumbnailView::showEvent(QShowEvent *event)
0837 {
0838     QListView::showEvent(event);
0839     d->scheduleThumbnailGeneration();
0840     QTimer::singleShot(0, this, &ThumbnailView::scrollToSelectedIndex);
0841 }
0842 
0843 void ThumbnailView::wheelEvent(QWheelEvent *event)
0844 {
0845     // If we don't adjust the single step, the wheel scroll exactly one item up
0846     // and down, giving the impression that the items do not move but only
0847     // their label changes.
0848     // For some reason it is necessary to set the step here: setting it in
0849     // setThumbnailSize() does not work
0850     // verticalScrollBar()->setSingleStep(d->mThumbnailSize / 5);
0851     if (event->modifiers() == Qt::ControlModifier) {
0852         int width = thumbnailSize().width() + (event->angleDelta().y() > 0 ? 1 : -1) * WHEEL_ZOOM_MULTIPLIER;
0853         width = qMax(int(MinThumbnailSize), qMin(width, int(MaxThumbnailSize)));
0854         setThumbnailWidth(width);
0855     } else {
0856         QListView::wheelEvent(event);
0857     }
0858 }
0859 
0860 void ThumbnailView::mousePressEvent(QMouseEvent *event)
0861 {
0862     switch (event->button()) {
0863     case Qt::ForwardButton:
0864     case Qt::BackButton:
0865         return;
0866     default:
0867         QListView::mousePressEvent(event);
0868     }
0869 }
0870 
0871 void ThumbnailView::scrollToSelectedIndex()
0872 {
0873     QModelIndexList list = selectedIndexes();
0874     if (list.count() >= 1) {
0875         scrollTo(list.first(), PositionAtCenter);
0876     }
0877 }
0878 
0879 void ThumbnailView::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
0880 {
0881     QListView::selectionChanged(selected, deselected);
0882     Q_EMIT selectionChangedSignal(selected, deselected);
0883 }
0884 
0885 void ThumbnailView::scrollContentsBy(int dx, int dy)
0886 {
0887     QListView::scrollContentsBy(dx, dy);
0888     d->scheduleThumbnailGeneration();
0889 }
0890 
0891 void ThumbnailView::generateThumbnailsForItems()
0892 {
0893     if (!isVisible() || !model()) {
0894         return;
0895     }
0896     const QRect visibleRect = viewport()->rect();
0897     const int visibleSurface = visibleRect.width() * visibleRect.height();
0898     const QPoint origin = visibleRect.center();
0899     // Keep thumbnails around that are at most two "screen heights" away.
0900     const int discardDistance = visibleRect.bottomRight().manhattanLength() * 2;
0901 
0902     // distance => item
0903     QMultiMap<int, KFileItem> itemMap;
0904 
0905     for (int row = 0; row < model()->rowCount(); ++row) {
0906         QModelIndex index = model()->index(row, 0);
0907         KFileItem item = fileItemForIndex(index);
0908         QUrl url = item.url();
0909 
0910         // Filter out remote items if necessary
0911         if (!d->mCreateThumbnailsForRemoteUrls && !url.isLocalFile()) {
0912             continue;
0913         }
0914 
0915         // Filter out archives
0916         MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item);
0917         if (kind == MimeTypeUtils::KIND_ARCHIVE) {
0918             continue;
0919         }
0920 
0921         // Immediately update modified items
0922         if (d->mDocumentInfoProvider && d->mDocumentInfoProvider->isModified(url)) {
0923             d->updateThumbnailForModifiedDocument(index);
0924             continue;
0925         }
0926 
0927         ThumbnailForUrl::ConstIterator it = d->mThumbnailForUrl.constFind(url);
0928 
0929         // Compute distance
0930         int distance;
0931         const QRect itemRect = visualRect(index);
0932         if (itemRect.isValid()) {
0933             // Item is visible, order thumbnails from left to right, top to bottom
0934             // Distance is computed so that it is between 0 and visibleSurface
0935             distance = itemRect.top() * visibleRect.width() + itemRect.left();
0936             // Make sure directory thumbnails are generated after image thumbnails:
0937             // Distance is between visibleSurface and 2 * visibleSurface
0938             if (kind == MimeTypeUtils::KIND_DIR) {
0939                 distance = distance + visibleSurface;
0940             }
0941         } else {
0942             // Calculate how far away the thumbnail is to determine if it could
0943             // become visible soon.
0944             qreal itemDistance = (itemRect.center() - origin).manhattanLength();
0945 
0946             if (itemDistance < discardDistance) {
0947                 // Item is not visible but within an area that may potentially
0948                 // become visible soon, order thumbnails according to distance
0949                 // Start at 2 * visibleSurface to ensure invisible thumbnails are
0950                 // generated *after* visible thumbnails
0951                 distance = 2 * visibleSurface + itemDistance;
0952             } else {
0953                 // Discard thumbnails that are too far away to prevent large
0954                 // directories from consuming massive amounts of RAM.
0955                 if (it != d->mThumbnailForUrl.constEnd()) {
0956                     // Thumbnail exists for this item, discard it.
0957                     const QUrl url = item.url();
0958                     d->mThumbnailForUrl.remove(url);
0959                     d->mSmoothThumbnailQueue.removeAll(url);
0960                     d->mThumbnailProvider->removeItems({item});
0961                 }
0962                 continue;
0963             }
0964         }
0965 
0966         // Filter out items which already have a thumbnail
0967         if (it != d->mThumbnailForUrl.constEnd() && it.value().isGroupPixAdaptedForSize(d->mThumbnailSize.height())) {
0968             continue;
0969         }
0970 
0971         // Add the item to our map
0972         itemMap.insert(distance, item);
0973 
0974         // Insert the thumbnail in mThumbnailForUrl, so that
0975         // setThumbnail() can find the item to update
0976         if (it == d->mThumbnailForUrl.constEnd()) {
0977             Thumbnail thumbnail = Thumbnail(QPersistentModelIndex(index), item.time(KFileItem::ModificationTime));
0978             d->mThumbnailForUrl.insert(url, thumbnail);
0979         }
0980     }
0981 
0982     if (!itemMap.isEmpty()) {
0983         d->appendItemsToThumbnailProvider(itemMap.values());
0984     }
0985 }
0986 
0987 void ThumbnailView::updateThumbnail(const QUrl &url)
0988 {
0989     const ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url);
0990     if (it == d->mThumbnailForUrl.end()) {
0991         return;
0992     }
0993 
0994     if (d->mDocumentInfoProvider) {
0995         d->updateThumbnailForModifiedDocument(it->mIndex);
0996     } else {
0997         const KFileItem item = fileItemForIndex(it->mIndex);
0998         d->appendItemsToThumbnailProvider(KFileItemList({item}));
0999     }
1000 }
1001 
1002 void ThumbnailView::updateThumbnailBusyState(const QUrl &url, bool busy)
1003 {
1004     const ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url);
1005     if (it == d->mThumbnailForUrl.end()) {
1006         return;
1007     }
1008 
1009     QPersistentModelIndex index(it->mIndex);
1010     if (busy && !d->mBusyIndexSet.contains(index)) {
1011         d->mBusyIndexSet << index;
1012         update(index);
1013         if (d->mBusyAnimationTimeLine->state() != QTimeLine::Running) {
1014             d->mBusyAnimationTimeLine->start();
1015         }
1016     } else if (!busy && d->mBusyIndexSet.remove(index)) {
1017         update(index);
1018         if (d->mBusyIndexSet.isEmpty()) {
1019             d->mBusyAnimationTimeLine->stop();
1020         }
1021     }
1022 }
1023 
1024 void ThumbnailView::updateBusyIndexes()
1025 {
1026     for (const QPersistentModelIndex &index : qAsConst(d->mBusyIndexSet)) {
1027         update(index);
1028     }
1029 }
1030 
1031 QPixmap ThumbnailView::busySequenceCurrentPixmap() const
1032 {
1033     return d->mBusySequence.frameAt(d->mBusyAnimationTimeLine->currentFrame());
1034 }
1035 
1036 void ThumbnailView::smoothNextThumbnail()
1037 {
1038     if (d->mSmoothThumbnailQueue.isEmpty()) {
1039         return;
1040     }
1041 
1042     if (d->mThumbnailProvider && d->mThumbnailProvider->isRunning()) {
1043         // give mThumbnailProvider priority over smoothing
1044         d->mSmoothThumbnailTimer.start(SMOOTH_DELAY);
1045         return;
1046     }
1047 
1048     QUrl url = d->mSmoothThumbnailQueue.dequeue();
1049     ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url);
1050     GV_RETURN_IF_FAIL2(it != d->mThumbnailForUrl.end(), url << "not in mThumbnailForUrl.");
1051 
1052     Thumbnail &thumbnail = it.value();
1053     thumbnail.mAdjustedPix = d->scale(thumbnail.mGroupPix, Qt::SmoothTransformation);
1054     thumbnail.mRough = false;
1055 
1056     GV_RETURN_IF_FAIL2(thumbnail.mIndex.isValid(), "index for" << url << "is invalid.");
1057     update(thumbnail.mIndex);
1058 
1059     if (!d->mSmoothThumbnailQueue.isEmpty()) {
1060         d->mSmoothThumbnailTimer.start(0);
1061     }
1062 }
1063 
1064 void ThumbnailView::reloadThumbnail(const QModelIndex &index)
1065 {
1066     QUrl url = urlForIndex(index);
1067     if (!url.isValid()) {
1068         qCWarning(GWENVIEW_LIB_LOG) << "Invalid url for index" << index;
1069         return;
1070     }
1071     ThumbnailProvider::deleteImageThumbnail(url);
1072     ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url);
1073     if (it == d->mThumbnailForUrl.end()) {
1074         return;
1075     }
1076     d->mThumbnailForUrl.erase(it);
1077     generateThumbnailsForItems();
1078 }
1079 
1080 void ThumbnailView::setCreateThumbnailsForRemoteUrls(bool createRemoteThumbs)
1081 {
1082     d->mCreateThumbnailsForRemoteUrls = createRemoteThumbs;
1083 }
1084 
1085 } // namespace
1086 
1087 #include "moc_thumbnailview.cpp"