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