diff --git a/lib/documentview/rasterimageview.cpp b/lib/documentview/rasterimageview.cpp index 20f13b22..ba5041f6 100644 --- a/lib/documentview/rasterimageview.cpp +++ b/lib/documentview/rasterimageview.cpp @@ -1,577 +1,577 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2011 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ // Self #include "rasterimageview.h" // Local #include #include #include #include #include // KDE // Qt #include #include #include #include #include #include namespace Gwenview { struct RasterImageViewPrivate { RasterImageView* q; ImageScaler* mScaler; bool mEmittedCompleted; // Config AbstractImageView::AlphaBackgroundMode mAlphaBackgroundMode; QColor mAlphaBackgroundColor; cmsUInt32Number mRenderingIntent; // /Config bool mBufferIsEmpty; QPixmap mCurrentBuffer; // The alternate buffer is useful when scrolling: existing content is copied // to mAlternateBuffer and buffers are swapped. This avoids allocating a new // QPixmap every time the image is scrolled. QPixmap mAlternateBuffer; QTimer* mUpdateTimer; QPointer mTool; bool mApplyDisplayTransform; // Defaults to true. Can be set to false if there is no need or no way to apply color profile cmsHTRANSFORM mDisplayTransform; void updateDisplayTransform(QImage::Format format) { GV_RETURN_IF_FAIL(format != QImage::Format_Invalid); mApplyDisplayTransform = false; if (mDisplayTransform) { cmsDeleteTransform(mDisplayTransform); } mDisplayTransform = nullptr; Cms::Profile::Ptr profile = q->document()->cmsProfile(); if (!profile) { // The assumption that something unmarked is *probably* sRGB is better than failing to apply any transform when one // has a wide-gamut screen. profile = Cms::Profile::getSRgbProfile(); } Cms::Profile::Ptr monitorProfile = Cms::Profile::getMonitorProfile(); if (!monitorProfile) { qWarning() << "Could not get monitor color profile"; return; } cmsUInt32Number cmsFormat = 0; switch (format) { case QImage::Format_RGB32: case QImage::Format_ARGB32: cmsFormat = TYPE_BGRA_8; break; case QImage::Format_Grayscale8: cmsFormat = TYPE_GRAY_8; break; default: qWarning() << "Gwenview can only apply color profile on RGB32 or ARGB32 images"; return; } mDisplayTransform = cmsCreateTransform(profile->handle(), cmsFormat, monitorProfile->handle(), cmsFormat, mRenderingIntent, cmsFLAGS_BLACKPOINTCOMPENSATION); mApplyDisplayTransform = true; } void setupUpdateTimer() { mUpdateTimer = new QTimer(q); mUpdateTimer->setInterval(500); mUpdateTimer->setSingleShot(true); QObject::connect(mUpdateTimer, SIGNAL(timeout()), q, SLOT(updateBuffer())); } void startAnimationIfNecessary() { if (q->document() && q->isVisible()) { q->document()->startAnimation(); } } QRectF mapViewportToZoomedImage(const QRectF& viewportRect) const { return QRectF( viewportRect.topLeft() - q->imageOffset() + q->scrollPos(), viewportRect.size() ); } void setScalerRegionToVisibleRect() { QRectF rect = mapViewportToZoomedImage(q->boundingRect()); mScaler->setDestinationRegion(QRegion(rect.toRect())); } void resizeBuffer() { const auto dpr = q->devicePixelRatio(); QSize size = q->visibleImageSize().toSize(); if (size * dpr == mCurrentBuffer.size()) { return; } if (!size.isValid()) { mAlternateBuffer = QPixmap(); mCurrentBuffer = QPixmap(); return; } mAlternateBuffer = QPixmap(size * dpr); mAlternateBuffer.setDevicePixelRatio(dpr); mAlternateBuffer.fill(Qt::transparent); { QPainter painter(&mAlternateBuffer); painter.drawPixmap(0, 0, mCurrentBuffer); } qSwap(mAlternateBuffer, mCurrentBuffer); mAlternateBuffer = QPixmap(); } void drawAlphaBackground(QPainter* painter, const QRect& viewportRect, const QPoint& zoomedImageTopLeft, const QPixmap &texture) { switch (mAlphaBackgroundMode) { case AbstractImageView::AlphaBackgroundNone: painter->fillRect(viewportRect, Qt::transparent); break; case AbstractImageView::AlphaBackgroundCheckBoard: { const QPoint textureOffset( zoomedImageTopLeft.x() % texture.width(), zoomedImageTopLeft.y() % texture.height()); painter->drawTiledPixmap( viewportRect, texture, textureOffset); break; } case AbstractImageView::AlphaBackgroundSolid: painter->fillRect(viewportRect, mAlphaBackgroundColor); break; default: Q_ASSERT(0); } } }; RasterImageView::RasterImageView(QGraphicsItem* parent) : AbstractImageView(parent) , d(new RasterImageViewPrivate) { d->q = this; d->mEmittedCompleted = false; d->mApplyDisplayTransform = true; d->mDisplayTransform = nullptr; d->mAlphaBackgroundMode = AlphaBackgroundNone; d->mAlphaBackgroundColor = Qt::black; d->mRenderingIntent = INTENT_PERCEPTUAL; d->mBufferIsEmpty = true; d->mScaler = new ImageScaler(this); connect(d->mScaler, &ImageScaler::scaledRect, this, &RasterImageView::updateFromScaler); d->setupUpdateTimer(); } RasterImageView::~RasterImageView() { if (d->mTool) { d->mTool.data()->toolDeactivated(); } if (d->mDisplayTransform) { cmsDeleteTransform(d->mDisplayTransform); } delete d; } void RasterImageView::setAlphaBackgroundMode(AlphaBackgroundMode mode) { d->mAlphaBackgroundMode = mode; if (document() && document()->hasAlphaChannel()) { d->mCurrentBuffer = QPixmap(); updateBuffer(); } } void RasterImageView::setAlphaBackgroundColor(const QColor& color) { d->mAlphaBackgroundColor = color; if (document() && document()->hasAlphaChannel()) { d->mCurrentBuffer = QPixmap(); updateBuffer(); } } void RasterImageView::setRenderingIntent(const RenderingIntent::Enum& renderingIntent) { if (d->mRenderingIntent != renderingIntent) { d->mRenderingIntent = renderingIntent; updateBuffer(); } } void RasterImageView::loadFromDocument() { Document::Ptr doc = document(); if (!doc) { return; } connect(doc.data(), &Document::metaInfoLoaded, this, &RasterImageView::slotDocumentMetaInfoLoaded); connect(doc.data(), &Document::isAnimatedUpdated, this, &RasterImageView::slotDocumentIsAnimatedUpdated); const Document::LoadingState state = doc->loadingState(); if (state == Document::MetaInfoLoaded || state == Document::Loaded) { slotDocumentMetaInfoLoaded(); } } void RasterImageView::slotDocumentMetaInfoLoaded() { if (document()->size().isValid()) { QMetaObject::invokeMethod(this, "finishSetDocument", Qt::QueuedConnection); } else { // Could not retrieve image size from meta info, we need to load the // full image now. connect(document().data(), &Document::loaded, this, &RasterImageView::finishSetDocument); document()->startLoadingFullImage(); } } void RasterImageView::finishSetDocument() { GV_RETURN_IF_FAIL(document()->size().isValid()); d->mScaler->setDocument(document()); d->resizeBuffer(); applyPendingScrollPos(); connect(document().data(), &Document::imageRectUpdated, this, &RasterImageView::updateImageRect); if (zoomToFit()) { // Force the update otherwise if computeZoomToFit() returns 1, setZoom() // will think zoom has not changed and won't update the image setZoom(computeZoomToFit(), QPointF(-1, -1), ForceUpdate); } else if (zoomToFill()) { setZoom(computeZoomToFill(), QPointF(-1, -1), ForceUpdate); } else { // Not only call updateBuffer, but also ensure the initial transformation mode // of the image scaler is set correctly when zoom is unchanged (see Bug 396736). onZoomChanged(); } d->startAnimationIfNecessary(); update(); } void RasterImageView::updateImageRect(const QRect& imageRect) { QRectF viewRect = mapToView(imageRect); if (!viewRect.intersects(boundingRect())) { return; } if (zoomToFit()) { setZoom(computeZoomToFit()); } else if (zoomToFill()) { setZoom(computeZoomToFill()); } else { applyPendingScrollPos(); } d->setScalerRegionToVisibleRect(); update(); emit imageRectUpdated(); } void RasterImageView::slotDocumentIsAnimatedUpdated() { d->startAnimationIfNecessary(); } void RasterImageView::updateFromScaler(int zoomedImageLeft, int zoomedImageTop, const QImage& image) { if (d->mApplyDisplayTransform) { d->updateDisplayTransform(image.format()); if (d->mDisplayTransform) { quint8 *bytes = const_cast(image.bits()); cmsDoTransform(d->mDisplayTransform, bytes, bytes, image.width() * image.height()); } } d->resizeBuffer(); int viewportLeft = zoomedImageLeft - scrollPos().x(); int viewportTop = zoomedImageTop - scrollPos().y(); d->mBufferIsEmpty = false; { QPainter painter(&d->mCurrentBuffer); painter.setCompositionMode(QPainter::CompositionMode_Source); if (document()->hasAlphaChannel()) { d->drawAlphaBackground( &painter, QRect(viewportLeft, viewportTop, image.width(), image.height()), QPoint(zoomedImageLeft, zoomedImageTop), alphaBackgroundTexture() ); // This is required so transparent pixels don't replace our background painter.setCompositionMode(QPainter::CompositionMode_SourceOver); } painter.drawImage(viewportLeft, viewportTop, image); } update(); if (!d->mEmittedCompleted) { d->mEmittedCompleted = true; emit completed(); } } void RasterImageView::onZoomChanged() { d->mScaler->setZoom(zoom()); if (!d->mUpdateTimer->isActive()) { updateBuffer(); } } void RasterImageView::onImageOffsetChanged() { update(); } void RasterImageView::onScrollPosChanged(const QPointF& oldPos) { QPointF delta = scrollPos() - oldPos; // Scroll existing { if (d->mAlternateBuffer.size() != d->mCurrentBuffer.size()) { d->mAlternateBuffer = QPixmap(d->mCurrentBuffer.size()); - d->mAlternateBuffer.setDevicePixelRatio(d->mCurrentBuffer.devicePixelRatioF()); + d->mAlternateBuffer.setDevicePixelRatio(d->mCurrentBuffer.devicePixelRatio()); } d->mAlternateBuffer.fill(Qt::transparent); QPainter painter(&d->mAlternateBuffer); painter.drawPixmap(-delta, d->mCurrentBuffer); } qSwap(d->mCurrentBuffer, d->mAlternateBuffer); // Scale missing parts QRegion bufferRegion = QRect(scrollPos().toPoint(), d->mCurrentBuffer.size() / devicePixelRatio()); QRegion updateRegion = bufferRegion - bufferRegion.translated(-delta.toPoint()); updateBuffer(updateRegion); update(); } void RasterImageView::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget* /*widget*/) { d->mCurrentBuffer.setDevicePixelRatio(devicePixelRatio()); QPointF topLeft = imageOffset(); if (zoomToFit()) { // In zoomToFit mode, scale crudely the buffer to fit the screen. This // provide an approximate rendered which will be replaced when the scheduled // proper scale is ready. // Round point and size independently, to keep consistency with the below (non zoomToFit) painting const QRect rect = QRect(topLeft.toPoint(), (dipDocumentSize() * zoom()).toSize()); painter->drawPixmap(rect, d->mCurrentBuffer); } else { painter->drawPixmap(topLeft.toPoint(), d->mCurrentBuffer); } if (d->mTool) { d->mTool.data()->paint(painter); } // Debug #if 0 QSizeF visibleSize = documentSize() * zoom(); painter->setPen(Qt::red); painter->drawRect(topLeft.x(), topLeft.y(), visibleSize.width() - 1, visibleSize.height() - 1); painter->setPen(Qt::blue); painter->drawRect(topLeft.x(), topLeft.y(), d->mCurrentBuffer.width() - 1, d->mCurrentBuffer.height() - 1); #endif } void RasterImageView::resizeEvent(QGraphicsSceneResizeEvent* event) { // If we are in zoomToFit mode and have something in our buffer, delay the // update: paint() will paint a scaled version of the buffer until resizing // is done. This is much faster than rescaling the whole image for each // resize event we receive. // mUpdateTimer must be started before calling AbstractImageView::resizeEvent() // because AbstractImageView::resizeEvent() will call onZoomChanged(), which // will trigger an immediate update unless the mUpdateTimer is active. if ((zoomToFit() || zoomToFill()) && !d->mBufferIsEmpty) { d->mUpdateTimer->start(); } AbstractImageView::resizeEvent(event); if (!zoomToFit() || !zoomToFill()) { // Only update buffer if we are not in zoomToFit mode: if we are // onZoomChanged() will have already updated the buffer. updateBuffer(); } } void RasterImageView::updateBuffer(const QRegion& region) { d->mUpdateTimer->stop(); if (region.isEmpty()) { d->setScalerRegionToVisibleRect(); } else { d->mScaler->setDestinationRegion(region); } } void RasterImageView::setCurrentTool(AbstractRasterImageViewTool* tool) { if (d->mTool) { d->mTool.data()->toolDeactivated(); d->mTool.data()->deleteLater(); } // Go back to default cursor when tool is deactivated. We need to call this here and // not further below in case toolActivated wants to set its own new cursor afterwards. updateCursor(); d->mTool = tool; if (d->mTool) { d->mTool.data()->toolActivated(); } emit currentToolChanged(tool); update(); } AbstractRasterImageViewTool* RasterImageView::currentTool() const { return d->mTool.data(); } void RasterImageView::mousePressEvent(QGraphicsSceneMouseEvent* event) { if (d->mTool) { d->mTool.data()->mousePressEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::mousePressEvent(event); } void RasterImageView::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event) { if (d->mTool) { d->mTool.data()->mouseDoubleClickEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::mouseDoubleClickEvent(event); } void RasterImageView::mouseMoveEvent(QGraphicsSceneMouseEvent* event) { if (d->mTool) { d->mTool.data()->mouseMoveEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::mouseMoveEvent(event); } void RasterImageView::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) { if (d->mTool) { d->mTool.data()->mouseReleaseEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::mouseReleaseEvent(event); } void RasterImageView::wheelEvent(QGraphicsSceneWheelEvent* event) { if (d->mTool) { d->mTool.data()->wheelEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::wheelEvent(event); } void RasterImageView::keyPressEvent(QKeyEvent* event) { if (d->mTool) { d->mTool.data()->keyPressEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::keyPressEvent(event); } void RasterImageView::keyReleaseEvent(QKeyEvent* event) { if (d->mTool) { d->mTool.data()->keyReleaseEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::keyReleaseEvent(event); } void RasterImageView::hoverMoveEvent(QGraphicsSceneHoverEvent* event) { if (d->mTool) { d->mTool.data()->hoverMoveEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::hoverMoveEvent(event); } } // namespace diff --git a/lib/thumbnailview/previewitemdelegate.cpp b/lib/thumbnailview/previewitemdelegate.cpp index e825048e..95899f4f 100644 --- a/lib/thumbnailview/previewitemdelegate.cpp +++ b/lib/thumbnailview/previewitemdelegate.cpp @@ -1,985 +1,985 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2008 Aurélien Gâteau This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ // Self #include "previewitemdelegate.h" #include // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE #include #endif // Local #include "archiveutils.h" #include "itemeditor.h" #include "paintutils.h" #include "thumbnailview.h" #include "timeutils.h" #include "tooltipwidget.h" #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE #include "../semanticinfo/semanticinfodirmodel.h" #endif // Define this to be able to fine tune the rendering of the selection // background through a config file //#define FINETUNE_SELECTION_BACKGROUND #ifdef FINETUNE_SELECTION_BACKGROUND #include #include #endif //#define DEBUG_DRAW_BORDER //#define DEBUG_DRAW_CURRENT namespace Gwenview { /** * Space between the item outer rect and the content, and between the * thumbnail and the caption */ const int ITEM_MARGIN = 5; /** How darker is the border line around selection */ const int SELECTION_BORDER_DARKNESS = 140; const int FOCUS_BORDER_DARKNESS = 200; /** Radius of the selection rounded corners, in pixels */ const int SELECTION_RADIUS = 5; /** Space between the item outer rect and the context bar */ const int CONTEXTBAR_MARGIN = 1; /** How dark is the shadow, 0 is invisible, 255 is as dark as possible */ const int SHADOW_STRENGTH = 128; /** How many pixels around the thumbnail are shadowed */ const int SHADOW_SIZE = 4; static KFileItem fileItemForIndex(const QModelIndex& index) { Q_ASSERT(index.isValid()); QVariant data = index.data(KDirModel::FileItemRole); return qvariant_cast(data); } static QUrl urlForIndex(const QModelIndex& index) { KFileItem item = fileItemForIndex(index); return item.url(); } struct PreviewItemDelegatePrivate { /** * Maps full text to elided text. */ mutable QHash mElidedTextCache; // Key is height * 1000 + width typedef QHash ShadowCache; mutable ShadowCache mShadowCache; PreviewItemDelegate* q; ThumbnailView* mView; QWidget* mContextBar; QToolButton* mSaveButton; QPixmap mSaveButtonPixmap; QToolButton* mToggleSelectionButton; QToolButton* mFullScreenButton; QToolButton* mRotateLeftButton; QToolButton* mRotateRightButton; #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE KRatingPainter mRatingPainter; #endif QPersistentModelIndex mIndexUnderCursor; QSize mThumbnailSize; PreviewItemDelegate::ThumbnailDetails mDetails; PreviewItemDelegate::ContextBarActions mContextBarActions; Qt::TextElideMode mTextElideMode; QPointer mToolTip; QScopedPointer mToolTipAnimation; void initSaveButtonPixmap() { if (!mSaveButtonPixmap.isNull()) { return; } // Necessary otherwise we won't see the save button itself mSaveButton->adjustSize(); mSaveButtonPixmap = QPixmap(mSaveButton->sizeHint()); mSaveButtonPixmap.fill(Qt::transparent); mSaveButton->render(&mSaveButtonPixmap, QPoint(), QRegion(), QWidget::DrawChildren); } void showContextBar(const QRect& rect, const QPixmap& thumbnailPix) { if (mContextBarActions == PreviewItemDelegate::NoAction) { return; } mContextBar->adjustSize(); // Center bar, except if only showing SelectionAction. const int posX = mContextBarActions == PreviewItemDelegate::SelectionAction ? 0 : (rect.width() - mContextBar->width()) / 2; - const int thumbnailPixHeight = qRound(thumbnailPix.height() / thumbnailPix.devicePixelRatioF()); + const int thumbnailPixHeight = qRound(thumbnailPix.height() / thumbnailPix.devicePixelRatio()); const int posY = qMax(CONTEXTBAR_MARGIN, mThumbnailSize.height() - thumbnailPixHeight - mContextBar->height()); mContextBar->move(rect.topLeft() + QPoint(posX, posY)); mContextBar->show(); } void initToolTip() { mToolTip = new ToolTipWidget(mView->viewport()); mToolTip->setOpacity(0); mToolTip->show(); } bool hoverEventFilter(QHoverEvent* event) { QModelIndex index = mView->indexAt(event->pos()); if (index != mIndexUnderCursor) { updateHoverUi(index); } else { // Same index, nothing to do, but repaint anyway in case we are // over the rating row mView->update(mIndexUnderCursor); } return false; } void updateHoverUi(const QModelIndex& index) { QModelIndex oldIndex = mIndexUnderCursor; mIndexUnderCursor = index; mView->update(oldIndex); if (QApplication::style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, mView)) { mView->setCursor(mIndexUnderCursor.isValid() ? Qt::PointingHandCursor : Qt::ArrowCursor); } if (mIndexUnderCursor.isValid()) { updateToggleSelectionButton(); updateImageButtons(); const QRect rect = mView->visualRect(mIndexUnderCursor); const QPixmap thumbnailPix = mView->thumbnailForIndex(index); showContextBar(rect, thumbnailPix); if (mView->isModified(mIndexUnderCursor)) { showSaveButton(rect); } else { mSaveButton->hide(); } showToolTip(index); mView->update(mIndexUnderCursor); } else { mContextBar->hide(); mSaveButton->hide(); hideToolTip(); } } QRect ratingRectFromIndexRect(const QRect& rect) const { return QRect( rect.left(), rect.bottom() - ratingRowHeight() - ITEM_MARGIN, rect.width(), ratingRowHeight()); } #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE int ratingFromCursorPosition(const QRect& ratingRect) const { const QPoint pos = mView->viewport()->mapFromGlobal(QCursor::pos()); return mRatingPainter.ratingFromPosition(ratingRect, pos); } #endif bool mouseButtonEventFilter(QEvent::Type type) { #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE const QRect rect = ratingRectFromIndexRect(mView->visualRect(mIndexUnderCursor)); const int rating = ratingFromCursorPosition(rect); if (rating == -1) { return false; } if (type == QEvent::MouseButtonRelease) { q->setDocumentRatingRequested(urlForIndex(mIndexUnderCursor) , rating); } return true; #else return false; #endif } QPoint saveButtonPosition(const QRect& itemRect) const { QSize buttonSize = mSaveButton->sizeHint(); int posX = itemRect.right() - buttonSize.width(); int posY = itemRect.top() + mThumbnailSize.height() + 2 * ITEM_MARGIN - buttonSize.height(); return QPoint(posX, posY); } void showSaveButton(const QRect& itemRect) const { mSaveButton->move(saveButtonPosition(itemRect)); mSaveButton->show(); } void drawBackground(QPainter* painter, const QRect& rect, const QColor& bgColor, const QColor& borderColor) const { int bgH, bgS, bgV; int borderH, borderS, borderV, borderMargin; #ifdef FINETUNE_SELECTION_BACKGROUND QSettings settings(QDir::homePath() + "/colors.ini", QSettings::IniFormat); bgH = settings.value("bg/h").toInt(); bgS = settings.value("bg/s").toInt(); bgV = settings.value("bg/v").toInt(); borderH = settings.value("border/h").toInt(); borderS = settings.value("border/s").toInt(); borderV = settings.value("border/v").toInt(); borderMargin = settings.value("border/margin").toInt(); #else bgH = 0; bgS = -20; bgV = 43; borderH = 0; borderS = -100; borderV = 60; borderMargin = 1; #endif painter->setRenderHint(QPainter::Antialiasing); QRectF rectF = QRectF(rect).adjusted(0.5, 0.5, -0.5, -0.5); QPainterPath path = PaintUtils::roundedRectangle(rectF, SELECTION_RADIUS); QLinearGradient gradient(rectF.topLeft(), rectF.bottomLeft()); gradient.setColorAt(0, PaintUtils::adjustedHsv(bgColor, bgH, bgS, bgV)); gradient.setColorAt(1, bgColor); painter->fillPath(path, gradient); painter->setPen(borderColor); painter->drawPath(path); painter->setPen(PaintUtils::adjustedHsv(borderColor, borderH, borderS, borderV)); rectF = rectF.adjusted(borderMargin, borderMargin, -borderMargin, -borderMargin); path = PaintUtils::roundedRectangle(rectF, SELECTION_RADIUS); painter->drawPath(path); } void drawShadow(QPainter* painter, const QRect& rect) const { const QPoint shadowOffset(-SHADOW_SIZE, -SHADOW_SIZE + 1); const auto dpr = painter->device()->devicePixelRatioF(); int key = qRound((rect.height() * 1000 + rect.width()) * dpr); ShadowCache::Iterator it = mShadowCache.find(key); if (it == mShadowCache.end()) { QSize size = QSize(rect.width() + 2 * SHADOW_SIZE, rect.height() + 2 * SHADOW_SIZE); QColor color(0, 0, 0, SHADOW_STRENGTH); QPixmap shadow = PaintUtils::generateFuzzyRect(size * dpr, color, qRound(SHADOW_SIZE * dpr)); shadow.setDevicePixelRatio(dpr); it = mShadowCache.insert(key, shadow); } painter->drawPixmap(rect.topLeft() + shadowOffset, it.value()); } void drawText(QPainter* painter, const QRect& rect, const QColor& fgColor, const QString& fullText) const { QFontMetrics fm = mView->fontMetrics(); // Elide text QString text; QHash::const_iterator it = mElidedTextCache.constFind(fullText); if (it == mElidedTextCache.constEnd()) { text = fm.elidedText(fullText, mTextElideMode, rect.width()); mElidedTextCache[fullText] = text; } else { text = it.value(); } // Compute x pos int posX; if (text.length() == fullText.length()) { // Not elided, center text posX = (rect.width() - fm.boundingRect(text).width()) / 2; } else { // Elided, left align posX = 0; } // Draw text painter->setPen(fgColor); painter->drawText(rect.left() + posX, rect.top() + fm.ascent(), text); } void drawRating(QPainter* painter, const QRect& rect, const QVariant& value) { #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE const int rating = value.toInt(); const QRect ratingRect = ratingRectFromIndexRect(rect); const int hoverRating = ratingFromCursorPosition(ratingRect); mRatingPainter.paint(painter, ratingRect, rating, hoverRating); #endif } bool isTextElided(const QString& text) const { QHash::const_iterator it = mElidedTextCache.constFind(text); if (it == mElidedTextCache.constEnd()) { return false; } return it.value().length() < text.length(); } /** * Show a tooltip only if the item has been elided. * This function places the tooltip over the item text. */ void showToolTip(const QModelIndex& index) { if (mDetails == 0 || mDetails == PreviewItemDelegate::RatingDetail) { // No text to display return; } // Gather tip text QStringList textList; bool elided = false; if (mDetails & PreviewItemDelegate::FileNameDetail) { const QString text = index.data().toString(); elided |= isTextElided(text); textList << text; } // FIXME: Duplicated from drawText const KFileItem fileItem = fileItemForIndex(index); const bool isDirOrArchive = ArchiveUtils::fileItemIsDirOrArchive(fileItem); if (mDetails & PreviewItemDelegate::DateDetail) { if (!ArchiveUtils::fileItemIsDirOrArchive(fileItem)) { const QDateTime dt = TimeUtils::dateTimeForFileItem(fileItem); const QString text = QLocale().toString(dt, QLocale::ShortFormat); elided |= isTextElided(text); textList << text; } } if (!isDirOrArchive && (mDetails & PreviewItemDelegate::ImageSizeDetail)) { QSize fullSize; QPixmap thumbnailPix = mView->thumbnailForIndex(index, &fullSize); if (fullSize.isValid()) { const QString text = QStringLiteral("%1x%2").arg(fullSize.width()).arg(fullSize.height()); elided |= isTextElided(text); textList << text; } } if (!isDirOrArchive && (mDetails & PreviewItemDelegate::FileSizeDetail)) { const KIO::filesize_t size = fileItem.size(); if (size > 0) { const QString text = KIO::convertSize(size); elided |= isTextElided(text); textList << text; } } if (!elided) { hideToolTip(); return; } bool newTipLabel = !mToolTip; if (!mToolTip) { initToolTip(); } mToolTip->setText(textList.join(QLatin1Char('\n'))); QSize tipSize = mToolTip->sizeHint(); // Compute tip position QRect rect = mView->visualRect(index); const int textY = ITEM_MARGIN + mThumbnailSize.height() + ITEM_MARGIN; const int spacing = 1; QRect geometry( QPoint(rect.topLeft() + QPoint((rect.width() - tipSize.width()) / 2, textY + spacing)), tipSize ); if (geometry.left() < 0) { geometry.moveLeft(0); } else if (geometry.right() > mView->viewport()->width()) { geometry.moveRight(mView->viewport()->width()); } // Show tip QParallelAnimationGroup* anim = new QParallelAnimationGroup(); QPropertyAnimation* fadeIn = new QPropertyAnimation(mToolTip, "opacity"); fadeIn->setStartValue(mToolTip->opacity()); fadeIn->setEndValue(1.); anim->addAnimation(fadeIn); if (newTipLabel) { mToolTip->setGeometry(geometry); } else { QPropertyAnimation* move = new QPropertyAnimation(mToolTip, "geometry"); move->setStartValue(mToolTip->geometry()); move->setEndValue(geometry); anim->addAnimation(move); } mToolTipAnimation.reset(anim); mToolTipAnimation->start(); } void hideToolTip() { if (!mToolTip) { return; } QSequentialAnimationGroup* anim = new QSequentialAnimationGroup(); if (mToolTipAnimation->state() == QPropertyAnimation::Stopped) { anim->addPause(500); } QPropertyAnimation* fadeOut = new QPropertyAnimation(mToolTip, "opacity"); fadeOut->setStartValue(mToolTip->opacity()); fadeOut->setEndValue(0.); anim->addAnimation(fadeOut); mToolTipAnimation.reset(anim); mToolTipAnimation->start(); QObject::connect(anim, &QSequentialAnimationGroup::finished, mToolTip.data(), &ToolTipWidget::deleteLater); } int itemWidth() const { return mThumbnailSize.width() + 2 * ITEM_MARGIN; } int ratingRowHeight() const { #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE return qMax(mView->fontMetrics().ascent(), int(KIconLoader::SizeSmall)); #else return 0; #endif } int itemHeight() const { const int lineHeight = mView->fontMetrics().height(); int textHeight = 0; if (mDetails & PreviewItemDelegate::FileNameDetail) { textHeight += lineHeight; } if (mDetails & PreviewItemDelegate::DateDetail) { textHeight += lineHeight; } if (mDetails & PreviewItemDelegate::ImageSizeDetail) { textHeight += lineHeight; } if (mDetails & PreviewItemDelegate::FileSizeDetail) { textHeight += lineHeight; } if (mDetails & PreviewItemDelegate::RatingDetail) { textHeight += ratingRowHeight(); } if (textHeight == 0) { // Keep at least one row of text, so that we can show folder names textHeight = lineHeight; } return mThumbnailSize.height() + textHeight + 3 * ITEM_MARGIN; } void selectIndexUnderCursorIfNoMultiSelection() { if (mView->selectionModel()->selectedIndexes().size() <= 1) { mView->setCurrentIndex(mIndexUnderCursor); } } void updateToggleSelectionButton() { mToggleSelectionButton->setIcon(QIcon::fromTheme( mView->selectionModel()->isSelected(mIndexUnderCursor) ? QStringLiteral("list-remove") : QStringLiteral("list-add") )); } void updateImageButtons() { const KFileItem item = fileItemForIndex(mIndexUnderCursor); const bool isImage = !ArchiveUtils::fileItemIsDirOrArchive(item); mFullScreenButton->setEnabled(isImage); mRotateLeftButton->setEnabled(isImage); mRotateRightButton->setEnabled(isImage); } void updateContextBar() { if (mContextBarActions == PreviewItemDelegate::NoAction) { mContextBar->hide(); return; } const int width = itemWidth(); const int buttonWidth = mRotateRightButton->sizeHint().width(); mFullScreenButton->setVisible(mContextBarActions & PreviewItemDelegate::FullScreenAction); bool rotate = mContextBarActions & PreviewItemDelegate::RotateAction; mRotateLeftButton->setVisible(rotate && width >= 3 * buttonWidth); mRotateRightButton->setVisible(rotate && width >= 4 * buttonWidth); mContextBar->adjustSize(); } void updateViewGridSize() { mView->setGridSize(QSize(itemWidth(), itemHeight())); } }; PreviewItemDelegate::PreviewItemDelegate(ThumbnailView* view) : QItemDelegate(view) , d(new PreviewItemDelegatePrivate) { d->q = this; d->mView = view; view->viewport()->installEventFilter(this); // Set this attribute so that the viewport receives QEvent::HoverMove and // QEvent::HoverLeave events. We use these events in the event filter // installed on the viewport. // Some styles set this attribute themselves (Oxygen and Skulpture do) but // others do not (Plastique, Cleanlooks...) view->viewport()->setAttribute(Qt::WA_Hover); d->mThumbnailSize = view->thumbnailSize(); d->mDetails = FileNameDetail; d->mContextBarActions = SelectionAction | FullScreenAction | RotateAction; d->mTextElideMode = Qt::ElideRight; connect(view, &ThumbnailView::rowsRemovedSignal, this, &PreviewItemDelegate::slotRowsChanged); connect(view, &ThumbnailView::rowsInsertedSignal, this, &PreviewItemDelegate::slotRowsChanged); connect(view, &ThumbnailView::selectionChangedSignal, [this]() { d->updateToggleSelectionButton(); }); #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE d->mRatingPainter.setAlignment(Qt::AlignHCenter | Qt::AlignBottom); d->mRatingPainter.setLayoutDirection(view->layoutDirection()); d->mRatingPainter.setMaxRating(10); #endif connect(view, &ThumbnailView::thumbnailSizeChanged, this, &PreviewItemDelegate::setThumbnailSize); // Button frame d->mContextBar = new QWidget(d->mView->viewport()); d->mContextBar->hide(); d->mToggleSelectionButton = new QToolButton; d->mToggleSelectionButton->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); connect(d->mToggleSelectionButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotToggleSelectionClicked); d->mFullScreenButton = new QToolButton; d->mFullScreenButton->setIcon(QIcon::fromTheme(QStringLiteral("view-fullscreen"))); connect(d->mFullScreenButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotFullScreenClicked); d->mRotateLeftButton = new QToolButton; d->mRotateLeftButton->setIcon(QIcon::fromTheme(QStringLiteral("object-rotate-left"))); connect(d->mRotateLeftButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotRotateLeftClicked); d->mRotateRightButton = new QToolButton; d->mRotateRightButton->setIcon(QIcon::fromTheme(QStringLiteral("object-rotate-right"))); connect(d->mRotateRightButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotRotateRightClicked); QHBoxLayout* layout = new QHBoxLayout(d->mContextBar); layout->setContentsMargins(2, 2, 2, 2); layout->setSpacing(2); layout->addWidget(d->mToggleSelectionButton); layout->addWidget(d->mFullScreenButton); layout->addWidget(d->mRotateLeftButton); layout->addWidget(d->mRotateRightButton); // Save button d->mSaveButton = new QToolButton(d->mView->viewport()); d->mSaveButton->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); d->mSaveButton->hide(); connect(d->mSaveButton, &QToolButton::clicked, this, &PreviewItemDelegate::slotSaveClicked); } PreviewItemDelegate::~PreviewItemDelegate() { delete d; } QSize PreviewItemDelegate::sizeHint(const QStyleOptionViewItem & /*option*/, const QModelIndex & /*index*/) const { return d->mView->gridSize(); } bool PreviewItemDelegate::eventFilter(QObject* object, QEvent* event) { if (object == d->mView->viewport()) { switch (event->type()) { case QEvent::ToolTip: return true; case QEvent::HoverMove: case QEvent::HoverLeave: return d->hoverEventFilter(static_cast(event)); case QEvent::MouseButtonPress: case QEvent::MouseButtonRelease: return d->mouseButtonEventFilter(event->type()); default: return false; } } else { // Necessary for the item editor to work correctly (especially closing // the editor with the Escape key) return QItemDelegate::eventFilter(object, event); } } void PreviewItemDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const { int thumbnailHeight = d->mThumbnailSize.height(); QSize fullSize; QPixmap thumbnailPix = d->mView->thumbnailForIndex(index, &fullSize); - QSize thumbnailSize = thumbnailPix.size() / thumbnailPix.devicePixelRatioF(); + QSize thumbnailSize = thumbnailPix.size() / thumbnailPix.devicePixelRatio(); const KFileItem fileItem = fileItemForIndex(index); const bool opaque = !thumbnailPix.hasAlphaChannel(); const bool isDirOrArchive = ArchiveUtils::fileItemIsDirOrArchive(fileItem); QRect rect = option.rect; const bool selected = option.state & QStyle::State_Selected; const bool underMouse = option.state & QStyle::State_MouseOver; const bool hasFocus = option.state & QStyle::State_HasFocus; const QWidget* viewport = d->mView->viewport(); #ifdef DEBUG_DRAW_BORDER painter->setPen(Qt::red); painter->setBrush(Qt::NoBrush); painter->drawRect(rect); #endif // Select color group QPalette::ColorGroup cg; if ((option.state & QStyle::State_Enabled) && (option.state & QStyle::State_Active)) { cg = QPalette::Normal; } else if ((option.state & QStyle::State_Enabled)) { cg = QPalette::Inactive; } else { cg = QPalette::Disabled; } // Select colors QColor bgColor, borderColor, fgColor; fgColor = viewport->palette().color(viewport->foregroundRole()); if (selected || underMouse) { bgColor = option.palette.color(cg, QPalette::Highlight); if (hasFocus) { borderColor = bgColor.darker(FOCUS_BORDER_DARKNESS); } else { borderColor = bgColor.darker(SELECTION_BORDER_DARKNESS); } } else { bgColor = viewport->palette().color(viewport->backgroundRole()); if (hasFocus) { borderColor = fgColor; } else { borderColor = bgColor.lighter(200); } } // Compute thumbnailRect QRect thumbnailRect = QRect( rect.left() + (rect.width() - thumbnailSize.width()) / 2, rect.top() + (thumbnailHeight - thumbnailSize.height()) + ITEM_MARGIN, thumbnailSize.width(), thumbnailSize.height()); // Draw background const QRect backgroundRect = thumbnailRect.adjusted(-ITEM_MARGIN, -ITEM_MARGIN, ITEM_MARGIN, ITEM_MARGIN); if (selected) { d->drawBackground(painter, backgroundRect, bgColor, borderColor); } else if (underMouse) { painter->setOpacity(0.2); d->drawBackground(painter, backgroundRect, bgColor, borderColor); painter->setOpacity(1.); } else if (opaque) { d->drawShadow(painter, thumbnailRect); } // Draw thumbnail if (opaque) { painter->setPen(borderColor); painter->setRenderHint(QPainter::Antialiasing, false); QRect borderRect = thumbnailRect.adjusted(-1, -1, 0, 0); painter->drawRect(borderRect); } else if (hasFocus && !selected) { painter->setPen(option.palette.color(cg, QPalette::Highlight)); painter->setRenderHint(QPainter::Antialiasing, false); QLine underLine = QLine(thumbnailRect.bottomLeft(), thumbnailRect.bottomRight()); underLine.translate(0, 3); painter->drawLine(underLine); } painter->drawPixmap(thumbnailRect.left(), thumbnailRect.top(), thumbnailPix); // Draw modified indicator bool isModified = d->mView->isModified(index); if (isModified) { // Draws a pixmap of the save button frame, as an indicator that // the image has been modified QPoint framePosition = d->saveButtonPosition(rect); d->initSaveButtonPixmap(); painter->drawPixmap(framePosition, d->mSaveButtonPixmap); } // Draw busy indicator if (d->mView->isBusy(index)) { QPixmap pix = d->mView->busySequenceCurrentPixmap(); painter->drawPixmap( thumbnailRect.left() + (thumbnailRect.width() - pix.width()) / 2, thumbnailRect.top() + (thumbnailRect.height() - pix.height()) / 2, pix); } if (index == d->mIndexUnderCursor) { // Show bar again: if the thumbnail has changed, we may need to update // its position. Don't do it if we are over rotate buttons, though: it // would not be nice to move the button now, the user may want to // rotate the image one more time. // The button will get moved when the mouse leaves. if (!d->mRotateLeftButton->underMouse() && !d->mRotateRightButton->underMouse()) { d->showContextBar(rect, thumbnailPix); } if (isModified) { // If we just rotated the image with the buttons from the // button frame, we need to show the save button frame right now. d->showSaveButton(rect); } else { d->mSaveButton->hide(); } } QRect textRect( rect.left() + ITEM_MARGIN, rect.top() + 2 * ITEM_MARGIN + thumbnailHeight, rect.width() - 2 * ITEM_MARGIN, d->mView->fontMetrics().height()); if (isDirOrArchive || (d->mDetails & PreviewItemDelegate::FileNameDetail)) { d->drawText(painter, textRect, fgColor, index.data().toString()); textRect.moveTop(textRect.bottom()); } if (!isDirOrArchive && (d->mDetails & PreviewItemDelegate::DateDetail)) { const QDateTime dt = TimeUtils::dateTimeForFileItem(fileItem); d->drawText(painter, textRect, fgColor, QLocale().toString(dt, QLocale::ShortFormat)); textRect.moveTop(textRect.bottom()); } if (!isDirOrArchive && (d->mDetails & PreviewItemDelegate::ImageSizeDetail)) { if (fullSize.isValid()) { const QString text = QStringLiteral("%1x%2").arg(fullSize.width()).arg(fullSize.height()); d->drawText(painter, textRect, fgColor, text); textRect.moveTop(textRect.bottom()); } } if (!isDirOrArchive && (d->mDetails & PreviewItemDelegate::FileSizeDetail)) { const KIO::filesize_t size = fileItem.size(); if (size > 0) { const QString st = KIO::convertSize(size); d->drawText(painter, textRect, fgColor, st); textRect.moveTop(textRect.bottom()); } } if (!isDirOrArchive && (d->mDetails & PreviewItemDelegate::RatingDetail)) { #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE d->drawRating(painter, rect, index.data(SemanticInfoDirModel::RatingRole)); #endif } #ifdef DEBUG_DRAW_CURRENT if (d->mView->currentIndex() == index) { painter->fillRect(rect.left(), rect.top(), 12, 12, Qt::red); } #endif } void PreviewItemDelegate::setThumbnailSize(const QSize& value) { d->mThumbnailSize = value; d->updateViewGridSize(); d->updateContextBar(); d->mElidedTextCache.clear(); } void PreviewItemDelegate::slotSaveClicked() { emit saveDocumentRequested(urlForIndex(d->mIndexUnderCursor)); } void PreviewItemDelegate::slotRotateLeftClicked() { d->selectIndexUnderCursorIfNoMultiSelection(); emit rotateDocumentLeftRequested(urlForIndex(d->mIndexUnderCursor)); } void PreviewItemDelegate::slotRotateRightClicked() { d->selectIndexUnderCursorIfNoMultiSelection(); emit rotateDocumentRightRequested(urlForIndex(d->mIndexUnderCursor)); } void PreviewItemDelegate::slotFullScreenClicked() { emit showDocumentInFullScreenRequested(urlForIndex(d->mIndexUnderCursor)); } void PreviewItemDelegate::slotToggleSelectionClicked() { d->mView->selectionModel()->select(d->mIndexUnderCursor, QItemSelectionModel::Toggle); } PreviewItemDelegate::ThumbnailDetails PreviewItemDelegate::thumbnailDetails() const { return d->mDetails; } void PreviewItemDelegate::setThumbnailDetails(PreviewItemDelegate::ThumbnailDetails details) { d->mDetails = details; d->updateViewGridSize(); d->mView->scheduleDelayedItemsLayout(); } PreviewItemDelegate::ContextBarActions PreviewItemDelegate::contextBarActions() const { return d->mContextBarActions; } void PreviewItemDelegate::setContextBarActions(PreviewItemDelegate::ContextBarActions actions) { d->mContextBarActions = actions; d->updateContextBar(); } Qt::TextElideMode PreviewItemDelegate::textElideMode() const { return d->mTextElideMode; } void PreviewItemDelegate::setTextElideMode(Qt::TextElideMode mode) { if (d->mTextElideMode == mode) { return; } d->mTextElideMode = mode; d->mElidedTextCache.clear(); d->mView->viewport()->update(); } void PreviewItemDelegate::slotRowsChanged() { // We need to update hover ui because the current index may have // disappeared: for example if the current image is removed with "del". QPoint pos = d->mView->viewport()->mapFromGlobal(QCursor::pos()); QModelIndex index = d->mView->indexAt(pos); d->updateHoverUi(index); } QWidget * PreviewItemDelegate::createEditor(QWidget* parent, const QStyleOptionViewItem& /*option*/, const QModelIndex& /*index*/) const { return new ItemEditor(parent); } void PreviewItemDelegate::setEditorData(QWidget* widget, const QModelIndex& index) const { ItemEditor* edit = qobject_cast(widget); if (!edit) { return; } edit->setText(index.data().toString()); } void PreviewItemDelegate::updateEditorGeometry(QWidget* widget, const QStyleOptionViewItem& option, const QModelIndex& index) const { ItemEditor* edit = qobject_cast(widget); if (!edit) { return; } QString text = index.data().toString(); int textWidth = edit->fontMetrics().boundingRect(QLatin1String(" ") + text + QLatin1String(" ")).width(); QRect textRect( option.rect.left() + (option.rect.width() - textWidth) / 2, option.rect.top() + 2 * ITEM_MARGIN + d->mThumbnailSize.height(), textWidth, edit->sizeHint().height()); edit->setGeometry(textRect); } void PreviewItemDelegate::setModelData(QWidget* widget, QAbstractItemModel* model, const QModelIndex& index) const { ItemEditor* edit = qobject_cast(widget); if (!edit) { return; } if (index.data().toString() != edit->text()) { model->setData(index, edit->text(), Qt::EditRole); } } } // namespace diff --git a/lib/thumbnailview/thumbnailbarview.cpp b/lib/thumbnailview/thumbnailbarview.cpp index 4eb162c0..fafac6a0 100644 --- a/lib/thumbnailview/thumbnailbarview.cpp +++ b/lib/thumbnailview/thumbnailbarview.cpp @@ -1,550 +1,550 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2008 Aurélien Gâteau Copyright 2008 Ilya Konkov This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ // Self #include "thumbnailbarview.h" // Qt #include #include #include #include #include #include #include #include #include #ifdef WINDOWS_PROXY_STYLE #include #endif // KDE #include // Local #include "lib/hud/hudtheme.h" #include "lib/paintutils.h" #include "lib/thumbnailview/abstractthumbnailviewhelper.h" #include "gwenviewconfig.h" namespace Gwenview { /** * Duration in ms of the smooth scroll */ const int SMOOTH_SCROLL_DURATION = 250; /** * Space between the item outer rect and the content, and between the * thumbnail and the caption */ const int ITEM_MARGIN = 5; /** How dark is the shadow, 0 is invisible, 255 is as dark as possible */ const int SHADOW_STRENGTH = 127; /** How many pixels around the thumbnail are shadowed */ const int SHADOW_SIZE = 4; struct ThumbnailBarItemDelegatePrivate { // Key is height * 1000 + width typedef QMap ShadowCache; mutable ShadowCache mShadowCache; ThumbnailBarItemDelegate* q; ThumbnailView* mView; QToolButton* mToggleSelectionButton; QColor mBorderColor; QPersistentModelIndex mIndexUnderCursor; void setupToggleSelectionButton() { mToggleSelectionButton = new QToolButton(mView->viewport()); mToggleSelectionButton->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); mToggleSelectionButton->hide(); QObject::connect(mToggleSelectionButton, &QToolButton::clicked, q, &ThumbnailBarItemDelegate::toggleSelection); } void showToolTip(QHelpEvent* helpEvent) { QModelIndex index = mView->indexAt(helpEvent->pos()); if (!index.isValid()) { return; } QString fullText = index.data().toString(); QPoint pos = QCursor::pos(); QToolTip::showText(pos, fullText, mView); } void drawShadow(QPainter* painter, const QRect& rect) const { const QPoint shadowOffset(-SHADOW_SIZE, -SHADOW_SIZE + 1); const auto dpr = painter->device()->devicePixelRatioF(); int key = qRound((rect.height() * 1000 + rect.width()) * dpr); ShadowCache::Iterator it = mShadowCache.find(key); if (it == mShadowCache.end()) { QSize size = QSize(rect.width() + 2 * SHADOW_SIZE, rect.height() + 2 * SHADOW_SIZE); QColor color(0, 0, 0, SHADOW_STRENGTH); QPixmap shadow = PaintUtils::generateFuzzyRect(size * dpr, color, qRound(SHADOW_SIZE * dpr)); shadow.setDevicePixelRatio(dpr); it = mShadowCache.insert(key, shadow); } painter->drawPixmap(rect.topLeft() + shadowOffset, it.value()); } bool hoverEventFilter(QHoverEvent* event) { QModelIndex index = mView->indexAt(event->pos()); if (index != mIndexUnderCursor) { updateHoverUi(index); } return false; } void updateHoverUi(const QModelIndex& index) { mIndexUnderCursor = index; if (mIndexUnderCursor.isValid() && GwenviewConfig::thumbnailActions() != ThumbnailActions::None) { updateToggleSelectionButton(); const QRect rect = mView->visualRect(mIndexUnderCursor); mToggleSelectionButton->move(rect.topLeft() + QPoint(2, 2)); mToggleSelectionButton->show(); } else { mToggleSelectionButton->hide(); } } void updateToggleSelectionButton() { bool isSelected = mView->selectionModel()->isSelected(mIndexUnderCursor); mToggleSelectionButton->setIcon(QIcon::fromTheme(isSelected ? QStringLiteral("list-remove") : QStringLiteral("list-add"))); } }; ThumbnailBarItemDelegate::ThumbnailBarItemDelegate(ThumbnailView* view) : QAbstractItemDelegate(view) , d(new ThumbnailBarItemDelegatePrivate) { d->q = this; d->mView = view; d->setupToggleSelectionButton(); view->viewport()->installEventFilter(this); // Set this attribute so that the viewport receives QEvent::HoverMove and // QEvent::HoverLeave events. We use these events in the event filter // installed on the viewport. // Some styles set this attribute themselves (Oxygen and Skulpture do) but // others do not (Plastique, Cleanlooks...) view->viewport()->setAttribute(Qt::WA_Hover); d->mBorderColor = PaintUtils::alphaAdjustedF(QColor(Qt::white), 0.65); connect(view, &ThumbnailView::selectionChangedSignal, [this]() { d->updateToggleSelectionButton(); }); } QSize ThumbnailBarItemDelegate::sizeHint(const QStyleOptionViewItem & /*option*/, const QModelIndex & index) const { QSize size; if (d->mView->thumbnailScaleMode() == ThumbnailView::ScaleToFit) { size = d->mView->gridSize(); } else { QPixmap thumbnailPix = d->mView->thumbnailForIndex(index); - size = thumbnailPix.size() / thumbnailPix.devicePixelRatioF(); + size = thumbnailPix.size() / thumbnailPix.devicePixelRatio(); size.rwidth() += ITEM_MARGIN * 2; size.rheight() += ITEM_MARGIN * 2; } return size; } bool ThumbnailBarItemDelegate::eventFilter(QObject*, QEvent* event) { switch (event->type()) { case QEvent::ToolTip: d->showToolTip(static_cast(event)); return true; case QEvent::HoverMove: case QEvent::HoverLeave: return d->hoverEventFilter(static_cast(event)); default: break; } return false; } void ThumbnailBarItemDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const { bool isSelected = option.state & QStyle::State_Selected; bool isCurrent = d->mView->selectionModel()->currentIndex() == index; QPixmap thumbnailPix = d->mView->thumbnailForIndex(index); - QSize thumbnailSize = thumbnailPix.size() / thumbnailPix.devicePixelRatioF(); + QSize thumbnailSize = thumbnailPix.size() / thumbnailPix.devicePixelRatio(); QRect rect = option.rect; QStyleOptionViewItem opt = option; const QWidget* widget = opt.widget; QStyle* style = widget ? widget->style() : QApplication::style(); if (isSelected && !isCurrent) { // Draw selected but not current item backgrounds with some transparency // so that the current item stands out. painter->setOpacity(.33); } style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, widget); painter->setOpacity(1); // Draw thumbnail if (!thumbnailPix.isNull()) { QRect thumbnailRect = QRect( rect.left() + (rect.width() - thumbnailSize.width()) / 2, rect.top() + (rect.height() - thumbnailSize.height()) / 2 - 1, thumbnailSize.width(), thumbnailSize.height()); if (!thumbnailPix.hasAlphaChannel()) { d->drawShadow(painter, thumbnailRect); painter->setPen(d->mBorderColor); painter->setRenderHint(QPainter::Antialiasing, false); QRect borderRect = thumbnailRect.adjusted(-1, -1, 0, 0); painter->drawRect(borderRect); } painter->drawPixmap(thumbnailRect.left(), thumbnailRect.top(), thumbnailPix); // Draw busy indicator if (d->mView->isBusy(index)) { QPixmap pix = d->mView->busySequenceCurrentPixmap(); painter->drawPixmap( thumbnailRect.left() + (thumbnailRect.width() - pix.width()) / 2, thumbnailRect.top() + (thumbnailRect.height() - pix.height()) / 2, pix); } } } void ThumbnailBarItemDelegate::toggleSelection() { d->mView->selectionModel()->select(d->mIndexUnderCursor, QItemSelectionModel::Toggle); } ThumbnailBarItemDelegate::~ThumbnailBarItemDelegate() { delete d; } //this is disabled by David Edmundson as I can't figure out how to port it //I hope with breeze being the default we don't want to start making our own styles anyway #ifdef WINDOWS_PROXY_STYLE /** * This proxy style makes it possible to override the value returned by * styleHint() which leads to not-so-nice results with some styles. * * We cannot use QProxyStyle because it takes ownership of the base style, * which causes crash when user change styles. */ class ProxyStyle : public QWindowsStyle { public: ProxyStyle() : QWindowsStyle() { } void drawPrimitive(PrimitiveElement pe, const QStyleOption *opt, QPainter *p, const QWidget *w = 0) const { QApplication::style()->drawPrimitive(pe, opt, p, w); } void drawControl(ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *w = 0) const { QApplication::style()->drawControl(element, opt, p, w); } void drawComplexControl(ComplexControl cc, const QStyleOptionComplex *opt, QPainter *p, const QWidget *w = 0) const { QApplication::style()->drawComplexControl(cc, opt, p, w); } int styleHint(StyleHint sh, const QStyleOption *opt = 0, const QWidget *w = 0, QStyleHintReturn *shret = 0) const { switch (sh) { case SH_ItemView_ShowDecorationSelected: // We want the highlight to cover our thumbnail return true; case SH_ScrollView_FrameOnlyAroundContents: // Ensure the frame does not include the scrollbar. This ensure the // scrollbar touches the edge of the window and thus can touch the // edge of the screen when maximized return false; default: return QApplication::style()->styleHint(sh, opt, w, shret); } } void polish(QApplication* application) { QApplication::style()->polish(application); } void polish(QPalette& palette) { QApplication::style()->polish(palette); } void polish(QWidget* widget) { QApplication::style()->polish(widget); } void unpolish(QWidget* widget) { QApplication::style()->unpolish(widget); } void unpolish(QApplication* application) { QApplication::style()->unpolish(application); } int pixelMetric(PixelMetric pm, const QStyleOption* opt, const QWidget* widget) const { switch (pm) { case PM_MaximumDragDistance: // Ensure the fullscreen thumbnailbar does not go away while // dragging the scrollbar if the mouse cursor is too far away from // the widget return -1; default: return QApplication::style()->pixelMetric(pm, opt, widget); } } }; #endif// WINDOWS_PROXY_STYLE typedef int (QSize::*QSizeDimension)() const; struct ThumbnailBarViewPrivate { ThumbnailBarView* q; QStyle* mStyle; QTimeLine* mTimeLine; Qt::Orientation mOrientation; int mRowCount; QScrollBar* scrollBar() const { return mOrientation == Qt::Horizontal ? q->horizontalScrollBar() : q->verticalScrollBar(); } QSizeDimension mainDimension() const { return mOrientation == Qt::Horizontal ? &QSize::width : &QSize::height; } QSizeDimension oppositeDimension() const { return mOrientation == Qt::Horizontal ? &QSize::height : &QSize::width; } void smoothScrollTo(const QModelIndex& index) { if (!index.isValid()) { return; } const QRect rect = q->visualRect(index); int oldValue = scrollBar()->value(); int newValue = scrollToValue(rect); if (mTimeLine->state() == QTimeLine::Running) { mTimeLine->stop(); } mTimeLine->setFrameRange(oldValue, newValue); mTimeLine->start(); } int scrollToValue(const QRect& rect) { // This code is a much simplified version of // QListViewPrivate::horizontalScrollToValue() const QRect area = q->viewport()->rect(); int value = scrollBar()->value(); if (mOrientation == Qt::Horizontal) { if (q->isRightToLeft()) { value += (area.width() - rect.width()) / 2 - rect.left(); } else { value += rect.left() - (area.width() - rect.width()) / 2; } } else { value += rect.top() - (area.height() - rect.height()) / 2; } return value; } void updateMinMaxSizes() { QSizeDimension dimension = oppositeDimension(); int scrollBarSize = (scrollBar()->sizeHint().*dimension)(); QSize minSize(0, mRowCount * 48 + scrollBarSize); QSize maxSize(QWIDGETSIZE_MAX, mRowCount * 256 + scrollBarSize); if (mOrientation == Qt::Vertical) { minSize.transpose(); maxSize.transpose(); } q->setMinimumSize(minSize); q->setMaximumSize(maxSize); } void updateThumbnailSize() { QSizeDimension dimension = oppositeDimension(); int scrollBarSize = (scrollBar()->sizeHint().*dimension)(); int widgetSize = (q->size().*dimension)(); if (mRowCount > 1) { // Decrease widgetSize because otherwise the view sometimes wraps at // mRowCount-1 instead of mRowCount. Probably because gridSize * // mRowCount is too close to widgetSize. --widgetSize; } int gridWidth, gridHeight; if (mOrientation == Qt::Horizontal) { gridHeight = (widgetSize - scrollBarSize - 2 * q->frameWidth()) / mRowCount; gridWidth = qRound(gridHeight * q->thumbnailAspectRatio()); } else { gridWidth = (widgetSize - scrollBarSize - 2 * q->frameWidth()) / mRowCount; gridHeight = qRound(gridWidth / q->thumbnailAspectRatio()); } if (q->thumbnailScaleMode() == ThumbnailView::ScaleToFit) { q->setGridSize(QSize(gridWidth, gridHeight)); } q->setThumbnailWidth(gridWidth - ITEM_MARGIN * 2); } }; ThumbnailBarView::ThumbnailBarView(QWidget* parent) : ThumbnailView(parent) , d(new ThumbnailBarViewPrivate) { d->q = this; d->mTimeLine = new QTimeLine(SMOOTH_SCROLL_DURATION, this); connect(d->mTimeLine, &QTimeLine::frameChanged, this, &ThumbnailBarView::slotFrameChanged); d->mRowCount = 1; d->mOrientation = Qt::Vertical; // To pass value-has-changed check in setOrientation() setOrientation(Qt::Horizontal); setObjectName(QStringLiteral("thumbnailBarView")); setWrapping(true); #ifdef WINDOWS_PROXY_STYLE d->mStyle = new ProxyStyle; setStyle(d->mStyle); #endif } ThumbnailBarView::~ThumbnailBarView() { #ifdef WINDOWS_PROXY_STYLE delete d->mStyle; #endif delete d; } Qt::Orientation ThumbnailBarView::orientation() const { return d->mOrientation; } void ThumbnailBarView::setOrientation(Qt::Orientation orientation) { if (d->mOrientation == orientation) { return; } d->mOrientation = orientation; if (d->mOrientation == Qt::Vertical) { setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setFlow(LeftToRight); } else { setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); setFlow(TopToBottom); } d->updateMinMaxSizes(); } void ThumbnailBarView::slotFrameChanged(int value) { d->scrollBar()->setValue(value); } void ThumbnailBarView::resizeEvent(QResizeEvent *event) { ThumbnailView::resizeEvent(event); d->updateThumbnailSize(); } void ThumbnailBarView::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { ThumbnailView::selectionChanged(selected, deselected); QModelIndexList oldList = deselected.indexes(); QModelIndexList newList = selected.indexes(); // Only scroll the list if the user went from one image to another. If the // user just unselected one image from a set of two, he might want to // reselect it again, scrolling the thumbnails would prevent him from // reselecting it by clicking again without moving the mouse. if (oldList.count() == 1 && newList.count() == 1 && isVisible()) { d->smoothScrollTo(newList.first()); } } void ThumbnailBarView::wheelEvent(QWheelEvent* event) { d->scrollBar()->setValue(d->scrollBar()->value() - event->angleDelta().y()); } int ThumbnailBarView::rowCount() const { return d->mRowCount; } void ThumbnailBarView::setRowCount(int rowCount) { Q_ASSERT(rowCount > 0); d->mRowCount = rowCount; d->updateMinMaxSizes(); d->updateThumbnailSize(); } } // namespace