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"