1 /*
2 Gwenview: an image viewer
3 Copyright 2007 Aurélien Gâteau <agateau@kde.org>
4 
5 This program is free software; you can redistribute it and/or
6 modify it under the terms of the GNU General Public License
7 as published by the Free Software Foundation; either version 2
8 of the License, or (at your option) any later version.
9 
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU General Public License for more details.
14 
15 You should have received a copy of the GNU General Public License
16 along with this program; if not, write to the Free Software
17 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18 
19 */
20 #include "thumbnailview.h"
21 
22 // STL
23 #include <cmath>
24 
25 // Qt
26 #include <QApplication>
27 #include <QDateTime>
28 #include <QDrag>
29 #include <QDragEnterEvent>
30 #include <QDropEvent>
31 #include <QGestureEvent>
32 #include <QMimeData>
33 #include <QPainter>
34 #include <QPointer>
35 #include <QQueue>
36 #include <QScrollBar>
37 #include <QScroller>
38 #include <QTimeLine>
39 #include <QTimer>
40 
41 // KF
42 #include <KDirModel>
43 #include <KIconLoader>
44 #include <KPixmapSequence>
45 #include <KUrlMimeData>
46 
47 // Local
48 #include "abstractdocumentinfoprovider.h"
49 #include "abstractthumbnailviewhelper.h"
50 #include "archiveutils.h"
51 #include "dragpixmapgenerator.h"
52 #include "gwenview_lib_debug.h"
53 #include "gwenviewconfig.h"
54 #include "mimetypeutils.h"
55 #include "urlutils.h"
56 #include <lib/gvdebug.h>
57 #include <lib/scrollerutils.h>
58 #include <lib/thumbnailprovider/thumbnailprovider.h>
59 #include <lib/touch/touch.h>
60 
61 namespace Gwenview
62 {
63 #undef ENABLE_LOG
64 #undef LOG
65 //#define ENABLE_LOG
66 #ifdef ENABLE_LOG
67 #define LOG(x) // qCDebug(GWENVIEW_LIB_LOG) << x
68 #else
69 #define LOG(x) ;
70 #endif
71 
72 /** How many msec to wait before starting to smooth thumbnails */
73 const int SMOOTH_DELAY = 500;
74 
75 const int WHEEL_ZOOM_MULTIPLIER = 4;
76 
fileItemForIndex(const QModelIndex & index)77 static KFileItem fileItemForIndex(const QModelIndex &index)
78 {
79     if (!index.isValid()) {
80         LOG("Invalid index");
81         return KFileItem();
82     }
83     QVariant data = index.data(KDirModel::FileItemRole);
84     return qvariant_cast<KFileItem>(data);
85 }
86 
urlForIndex(const QModelIndex & index)87 static QUrl urlForIndex(const QModelIndex &index)
88 {
89     KFileItem item = fileItemForIndex(index);
90     return item.isNull() ? QUrl() : item.url();
91 }
92 
93 struct Thumbnail {
ThumbnailGwenview::Thumbnail94     Thumbnail(const QPersistentModelIndex &index_, const QDateTime &mtime)
95         : mIndex(index_)
96         , mModificationTime(mtime)
97         , mFileSize(0)
98         , mRough(true)
99         , mWaitingForThumbnail(true)
100     {
101     }
102 
ThumbnailGwenview::Thumbnail103     Thumbnail()
104         : mFileSize(0)
105         , mRough(true)
106         , mWaitingForThumbnail(true)
107     {
108     }
109 
110     /**
111      * Init the thumbnail based on a icon
112      */
initAsIconGwenview::Thumbnail113     void initAsIcon(const QPixmap &pix)
114     {
115         mGroupPix = pix;
116         int largeGroupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::Large);
117         mFullSize = QSize(largeGroupSize, largeGroupSize);
118     }
119 
isGroupPixAdaptedForSizeGwenview::Thumbnail120     bool isGroupPixAdaptedForSize(int size) const
121     {
122         if (mWaitingForThumbnail) {
123             return false;
124         }
125         if (mGroupPix.isNull()) {
126             return false;
127         }
128         const int groupSize = qMax(mGroupPix.width(), mGroupPix.height());
129         if (groupSize >= size) {
130             return true;
131         }
132 
133         // groupSize is less than size, but this may be because the full image
134         // is the same size as groupSize
135         return groupSize == qMax(mFullSize.width(), mFullSize.height());
136     }
137 
prepareForRefreshGwenview::Thumbnail138     void prepareForRefresh(const QDateTime &mtime)
139     {
140         mModificationTime = mtime;
141         mFileSize = 0;
142         mGroupPix = QPixmap();
143         mAdjustedPix = QPixmap();
144         mFullSize = QSize();
145         mRealFullSize = QSize();
146         mRough = true;
147         mWaitingForThumbnail = true;
148     }
149 
150     QPersistentModelIndex mIndex;
151     QDateTime mModificationTime;
152     /// The pix loaded from .thumbnails/{large,normal}
153     QPixmap mGroupPix;
154     /// Scaled version of mGroupPix, adjusted to ThumbnailView::thumbnailSize
155     QPixmap mAdjustedPix;
156     /// Size of the full image
157     QSize mFullSize;
158     /// Real size of the full image, invalid unless the thumbnail
159     /// represents a raster image (not an icon)
160     QSize mRealFullSize;
161     /// File size of the full image
162     KIO::filesize_t mFileSize;
163     /// Whether mAdjustedPix represents has been scaled using fast or smooth
164     /// transformation
165     bool mRough;
166     /// Set to true if mGroupPix should be replaced with a real thumbnail
167     bool mWaitingForThumbnail;
168 };
169 
170 using ThumbnailForUrl = QHash<QUrl, Thumbnail>;
171 using UrlQueue = QQueue<QUrl>;
172 using PersistentModelIndexSet = QSet<QPersistentModelIndex>;
173 
174 struct ThumbnailViewPrivate {
175     ThumbnailView *q;
176     ThumbnailView::ThumbnailScaleMode mScaleMode;
177     QSize mThumbnailSize;
178     qreal mThumbnailAspectRatio;
179     AbstractDocumentInfoProvider *mDocumentInfoProvider;
180     AbstractThumbnailViewHelper *mThumbnailViewHelper;
181     ThumbnailForUrl mThumbnailForUrl;
182     QTimer mScheduledThumbnailGenerationTimer;
183 
184     UrlQueue mSmoothThumbnailQueue;
185     QTimer mSmoothThumbnailTimer;
186 
187     QPixmap mWaitingThumbnail;
188     QPointer<ThumbnailProvider> mThumbnailProvider;
189 
190     PersistentModelIndexSet mBusyIndexSet;
191     KPixmapSequence mBusySequence;
192     QTimeLine *mBusyAnimationTimeLine;
193 
194     bool mCreateThumbnailsForRemoteUrls;
195 
196     QScroller *mScroller;
197     Touch *mTouch;
198 
setupBusyAnimationGwenview::ThumbnailViewPrivate199     void setupBusyAnimation()
200     {
201         mBusySequence = KIconLoader::global()->loadPixmapSequence(QStringLiteral("process-working"), 22);
202         mBusyAnimationTimeLine = new QTimeLine(100 * mBusySequence.frameCount(), q);
203         mBusyAnimationTimeLine->setEasingCurve(QEasingCurve::Linear);
204         mBusyAnimationTimeLine->setEndFrame(mBusySequence.frameCount() - 1);
205         mBusyAnimationTimeLine->setLoopCount(0);
206         QObject::connect(mBusyAnimationTimeLine, &QTimeLine::frameChanged, q, &ThumbnailView::updateBusyIndexes);
207     }
208 
scheduleThumbnailGenerationGwenview::ThumbnailViewPrivate209     void scheduleThumbnailGeneration()
210     {
211         if (mThumbnailProvider) {
212             mThumbnailProvider->removePendingItems();
213         }
214         mSmoothThumbnailQueue.clear();
215         if (!mScheduledThumbnailGenerationTimer.isActive()) {
216             mScheduledThumbnailGenerationTimer.start();
217         }
218     }
219 
updateThumbnailForModifiedDocumentGwenview::ThumbnailViewPrivate220     void updateThumbnailForModifiedDocument(const QModelIndex &index)
221     {
222         Q_ASSERT(mDocumentInfoProvider);
223         KFileItem item = fileItemForIndex(index);
224         QUrl url = item.url();
225         ThumbnailGroup::Enum group = ThumbnailGroup::fromPixelSize(mThumbnailSize.width());
226         QPixmap pix;
227         QSize fullSize;
228         mDocumentInfoProvider->thumbnailForDocument(url, group, &pix, &fullSize);
229         mThumbnailForUrl[url] = Thumbnail(QPersistentModelIndex(index), QDateTime::currentDateTime());
230         q->setThumbnail(item, pix, fullSize, 0);
231     }
232 
appendItemsToThumbnailProviderGwenview::ThumbnailViewPrivate233     void appendItemsToThumbnailProvider(const KFileItemList &list)
234     {
235         if (mThumbnailProvider) {
236             ThumbnailGroup::Enum group = ThumbnailGroup::fromPixelSize(mThumbnailSize.width());
237             mThumbnailProvider->setThumbnailGroup(group);
238             mThumbnailProvider->appendItems(list);
239         }
240     }
241 
roughAdjustThumbnailGwenview::ThumbnailViewPrivate242     void roughAdjustThumbnail(Thumbnail *thumbnail)
243     {
244         const QPixmap &mGroupPix = thumbnail->mGroupPix;
245         const int groupSize = qMax(mGroupPix.width(), mGroupPix.height());
246         const int fullSize = qMax(thumbnail->mFullSize.width(), thumbnail->mFullSize.height());
247         if (fullSize == groupSize && mGroupPix.height() <= mThumbnailSize.height() && mGroupPix.width() <= mThumbnailSize.width()) {
248             thumbnail->mAdjustedPix = mGroupPix;
249             thumbnail->mRough = false;
250         } else {
251             thumbnail->mAdjustedPix = scale(mGroupPix, Qt::FastTransformation);
252             thumbnail->mRough = true;
253         }
254     }
255 
initDragPixmapGwenview::ThumbnailViewPrivate256     void initDragPixmap(QDrag *drag, const QModelIndexList &indexes)
257     {
258         const int thumbCount = qMin(indexes.count(), int(DragPixmapGenerator::MaxCount));
259         QList<QPixmap> lst;
260         for (int row = 0; row < thumbCount; ++row) {
261             const QUrl url = urlForIndex(indexes[row]);
262             lst << mThumbnailForUrl.value(url).mAdjustedPix;
263         }
264         DragPixmapGenerator::DragPixmap dragPixmap = DragPixmapGenerator::generate(lst, indexes.count());
265         drag->setPixmap(dragPixmap.pix);
266         drag->setHotSpot(dragPixmap.hotSpot);
267     }
268 
scaleGwenview::ThumbnailViewPrivate269     QPixmap scale(const QPixmap &pix, Qt::TransformationMode transformationMode)
270     {
271         switch (mScaleMode) {
272         case ThumbnailView::ScaleToFit:
273             return pix.scaled(mThumbnailSize.width(), mThumbnailSize.height(), Qt::KeepAspectRatio, transformationMode);
274         case ThumbnailView::ScaleToSquare: {
275             int minSize = qMin(pix.width(), pix.height());
276             QPixmap pix2 = pix.copy((pix.width() - minSize) / 2, (pix.height() - minSize) / 2, minSize, minSize);
277             return pix2.scaled(mThumbnailSize.width(), mThumbnailSize.height(), Qt::KeepAspectRatio, transformationMode);
278         }
279         case ThumbnailView::ScaleToHeight:
280             return pix.scaledToHeight(mThumbnailSize.height(), transformationMode);
281         case ThumbnailView::ScaleToWidth:
282             return pix.scaledToWidth(mThumbnailSize.width(), transformationMode);
283         }
284         // Keep compiler happy
285         Q_ASSERT(0);
286         return QPixmap();
287     }
288 };
289 
ThumbnailView(QWidget * parent)290 ThumbnailView::ThumbnailView(QWidget *parent)
291     : QListView(parent)
292     , d(new ThumbnailViewPrivate)
293 {
294     d->q = this;
295     d->mScaleMode = ScaleToFit;
296     d->mThumbnailViewHelper = nullptr;
297     d->mDocumentInfoProvider = nullptr;
298     d->mThumbnailProvider = nullptr;
299     // Init to some stupid value so that the first call to setThumbnailSize()
300     // is not ignored (do not use 0 in case someone try to divide by
301     // mThumbnailSize...)
302     d->mThumbnailSize = QSize(1, 1);
303     d->mThumbnailAspectRatio = 1;
304     d->mCreateThumbnailsForRemoteUrls = true;
305 
306     setFrameShape(QFrame::NoFrame);
307     setViewMode(QListView::IconMode);
308     setResizeMode(QListView::Adjust);
309     setDragEnabled(true);
310     setAcceptDrops(true);
311     setDropIndicatorShown(true);
312     setUniformItemSizes(true);
313     setEditTriggers(QAbstractItemView::EditKeyPressed);
314 
315     d->setupBusyAnimation();
316 
317     setVerticalScrollMode(ScrollPerPixel);
318     setHorizontalScrollMode(ScrollPerPixel);
319 
320     d->mScheduledThumbnailGenerationTimer.setSingleShot(true);
321     d->mScheduledThumbnailGenerationTimer.setInterval(500);
322     connect(&d->mScheduledThumbnailGenerationTimer, &QTimer::timeout, this, &ThumbnailView::generateThumbnailsForItems);
323 
324     d->mSmoothThumbnailTimer.setSingleShot(true);
325     connect(&d->mSmoothThumbnailTimer, &QTimer::timeout, this, &ThumbnailView::smoothNextThumbnail);
326 
327     setContextMenuPolicy(Qt::CustomContextMenu);
328     connect(this, &ThumbnailView::customContextMenuRequested, this, &ThumbnailView::showContextMenu);
329 
330     connect(this, &ThumbnailView::activated, this, &ThumbnailView::emitIndexActivatedIfNoModifiers);
331 
332     d->mScroller = ScrollerUtils::setQScroller(this->viewport());
333     d->mTouch = new Touch(viewport());
334     connect(d->mTouch, &Touch::twoFingerTapTriggered, this, &ThumbnailView::showContextMenu);
335     connect(d->mTouch, &Touch::pinchZoomTriggered, this, &ThumbnailView::zoomGesture);
336     connect(d->mTouch, &Touch::pinchGestureStarted, this, &ThumbnailView::setZoomParameter);
337     connect(d->mTouch, &Touch::tapTriggered, this, &ThumbnailView::tapGesture);
338     connect(d->mTouch, &Touch::tapHoldAndMovingTriggered, this, &ThumbnailView::startDragFromTouch);
339 
340     const QFontMetrics metrics(viewport()->font());
341     const int singleStep = metrics.height() * QApplication::wheelScrollLines();
342 
343     verticalScrollBar()->setSingleStep(singleStep);
344     horizontalScrollBar()->setSingleStep(singleStep);
345 }
346 
~ThumbnailView()347 ThumbnailView::~ThumbnailView()
348 {
349     delete d->mTouch;
350     delete d;
351 }
352 
thumbnailScaleMode() const353 ThumbnailView::ThumbnailScaleMode ThumbnailView::thumbnailScaleMode() const
354 {
355     return d->mScaleMode;
356 }
357 
setThumbnailScaleMode(ThumbnailScaleMode mode)358 void ThumbnailView::setThumbnailScaleMode(ThumbnailScaleMode mode)
359 {
360     d->mScaleMode = mode;
361     setUniformItemSizes(mode == ScaleToFit || mode == ScaleToSquare);
362 }
363 
setModel(QAbstractItemModel * newModel)364 void ThumbnailView::setModel(QAbstractItemModel *newModel)
365 {
366     if (model()) {
367         disconnect(model(), nullptr, this, nullptr);
368     }
369     QListView::setModel(newModel);
370 
371     connect(model(), &QAbstractItemModel::rowsRemoved, this, [=](const QModelIndex &index, int first, int last) {
372         // Avoid the delegate doing a ton of work if we're not visible
373         if (isVisible()) {
374             Q_EMIT rowsRemovedSignal(index, first, last);
375         }
376     });
377 }
378 
setThumbnailProvider(ThumbnailProvider * thumbnailProvider)379 void ThumbnailView::setThumbnailProvider(ThumbnailProvider *thumbnailProvider)
380 {
381     GV_RETURN_IF_FAIL(d->mThumbnailProvider != thumbnailProvider);
382     if (thumbnailProvider) {
383         connect(thumbnailProvider, &ThumbnailProvider::thumbnailLoaded, this, &ThumbnailView::setThumbnail);
384         connect(thumbnailProvider, &ThumbnailProvider::thumbnailLoadingFailed, this, &ThumbnailView::setBrokenThumbnail);
385     } else {
386         disconnect(d->mThumbnailProvider, nullptr, this, nullptr);
387     }
388     d->mThumbnailProvider = thumbnailProvider;
389 }
390 
updateThumbnailSize()391 void ThumbnailView::updateThumbnailSize()
392 {
393     QSize value = d->mThumbnailSize;
394     // mWaitingThumbnail
395     const auto dpr = devicePixelRatioF();
396     int waitingThumbnailSize;
397     if (value.width() > 64 * dpr) {
398         waitingThumbnailSize = qRound(48 * dpr);
399     } else {
400         waitingThumbnailSize = qRound(32 * dpr);
401     }
402     QPixmap icon = QIcon::fromTheme(QStringLiteral("chronometer")).pixmap(waitingThumbnailSize);
403     QPixmap pix(value);
404     pix.fill(Qt::transparent);
405     QPainter painter(&pix);
406     painter.setOpacity(0.5);
407     painter.drawPixmap((value.width() - icon.width()) / 2, (value.height() - icon.height()) / 2, icon);
408     painter.end();
409     d->mWaitingThumbnail = pix;
410     d->mWaitingThumbnail.setDevicePixelRatio(dpr);
411 
412     // Stop smoothing
413     d->mSmoothThumbnailTimer.stop();
414     d->mSmoothThumbnailQueue.clear();
415 
416     // Clear adjustedPixes
417     ThumbnailForUrl::iterator it = d->mThumbnailForUrl.begin(), end = d->mThumbnailForUrl.end();
418     for (; it != end; ++it) {
419         it.value().mAdjustedPix = QPixmap();
420     }
421 
422     Q_EMIT thumbnailSizeChanged(value / dpr);
423     Q_EMIT thumbnailWidthChanged(qRound(value.width() / dpr));
424     if (d->mScaleMode != ScaleToFit) {
425         scheduleDelayedItemsLayout();
426     }
427     d->scheduleThumbnailGeneration();
428 }
429 
setThumbnailWidth(int width)430 void ThumbnailView::setThumbnailWidth(int width)
431 {
432     const auto dpr = devicePixelRatioF();
433     const qreal newWidthF = width * dpr;
434     const int newWidth = qRound(newWidthF);
435     if (d->mThumbnailSize.width() == newWidth) {
436         return;
437     }
438     int height = qRound(newWidthF / d->mThumbnailAspectRatio);
439     d->mThumbnailSize = QSize(newWidth, height);
440     updateThumbnailSize();
441 }
442 
setThumbnailAspectRatio(qreal ratio)443 void ThumbnailView::setThumbnailAspectRatio(qreal ratio)
444 {
445     if (d->mThumbnailAspectRatio == ratio) {
446         return;
447     }
448     d->mThumbnailAspectRatio = ratio;
449     int width = d->mThumbnailSize.width();
450     int height = round((qreal)width / d->mThumbnailAspectRatio);
451     d->mThumbnailSize = QSize(width, height);
452     updateThumbnailSize();
453 }
454 
thumbnailAspectRatio() const455 qreal ThumbnailView::thumbnailAspectRatio() const
456 {
457     return d->mThumbnailAspectRatio;
458 }
459 
thumbnailSize() const460 QSize ThumbnailView::thumbnailSize() const
461 {
462     return d->mThumbnailSize / devicePixelRatioF();
463 }
464 
setThumbnailViewHelper(AbstractThumbnailViewHelper * helper)465 void ThumbnailView::setThumbnailViewHelper(AbstractThumbnailViewHelper *helper)
466 {
467     d->mThumbnailViewHelper = helper;
468 }
469 
thumbnailViewHelper() const470 AbstractThumbnailViewHelper *ThumbnailView::thumbnailViewHelper() const
471 {
472     return d->mThumbnailViewHelper;
473 }
474 
setDocumentInfoProvider(AbstractDocumentInfoProvider * provider)475 void ThumbnailView::setDocumentInfoProvider(AbstractDocumentInfoProvider *provider)
476 {
477     d->mDocumentInfoProvider = provider;
478     if (provider) {
479         connect(provider, &AbstractDocumentInfoProvider::busyStateChanged, this, &ThumbnailView::updateThumbnailBusyState);
480         connect(provider, &AbstractDocumentInfoProvider::documentChanged, this, &ThumbnailView::updateThumbnail);
481     }
482 }
483 
documentInfoProvider() const484 AbstractDocumentInfoProvider *ThumbnailView::documentInfoProvider() const
485 {
486     return d->mDocumentInfoProvider;
487 }
488 
rowsAboutToBeRemoved(const QModelIndex & parent,int start,int end)489 void ThumbnailView::rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end)
490 {
491     QListView::rowsAboutToBeRemoved(parent, start, end);
492 
493     // Remove references to removed items
494     KFileItemList itemList;
495     for (int pos = start; pos <= end; ++pos) {
496         QModelIndex index = model()->index(pos, 0, parent);
497         KFileItem item = fileItemForIndex(index);
498         if (item.isNull()) {
499             // qCDebug(GWENVIEW_LIB_LOG) << "Skipping invalid item!" << index.data().toString();
500             continue;
501         }
502 
503         QUrl url = item.url();
504         d->mThumbnailForUrl.remove(url);
505         d->mSmoothThumbnailQueue.removeAll(url);
506 
507         itemList.append(item);
508     }
509 
510     if (d->mThumbnailProvider) {
511         d->mThumbnailProvider->removeItems(itemList);
512     }
513 
514     // Removing rows might make new images visible, make sure their thumbnail
515     // is generated
516     if (!d->mScheduledThumbnailGenerationTimer.isActive()) {
517         d->mScheduledThumbnailGenerationTimer.start();
518     }
519 }
520 
rowsInserted(const QModelIndex & parent,int start,int end)521 void ThumbnailView::rowsInserted(const QModelIndex &parent, int start, int end)
522 {
523     QListView::rowsInserted(parent, start, end);
524 
525     if (!d->mScheduledThumbnailGenerationTimer.isActive()) {
526         d->mScheduledThumbnailGenerationTimer.start();
527     }
528 
529     if (isVisible()) {
530         Q_EMIT rowsInsertedSignal(parent, start, end);
531     }
532 }
533 
dataChanged(const QModelIndex & topLeft,const QModelIndex & bottomRight,const QVector<int> & roles)534 void ThumbnailView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
535 {
536     QListView::dataChanged(topLeft, bottomRight, roles);
537     bool thumbnailsNeedRefresh = false;
538     for (int row = topLeft.row(); row <= bottomRight.row(); ++row) {
539         QModelIndex index = model()->index(row, 0);
540         KFileItem item = fileItemForIndex(index);
541         if (item.isNull()) {
542             qCWarning(GWENVIEW_LIB_LOG) << "Invalid item for index" << index << ". This should not happen!";
543             GV_FATAL_FAILS;
544             continue;
545         }
546 
547         ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(item.url());
548         if (it != d->mThumbnailForUrl.end()) {
549             // All thumbnail views are connected to the model, so
550             // ThumbnailView::dataChanged() is called for all of them. As a
551             // result this method will also be called for views which are not
552             // currently visible, and do not yet have a thumbnail for the
553             // modified url.
554             QDateTime mtime = item.time(KFileItem::ModificationTime);
555             if (it->mModificationTime != mtime || it->mFileSize != item.size()) {
556                 // dataChanged() is called when the file changes but also when
557                 // the model fetched additional data such as semantic info. To
558                 // avoid needless refreshes, we only trigger a refresh if the
559                 // modification time changes.
560                 thumbnailsNeedRefresh = true;
561                 it->prepareForRefresh(mtime);
562             }
563         }
564     }
565     if (thumbnailsNeedRefresh && !d->mScheduledThumbnailGenerationTimer.isActive()) {
566         d->mScheduledThumbnailGenerationTimer.start();
567     }
568 }
569 
showContextMenu()570 void ThumbnailView::showContextMenu()
571 {
572     d->mThumbnailViewHelper->showContextMenu(this);
573 }
574 
emitIndexActivatedIfNoModifiers(const QModelIndex & index)575 void ThumbnailView::emitIndexActivatedIfNoModifiers(const QModelIndex &index)
576 {
577     if (QApplication::keyboardModifiers() == Qt::NoModifier) {
578         Q_EMIT indexActivated(index);
579     }
580 }
581 
setThumbnail(const KFileItem & item,const QPixmap & pixmap,const QSize & size,qulonglong fileSize)582 void ThumbnailView::setThumbnail(const KFileItem &item, const QPixmap &pixmap, const QSize &size, qulonglong fileSize)
583 {
584     ThumbnailForUrl::iterator it = d->mThumbnailForUrl.find(item.url());
585     if (it == d->mThumbnailForUrl.end()) {
586         return;
587     }
588     Thumbnail &thumbnail = it.value();
589     thumbnail.mGroupPix = pixmap;
590     thumbnail.mAdjustedPix = QPixmap();
591     int largeGroupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::Large2x);
592     thumbnail.mFullSize = size.isValid() ? size : QSize(largeGroupSize, largeGroupSize);
593     thumbnail.mRealFullSize = size;
594     thumbnail.mWaitingForThumbnail = false;
595     thumbnail.mFileSize = fileSize;
596 
597     update(thumbnail.mIndex);
598     if (d->mScaleMode != ScaleToFit) {
599         scheduleDelayedItemsLayout();
600     }
601 }
602 
setBrokenThumbnail(const KFileItem & item)603 void ThumbnailView::setBrokenThumbnail(const KFileItem &item)
604 {
605     ThumbnailForUrl::iterator it = d->mThumbnailForUrl.find(item.url());
606     if (it == d->mThumbnailForUrl.end()) {
607         return;
608     }
609     Thumbnail &thumbnail = it.value();
610     MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item);
611     if (kind == MimeTypeUtils::KIND_VIDEO) {
612         // Special case for videos because our kde install may come without
613         // support for video thumbnails so we show the mimetype icon instead of
614         // a broken image icon
615         const QPixmap pix = QIcon::fromTheme(item.iconName()).pixmap(d->mThumbnailSize.height());
616         thumbnail.initAsIcon(pix);
617     } else if (kind == MimeTypeUtils::KIND_DIR) {
618         // Special case for folders because ThumbnailProvider does not return a
619         // thumbnail if there is no images
620         thumbnail.mWaitingForThumbnail = false;
621         return;
622     } else {
623         thumbnail.initAsIcon(QIcon::fromTheme(QStringLiteral("image-missing")).pixmap(48));
624         thumbnail.mFullSize = thumbnail.mGroupPix.size();
625     }
626     update(thumbnail.mIndex);
627 }
628 
thumbnailForIndex(const QModelIndex & index,QSize * fullSize)629 QPixmap ThumbnailView::thumbnailForIndex(const QModelIndex &index, QSize *fullSize)
630 {
631     KFileItem item = fileItemForIndex(index);
632     if (item.isNull()) {
633         LOG("Invalid item");
634         if (fullSize) {
635             *fullSize = QSize();
636         }
637         return QPixmap();
638     }
639     QUrl url = item.url();
640 
641     // Find or create Thumbnail instance
642     ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url);
643     if (it == d->mThumbnailForUrl.end()) {
644         Thumbnail thumbnail = Thumbnail(QPersistentModelIndex(index), item.time(KFileItem::ModificationTime));
645         it = d->mThumbnailForUrl.insert(url, thumbnail);
646     }
647     Thumbnail &thumbnail = it.value();
648 
649     // If dir or archive, generate a thumbnail from fileitem pixmap
650     MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item);
651     if (kind == MimeTypeUtils::KIND_ARCHIVE || kind == MimeTypeUtils::KIND_DIR) {
652         int groupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::fromPixelSize(d->mThumbnailSize.height()));
653         if (thumbnail.mGroupPix.isNull() || thumbnail.mGroupPix.height() < groupSize) {
654             const QPixmap pix = QIcon::fromTheme(item.iconName()).pixmap(d->mThumbnailSize.height());
655 
656             thumbnail.initAsIcon(pix);
657             if (kind == MimeTypeUtils::KIND_ARCHIVE) {
658                 // No thumbnails for archives
659                 thumbnail.mWaitingForThumbnail = false;
660             } else if (!d->mCreateThumbnailsForRemoteUrls && !UrlUtils::urlIsFastLocalFile(url)) {
661                 // If we don't want thumbnails for remote urls, use
662                 // "folder-remote" icon for remote folders, so that they do
663                 // not look like regular folders
664                 thumbnail.mWaitingForThumbnail = false;
665                 thumbnail.initAsIcon(QIcon::fromTheme(QStringLiteral("folder-remote")).pixmap(groupSize));
666             } else {
667                 // set mWaitingForThumbnail to true (necessary in the case
668                 // 'thumbnail' already existed before, but with a too small
669                 // mGroupPix)
670                 thumbnail.mWaitingForThumbnail = true;
671             }
672         }
673     }
674 
675     if (thumbnail.mGroupPix.isNull()) {
676         if (fullSize) {
677             *fullSize = QSize();
678         }
679         return d->mWaitingThumbnail;
680     }
681 
682     // Adjust thumbnail
683     if (thumbnail.mAdjustedPix.isNull()) {
684         d->roughAdjustThumbnail(&thumbnail);
685     }
686     if (GwenviewConfig::lowResourceUsageMode() && thumbnail.mRough && !d->mSmoothThumbnailQueue.contains(url)) {
687         d->mSmoothThumbnailQueue.enqueue(url);
688         if (!d->mSmoothThumbnailTimer.isActive()) {
689             d->mSmoothThumbnailTimer.start(SMOOTH_DELAY);
690         }
691     }
692     if (fullSize) {
693         *fullSize = thumbnail.mRealFullSize;
694     }
695     thumbnail.mAdjustedPix.setDevicePixelRatio(devicePixelRatioF());
696     return thumbnail.mAdjustedPix;
697 }
698 
isModified(const QModelIndex & index) const699 bool ThumbnailView::isModified(const QModelIndex &index) const
700 {
701     if (!d->mDocumentInfoProvider) {
702         return false;
703     }
704     QUrl url = urlForIndex(index);
705     return d->mDocumentInfoProvider->isModified(url);
706 }
707 
isBusy(const QModelIndex & index) const708 bool ThumbnailView::isBusy(const QModelIndex &index) const
709 {
710     if (!d->mDocumentInfoProvider) {
711         return false;
712     }
713     QUrl url = urlForIndex(index);
714     return d->mDocumentInfoProvider->isBusy(url);
715 }
716 
startDrag(Qt::DropActions)717 void ThumbnailView::startDrag(Qt::DropActions)
718 {
719     const QModelIndexList indexes = selectionModel()->selectedIndexes();
720     if (indexes.isEmpty()) {
721         return;
722     }
723 
724     KFileItemList selectedFiles;
725     for (const auto &index : indexes) {
726         selectedFiles << fileItemForIndex(index);
727     }
728 
729     auto *drag = new QDrag(this);
730     drag->setMimeData(MimeTypeUtils::selectionMimeData(selectedFiles, MimeTypeUtils::DropTarget));
731     d->initDragPixmap(drag, indexes);
732     drag->exec(Qt::MoveAction | Qt::CopyAction | Qt::LinkAction, Qt::CopyAction);
733 }
734 
setZoomParameter()735 void ThumbnailView::setZoomParameter()
736 {
737     const qreal sensitivityModifier = 0.25;
738     d->mTouch->setZoomParameter(sensitivityModifier, thumbnailSize().width());
739 }
740 
zoomGesture(qreal newZoom,const QPoint &)741 void ThumbnailView::zoomGesture(qreal newZoom, const QPoint &)
742 {
743     if (newZoom >= 0.0) {
744         int width = qBound(int(MinThumbnailSize), static_cast<int>(newZoom), int(MaxThumbnailSize));
745         setThumbnailWidth(width);
746     }
747 }
748 
tapGesture(const QPoint & pos)749 void ThumbnailView::tapGesture(const QPoint &pos)
750 {
751     const QRect rect = QRect(pos, QSize(1, 1));
752     setSelection(rect, QItemSelectionModel::ClearAndSelect);
753     Q_EMIT activated(indexAt(pos));
754 }
755 
startDragFromTouch(const QPoint & pos)756 void ThumbnailView::startDragFromTouch(const QPoint &pos)
757 {
758     QModelIndex index = indexAt(pos);
759     if (index.isValid()) {
760         setCurrentIndex(index);
761         d->mScroller->stop();
762         startDrag(Qt::CopyAction);
763     }
764 }
765 
dragEnterEvent(QDragEnterEvent * event)766 void ThumbnailView::dragEnterEvent(QDragEnterEvent *event)
767 {
768     QAbstractItemView::dragEnterEvent(event);
769     if (event->mimeData()->hasUrls()) {
770         event->acceptProposedAction();
771     }
772 }
773 
dragMoveEvent(QDragMoveEvent * event)774 void ThumbnailView::dragMoveEvent(QDragMoveEvent *event)
775 {
776     // Necessary, otherwise we don't reach dropEvent()
777     QAbstractItemView::dragMoveEvent(event);
778     event->acceptProposedAction();
779 }
780 
dropEvent(QDropEvent * event)781 void ThumbnailView::dropEvent(QDropEvent *event)
782 {
783     const QList<QUrl> urlList = KUrlMimeData::urlsFromMimeData(event->mimeData());
784     if (urlList.isEmpty()) {
785         return;
786     }
787 
788     QModelIndex destIndex = indexAt(event->pos());
789     if (destIndex.isValid()) {
790         KFileItem item = fileItemForIndex(destIndex);
791         if (item.isDir()) {
792             QUrl destUrl = item.url();
793             d->mThumbnailViewHelper->showMenuForUrlDroppedOnDir(this, urlList, destUrl);
794             return;
795         }
796     }
797 
798     d->mThumbnailViewHelper->showMenuForUrlDroppedOnViewport(this, urlList);
799 
800     event->acceptProposedAction();
801 }
802 
keyPressEvent(QKeyEvent * event)803 void ThumbnailView::keyPressEvent(QKeyEvent *event)
804 {
805     if (event->key() == Qt::Key_Return) {
806         const QModelIndex index = selectionModel()->currentIndex();
807         if (index.isValid() && selectionModel()->selectedIndexes().count() == 1) {
808             Q_EMIT indexActivated(index);
809         }
810     } else if (event->key() == Qt::Key_Left && event->modifiers() == Qt::NoModifier) {
811         if (flow() == LeftToRight && QApplication::isRightToLeft()) {
812             setCurrentIndex(moveCursor(QAbstractItemView::MoveRight, Qt::NoModifier));
813         } else {
814             setCurrentIndex(moveCursor(QAbstractItemView::MoveLeft, Qt::NoModifier));
815         }
816         return;
817     } else if (event->key() == Qt::Key_Right && event->modifiers() == Qt::NoModifier) {
818         if (flow() == LeftToRight && QApplication::isRightToLeft()) {
819             setCurrentIndex(moveCursor(QAbstractItemView::MoveLeft, Qt::NoModifier));
820         } else {
821             setCurrentIndex(moveCursor(QAbstractItemView::MoveRight, Qt::NoModifier));
822         }
823         return;
824     }
825 
826     QListView::keyPressEvent(event);
827 }
828 
resizeEvent(QResizeEvent * event)829 void ThumbnailView::resizeEvent(QResizeEvent *event)
830 {
831     QListView::resizeEvent(event);
832     d->scheduleThumbnailGeneration();
833 }
834 
showEvent(QShowEvent * event)835 void ThumbnailView::showEvent(QShowEvent *event)
836 {
837     QListView::showEvent(event);
838     d->scheduleThumbnailGeneration();
839     QTimer::singleShot(0, this, &ThumbnailView::scrollToSelectedIndex);
840 }
841 
wheelEvent(QWheelEvent * event)842 void ThumbnailView::wheelEvent(QWheelEvent *event)
843 {
844     // If we don't adjust the single step, the wheel scroll exactly one item up
845     // and down, giving the impression that the items do not move but only
846     // their label changes.
847     // For some reason it is necessary to set the step here: setting it in
848     // setThumbnailSize() does not work
849     // verticalScrollBar()->setSingleStep(d->mThumbnailSize / 5);
850     if (event->modifiers() == Qt::ControlModifier) {
851         int width = thumbnailSize().width() + (event->angleDelta().y() > 0 ? 1 : -1) * WHEEL_ZOOM_MULTIPLIER;
852         width = qMax(int(MinThumbnailSize), qMin(width, int(MaxThumbnailSize)));
853         setThumbnailWidth(width);
854     } else {
855         QListView::wheelEvent(event);
856     }
857 }
858 
mousePressEvent(QMouseEvent * event)859 void ThumbnailView::mousePressEvent(QMouseEvent *event)
860 {
861     switch (event->button()) {
862     case Qt::ForwardButton:
863     case Qt::BackButton:
864         return;
865     default:
866         QListView::mousePressEvent(event);
867     }
868 }
869 
scrollToSelectedIndex()870 void ThumbnailView::scrollToSelectedIndex()
871 {
872     QModelIndexList list = selectedIndexes();
873     if (list.count() >= 1) {
874         scrollTo(list.first(), PositionAtCenter);
875     }
876 }
877 
selectionChanged(const QItemSelection & selected,const QItemSelection & deselected)878 void ThumbnailView::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
879 {
880     QListView::selectionChanged(selected, deselected);
881     Q_EMIT selectionChangedSignal(selected, deselected);
882 }
883 
scrollContentsBy(int dx,int dy)884 void ThumbnailView::scrollContentsBy(int dx, int dy)
885 {
886     QListView::scrollContentsBy(dx, dy);
887     d->scheduleThumbnailGeneration();
888 }
889 
generateThumbnailsForItems()890 void ThumbnailView::generateThumbnailsForItems()
891 {
892     if (!isVisible() || !model()) {
893         return;
894     }
895     const QRect visibleRect = viewport()->rect();
896     const int visibleSurface = visibleRect.width() * visibleRect.height();
897     const QPoint origin = visibleRect.center();
898     // Keep thumbnails around that are at most two "screen heights" away.
899     const int discardDistance = visibleRect.bottomRight().manhattanLength() * 2;
900 
901     // distance => item
902     QMultiMap<int, KFileItem> itemMap;
903 
904     for (int row = 0; row < model()->rowCount(); ++row) {
905         QModelIndex index = model()->index(row, 0);
906         KFileItem item = fileItemForIndex(index);
907         QUrl url = item.url();
908 
909         // Filter out remote items if necessary
910         if (!d->mCreateThumbnailsForRemoteUrls && !url.isLocalFile()) {
911             continue;
912         }
913 
914         // Filter out archives
915         MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item);
916         if (kind == MimeTypeUtils::KIND_ARCHIVE) {
917             continue;
918         }
919 
920         // Immediately update modified items
921         if (d->mDocumentInfoProvider && d->mDocumentInfoProvider->isModified(url)) {
922             d->updateThumbnailForModifiedDocument(index);
923             continue;
924         }
925 
926         ThumbnailForUrl::ConstIterator it = d->mThumbnailForUrl.constFind(url);
927 
928         // Compute distance
929         int distance;
930         const QRect itemRect = visualRect(index);
931         if (itemRect.isValid()) {
932             // Item is visible, order thumbnails from left to right, top to bottom
933             // Distance is computed so that it is between 0 and visibleSurface
934             distance = itemRect.top() * visibleRect.width() + itemRect.left();
935             // Make sure directory thumbnails are generated after image thumbnails:
936             // Distance is between visibleSurface and 2 * visibleSurface
937             if (kind == MimeTypeUtils::KIND_DIR) {
938                 distance = distance + visibleSurface;
939             }
940         } else {
941             // Calculate how far away the thumbnail is to determine if it could
942             // become visible soon.
943             qreal itemDistance = (itemRect.center() - origin).manhattanLength();
944 
945             if (itemDistance < discardDistance) {
946                 // Item is not visible but within an area that may potentially
947                 // become visible soon, order thumbnails according to distance
948                 // Start at 2 * visibleSurface to ensure invisible thumbnails are
949                 // generated *after* visible thumbnails
950                 distance = 2 * visibleSurface + itemDistance;
951             } else {
952                 // Discard thumbnails that are too far away to prevent large
953                 // directories from consuming massive amounts of RAM.
954                 if (it != d->mThumbnailForUrl.constEnd()) {
955                     // Thumbnail exists for this item, discard it.
956                     const QUrl url = item.url();
957                     d->mThumbnailForUrl.remove(url);
958                     d->mSmoothThumbnailQueue.removeAll(url);
959                     d->mThumbnailProvider->removeItems({item});
960                 }
961                 continue;
962             }
963         }
964 
965         // Filter out items which already have a thumbnail
966         if (it != d->mThumbnailForUrl.constEnd() && it.value().isGroupPixAdaptedForSize(d->mThumbnailSize.height())) {
967             continue;
968         }
969 
970         // Add the item to our map
971         itemMap.insert(distance, item);
972 
973         // Insert the thumbnail in mThumbnailForUrl, so that
974         // setThumbnail() can find the item to update
975         if (it == d->mThumbnailForUrl.constEnd()) {
976             Thumbnail thumbnail = Thumbnail(QPersistentModelIndex(index), item.time(KFileItem::ModificationTime));
977             d->mThumbnailForUrl.insert(url, thumbnail);
978         }
979     }
980 
981     if (!itemMap.isEmpty()) {
982         d->appendItemsToThumbnailProvider(itemMap.values());
983     }
984 }
985 
updateThumbnail(const QUrl & url)986 void ThumbnailView::updateThumbnail(const QUrl &url)
987 {
988     const ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url);
989     if (it == d->mThumbnailForUrl.end()) {
990         return;
991     }
992 
993     if (d->mDocumentInfoProvider) {
994         d->updateThumbnailForModifiedDocument(it->mIndex);
995     } else {
996         const KFileItem item = fileItemForIndex(it->mIndex);
997         d->appendItemsToThumbnailProvider(KFileItemList({item}));
998     }
999 }
1000 
updateThumbnailBusyState(const QUrl & url,bool busy)1001 void ThumbnailView::updateThumbnailBusyState(const QUrl &url, bool busy)
1002 {
1003     const ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url);
1004     if (it == d->mThumbnailForUrl.end()) {
1005         return;
1006     }
1007 
1008     QPersistentModelIndex index(it->mIndex);
1009     if (busy && !d->mBusyIndexSet.contains(index)) {
1010         d->mBusyIndexSet << index;
1011         update(index);
1012         if (d->mBusyAnimationTimeLine->state() != QTimeLine::Running) {
1013             d->mBusyAnimationTimeLine->start();
1014         }
1015     } else if (!busy && d->mBusyIndexSet.remove(index)) {
1016         update(index);
1017         if (d->mBusyIndexSet.isEmpty()) {
1018             d->mBusyAnimationTimeLine->stop();
1019         }
1020     }
1021 }
1022 
updateBusyIndexes()1023 void ThumbnailView::updateBusyIndexes()
1024 {
1025     for (const QPersistentModelIndex &index : qAsConst(d->mBusyIndexSet)) {
1026         update(index);
1027     }
1028 }
1029 
busySequenceCurrentPixmap() const1030 QPixmap ThumbnailView::busySequenceCurrentPixmap() const
1031 {
1032     return d->mBusySequence.frameAt(d->mBusyAnimationTimeLine->currentFrame());
1033 }
1034 
smoothNextThumbnail()1035 void ThumbnailView::smoothNextThumbnail()
1036 {
1037     if (d->mSmoothThumbnailQueue.isEmpty()) {
1038         return;
1039     }
1040 
1041     if (d->mThumbnailProvider && d->mThumbnailProvider->isRunning()) {
1042         // give mThumbnailProvider priority over smoothing
1043         d->mSmoothThumbnailTimer.start(SMOOTH_DELAY);
1044         return;
1045     }
1046 
1047     QUrl url = d->mSmoothThumbnailQueue.dequeue();
1048     ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url);
1049     GV_RETURN_IF_FAIL2(it != d->mThumbnailForUrl.end(), url << "not in mThumbnailForUrl.");
1050 
1051     Thumbnail &thumbnail = it.value();
1052     thumbnail.mAdjustedPix = d->scale(thumbnail.mGroupPix, Qt::SmoothTransformation);
1053     thumbnail.mRough = false;
1054 
1055     GV_RETURN_IF_FAIL2(thumbnail.mIndex.isValid(), "index for" << url << "is invalid.");
1056     update(thumbnail.mIndex);
1057 
1058     if (!d->mSmoothThumbnailQueue.isEmpty()) {
1059         d->mSmoothThumbnailTimer.start(0);
1060     }
1061 }
1062 
reloadThumbnail(const QModelIndex & index)1063 void ThumbnailView::reloadThumbnail(const QModelIndex &index)
1064 {
1065     QUrl url = urlForIndex(index);
1066     if (!url.isValid()) {
1067         qCWarning(GWENVIEW_LIB_LOG) << "Invalid url for index" << index;
1068         return;
1069     }
1070     ThumbnailProvider::deleteImageThumbnail(url);
1071     ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url);
1072     if (it == d->mThumbnailForUrl.end()) {
1073         return;
1074     }
1075     d->mThumbnailForUrl.erase(it);
1076     generateThumbnailsForItems();
1077 }
1078 
setCreateThumbnailsForRemoteUrls(bool createRemoteThumbs)1079 void ThumbnailView::setCreateThumbnailsForRemoteUrls(bool createRemoteThumbs)
1080 {
1081     d->mCreateThumbnailsForRemoteUrls = createRemoteThumbs;
1082 }
1083 
1084 } // namespace
1085