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"