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"