1 // vim: set tabstop=4 shiftwidth=4 expandtab:
2 /*
3 Gwenview: an image viewer
4 Copyright 2008 Aurélien Gâteau <agateau@kde.org>
5 Copyright 2008 Ilya Konkov <eruart@gmail.com>
6 
7 This program is free software; you can redistribute it and/or
8 modify it under the terms of the GNU General Public License
9 as published by the Free Software Foundation; either version 2
10 of the License, or (at your option) any later version.
11 
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 GNU General Public License for more details.
16 
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.
20 
21 */
22 // Self
23 #include "thumbnailbarview.h"
24 
25 // Qt
26 #include <QApplication>
27 #include <QHelpEvent>
28 #include <QItemSelectionModel>
29 #include <QPainter>
30 #include <QScrollBar>
31 #include <QTimeLine>
32 #include <QToolButton>
33 #include <QToolTip>
34 
35 #ifdef WINDOWS_PROXY_STYLE
36 #include <QWindowsStyle>
37 #endif
38 
39 // KF
40 #include <KIconLoader>
41 
42 // Local
43 #include "gwenview_lib_debug.h"
44 #include "gwenviewconfig.h"
45 #include "lib/hud/hudtheme.h"
46 #include "lib/paintutils.h"
47 #include "lib/thumbnailview/abstractthumbnailviewhelper.h"
48 
49 namespace Gwenview
50 {
51 /**
52  * Duration in ms of the smooth scroll
53  */
54 const int SMOOTH_SCROLL_DURATION = 250;
55 
56 /**
57  * Space between the item outer rect and the content, and between the
58  * thumbnail and the caption
59  */
60 const int ITEM_MARGIN = 5;
61 
62 /** How dark is the shadow, 0 is invisible, 255 is as dark as possible */
63 const int SHADOW_STRENGTH = 127;
64 
65 /** How many pixels around the thumbnail are shadowed */
66 const int SHADOW_SIZE = 4;
67 
68 struct ThumbnailBarItemDelegatePrivate {
69     // Key is height * 1000 + width
70     using ShadowCache = QMap<int, QPixmap>;
71     mutable ShadowCache mShadowCache;
72 
73     ThumbnailBarItemDelegate *q;
74     ThumbnailView *mView;
75     QToolButton *mToggleSelectionButton;
76 
77     QColor mBorderColor;
78     QPersistentModelIndex mIndexUnderCursor;
79 
setupToggleSelectionButtonGwenview::ThumbnailBarItemDelegatePrivate80     void setupToggleSelectionButton()
81     {
82         mToggleSelectionButton = new QToolButton(mView->viewport());
83         mToggleSelectionButton->setIcon(QIcon::fromTheme(QStringLiteral("list-add")));
84         mToggleSelectionButton->hide();
85         QObject::connect(mToggleSelectionButton, &QToolButton::clicked, q, &ThumbnailBarItemDelegate::toggleSelection);
86     }
87 
showToolTipGwenview::ThumbnailBarItemDelegatePrivate88     void showToolTip(QHelpEvent *helpEvent)
89     {
90         QModelIndex index = mView->indexAt(helpEvent->pos());
91         if (!index.isValid()) {
92             return;
93         }
94         QString fullText = index.data().toString();
95         QPoint pos = QCursor::pos();
96         QToolTip::showText(pos, fullText, mView);
97     }
98 
drawShadowGwenview::ThumbnailBarItemDelegatePrivate99     void drawShadow(QPainter *painter, const QRect &rect) const
100     {
101         const QPoint shadowOffset(-SHADOW_SIZE, -SHADOW_SIZE + 1);
102 
103         const auto dpr = painter->device()->devicePixelRatioF();
104         int key = qRound((rect.height() * 1000 + rect.width()) * dpr);
105 
106         ShadowCache::Iterator it = mShadowCache.find(key);
107         if (it == mShadowCache.end()) {
108             QSize size = QSize(rect.width() + 2 * SHADOW_SIZE, rect.height() + 2 * SHADOW_SIZE);
109             QColor color(0, 0, 0, SHADOW_STRENGTH);
110             QPixmap shadow = PaintUtils::generateFuzzyRect(size * dpr, color, qRound(SHADOW_SIZE * dpr));
111             shadow.setDevicePixelRatio(dpr);
112             it = mShadowCache.insert(key, shadow);
113         }
114         painter->drawPixmap(rect.topLeft() + shadowOffset, it.value());
115     }
116 
hoverEventFilterGwenview::ThumbnailBarItemDelegatePrivate117     bool hoverEventFilter(QHoverEvent *event)
118     {
119         QModelIndex index = mView->indexAt(event->pos());
120         if (index != mIndexUnderCursor) {
121             updateHoverUi(index);
122         }
123         return false;
124     }
125 
updateHoverUiGwenview::ThumbnailBarItemDelegatePrivate126     void updateHoverUi(const QModelIndex &index)
127     {
128         mIndexUnderCursor = index;
129 
130         if (mIndexUnderCursor.isValid() && GwenviewConfig::thumbnailActions() != ThumbnailActions::None) {
131             updateToggleSelectionButton();
132 
133             const QRect rect = mView->visualRect(mIndexUnderCursor);
134             mToggleSelectionButton->move(rect.topLeft() + QPoint(2, 2));
135             mToggleSelectionButton->show();
136         } else {
137             mToggleSelectionButton->hide();
138         }
139     }
140 
updateToggleSelectionButtonGwenview::ThumbnailBarItemDelegatePrivate141     void updateToggleSelectionButton()
142     {
143         bool isSelected = mView->selectionModel()->isSelected(mIndexUnderCursor);
144         mToggleSelectionButton->setIcon(QIcon::fromTheme(isSelected ? QStringLiteral("list-remove") : QStringLiteral("list-add")));
145     }
146 };
147 
ThumbnailBarItemDelegate(ThumbnailView * view)148 ThumbnailBarItemDelegate::ThumbnailBarItemDelegate(ThumbnailView *view)
149     : QAbstractItemDelegate(view)
150     , d(new ThumbnailBarItemDelegatePrivate)
151 {
152     d->q = this;
153     d->mView = view;
154     d->setupToggleSelectionButton();
155     view->viewport()->installEventFilter(this);
156 
157     // Set this attribute so that the viewport receives QEvent::HoverMove and
158     // QEvent::HoverLeave events. We use these events in the event filter
159     // installed on the viewport.
160     // Some styles set this attribute themselves (Oxygen and Skulpture do) but
161     // others do not (Plastique, Cleanlooks...)
162     view->viewport()->setAttribute(Qt::WA_Hover);
163 
164     d->mBorderColor = PaintUtils::alphaAdjustedF(QColor(Qt::white), 0.65);
165 
166     connect(view, &ThumbnailView::selectionChangedSignal, [this]() {
167         d->updateToggleSelectionButton();
168     });
169 }
170 
sizeHint(const QStyleOptionViewItem &,const QModelIndex & index) const171 QSize ThumbnailBarItemDelegate::sizeHint(const QStyleOptionViewItem & /*option*/, const QModelIndex &index) const
172 {
173     QSize size;
174     if (d->mView->thumbnailScaleMode() == ThumbnailView::ScaleToFit) {
175         size = d->mView->gridSize();
176     } else {
177         QPixmap thumbnailPix = d->mView->thumbnailForIndex(index);
178         size = thumbnailPix.size() / thumbnailPix.devicePixelRatio();
179         size.rwidth() += ITEM_MARGIN * 2;
180         size.rheight() += ITEM_MARGIN * 2;
181     }
182     return size;
183 }
184 
eventFilter(QObject *,QEvent * event)185 bool ThumbnailBarItemDelegate::eventFilter(QObject *, QEvent *event)
186 {
187     switch (event->type()) {
188     case QEvent::ToolTip:
189         d->showToolTip(static_cast<QHelpEvent *>(event));
190         return true;
191     case QEvent::HoverMove:
192     case QEvent::HoverLeave:
193         return d->hoverEventFilter(static_cast<QHoverEvent *>(event));
194     default:
195         break;
196     }
197 
198     return false;
199 }
200 
paint(QPainter * painter,const QStyleOptionViewItem & option,const QModelIndex & index) const201 void ThumbnailBarItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
202 {
203     bool isSelected = option.state & QStyle::State_Selected;
204     bool isCurrent = d->mView->selectionModel()->currentIndex() == index;
205     QPixmap thumbnailPix = d->mView->thumbnailForIndex(index);
206     QSize thumbnailSize = thumbnailPix.size() / thumbnailPix.devicePixelRatio();
207     QRect rect = option.rect;
208 
209     QStyleOptionViewItem opt = option;
210     const QWidget *widget = opt.widget;
211     QStyle *style = widget ? widget->style() : QApplication::style();
212     if (isSelected && !isCurrent) {
213         // Draw selected but not current item backgrounds with some transparency
214         // so that the current item stands out.
215         painter->setOpacity(.33);
216     }
217     style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget);
218     painter->setOpacity(1);
219 
220     // Draw thumbnail
221     if (!thumbnailPix.isNull()) {
222         QRect thumbnailRect = QRect(rect.left() + (rect.width() - thumbnailSize.width()) / 2,
223                                     rect.top() + (rect.height() - thumbnailSize.height()) / 2 - 1,
224                                     thumbnailSize.width(),
225                                     thumbnailSize.height());
226 
227         if (!thumbnailPix.hasAlphaChannel()) {
228             d->drawShadow(painter, thumbnailRect);
229             painter->setPen(d->mBorderColor);
230             painter->setRenderHint(QPainter::Antialiasing, false);
231             QRect borderRect = thumbnailRect.adjusted(-1, -1, 0, 0);
232             painter->drawRect(borderRect);
233         }
234         painter->drawPixmap(thumbnailRect.left(), thumbnailRect.top(), thumbnailPix);
235 
236         // Draw busy indicator
237         if (d->mView->isBusy(index)) {
238             QPixmap pix = d->mView->busySequenceCurrentPixmap();
239             painter->drawPixmap(thumbnailRect.left() + (thumbnailRect.width() - pix.width()) / 2,
240                                 thumbnailRect.top() + (thumbnailRect.height() - pix.height()) / 2,
241                                 pix);
242         }
243     }
244 }
245 
toggleSelection()246 void ThumbnailBarItemDelegate::toggleSelection()
247 {
248     d->mView->selectionModel()->select(d->mIndexUnderCursor, QItemSelectionModel::Toggle);
249 }
250 
~ThumbnailBarItemDelegate()251 ThumbnailBarItemDelegate::~ThumbnailBarItemDelegate()
252 {
253     delete d;
254 }
255 
256 // this is disabled by David Edmundson as I can't figure out how to port it
257 // I hope with breeze being the default we don't want to start making our own styles anyway
258 #ifdef WINDOWS_PROXY_STYLE
259 /**
260  * This proxy style makes it possible to override the value returned by
261  * styleHint() which leads to not-so-nice results with some styles.
262  *
263  * We cannot use QProxyStyle because it takes ownership of the base style,
264  * which causes crash when user change styles.
265  */
266 class ProxyStyle : public QWindowsStyle
267 {
268 public:
ProxyStyle()269     ProxyStyle()
270         : QWindowsStyle()
271     {
272     }
273 
drawPrimitive(PrimitiveElement pe,const QStyleOption * opt,QPainter * p,const QWidget * w=0) const274     void drawPrimitive(PrimitiveElement pe, const QStyleOption *opt, QPainter *p, const QWidget *w = 0) const
275     {
276         QApplication::style()->drawPrimitive(pe, opt, p, w);
277     }
278 
drawControl(ControlElement element,const QStyleOption * opt,QPainter * p,const QWidget * w=0) const279     void drawControl(ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *w = 0) const
280     {
281         QApplication::style()->drawControl(element, opt, p, w);
282     }
283 
drawComplexControl(ComplexControl cc,const QStyleOptionComplex * opt,QPainter * p,const QWidget * w=0) const284     void drawComplexControl(ComplexControl cc, const QStyleOptionComplex *opt, QPainter *p, const QWidget *w = 0) const
285     {
286         QApplication::style()->drawComplexControl(cc, opt, p, w);
287     }
288 
styleHint(StyleHint sh,const QStyleOption * opt=0,const QWidget * w=0,QStyleHintReturn * shret=0) const289     int styleHint(StyleHint sh, const QStyleOption *opt = 0, const QWidget *w = 0, QStyleHintReturn *shret = 0) const
290     {
291         switch (sh) {
292         case SH_ItemView_ShowDecorationSelected:
293             // We want the highlight to cover our thumbnail
294             return true;
295         case SH_ScrollView_FrameOnlyAroundContents:
296             // Ensure the frame does not include the scrollbar. This ensure the
297             // scrollbar touches the edge of the window and thus can touch the
298             // edge of the screen when maximized
299             return false;
300         default:
301             return QApplication::style()->styleHint(sh, opt, w, shret);
302         }
303     }
304 
polish(QApplication * application)305     void polish(QApplication *application)
306     {
307         QApplication::style()->polish(application);
308     }
309 
polish(QPalette & palette)310     void polish(QPalette &palette)
311     {
312         QApplication::style()->polish(palette);
313     }
314 
polish(QWidget * widget)315     void polish(QWidget *widget)
316     {
317         QApplication::style()->polish(widget);
318     }
319 
unpolish(QWidget * widget)320     void unpolish(QWidget *widget)
321     {
322         QApplication::style()->unpolish(widget);
323     }
324 
unpolish(QApplication * application)325     void unpolish(QApplication *application)
326     {
327         QApplication::style()->unpolish(application);
328     }
329 
pixelMetric(PixelMetric pm,const QStyleOption * opt,const QWidget * widget) const330     int pixelMetric(PixelMetric pm, const QStyleOption *opt, const QWidget *widget) const
331     {
332         switch (pm) {
333         case PM_MaximumDragDistance:
334             // Ensure the fullscreen thumbnailbar does not go away while
335             // dragging the scrollbar if the mouse cursor is too far away from
336             // the widget
337             return -1;
338         default:
339             return QApplication::style()->pixelMetric(pm, opt, widget);
340         }
341     }
342 };
343 #endif // WINDOWS_PROXY_STYLE
344 
345 using QSizeDimension = int (QSize::*)() const;
346 
347 struct ThumbnailBarViewPrivate {
348     ThumbnailBarView *q;
349     QStyle *mStyle;
350     QTimeLine *mTimeLine;
351 
352     Qt::Orientation mOrientation;
353     int mRowCount;
354 
scrollBarGwenview::ThumbnailBarViewPrivate355     QScrollBar *scrollBar() const
356     {
357         return mOrientation == Qt::Horizontal ? q->horizontalScrollBar() : q->verticalScrollBar();
358     }
359 
mainDimensionGwenview::ThumbnailBarViewPrivate360     QSizeDimension mainDimension() const
361     {
362         return mOrientation == Qt::Horizontal ? &QSize::width : &QSize::height;
363     }
364 
oppositeDimensionGwenview::ThumbnailBarViewPrivate365     QSizeDimension oppositeDimension() const
366     {
367         return mOrientation == Qt::Horizontal ? &QSize::height : &QSize::width;
368     }
369 
smoothScrollToGwenview::ThumbnailBarViewPrivate370     void smoothScrollTo(const QModelIndex &index)
371     {
372         if (!index.isValid()) {
373             return;
374         }
375 
376         const QRect rect = q->visualRect(index);
377 
378         int oldValue = scrollBar()->value();
379         int newValue = scrollToValue(rect);
380         if (mTimeLine->state() == QTimeLine::Running) {
381             mTimeLine->stop();
382         }
383         mTimeLine->setFrameRange(oldValue, newValue);
384         mTimeLine->start();
385     }
386 
scrollToValueGwenview::ThumbnailBarViewPrivate387     int scrollToValue(const QRect &rect)
388     {
389         // This code is a much simplified version of
390         // QListViewPrivate::horizontalScrollToValue()
391         const QRect area = q->viewport()->rect();
392         int value = scrollBar()->value();
393 
394         if (mOrientation == Qt::Horizontal) {
395             if (q->isRightToLeft()) {
396                 value += (area.width() - rect.width()) / 2 - rect.left();
397             } else {
398                 value += rect.left() - (area.width() - rect.width()) / 2;
399             }
400         } else {
401             value += rect.top() - (area.height() - rect.height()) / 2;
402         }
403         return value;
404     }
405 
updateMinMaxSizesGwenview::ThumbnailBarViewPrivate406     void updateMinMaxSizes()
407     {
408         QSizeDimension dimension = oppositeDimension();
409         int scrollBarSize = (scrollBar()->sizeHint().*dimension)();
410         QSize minSize(0, mRowCount * 48 + scrollBarSize);
411         QSize maxSize(QWIDGETSIZE_MAX, mRowCount * 256 + scrollBarSize);
412         if (mOrientation == Qt::Vertical) {
413             minSize.transpose();
414             maxSize.transpose();
415         }
416         q->setMinimumSize(minSize);
417         q->setMaximumSize(maxSize);
418     }
419 
updateThumbnailSizeGwenview::ThumbnailBarViewPrivate420     void updateThumbnailSize()
421     {
422         QSizeDimension dimension = oppositeDimension();
423         int scrollBarSize = (scrollBar()->sizeHint().*dimension)();
424         int widgetSize = (q->size().*dimension)();
425 
426         if (mRowCount > 1) {
427             // Decrease widgetSize because otherwise the view sometimes wraps at
428             // mRowCount-1 instead of mRowCount. Probably because gridSize *
429             // mRowCount is too close to widgetSize.
430             --widgetSize;
431         }
432 
433         int gridWidth, gridHeight;
434         if (mOrientation == Qt::Horizontal) {
435             gridHeight = (widgetSize - scrollBarSize - 2 * q->frameWidth()) / mRowCount;
436             gridWidth = qRound(gridHeight * q->thumbnailAspectRatio());
437         } else {
438             gridWidth = (widgetSize - scrollBarSize - 2 * q->frameWidth()) / mRowCount;
439             gridHeight = qRound(gridWidth / q->thumbnailAspectRatio());
440         }
441         if (q->thumbnailScaleMode() == ThumbnailView::ScaleToFit) {
442             q->setGridSize(QSize(gridWidth, gridHeight));
443         }
444         q->setThumbnailWidth(gridWidth - ITEM_MARGIN * 2);
445     }
446 };
447 
ThumbnailBarView(QWidget * parent)448 ThumbnailBarView::ThumbnailBarView(QWidget *parent)
449     : ThumbnailView(parent)
450     , d(new ThumbnailBarViewPrivate)
451 {
452     d->q = this;
453     d->mTimeLine = new QTimeLine(SMOOTH_SCROLL_DURATION, this);
454     connect(d->mTimeLine, &QTimeLine::frameChanged, this, &ThumbnailBarView::slotFrameChanged);
455 
456     d->mRowCount = 1;
457     d->mOrientation = Qt::Vertical; // To pass value-has-changed check in setOrientation()
458     setOrientation(Qt::Horizontal);
459 
460     setObjectName(QStringLiteral("thumbnailBarView"));
461     setWrapping(true);
462 
463 #ifdef WINDOWS_PROXY_STYLE
464     d->mStyle = new ProxyStyle;
465     setStyle(d->mStyle);
466 #endif
467 }
468 
~ThumbnailBarView()469 ThumbnailBarView::~ThumbnailBarView()
470 {
471 #ifdef WINDOWS_PROXY_STYLE
472     delete d->mStyle;
473 #endif
474     delete d;
475 }
476 
orientation() const477 Qt::Orientation ThumbnailBarView::orientation() const
478 {
479     return d->mOrientation;
480 }
481 
setOrientation(Qt::Orientation orientation)482 void ThumbnailBarView::setOrientation(Qt::Orientation orientation)
483 {
484     if (d->mOrientation == orientation) {
485         return;
486     }
487     d->mOrientation = orientation;
488 
489     if (d->mOrientation == Qt::Vertical) {
490         setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
491         setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
492         setFlow(LeftToRight);
493     } else {
494         setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
495         setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
496         setFlow(TopToBottom);
497     }
498 
499     d->updateMinMaxSizes();
500 }
501 
slotFrameChanged(int value)502 void ThumbnailBarView::slotFrameChanged(int value)
503 {
504     d->scrollBar()->setValue(value);
505 }
506 
resizeEvent(QResizeEvent * event)507 void ThumbnailBarView::resizeEvent(QResizeEvent *event)
508 {
509     ThumbnailView::resizeEvent(event);
510     d->updateThumbnailSize();
511 }
512 
selectionChanged(const QItemSelection & selected,const QItemSelection & deselected)513 void ThumbnailBarView::selectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
514 {
515     ThumbnailView::selectionChanged(selected, deselected);
516 
517     QModelIndexList oldList = deselected.indexes();
518     QModelIndexList newList = selected.indexes();
519     // Only scroll the list if the user went from one image to another. If the
520     // user just unselected one image from a set of two, he might want to
521     // reselect it again, scrolling the thumbnails would prevent him from
522     // reselecting it by clicking again without moving the mouse.
523     if (oldList.count() == 1 && newList.count() == 1 && isVisible()) {
524         d->smoothScrollTo(newList.first());
525     }
526 }
527 
wheelEvent(QWheelEvent * event)528 void ThumbnailBarView::wheelEvent(QWheelEvent *event)
529 {
530     d->scrollBar()->setValue(d->scrollBar()->value() - event->angleDelta().y());
531 }
532 
rowCount() const533 int ThumbnailBarView::rowCount() const
534 {
535     return d->mRowCount;
536 }
537 
setRowCount(int rowCount)538 void ThumbnailBarView::setRowCount(int rowCount)
539 {
540     Q_ASSERT(rowCount > 0);
541     d->mRowCount = rowCount;
542     d->updateMinMaxSizes();
543     d->updateThumbnailSize();
544 }
545 
546 } // namespace
547