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