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 Copyright 2008 Ilya Konkov <eruart@gmail.com>
0006 
0007 This program is free software; you can redistribute it and/or
0008 modify it under the terms of the GNU General Public License
0009 as published by the Free Software Foundation; either version 2
0010 of the License, or (at your option) any later version.
0011 
0012 This program is distributed in the hope that it will be useful,
0013 but WITHOUT ANY WARRANTY; without even the implied warranty of
0014 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0015 GNU General Public License for more details.
0016 
0017 You should have received a copy of the GNU General Public License
0018 along with this program; if not, write to the Free Software
0019 Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.
0020 
0021 */
0022 // Self
0023 #include "thumbnailbarview.h"
0024 
0025 // Qt
0026 #include <QApplication>
0027 #include <QHelpEvent>
0028 #include <QItemSelectionModel>
0029 #include <QPainter>
0030 #include <QScrollBar>
0031 #include <QTimeLine>
0032 #include <QToolButton>
0033 #include <QToolTip>
0034 
0035 #ifdef WINDOWS_PROXY_STYLE
0036 #include <QWindowsStyle>
0037 #endif
0038 
0039 // KF
0040 
0041 // Local
0042 #include "gwenview_lib_debug.h"
0043 #include "gwenviewconfig.h"
0044 #include "lib/paintutils.h"
0045 
0046 namespace Gwenview
0047 {
0048 /**
0049  * Duration in ms of the smooth scroll
0050  */
0051 const int SMOOTH_SCROLL_DURATION = 250;
0052 
0053 /**
0054  * Space between the item outer rect and the content, and between the
0055  * thumbnail and the caption
0056  */
0057 const int ITEM_MARGIN = 5;
0058 
0059 /** How dark is the shadow, 0 is invisible, 255 is as dark as possible */
0060 const int SHADOW_STRENGTH = 127;
0061 
0062 /** How many pixels around the thumbnail are shadowed */
0063 const int SHADOW_SIZE = 4;
0064 
0065 struct ThumbnailBarItemDelegatePrivate {
0066     // Key is height * 1000 + width
0067     using ShadowCache = QMap<int, QPixmap>;
0068     mutable ShadowCache mShadowCache;
0069 
0070     ThumbnailBarItemDelegate *q = nullptr;
0071     ThumbnailView *mView = nullptr;
0072     QToolButton *mToggleSelectionButton = nullptr;
0073 
0074     QColor mBorderColor;
0075     QPersistentModelIndex mIndexUnderCursor;
0076 
0077     void setupToggleSelectionButton()
0078     {
0079         mToggleSelectionButton = new QToolButton(mView->viewport());
0080         mToggleSelectionButton->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
0081         mToggleSelectionButton->hide();
0082         QObject::connect(mToggleSelectionButton, &QToolButton::clicked, q, &ThumbnailBarItemDelegate::toggleSelection);
0083     }
0084 
0085     void showToolTip(QHelpEvent *helpEvent)
0086     {
0087         QModelIndex index = mView->indexAt(helpEvent->pos());
0088         if (!index.isValid()) {
0089             return;
0090         }
0091         QString fullText = index.data().toString();
0092         QPoint pos = QCursor::pos();
0093         QToolTip::showText(pos, fullText, mView);
0094     }
0095 
0096     void drawShadow(QPainter *painter, const QRect &rect) const
0097     {
0098         const QPoint shadowOffset(-SHADOW_SIZE, -SHADOW_SIZE + 1);
0099 
0100         const auto dpr = painter->device()->devicePixelRatioF();
0101         int key = qRound((rect.height() * 1000 + rect.width()) * dpr);
0102 
0103         ShadowCache::Iterator it = mShadowCache.find(key);
0104         if (it == mShadowCache.end()) {
0105             QSize size = QSize(rect.width() + 2 * SHADOW_SIZE, rect.height() + 2 * SHADOW_SIZE);
0106             QColor color(0, 0, 0, SHADOW_STRENGTH);
0107             QPixmap shadow = PaintUtils::generateFuzzyRect(size * dpr, color, qRound(SHADOW_SIZE * dpr));
0108             shadow.setDevicePixelRatio(dpr);
0109             it = mShadowCache.insert(key, shadow);
0110         }
0111         painter->drawPixmap(rect.topLeft() + shadowOffset, it.value());
0112     }
0113 
0114     bool hoverEventFilter(QHoverEvent *event)
0115     {
0116         QModelIndex index = mView->indexAt(event->pos());
0117         if (index != mIndexUnderCursor) {
0118             updateHoverUi(index);
0119         }
0120         return false;
0121     }
0122 
0123     void updateHoverUi(const QModelIndex &index)
0124     {
0125         mIndexUnderCursor = index;
0126 
0127         if (mIndexUnderCursor.isValid() && GwenviewConfig::thumbnailActions() != ThumbnailActions::None) {
0128             updateToggleSelectionButton();
0129 
0130             const QRect rect = mView->visualRect(mIndexUnderCursor);
0131             mToggleSelectionButton->move(rect.topLeft() + QPoint(2, 2));
0132             mToggleSelectionButton->show();
0133         } else {
0134             mToggleSelectionButton->hide();
0135         }
0136     }
0137 
0138     void updateToggleSelectionButton()
0139     {
0140         bool isSelected = mView->selectionModel()->isSelected(mIndexUnderCursor);
0141         mToggleSelectionButton->setIcon(QIcon::fromTheme(isSelected ? QStringLiteral("list-remove") : QStringLiteral("list-add")));
0142     }
0143 };
0144 
0145 ThumbnailBarItemDelegate::ThumbnailBarItemDelegate(ThumbnailView *view)
0146     : QAbstractItemDelegate(view)
0147     , d(new ThumbnailBarItemDelegatePrivate)
0148 {
0149     d->q = this;
0150     d->mView = view;
0151     d->setupToggleSelectionButton();
0152     view->viewport()->installEventFilter(this);
0153 
0154     // Set this attribute so that the viewport receives QEvent::HoverMove and
0155     // QEvent::HoverLeave events. We use these events in the event filter
0156     // installed on the viewport.
0157     // Some styles set this attribute themselves (Oxygen and Skulpture do) but
0158     // others do not (Plastique, Cleanlooks...)
0159     view->viewport()->setAttribute(Qt::WA_Hover);
0160 
0161     d->mBorderColor = PaintUtils::alphaAdjustedF(QColor(Qt::white), 0.65);
0162 
0163     connect(view, &ThumbnailView::selectionChangedSignal, [this]() {
0164         d->updateToggleSelectionButton();
0165     });
0166 }
0167 
0168 QSize ThumbnailBarItemDelegate::sizeHint(const QStyleOptionViewItem & /*option*/, const QModelIndex &index) const
0169 {
0170     QSize size;
0171     if (d->mView->thumbnailScaleMode() == ThumbnailView::ScaleToFit) {
0172         size = d->mView->gridSize();
0173     } else {
0174         QPixmap thumbnailPix = d->mView->thumbnailForIndex(index);
0175         size = thumbnailPix.size() / thumbnailPix.devicePixelRatio();
0176         size.rwidth() += ITEM_MARGIN * 2;
0177         size.rheight() += ITEM_MARGIN * 2;
0178     }
0179     return size;
0180 }
0181 
0182 bool ThumbnailBarItemDelegate::eventFilter(QObject *, QEvent *event)
0183 {
0184     switch (event->type()) {
0185     case QEvent::ToolTip:
0186         d->showToolTip(static_cast<QHelpEvent *>(event));
0187         return true;
0188     case QEvent::HoverMove:
0189     case QEvent::HoverLeave:
0190         return d->hoverEventFilter(static_cast<QHoverEvent *>(event));
0191     default:
0192         break;
0193     }
0194 
0195     return false;
0196 }
0197 
0198 void ThumbnailBarItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
0199 {
0200     bool isSelected = option.state & QStyle::State_Selected;
0201     bool isCurrent = d->mView->selectionModel()->currentIndex() == index;
0202     QPixmap thumbnailPix = d->mView->thumbnailForIndex(index);
0203     QSize thumbnailSize = thumbnailPix.size() / thumbnailPix.devicePixelRatio();
0204     QRect rect = option.rect;
0205 
0206     QStyleOptionViewItem opt = option;
0207     const QWidget *widget = opt.widget;
0208     QStyle *style = widget ? widget->style() : QApplication::style();
0209     if (isSelected && !isCurrent) {
0210         // Draw selected but not current item backgrounds with some transparency
0211         // so that the current item stands out.
0212         painter->setOpacity(.33);
0213     }
0214     style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget);
0215     painter->setOpacity(1);
0216 
0217     // Draw thumbnail
0218     if (!thumbnailPix.isNull()) {
0219         QRect thumbnailRect = QRect(rect.left() + (rect.width() - thumbnailSize.width()) / 2,
0220                                     rect.top() + (rect.height() - thumbnailSize.height()) / 2 - 1,
0221                                     thumbnailSize.width(),
0222                                     thumbnailSize.height());
0223 
0224         if (!thumbnailPix.hasAlphaChannel()) {
0225             d->drawShadow(painter, thumbnailRect);
0226             painter->setPen(d->mBorderColor);
0227             painter->setRenderHint(QPainter::Antialiasing, false);
0228             QRect borderRect = thumbnailRect.adjusted(-1, -1, 0, 0);
0229             painter->drawRect(borderRect);
0230         }
0231         painter->drawPixmap(thumbnailRect.left(), thumbnailRect.top(), thumbnailPix);
0232 
0233         // Draw busy indicator
0234         if (d->mView->isBusy(index)) {
0235             QPixmap pix = d->mView->busySequenceCurrentPixmap();
0236             painter->drawPixmap(thumbnailRect.left() + (thumbnailRect.width() - pix.width()) / 2,
0237                                 thumbnailRect.top() + (thumbnailRect.height() - pix.height()) / 2,
0238                                 pix);
0239         }
0240     }
0241 }
0242 
0243 void ThumbnailBarItemDelegate::toggleSelection()
0244 {
0245     d->mView->selectionModel()->select(d->mIndexUnderCursor, QItemSelectionModel::Toggle);
0246 }
0247 
0248 ThumbnailBarItemDelegate::~ThumbnailBarItemDelegate()
0249 {
0250     delete d;
0251 }
0252 
0253 // this is disabled by David Edmundson as I can't figure out how to port it
0254 // I hope with breeze being the default we don't want to start making our own styles anyway
0255 #ifdef WINDOWS_PROXY_STYLE
0256 /**
0257  * This proxy style makes it possible to override the value returned by
0258  * styleHint() which leads to not-so-nice results with some styles.
0259  *
0260  * We cannot use QProxyStyle because it takes ownership of the base style,
0261  * which causes crash when user change styles.
0262  */
0263 class ProxyStyle : public QWindowsStyle
0264 {
0265 public:
0266     ProxyStyle()
0267         : QWindowsStyle()
0268     {
0269     }
0270 
0271     void drawPrimitive(PrimitiveElement pe, const QStyleOption *opt, QPainter *p, const QWidget *w = 0) const
0272     {
0273         QApplication::style()->drawPrimitive(pe, opt, p, w);
0274     }
0275 
0276     void drawControl(ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *w = 0) const
0277     {
0278         QApplication::style()->drawControl(element, opt, p, w);
0279     }
0280 
0281     void drawComplexControl(ComplexControl cc, const QStyleOptionComplex *opt, QPainter *p, const QWidget *w = 0) const
0282     {
0283         QApplication::style()->drawComplexControl(cc, opt, p, w);
0284     }
0285 
0286     int styleHint(StyleHint sh, const QStyleOption *opt = 0, const QWidget *w = 0, QStyleHintReturn *shret = 0) const
0287     {
0288         switch (sh) {
0289         case SH_ItemView_ShowDecorationSelected:
0290             // We want the highlight to cover our thumbnail
0291             return true;
0292         case SH_ScrollView_FrameOnlyAroundContents:
0293             // Ensure the frame does not include the scrollbar. This ensure the
0294             // scrollbar touches the edge of the window and thus can touch the
0295             // edge of the screen when maximized
0296             return false;
0297         default:
0298             return QApplication::style()->styleHint(sh, opt, w, shret);
0299         }
0300     }
0301 
0302     void polish(QApplication *application)
0303     {
0304         QApplication::style()->polish(application);
0305     }
0306 
0307     void polish(QPalette &palette)
0308     {
0309         QApplication::style()->polish(palette);
0310     }
0311 
0312     void polish(QWidget *widget)
0313     {
0314         QApplication::style()->polish(widget);
0315     }
0316 
0317     void unpolish(QWidget *widget)
0318     {
0319         QApplication::style()->unpolish(widget);
0320     }
0321 
0322     void unpolish(QApplication *application)
0323     {
0324         QApplication::style()->unpolish(application);
0325     }
0326 
0327     int pixelMetric(PixelMetric pm, const QStyleOption *opt, const QWidget *widget) const
0328     {
0329         switch (pm) {
0330         case PM_MaximumDragDistance:
0331             // Ensure the fullscreen thumbnailbar does not go away while
0332             // dragging the scrollbar if the mouse cursor is too far away from
0333             // the widget
0334             return -1;
0335         default:
0336             return QApplication::style()->pixelMetric(pm, opt, widget);
0337         }
0338     }
0339 };
0340 #endif // WINDOWS_PROXY_STYLE
0341 
0342 using QSizeDimension = int (QSize::*)() const;
0343 
0344 struct ThumbnailBarViewPrivate {
0345     ThumbnailBarView *q;
0346     QStyle *mStyle;
0347     QTimeLine *mTimeLine;
0348 
0349     Qt::Orientation mOrientation;
0350     int mRowCount;
0351 
0352     QScrollBar *scrollBar() const
0353     {
0354         return mOrientation == Qt::Horizontal ? q->horizontalScrollBar() : q->verticalScrollBar();
0355     }
0356 
0357     QSizeDimension mainDimension() const
0358     {
0359         return mOrientation == Qt::Horizontal ? &QSize::width : &QSize::height;
0360     }
0361 
0362     QSizeDimension oppositeDimension() const
0363     {
0364         return mOrientation == Qt::Horizontal ? &QSize::height : &QSize::width;
0365     }
0366 
0367     void smoothScrollTo(const QModelIndex &index)
0368     {
0369         if (!index.isValid()) {
0370             return;
0371         }
0372 
0373         const QRect rect = q->visualRect(index);
0374 
0375         int oldValue = scrollBar()->value();
0376         int newValue = scrollToValue(rect);
0377         if (mTimeLine->state() == QTimeLine::Running) {
0378             mTimeLine->stop();
0379         }
0380         mTimeLine->setFrameRange(oldValue, newValue);
0381         mTimeLine->start();
0382     }
0383 
0384     int scrollToValue(const QRect &rect)
0385     {
0386         // This code is a much simplified version of
0387         // QListViewPrivate::horizontalScrollToValue()
0388         const QRect area = q->viewport()->rect();
0389         int value = scrollBar()->value();
0390 
0391         if (mOrientation == Qt::Horizontal) {
0392             if (q->isRightToLeft()) {
0393                 value += (area.width() - rect.width()) / 2 - rect.left();
0394             } else {
0395                 value += rect.left() - (area.width() - rect.width()) / 2;
0396             }
0397         } else {
0398             value += rect.top() - (area.height() - rect.height()) / 2;
0399         }
0400         return value;
0401     }
0402 
0403     void updateMinMaxSizes()
0404     {
0405         QSizeDimension dimension = oppositeDimension();
0406         int scrollBarSize = (scrollBar()->sizeHint().*dimension)();
0407         QSize minSize(0, mRowCount * 48 + scrollBarSize);
0408         QSize maxSize(QWIDGETSIZE_MAX, mRowCount * 256 + scrollBarSize);
0409         if (mOrientation == Qt::Vertical) {
0410             minSize.transpose();
0411             maxSize.transpose();
0412         }
0413         q->setMinimumSize(minSize);
0414         q->setMaximumSize(maxSize);
0415     }
0416 
0417     void updateThumbnailSize()
0418     {
0419         QSizeDimension dimension = oppositeDimension();
0420         int scrollBarSize = (scrollBar()->sizeHint().*dimension)();
0421         int widgetSize = (q->size().*dimension)();
0422 
0423         if (mRowCount > 1) {
0424             // Decrease widgetSize because otherwise the view sometimes wraps at
0425             // mRowCount-1 instead of mRowCount. Probably because gridSize *
0426             // mRowCount is too close to widgetSize.
0427             --widgetSize;
0428         }
0429 
0430         int gridWidth, gridHeight;
0431         if (mOrientation == Qt::Horizontal) {
0432             gridHeight = (widgetSize - scrollBarSize - 2 * q->frameWidth()) / mRowCount;
0433             gridWidth = qRound(gridHeight * q->thumbnailAspectRatio());
0434         } else {
0435             gridWidth = (widgetSize - scrollBarSize - 2 * q->frameWidth()) / mRowCount;
0436             gridHeight = qRound(gridWidth / q->thumbnailAspectRatio());
0437         }
0438         if (q->thumbnailScaleMode() == ThumbnailView::ScaleToFit) {
0439             q->setGridSize(QSize(gridWidth, gridHeight));
0440         }
0441         q->setThumbnailWidth(gridWidth - ITEM_MARGIN * 2);
0442     }
0443 };
0444 
0445 ThumbnailBarView::ThumbnailBarView(QWidget *parent)
0446     : ThumbnailView(parent)
0447     , d(new ThumbnailBarViewPrivate)
0448 {
0449     d->q = this;
0450     d->mTimeLine = new QTimeLine(SMOOTH_SCROLL_DURATION, this);
0451     connect(d->mTimeLine, &QTimeLine::frameChanged, this, &ThumbnailBarView::slotFrameChanged);
0452 
0453     d->mRowCount = 1;
0454     d->mOrientation = Qt::Vertical; // To pass value-has-changed check in setOrientation()
0455     setOrientation(Qt::Horizontal);
0456 
0457     setObjectName(QStringLiteral("thumbnailBarView"));
0458     setWrapping(true);
0459 
0460 #ifdef WINDOWS_PROXY_STYLE
0461     d->mStyle = new ProxyStyle;
0462     setStyle(d->mStyle);
0463 #endif
0464 }
0465 
0466 ThumbnailBarView::~ThumbnailBarView()
0467 {
0468 #ifdef WINDOWS_PROXY_STYLE
0469     delete d->mStyle;
0470 #endif
0471     delete d;
0472 }
0473 
0474 Qt::Orientation ThumbnailBarView::orientation() const
0475 {
0476     return d->mOrientation;
0477 }
0478 
0479 void ThumbnailBarView::setOrientation(Qt::Orientation orientation)
0480 {
0481     if (d->mOrientation == orientation) {
0482         return;
0483     }
0484     d->mOrientation = orientation;
0485 
0486     if (d->mOrientation == Qt::Vertical) {
0487         setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0488         setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
0489         setFlow(LeftToRight);
0490     } else {
0491         setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
0492         setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
0493         setFlow(TopToBottom);
0494     }
0495 
0496     d->updateMinMaxSizes();
0497 }
0498 
0499 void ThumbnailBarView::slotFrameChanged(int value)
0500 {
0501     d->scrollBar()->setValue(value);
0502 }
0503 
0504 void ThumbnailBarView::resizeEvent(QResizeEvent *event)
0505 {
0506     ThumbnailView::resizeEvent(event);
0507     d->updateThumbnailSize();
0508 }
0509 
0510 void ThumbnailBarView::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
0511 {
0512     ThumbnailView::selectionChanged(selected, deselected);
0513 
0514     QModelIndexList oldList = deselected.indexes();
0515     QModelIndexList newList = selected.indexes();
0516     // Only scroll the list if the user went from one image to another. If the
0517     // user just unselected one image from a set of two, he might want to
0518     // reselect it again, scrolling the thumbnails would prevent him from
0519     // reselecting it by clicking again without moving the mouse.
0520     if (oldList.count() == 1 && newList.count() == 1 && isVisible()) {
0521         d->smoothScrollTo(newList.first());
0522     }
0523 }
0524 
0525 void ThumbnailBarView::wheelEvent(QWheelEvent *event)
0526 {
0527     d->scrollBar()->setValue(d->scrollBar()->value() - event->angleDelta().y());
0528 }
0529 
0530 int ThumbnailBarView::rowCount() const
0531 {
0532     return d->mRowCount;
0533 }
0534 
0535 void ThumbnailBarView::setRowCount(int rowCount)
0536 {
0537     Q_ASSERT(rowCount > 0);
0538     d->mRowCount = rowCount;
0539     d->updateMinMaxSizes();
0540     d->updateThumbnailSize();
0541 }
0542 
0543 } // namespace
0544 
0545 #include "moc_thumbnailbarview.cpp"