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

0001 // vim: set tabstop=4 shiftwidth=4 expandtab:
0002 /*
0003 Gwenview: an image viewer
0004 Copyright 2008 Aurélien Gâteau <agateau@kde.org>
0005 
0006 This program is free software; you can redistribute it and/or
0007 modify it under the terms of the GNU General Public License
0008 as published by the Free Software Foundation; either version 2
0009 of the License, or (at your option) any later version.
0010 
0011 This program is distributed in the hope that it will be useful,
0012 but WITHOUT ANY WARRANTY; without even the implied warranty of
0013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0014 GNU General Public License for more details.
0015 
0016 You should have received a copy of the GNU General Public License
0017 along with this program; if not, write to the Free Software
0018 Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.
0019 
0020 */
0021 // Self
0022 #include "previewitemdelegate.h"
0023 #include <config-gwenview.h>
0024 
0025 // Qt
0026 #include <QApplication>
0027 #include <QDateTime>
0028 #include <QEvent>
0029 #include <QHBoxLayout>
0030 #include <QHash>
0031 #include <QHoverEvent>
0032 #include <QPainter>
0033 #include <QPainterPath>
0034 #include <QParallelAnimationGroup>
0035 #include <QPointer>
0036 #include <QPropertyAnimation>
0037 #include <QSequentialAnimationGroup>
0038 #include <QToolButton>
0039 #include <QUrl>
0040 
0041 // KF
0042 #include <KDirModel>
0043 #include <KIconLoader>
0044 #include <KLocalizedString>
0045 
0046 #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
0047 #include <KRatingPainter>
0048 #endif
0049 
0050 // Local
0051 #include "archiveutils.h"
0052 #include "gwenview_lib_debug.h"
0053 #include "itemeditor.h"
0054 #include "paintutils.h"
0055 #include "thumbnailview.h"
0056 #include "timeutils.h"
0057 #include "tooltipwidget.h"
0058 #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
0059 #include "../semanticinfo/semanticinfodirmodel.h"
0060 #endif
0061 
0062 // Define this to be able to fine tune the rendering of the selection
0063 // background through a config file
0064 // #define FINETUNE_SELECTION_BACKGROUND
0065 #ifdef FINETUNE_SELECTION_BACKGROUND
0066 #include <QDir>
0067 #include <QSettings>
0068 #endif
0069 
0070 // #define DEBUG_DRAW_BORDER
0071 // #define DEBUG_DRAW_CURRENT
0072 
0073 namespace Gwenview
0074 {
0075 /**
0076  * Space between the item outer rect and the content, and between the
0077  * thumbnail and the caption
0078  */
0079 const int ITEM_MARGIN_DELEGATE = 5;
0080 
0081 /** How darker is the border line around selection */
0082 const int SELECTION_BORDER_DARKNESS = 140;
0083 const int FOCUS_BORDER_DARKNESS = 200;
0084 
0085 /** Radius of the selection rounded corners, in pixels */
0086 const int SELECTION_RADIUS = 5;
0087 
0088 /** Space between the item outer rect and the context bar */
0089 const int CONTEXTBAR_MARGIN = 1;
0090 
0091 /** How dark is the shadow, 0 is invisible, 255 is as dark as possible */
0092 const int SHADOW_STRENGTH_DELEGATE = 128;
0093 
0094 /** How many pixels around the thumbnail are shadowed */
0095 const int SHADOW_SIZE_DELEGATE = 4;
0096 
0097 static KFileItem fileItemForIndexThumbnailView(const QModelIndex &index)
0098 {
0099     Q_ASSERT(index.isValid());
0100     QVariant data = index.data(KDirModel::FileItemRole);
0101     return qvariant_cast<KFileItem>(data);
0102 }
0103 
0104 static QUrl urlForIndexThumbnailView(const QModelIndex &index)
0105 {
0106     KFileItem item = fileItemForIndexThumbnailView(index);
0107     return item.url();
0108 }
0109 
0110 struct PreviewItemDelegatePrivate {
0111     /**
0112      * Maps full text to elided text.
0113      */
0114     mutable QHash<QString, QString> mElidedTextCache;
0115 
0116     // Key is height * 1000 + width
0117     using ShadowCache = QHash<int, QPixmap>;
0118     mutable ShadowCache mShadowCache;
0119 
0120     PreviewItemDelegate *q;
0121     QPointer<ThumbnailView> mView;
0122     QWidget *mContextBar;
0123     QToolButton *mSaveButton;
0124     QPixmap mSaveButtonPixmap;
0125 
0126     QToolButton *mToggleSelectionButton;
0127     QToolButton *mFullScreenButton;
0128     QToolButton *mRotateLeftButton;
0129     QToolButton *mRotateRightButton;
0130 #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
0131     KRatingPainter mRatingPainter;
0132 #endif
0133 
0134     QPersistentModelIndex mIndexUnderCursor;
0135     QSize mThumbnailSize;
0136     PreviewItemDelegate::ThumbnailDetails mDetails;
0137     PreviewItemDelegate::ContextBarActions mContextBarActions;
0138     Qt::TextElideMode mTextElideMode;
0139 
0140     QPointer<ToolTipWidget> mToolTip;
0141     QScopedPointer<QAbstractAnimation> mToolTipAnimation;
0142 
0143     void initSaveButtonPixmap()
0144     {
0145         if (!mSaveButtonPixmap.isNull()) {
0146             return;
0147         }
0148         // Necessary otherwise we won't see the save button itself
0149         mSaveButton->adjustSize();
0150 
0151         mSaveButtonPixmap = QPixmap(mSaveButton->sizeHint());
0152         mSaveButtonPixmap.fill(Qt::transparent);
0153         mSaveButton->render(&mSaveButtonPixmap, QPoint(), QRegion(), QWidget::DrawChildren);
0154     }
0155 
0156     void showContextBar(const QRect &rect, const QPixmap &thumbnailPix)
0157     {
0158         if (mContextBarActions == PreviewItemDelegate::NoAction) {
0159             return;
0160         }
0161         mContextBar->adjustSize();
0162         // Center bar, except if only showing SelectionAction.
0163         const int posX = mContextBarActions == PreviewItemDelegate::SelectionAction ? 0 : (rect.width() - mContextBar->width()) / 2;
0164         const int thumbnailPixHeight = qRound(thumbnailPix.height() / thumbnailPix.devicePixelRatio());
0165         const int posY = qMax(CONTEXTBAR_MARGIN, mThumbnailSize.height() - thumbnailPixHeight - mContextBar->height());
0166         mContextBar->move(rect.topLeft() + QPoint(posX, posY));
0167         mContextBar->show();
0168     }
0169 
0170     void initToolTip()
0171     {
0172         mToolTip = new ToolTipWidget(mView->viewport());
0173         mToolTip->setOpacity(0);
0174         mToolTip->show();
0175     }
0176 
0177     bool hoverEventFilter(QHoverEvent *event)
0178     {
0179         QModelIndex index = mView->indexAt(event->pos());
0180         if (index != mIndexUnderCursor) {
0181             updateHoverUi(index);
0182         } else {
0183             // Same index, nothing to do, but repaint anyway in case we are
0184             // over the rating row
0185             mView->update(mIndexUnderCursor);
0186         }
0187         return false;
0188     }
0189 
0190     void updateHoverUi(const QModelIndex &index)
0191     {
0192         QModelIndex oldIndex = mIndexUnderCursor;
0193         mIndexUnderCursor = index;
0194         mView->update(oldIndex);
0195 
0196         if (QApplication::style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, mView)) {
0197             mView->setCursor(mIndexUnderCursor.isValid() ? Qt::PointingHandCursor : Qt::ArrowCursor);
0198         }
0199 
0200         if (mIndexUnderCursor.isValid()) {
0201             updateToggleSelectionButton();
0202             updateImageButtons();
0203 
0204             const QRect rect = mView->visualRect(mIndexUnderCursor);
0205             const QPixmap thumbnailPix = mView->thumbnailForIndex(index);
0206             showContextBar(rect, thumbnailPix);
0207             if (mView->isModified(mIndexUnderCursor)) {
0208                 showSaveButton(rect);
0209             } else {
0210                 mSaveButton->hide();
0211             }
0212 
0213             showToolTip(index);
0214             mView->update(mIndexUnderCursor);
0215 
0216         } else {
0217             mContextBar->hide();
0218             mSaveButton->hide();
0219             hideToolTip();
0220         }
0221     }
0222 
0223     QRect ratingRectFromIndexRect(const QRect &rect) const
0224     {
0225         return QRect(rect.left(), rect.bottom() - ratingRowHeight() - ITEM_MARGIN_DELEGATE, rect.width(), ratingRowHeight());
0226     }
0227 
0228 #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
0229     int ratingFromCursorPosition(const QRect &ratingRect) const
0230     {
0231         const QPoint pos = mView->viewport()->mapFromGlobal(QCursor::pos());
0232         return mRatingPainter.ratingFromPosition(ratingRect, pos);
0233     }
0234 #endif
0235 
0236     bool mouseButtonEventFilter(QEvent::Type type)
0237     {
0238 #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
0239         const QRect rect = ratingRectFromIndexRect(mView->visualRect(mIndexUnderCursor));
0240         const int rating = ratingFromCursorPosition(rect);
0241         if (rating == -1) {
0242             return false;
0243         }
0244         if (type == QEvent::MouseButtonRelease) {
0245             q->setDocumentRatingRequested(urlForIndexThumbnailView(mIndexUnderCursor), rating);
0246         }
0247         return true;
0248 #else
0249         return false;
0250 #endif
0251     }
0252 
0253     QPoint saveButtonPosition(const QRect &itemRect) const
0254     {
0255         QSize buttonSize = mSaveButton->sizeHint();
0256         int posX = itemRect.right() - buttonSize.width();
0257         int posY = itemRect.top() + mThumbnailSize.height() + 2 * ITEM_MARGIN_DELEGATE - buttonSize.height();
0258 
0259         return QPoint(posX, posY);
0260     }
0261 
0262     void showSaveButton(const QRect &itemRect) const
0263     {
0264         mSaveButton->move(saveButtonPosition(itemRect));
0265         mSaveButton->show();
0266     }
0267 
0268     void drawBackground(QPainter *painter, const QRect &rect, const QColor &bgColor, const QColor &borderColor) const
0269     {
0270         int bgH, bgS, bgV;
0271         int borderH, borderS, borderV, borderMargin;
0272 #ifdef FINETUNE_SELECTION_BACKGROUND
0273         QSettings settings(QDir::homePath() + "/colors.ini", QSettings::IniFormat);
0274         bgH = settings.value("bg/h").toInt();
0275         bgS = settings.value("bg/s").toInt();
0276         bgV = settings.value("bg/v").toInt();
0277         borderH = settings.value("border/h").toInt();
0278         borderS = settings.value("border/s").toInt();
0279         borderV = settings.value("border/v").toInt();
0280         borderMargin = settings.value("border/margin").toInt();
0281 #else
0282         bgH = 0;
0283         bgS = -20;
0284         bgV = 43;
0285         borderH = 0;
0286         borderS = -100;
0287         borderV = 60;
0288         borderMargin = 1;
0289 #endif
0290         painter->setRenderHint(QPainter::Antialiasing);
0291 
0292         QRectF rectF = QRectF(rect).adjusted(0.5, 0.5, -0.5, -0.5);
0293 
0294         QPainterPath path = PaintUtils::roundedRectangle(rectF, SELECTION_RADIUS);
0295 
0296         QLinearGradient gradient(rectF.topLeft(), rectF.bottomLeft());
0297         gradient.setColorAt(0, PaintUtils::adjustedHsv(bgColor, bgH, bgS, bgV));
0298         gradient.setColorAt(1, bgColor);
0299         painter->fillPath(path, gradient);
0300 
0301         painter->setPen(borderColor);
0302         painter->drawPath(path);
0303 
0304         painter->setPen(PaintUtils::adjustedHsv(borderColor, borderH, borderS, borderV));
0305         rectF = rectF.adjusted(borderMargin, borderMargin, -borderMargin, -borderMargin);
0306         path = PaintUtils::roundedRectangle(rectF, SELECTION_RADIUS);
0307         painter->drawPath(path);
0308     }
0309 
0310     void drawShadow(QPainter *painter, const QRect &rect) const
0311     {
0312         const QPoint shadowOffset(-SHADOW_SIZE_DELEGATE, -SHADOW_SIZE_DELEGATE + 1);
0313 
0314         const auto dpr = painter->device()->devicePixelRatioF();
0315         int key = qRound((rect.height() * 1000 + rect.width()) * dpr);
0316 
0317         ShadowCache::Iterator it = mShadowCache.find(key);
0318         if (it == mShadowCache.end()) {
0319             QSize size = QSize(rect.width() + 2 * SHADOW_SIZE_DELEGATE, rect.height() + 2 * SHADOW_SIZE_DELEGATE);
0320             QColor color(0, 0, 0, SHADOW_STRENGTH_DELEGATE);
0321             QPixmap shadow = PaintUtils::generateFuzzyRect(size * dpr, color, qRound(SHADOW_SIZE_DELEGATE * dpr));
0322             shadow.setDevicePixelRatio(dpr);
0323             it = mShadowCache.insert(key, shadow);
0324         }
0325         painter->drawPixmap(rect.topLeft() + shadowOffset, it.value());
0326     }
0327 
0328     void drawText(QPainter *painter, const QRect &rect, const QColor &fgColor, const QString &fullText) const
0329     {
0330         QFontMetrics fm = mView->fontMetrics();
0331 
0332         // Elide text
0333         QString text;
0334         QHash<QString, QString>::const_iterator it = mElidedTextCache.constFind(fullText);
0335         if (it == mElidedTextCache.constEnd()) {
0336             text = fm.elidedText(fullText, mTextElideMode, rect.width());
0337             mElidedTextCache[fullText] = text;
0338         } else {
0339             text = it.value();
0340         }
0341 
0342         // Compute x pos
0343         int posX;
0344         if (text.length() == fullText.length()) {
0345             // Not elided, center text
0346             posX = (rect.width() - fm.boundingRect(text).width()) / 2;
0347         } else {
0348             // Elided, left align
0349             posX = 0;
0350         }
0351 
0352         // Draw text
0353         painter->setPen(fgColor);
0354         painter->drawText(rect.left() + posX, rect.top() + fm.ascent(), text);
0355     }
0356 
0357     void drawRating(QPainter *painter, const QRect &rect, const QVariant &value)
0358     {
0359 #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
0360         const int rating = value.toInt();
0361         const QRect ratingRect = ratingRectFromIndexRect(rect);
0362         const int hoverRating = ratingFromCursorPosition(ratingRect);
0363         mRatingPainter.paint(painter, ratingRect, rating, hoverRating);
0364 #endif
0365     }
0366 
0367     bool isTextElided(const QString &text) const
0368     {
0369         QHash<QString, QString>::const_iterator it = mElidedTextCache.constFind(text);
0370         if (it == mElidedTextCache.constEnd()) {
0371             return false;
0372         }
0373         return it.value().length() < text.length();
0374     }
0375 
0376     /**
0377      * Show a tooltip only if the item has been elided.
0378      * This function places the tooltip over the item text.
0379      */
0380     void showToolTip(const QModelIndex &index)
0381     {
0382         if (mDetails == 0 || mDetails == PreviewItemDelegate::RatingDetail) {
0383             // No text to display
0384             return;
0385         }
0386 
0387         // Gather tip text
0388         QStringList textList;
0389         bool elided = false;
0390         if (mDetails & PreviewItemDelegate::FileNameDetail) {
0391             const QString text = index.data().toString();
0392             elided |= isTextElided(text);
0393             textList << text;
0394         }
0395 
0396         // FIXME: Duplicated from drawText
0397         const KFileItem fileItem = fileItemForIndexThumbnailView(index);
0398         const bool isDirOrArchive = ArchiveUtils::fileItemIsDirOrArchive(fileItem);
0399         if (mDetails & PreviewItemDelegate::DateDetail) {
0400             if (!ArchiveUtils::fileItemIsDirOrArchive(fileItem)) {
0401                 const QDateTime dt = TimeUtils::dateTimeForFileItem(fileItem);
0402                 const QString text = QLocale().toString(dt, QLocale::ShortFormat);
0403                 elided |= isTextElided(text);
0404                 textList << text;
0405             }
0406         }
0407 
0408         if (!isDirOrArchive && (mDetails & PreviewItemDelegate::ImageSizeDetail)) {
0409             QSize fullSize;
0410             QPixmap thumbnailPix = mView->thumbnailForIndex(index, &fullSize);
0411             if (fullSize.isValid()) {
0412                 const QString text = i18nc("@item:intable %1 is image width, %2 is image height", "%1x%2", fullSize.width(), fullSize.height());
0413                 elided |= isTextElided(text);
0414                 textList << text;
0415             }
0416         }
0417 
0418         if (!isDirOrArchive && (mDetails & PreviewItemDelegate::FileSizeDetail)) {
0419             const KIO::filesize_t size = fileItem.size();
0420             if (size > 0) {
0421                 const QString text = KIO::convertSize(size);
0422                 elided |= isTextElided(text);
0423                 textList << text;
0424             }
0425         }
0426 
0427         if (!elided) {
0428             hideToolTip();
0429             return;
0430         }
0431 
0432         bool newTipLabel = !mToolTip;
0433         if (!mToolTip) {
0434             initToolTip();
0435         }
0436         mToolTip->setText(textList.join(QLatin1Char('\n')));
0437         QSize tipSize = mToolTip->sizeHint();
0438 
0439         // Compute tip position
0440         QRect rect = mView->visualRect(index);
0441         const int textY = ITEM_MARGIN_DELEGATE + mThumbnailSize.height() + ITEM_MARGIN_DELEGATE;
0442         const int spacing = 1;
0443         QRect geometry(QPoint(rect.topLeft() + QPoint((rect.width() - tipSize.width()) / 2, textY + spacing)), tipSize);
0444         if (geometry.left() < 0) {
0445             geometry.moveLeft(0);
0446         } else if (geometry.right() > mView->viewport()->width()) {
0447             geometry.moveRight(mView->viewport()->width());
0448         }
0449 
0450         // Show tip
0451         auto anim = new QParallelAnimationGroup();
0452         auto fadeIn = new QPropertyAnimation(mToolTip, "opacity");
0453         fadeIn->setStartValue(mToolTip->opacity());
0454         fadeIn->setEndValue(1.);
0455         anim->addAnimation(fadeIn);
0456 
0457         if (newTipLabel) {
0458             mToolTip->setGeometry(geometry);
0459         } else {
0460             auto move = new QPropertyAnimation(mToolTip, "geometry");
0461             move->setStartValue(mToolTip->geometry());
0462             move->setEndValue(geometry);
0463             anim->addAnimation(move);
0464         }
0465 
0466         mToolTipAnimation.reset(anim);
0467         mToolTipAnimation->start();
0468     }
0469 
0470     void hideToolTip()
0471     {
0472         if (!mToolTip) {
0473             return;
0474         }
0475         auto anim = new QSequentialAnimationGroup();
0476         if (mToolTipAnimation->state() == QPropertyAnimation::Stopped) {
0477             anim->addPause(500);
0478         }
0479         auto fadeOut = new QPropertyAnimation(mToolTip, "opacity");
0480         fadeOut->setStartValue(mToolTip->opacity());
0481         fadeOut->setEndValue(0.);
0482         anim->addAnimation(fadeOut);
0483         mToolTipAnimation.reset(anim);
0484         mToolTipAnimation->start();
0485         QObject::connect(anim, &QSequentialAnimationGroup::finished, mToolTip.data(), &ToolTipWidget::deleteLater);
0486     }
0487 
0488     int itemWidth() const
0489     {
0490         return mThumbnailSize.width() + 2 * ITEM_MARGIN_DELEGATE;
0491     }
0492 
0493     int ratingRowHeight() const
0494     {
0495 #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
0496         return qMax(mView->fontMetrics().ascent(), int(KIconLoader::SizeSmall));
0497 #else
0498         return 0;
0499 #endif
0500     }
0501 
0502     int itemHeight() const
0503     {
0504         const int lineHeight = mView->fontMetrics().height();
0505         int textHeight = 0;
0506         if (mDetails & PreviewItemDelegate::FileNameDetail) {
0507             textHeight += lineHeight;
0508         }
0509         if (mDetails & PreviewItemDelegate::DateDetail) {
0510             textHeight += lineHeight;
0511         }
0512         if (mDetails & PreviewItemDelegate::ImageSizeDetail) {
0513             textHeight += lineHeight;
0514         }
0515         if (mDetails & PreviewItemDelegate::FileSizeDetail) {
0516             textHeight += lineHeight;
0517         }
0518         if (mDetails & PreviewItemDelegate::RatingDetail) {
0519             textHeight += ratingRowHeight();
0520         }
0521         if (textHeight == 0) {
0522             // Keep at least one row of text, so that we can show folder names
0523             textHeight = lineHeight;
0524         }
0525         return mThumbnailSize.height() + textHeight + 3 * ITEM_MARGIN_DELEGATE;
0526     }
0527 
0528     void selectIndexUnderCursorIfNoMultiSelection()
0529     {
0530         if (mView->selectionModel()->selectedIndexes().size() <= 1) {
0531             mView->setCurrentIndex(mIndexUnderCursor);
0532         }
0533     }
0534 
0535     void updateToggleSelectionButton()
0536     {
0537         mToggleSelectionButton->setIcon(
0538             QIcon::fromTheme(mView->selectionModel()->isSelected(mIndexUnderCursor) ? QStringLiteral("list-remove") : QStringLiteral("list-add")));
0539     }
0540 
0541     void updateImageButtons()
0542     {
0543         const KFileItem item = fileItemForIndexThumbnailView(mIndexUnderCursor);
0544         const bool isImage = !ArchiveUtils::fileItemIsDirOrArchive(item);
0545         mFullScreenButton->setEnabled(isImage);
0546         mRotateLeftButton->setEnabled(isImage);
0547         mRotateRightButton->setEnabled(isImage);
0548     }
0549 
0550     void updateContextBar()
0551     {
0552         if (mContextBarActions == PreviewItemDelegate::NoAction) {
0553             mContextBar->hide();
0554             return;
0555         }
0556         const int width = itemWidth();
0557         const int buttonWidth = mRotateRightButton->sizeHint().width();
0558         mFullScreenButton->setVisible(mContextBarActions & PreviewItemDelegate::FullScreenAction);
0559         bool rotate = mContextBarActions & PreviewItemDelegate::RotateAction;
0560         mRotateLeftButton->setVisible(rotate && width >= 3 * buttonWidth);
0561         mRotateRightButton->setVisible(rotate && width >= 4 * buttonWidth);
0562         mContextBar->adjustSize();
0563     }
0564 
0565     void updateViewGridSize()
0566     {
0567         mView->setGridSize(QSize(itemWidth() + 8, itemHeight() + 8));
0568     }
0569 };
0570 
0571 PreviewItemDelegate::PreviewItemDelegate(ThumbnailView *view)
0572     : QItemDelegate(view)
0573     , d(new PreviewItemDelegatePrivate)
0574 {
0575     d->q = this;
0576     d->mView = view;
0577     view->viewport()->installEventFilter(this);
0578 
0579     // Set this attribute so that the viewport receives QEvent::HoverMove and
0580     // QEvent::HoverLeave events. We use these events in the event filter
0581     // installed on the viewport.
0582     // Some styles set this attribute themselves (Oxygen and Skulpture do) but
0583     // others do not (Plastique, Cleanlooks...)
0584     view->viewport()->setAttribute(Qt::WA_Hover);
0585 
0586     d->mThumbnailSize = view->thumbnailSize();
0587     d->mDetails = FileNameDetail;
0588     d->mContextBarActions = SelectionAction | FullScreenAction | RotateAction;
0589     d->mTextElideMode = Qt::ElideRight;
0590 
0591     connect(view, &ThumbnailView::rowsRemovedSignal, this, &PreviewItemDelegate::slotRowsChanged);
0592     connect(view, &ThumbnailView::rowsInsertedSignal, this, &PreviewItemDelegate::slotRowsChanged);
0593     connect(view, &ThumbnailView::selectionChangedSignal, [this]() {
0594         d->updateToggleSelectionButton();
0595     });
0596 
0597 #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
0598     d->mRatingPainter.setAlignment(Qt::AlignHCenter | Qt::AlignBottom);
0599     d->mRatingPainter.setLayoutDirection(view->layoutDirection());
0600     d->mRatingPainter.setMaxRating(10);
0601 #endif
0602 
0603     connect(view, &ThumbnailView::thumbnailSizeChanged, this, &PreviewItemDelegate::setThumbnailSize);
0604 
0605     // Button frame
0606     d->mContextBar = new QWidget(d->mView->viewport());
0607     d->mContextBar->hide();
0608 
0609     d->mToggleSelectionButton = new QToolButton;
0610     d->mToggleSelectionButton->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
0611     connect(d->mToggleSelectionButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotToggleSelectionClicked);
0612 
0613     d->mFullScreenButton = new QToolButton;
0614     d->mFullScreenButton->setIcon(QIcon::fromTheme(QStringLiteral("view-fullscreen")));
0615     connect(d->mFullScreenButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotFullScreenClicked);
0616 
0617     d->mRotateLeftButton = new QToolButton;
0618     d->mRotateLeftButton->setIcon(QIcon::fromTheme(QStringLiteral("object-rotate-left")));
0619     connect(d->mRotateLeftButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotRotateLeftClicked);
0620 
0621     d->mRotateRightButton = new QToolButton;
0622     d->mRotateRightButton->setIcon(QIcon::fromTheme(QStringLiteral("object-rotate-right")));
0623     connect(d->mRotateRightButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotRotateRightClicked);
0624 
0625     auto layout = new QHBoxLayout(d->mContextBar);
0626     layout->setContentsMargins(2, 2, 2, 2);
0627     layout->setSpacing(2);
0628     layout->addWidget(d->mToggleSelectionButton);
0629     layout->addWidget(d->mFullScreenButton);
0630     layout->addWidget(d->mRotateLeftButton);
0631     layout->addWidget(d->mRotateRightButton);
0632 
0633     // Save button
0634     d->mSaveButton = new QToolButton(d->mView->viewport());
0635     d->mSaveButton->setIcon(QIcon::fromTheme(QStringLiteral("document-save")));
0636     d->mSaveButton->hide();
0637     connect(d->mSaveButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotSaveClicked);
0638 }
0639 
0640 PreviewItemDelegate::~PreviewItemDelegate()
0641 {
0642     delete d;
0643 }
0644 
0645 QSize PreviewItemDelegate::sizeHint(const QStyleOptionViewItem & /*option*/, const QModelIndex & /*index*/) const
0646 {
0647     return d->mView->gridSize();
0648 }
0649 
0650 bool PreviewItemDelegate::eventFilter(QObject *object, QEvent *event)
0651 {
0652     if (event->type() == QEvent::Destroy || event->type() == QEvent::ChildRemoved || !d->mView) {
0653         return QItemDelegate::eventFilter(object, event);
0654     }
0655 
0656     if (object == d->mView->viewport()) {
0657         switch (event->type()) {
0658         case QEvent::ToolTip:
0659             return true;
0660 
0661         case QEvent::HoverMove:
0662         case QEvent::HoverLeave:
0663             return d->hoverEventFilter(static_cast<QHoverEvent *>(event));
0664 
0665         case QEvent::MouseButtonPress:
0666         case QEvent::MouseButtonRelease:
0667             return d->mouseButtonEventFilter(event->type());
0668 
0669         default:
0670             return false;
0671         }
0672     } else {
0673         // Necessary for the item editor to work correctly (especially closing
0674         // the editor with the Escape key)
0675         return QItemDelegate::eventFilter(object, event);
0676     }
0677 }
0678 
0679 void PreviewItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
0680 {
0681     int thumbnailHeight = d->mThumbnailSize.height();
0682     QSize fullSize;
0683     QPixmap thumbnailPix = d->mView->thumbnailForIndex(index, &fullSize);
0684     QSize thumbnailSize = thumbnailPix.size() / thumbnailPix.devicePixelRatio();
0685     const KFileItem fileItem = fileItemForIndexThumbnailView(index);
0686     const bool opaque = !thumbnailPix.hasAlphaChannel();
0687     const bool isDirOrArchive = ArchiveUtils::fileItemIsDirOrArchive(fileItem);
0688     QRect rect = option.rect;
0689     const bool selected = option.state & QStyle::State_Selected;
0690     const bool underMouse = option.state & QStyle::State_MouseOver;
0691     const bool hasFocus = option.state & QStyle::State_HasFocus;
0692 
0693     const QWidget *viewport = d->mView->viewport();
0694 
0695 #ifdef DEBUG_DRAW_BORDER
0696     painter->setPen(Qt::red);
0697     painter->setBrush(Qt::NoBrush);
0698     painter->drawRect(rect);
0699 #endif
0700 
0701     // Select color group
0702     QPalette::ColorGroup cg;
0703 
0704     if ((option.state & QStyle::State_Enabled) && (option.state & QStyle::State_Active)) {
0705         cg = QPalette::Normal;
0706     } else if ((option.state & QStyle::State_Enabled)) {
0707         cg = QPalette::Inactive;
0708     } else {
0709         cg = QPalette::Disabled;
0710     }
0711 
0712     // Select colors
0713     QColor bgColor, borderColor, fgColor;
0714 
0715     fgColor = viewport->palette().color(viewport->foregroundRole());
0716 
0717     if (selected || underMouse) {
0718         bgColor = option.palette.color(cg, QPalette::Highlight);
0719 
0720         if (hasFocus) {
0721             borderColor = bgColor.darker(FOCUS_BORDER_DARKNESS);
0722         } else {
0723             borderColor = bgColor.darker(SELECTION_BORDER_DARKNESS);
0724         }
0725     } else {
0726         bgColor = viewport->palette().color(viewport->backgroundRole());
0727         if (hasFocus) {
0728             borderColor = fgColor;
0729         } else {
0730             borderColor = bgColor.lighter(200);
0731         }
0732     }
0733 
0734     // Compute thumbnailRect
0735     QRect thumbnailRect = QRect(rect.left() + (rect.width() - thumbnailSize.width()) / 2,
0736                                 rect.top() + (thumbnailHeight - thumbnailSize.height()) + ITEM_MARGIN_DELEGATE,
0737                                 thumbnailSize.width(),
0738                                 thumbnailSize.height());
0739 
0740     // Draw background
0741     const QRect backgroundRect = thumbnailRect.adjusted(-ITEM_MARGIN_DELEGATE, -ITEM_MARGIN_DELEGATE, ITEM_MARGIN_DELEGATE, ITEM_MARGIN_DELEGATE);
0742     if (selected) {
0743         d->drawBackground(painter, backgroundRect, bgColor, borderColor);
0744     } else if (underMouse) {
0745         painter->setOpacity(0.2);
0746         d->drawBackground(painter, backgroundRect, bgColor, borderColor);
0747         painter->setOpacity(1.);
0748     } else if (opaque) {
0749         d->drawShadow(painter, thumbnailRect);
0750     }
0751 
0752     // Draw thumbnail
0753     if (opaque) {
0754         painter->setPen(borderColor);
0755         painter->setRenderHint(QPainter::Antialiasing, false);
0756         QRect borderRect = thumbnailRect.adjusted(-1, -1, 0, 0);
0757         painter->drawRect(borderRect);
0758     } else if (hasFocus && !selected) {
0759         painter->setPen(option.palette.color(cg, QPalette::Highlight));
0760         painter->setRenderHint(QPainter::Antialiasing, false);
0761         QLine underLine = QLine(thumbnailRect.bottomLeft(), thumbnailRect.bottomRight());
0762         underLine.translate(0, 3);
0763         painter->drawLine(underLine);
0764     }
0765     painter->drawPixmap(thumbnailRect.left(), thumbnailRect.top(), thumbnailPix);
0766 
0767     // Draw modified indicator
0768     bool isModified = d->mView->isModified(index);
0769     if (isModified) {
0770         // Draws a pixmap of the save button frame, as an indicator that
0771         // the image has been modified
0772         QPoint framePosition = d->saveButtonPosition(rect);
0773         d->initSaveButtonPixmap();
0774         painter->drawPixmap(framePosition, d->mSaveButtonPixmap);
0775     }
0776 
0777     // Draw busy indicator
0778     if (d->mView->isBusy(index)) {
0779         QPixmap pix = d->mView->busySequenceCurrentPixmap();
0780         painter->drawPixmap(thumbnailRect.left() + (thumbnailRect.width() - pix.width()) / 2,
0781                             thumbnailRect.top() + (thumbnailRect.height() - pix.height()) / 2,
0782                             pix);
0783     }
0784 
0785     if (index == d->mIndexUnderCursor) {
0786         // Show bar again: if the thumbnail has changed, we may need to update
0787         // its position. Don't do it if we are over rotate buttons, though: it
0788         // would not be nice to move the button now, the user may want to
0789         // rotate the image one more time.
0790         // The button will get moved when the mouse leaves.
0791         if (!d->mRotateLeftButton->underMouse() && !d->mRotateRightButton->underMouse()) {
0792             d->showContextBar(rect, thumbnailPix);
0793         }
0794         if (isModified) {
0795             // If we just rotated the image with the buttons from the
0796             // button frame, we need to show the save button frame right now.
0797             d->showSaveButton(rect);
0798         } else {
0799             d->mSaveButton->hide();
0800         }
0801     }
0802 
0803     QRect textRect(rect.left() + ITEM_MARGIN_DELEGATE,
0804                    rect.top() + 2 * ITEM_MARGIN_DELEGATE + thumbnailHeight,
0805                    rect.width() - 2 * ITEM_MARGIN_DELEGATE,
0806                    d->mView->fontMetrics().height());
0807     if (isDirOrArchive || (d->mDetails & PreviewItemDelegate::FileNameDetail)) {
0808         d->drawText(painter, textRect, fgColor, index.data().toString());
0809         textRect.moveTop(textRect.bottom());
0810     }
0811 
0812     if (!isDirOrArchive && (d->mDetails & PreviewItemDelegate::DateDetail)) {
0813         const QDateTime dt = TimeUtils::dateTimeForFileItem(fileItem);
0814         d->drawText(painter, textRect, fgColor, QLocale().toString(dt, QLocale::ShortFormat));
0815         textRect.moveTop(textRect.bottom());
0816     }
0817 
0818     if (!isDirOrArchive && (d->mDetails & PreviewItemDelegate::ImageSizeDetail)) {
0819         if (fullSize.isValid()) {
0820             const QString text = i18nc("@item:intable %1 is image width, %2 is image height", "%1x%2", fullSize.width(), fullSize.height());
0821             d->drawText(painter, textRect, fgColor, text);
0822             textRect.moveTop(textRect.bottom());
0823         }
0824     }
0825 
0826     if (!isDirOrArchive && (d->mDetails & PreviewItemDelegate::FileSizeDetail)) {
0827         const KIO::filesize_t size = fileItem.size();
0828         if (size > 0) {
0829             const QString st = KIO::convertSize(size);
0830             d->drawText(painter, textRect, fgColor, st);
0831             textRect.moveTop(textRect.bottom());
0832         }
0833     }
0834 
0835     if (!isDirOrArchive && (d->mDetails & PreviewItemDelegate::RatingDetail)) {
0836 #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE
0837         d->drawRating(painter, rect, index.data(SemanticInfoDirModel::RatingRole));
0838 #endif
0839     }
0840 
0841 #ifdef DEBUG_DRAW_CURRENT
0842     if (d->mView->currentIndex() == index) {
0843         painter->fillRect(rect.left(), rect.top(), 12, 12, Qt::red);
0844     }
0845 #endif
0846 }
0847 
0848 void PreviewItemDelegate::setThumbnailSize(const QSize &value)
0849 {
0850     d->mThumbnailSize = value;
0851     d->updateViewGridSize();
0852     d->updateContextBar();
0853     d->mElidedTextCache.clear();
0854 }
0855 
0856 void PreviewItemDelegate::slotSaveClicked()
0857 {
0858     Q_EMIT saveDocumentRequested(urlForIndexThumbnailView(d->mIndexUnderCursor));
0859 }
0860 
0861 void PreviewItemDelegate::slotRotateLeftClicked()
0862 {
0863     d->selectIndexUnderCursorIfNoMultiSelection();
0864     Q_EMIT rotateDocumentLeftRequested(urlForIndexThumbnailView(d->mIndexUnderCursor));
0865 }
0866 
0867 void PreviewItemDelegate::slotRotateRightClicked()
0868 {
0869     d->selectIndexUnderCursorIfNoMultiSelection();
0870     Q_EMIT rotateDocumentRightRequested(urlForIndexThumbnailView(d->mIndexUnderCursor));
0871 }
0872 
0873 void PreviewItemDelegate::slotFullScreenClicked()
0874 {
0875     Q_EMIT showDocumentInFullScreenRequested(urlForIndexThumbnailView(d->mIndexUnderCursor));
0876 }
0877 
0878 void PreviewItemDelegate::slotToggleSelectionClicked()
0879 {
0880     d->mView->selectionModel()->select(d->mIndexUnderCursor, QItemSelectionModel::Toggle);
0881 }
0882 
0883 PreviewItemDelegate::ThumbnailDetails PreviewItemDelegate::thumbnailDetails() const
0884 {
0885     return d->mDetails;
0886 }
0887 
0888 void PreviewItemDelegate::setThumbnailDetails(PreviewItemDelegate::ThumbnailDetails details)
0889 {
0890     d->mDetails = details;
0891     d->updateViewGridSize();
0892     d->mView->scheduleDelayedItemsLayout();
0893 }
0894 
0895 PreviewItemDelegate::ContextBarActions PreviewItemDelegate::contextBarActions() const
0896 {
0897     return d->mContextBarActions;
0898 }
0899 
0900 void PreviewItemDelegate::setContextBarActions(PreviewItemDelegate::ContextBarActions actions)
0901 {
0902     d->mContextBarActions = actions;
0903     d->updateContextBar();
0904 }
0905 
0906 Qt::TextElideMode PreviewItemDelegate::textElideMode() const
0907 {
0908     return d->mTextElideMode;
0909 }
0910 
0911 void PreviewItemDelegate::setTextElideMode(Qt::TextElideMode mode)
0912 {
0913     if (d->mTextElideMode == mode) {
0914         return;
0915     }
0916     d->mTextElideMode = mode;
0917     d->mElidedTextCache.clear();
0918     d->mView->viewport()->update();
0919 }
0920 
0921 void PreviewItemDelegate::slotRowsChanged()
0922 {
0923     // We need to update hover ui because the current index may have
0924     // disappeared: for example if the current image is removed with "del".
0925     QPoint pos = d->mView->viewport()->mapFromGlobal(QCursor::pos());
0926     QModelIndex index = d->mView->indexAt(pos);
0927     d->updateHoverUi(index);
0928 }
0929 
0930 QWidget *PreviewItemDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem & /*option*/, const QModelIndex & /*index*/) const
0931 {
0932     return new ItemEditor(parent);
0933 }
0934 
0935 void PreviewItemDelegate::setEditorData(QWidget *widget, const QModelIndex &index) const
0936 {
0937     auto edit = qobject_cast<ItemEditor *>(widget);
0938     if (!edit) {
0939         return;
0940     }
0941     edit->setText(index.data().toString());
0942 }
0943 
0944 void PreviewItemDelegate::updateEditorGeometry(QWidget *widget, const QStyleOptionViewItem &option, const QModelIndex &index) const
0945 {
0946     auto edit = qobject_cast<ItemEditor *>(widget);
0947     if (!edit) {
0948         return;
0949     }
0950     QString text = index.data().toString();
0951     int textWidth = edit->fontMetrics().boundingRect(QLatin1String("  ") + text + QLatin1String("  ")).width();
0952     QRect textRect(option.rect.left() + (option.rect.width() - textWidth) / 2,
0953                    option.rect.top() + 2 * ITEM_MARGIN_DELEGATE + d->mThumbnailSize.height(),
0954                    textWidth,
0955                    edit->sizeHint().height());
0956 
0957     edit->setGeometry(textRect);
0958 }
0959 
0960 void PreviewItemDelegate::setModelData(QWidget *widget, QAbstractItemModel *model, const QModelIndex &index) const
0961 {
0962     auto edit = qobject_cast<ItemEditor *>(widget);
0963     if (!edit) {
0964         return;
0965     }
0966     if (index.data().toString() != edit->text()) {
0967         model->setData(index, edit->text(), Qt::EditRole);
0968     }
0969 }
0970 
0971 } // namespace
0972 
0973 #include "moc_previewitemdelegate.cpp"