diff --git a/lib/thumbnailview/previewitemdelegate.cpp b/lib/thumbnailview/previewitemdelegate.cpp index 974053b8..f910b0c1 100644 --- a/lib/thumbnailview/previewitemdelegate.cpp +++ b/lib/thumbnailview/previewitemdelegate.cpp @@ -1,981 +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 posY = qMax(CONTEXTBAR_MARGIN, mThumbnailSize.height() - thumbnailPix.height() - mContextBar->height()); + 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); - int key = rect.height() * 1000 + rect.width(); + 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, color, SHADOW_SIZE); + 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.width(text)) / 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->setMargin(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.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() - thumbnailPix.width()) / 2, - rect.top() + (thumbnailHeight - thumbnailPix.height()) + ITEM_MARGIN, - thumbnailPix.width(), - thumbnailPix.height()); + 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().width(QStringLiteral(" ") + text + QStringLiteral(" ")); 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 f7e6e9a0..a3cc1930 100644 --- a/lib/thumbnailview/thumbnailbarview.cpp +++ b/lib/thumbnailview/thumbnailbarview.cpp @@ -1,547 +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); - int key = rect.height() * 1000 + rect.width(); + 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, color, SHADOW_SIZE); + 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(); + 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.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() - thumbnailPix.width()) / 2, - rect.top() + (rect.height() - thumbnailPix.height()) / 2 - 1, - thumbnailPix.width(), - thumbnailPix.height()); + 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->delta()); } int ThumbnailBarView::rowCount() const { return d->mRowCount; } void ThumbnailBarView::setRowCount(int rowCount) { Q_ASSERT(rowCount > 0); d->mRowCount = rowCount; d->updateMinMaxSizes(); d->updateThumbnailSize(); } } // namespace diff --git a/lib/thumbnailview/thumbnailslider.cpp b/lib/thumbnailview/thumbnailslider.cpp index 2c7fa2c9..03fabe26 100644 --- a/lib/thumbnailview/thumbnailslider.cpp +++ b/lib/thumbnailview/thumbnailslider.cpp @@ -1,73 +1,73 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2009 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 "thumbnailslider.h" // Local #include // Qt #include // KDE namespace Gwenview { struct ThumbnailSliderPrivate { }; ThumbnailSlider::ThumbnailSlider(QWidget* parent) : ZoomSlider(parent) , d(new ThumbnailSliderPrivate) { connect(slider(), SIGNAL(actionTriggered(int)), SLOT(slotActionTriggered(int))); slider()->setRange(ThumbnailView::MinThumbnailSize, ThumbnailView::MaxThumbnailSize); } ThumbnailSlider::~ThumbnailSlider() { delete d; } void ThumbnailSlider::slotActionTriggered(int actionTriggered) { updateToolTip(); if (actionTriggered != QAbstractSlider::SliderNoAction) { // If we are updating because of a direct action on the slider, show // the tooltip immediately. const QPoint pos = slider()->mapToGlobal(QPoint(0, slider()->height() / 2)); QToolTip::showText(pos, slider()->toolTip(), slider()); } } void ThumbnailSlider::updateToolTip() { // FIXME: i18n? - const int size = slider()->sliderPosition(); + const int size = qRound(slider()->sliderPosition() * devicePixelRatioF()); const QString text = QStringLiteral("%1 x %2").arg(size).arg(size); slider()->setToolTip(text); } } // namespace diff --git a/lib/thumbnailview/thumbnailview.cpp b/lib/thumbnailview/thumbnailview.cpp index a076dd1e..072a4479 100644 --- a/lib/thumbnailview/thumbnailview.cpp +++ b/lib/thumbnailview/thumbnailview.cpp @@ -1,1031 +1,1037 @@ /* Gwenview: an image viewer Copyright 2007 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, Boston, MA 02110-1301, USA. */ #include "thumbnailview.h" // Std #include // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include #include // Local #include "abstractdocumentinfoprovider.h" #include "abstractthumbnailviewhelper.h" #include "archiveutils.h" #include "dragpixmapgenerator.h" #include "mimetypeutils.h" #include "urlutils.h" #include #include #include #include namespace Gwenview { #undef ENABLE_LOG #undef LOG //#define ENABLE_LOG #ifdef ENABLE_LOG #define LOG(x) //qDebug() << x #else #define LOG(x) ; #endif /** How many msec to wait before starting to smooth thumbnails */ const int SMOOTH_DELAY = 500; const int WHEEL_ZOOM_MULTIPLIER = 4; static KFileItem fileItemForIndex(const QModelIndex& index) { if (!index.isValid()) { LOG("Invalid index"); return KFileItem(); } QVariant data = index.data(KDirModel::FileItemRole); return qvariant_cast(data); } static QUrl urlForIndex(const QModelIndex& index) { KFileItem item = fileItemForIndex(index); return item.isNull() ? QUrl() : item.url(); } struct Thumbnail { Thumbnail(const QPersistentModelIndex& index_, const QDateTime& mtime) : mIndex(index_) , mModificationTime(mtime) , mFileSize(0) , mRough(true) , mWaitingForThumbnail(true) {} Thumbnail() : mFileSize(0) , mRough(true) , mWaitingForThumbnail(true) {} /** * Init the thumbnail based on a icon */ void initAsIcon(const QPixmap& pix) { mGroupPix = pix; int largeGroupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::Large); mFullSize = QSize(largeGroupSize, largeGroupSize); } bool isGroupPixAdaptedForSize(int size) const { if (mWaitingForThumbnail) { return false; } if (mGroupPix.isNull()) { return false; } const int groupSize = qMax(mGroupPix.width(), mGroupPix.height()); if (groupSize >= size) { return true; } // groupSize is less than size, but this may be because the full image // is the same size as groupSize return groupSize == qMax(mFullSize.width(), mFullSize.height()); } void prepareForRefresh(const QDateTime& mtime) { mModificationTime = mtime; mFileSize = 0; mGroupPix = QPixmap(); mAdjustedPix = QPixmap(); mFullSize = QSize(); mRealFullSize = QSize(); mRough = true; mWaitingForThumbnail = true; } QPersistentModelIndex mIndex; QDateTime mModificationTime; /// The pix loaded from .thumbnails/{large,normal} QPixmap mGroupPix; /// Scaled version of mGroupPix, adjusted to ThumbnailView::thumbnailSize QPixmap mAdjustedPix; /// Size of the full image QSize mFullSize; /// Real size of the full image, invalid unless the thumbnail /// represents a raster image (not an icon) QSize mRealFullSize; /// File size of the full image KIO::filesize_t mFileSize; /// Whether mAdjustedPix represents has been scaled using fast or smooth /// transformation bool mRough; /// Set to true if mGroupPix should be replaced with a real thumbnail bool mWaitingForThumbnail; }; typedef QHash ThumbnailForUrl; typedef QQueue UrlQueue; typedef QSet PersistentModelIndexSet; struct ThumbnailViewPrivate { ThumbnailView* q; ThumbnailView::ThumbnailScaleMode mScaleMode; QSize mThumbnailSize; qreal mThumbnailAspectRatio; AbstractDocumentInfoProvider* mDocumentInfoProvider; AbstractThumbnailViewHelper* mThumbnailViewHelper; ThumbnailForUrl mThumbnailForUrl; QTimer mScheduledThumbnailGenerationTimer; UrlQueue mSmoothThumbnailQueue; QTimer mSmoothThumbnailTimer; QPixmap mWaitingThumbnail; QPointer mThumbnailProvider; PersistentModelIndexSet mBusyIndexSet; KPixmapSequence mBusySequence; QTimeLine* mBusyAnimationTimeLine; bool mCreateThumbnailsForRemoteUrls; QScroller* mScroller; Touch* mTouch; void setupBusyAnimation() { mBusySequence = KIconLoader::global()->loadPixmapSequence(QStringLiteral("process-working"), 22); mBusyAnimationTimeLine = new QTimeLine(100 * mBusySequence.frameCount(), q); mBusyAnimationTimeLine->setCurveShape(QTimeLine::LinearCurve); mBusyAnimationTimeLine->setEndFrame(mBusySequence.frameCount() - 1); mBusyAnimationTimeLine->setLoopCount(0); QObject::connect(mBusyAnimationTimeLine, &QTimeLine::frameChanged, q, &ThumbnailView::updateBusyIndexes); } void scheduleThumbnailGeneration() { if (mThumbnailProvider) { mThumbnailProvider->removePendingItems(); } mSmoothThumbnailQueue.clear(); mScheduledThumbnailGenerationTimer.start(); } void updateThumbnailForModifiedDocument(const QModelIndex& index) { Q_ASSERT(mDocumentInfoProvider); KFileItem item = fileItemForIndex(index); QUrl url = item.url(); ThumbnailGroup::Enum group = ThumbnailGroup::fromPixelSize(mThumbnailSize.width()); QPixmap pix; QSize fullSize; mDocumentInfoProvider->thumbnailForDocument(url, group, &pix, &fullSize); mThumbnailForUrl[url] = Thumbnail(QPersistentModelIndex(index), QDateTime::currentDateTime()); q->setThumbnail(item, pix, fullSize, 0); } void appendItemsToThumbnailProvider(const KFileItemList& list) { if (mThumbnailProvider) { ThumbnailGroup::Enum group = ThumbnailGroup::fromPixelSize(mThumbnailSize.width()); mThumbnailProvider->setThumbnailGroup(group); mThumbnailProvider->appendItems(list); } } void roughAdjustThumbnail(Thumbnail* thumbnail) { const QPixmap& mGroupPix = thumbnail->mGroupPix; const int groupSize = qMax(mGroupPix.width(), mGroupPix.height()); const int fullSize = qMax(thumbnail->mFullSize.width(), thumbnail->mFullSize.height()); if (fullSize == groupSize && mGroupPix.height() <= mThumbnailSize.height() && mGroupPix.width() <= mThumbnailSize.width()) { thumbnail->mAdjustedPix = mGroupPix; thumbnail->mRough = false; } else { thumbnail->mAdjustedPix = scale(mGroupPix, Qt::FastTransformation); thumbnail->mRough = true; } } void initDragPixmap(QDrag* drag, const QModelIndexList& indexes) { const int thumbCount = qMin(indexes.count(), int(DragPixmapGenerator::MaxCount)); QList lst; for (int row = 0; row < thumbCount; ++row) { const QUrl url = urlForIndex(indexes[row]); lst << mThumbnailForUrl.value(url).mAdjustedPix; } DragPixmapGenerator::DragPixmap dragPixmap = DragPixmapGenerator::generate(lst, indexes.count()); drag->setPixmap(dragPixmap.pix); drag->setHotSpot(dragPixmap.hotSpot); } QPixmap scale(const QPixmap& pix, Qt::TransformationMode transformationMode) { switch (mScaleMode) { case ThumbnailView::ScaleToFit: return pix.scaled(mThumbnailSize.width(), mThumbnailSize.height(), Qt::KeepAspectRatio, transformationMode); case ThumbnailView::ScaleToSquare: { int minSize = qMin(pix.width(), pix.height()); QPixmap pix2 = pix.copy((pix.width() - minSize) / 2, (pix.height() - minSize) / 2, minSize, minSize); return pix2.scaled(mThumbnailSize.width(), mThumbnailSize.height(), Qt::KeepAspectRatio, transformationMode); } case ThumbnailView::ScaleToHeight: return pix.scaledToHeight(mThumbnailSize.height(), transformationMode); case ThumbnailView::ScaleToWidth: return pix.scaledToWidth(mThumbnailSize.width(), transformationMode); } // Keep compiler happy Q_ASSERT(0); return QPixmap(); } }; ThumbnailView::ThumbnailView(QWidget* parent) : QListView(parent) , d(new ThumbnailViewPrivate) { d->q = this; d->mScaleMode = ScaleToFit; d->mThumbnailViewHelper = nullptr; d->mDocumentInfoProvider = nullptr; d->mThumbnailProvider = nullptr; // Init to some stupid value so that the first call to setThumbnailSize() // is not ignored (do not use 0 in case someone try to divide by // mThumbnailSize...) d->mThumbnailSize = QSize(1, 1); d->mThumbnailAspectRatio = 1; d->mCreateThumbnailsForRemoteUrls = true; setFrameShape(QFrame::NoFrame); setViewMode(QListView::IconMode); setResizeMode(QListView::Adjust); setDragEnabled(true); setAcceptDrops(true); setDropIndicatorShown(true); setUniformItemSizes(true); setEditTriggers(QAbstractItemView::EditKeyPressed); d->setupBusyAnimation(); setVerticalScrollMode(ScrollPerPixel); setHorizontalScrollMode(ScrollPerPixel); d->mScheduledThumbnailGenerationTimer.setSingleShot(true); d->mScheduledThumbnailGenerationTimer.setInterval(500); connect(&d->mScheduledThumbnailGenerationTimer, &QTimer::timeout, this, &ThumbnailView::generateThumbnailsForItems); d->mSmoothThumbnailTimer.setSingleShot(true); connect(&d->mSmoothThumbnailTimer, &QTimer::timeout, this, &ThumbnailView::smoothNextThumbnail); setContextMenuPolicy(Qt::CustomContextMenu); connect(this, &ThumbnailView::customContextMenuRequested, this, &ThumbnailView::showContextMenu); connect(this, &ThumbnailView::activated, this, &ThumbnailView::emitIndexActivatedIfNoModifiers); d->mScroller = ScrollerUtils::setQScroller(this->viewport()); d->mTouch = new Touch(viewport()); connect(d->mTouch, &Touch::twoFingerTapTriggered, this, &ThumbnailView::showContextMenu); connect(d->mTouch, &Touch::pinchZoomTriggered, this, &ThumbnailView::zoomGesture); connect(d->mTouch, &Touch::pinchGestureStarted, this, &ThumbnailView::setZoomParameter); connect(d->mTouch, &Touch::tapTriggered, this, &ThumbnailView::tapGesture); connect(d->mTouch, &Touch::tapHoldAndMovingTriggered, this, &ThumbnailView::startDragFromTouch); } ThumbnailView::~ThumbnailView() { delete d->mTouch; delete d; } ThumbnailView::ThumbnailScaleMode ThumbnailView::thumbnailScaleMode() const { return d->mScaleMode; } void ThumbnailView::setThumbnailScaleMode(ThumbnailScaleMode mode) { d->mScaleMode = mode; setUniformItemSizes(mode == ScaleToFit || mode == ScaleToSquare); } void ThumbnailView::setModel(QAbstractItemModel* newModel) { if (model()) { disconnect(model(), nullptr, this, nullptr); } QListView::setModel(newModel); connect(model(), &QAbstractItemModel::rowsRemoved, this, &ThumbnailView::rowsRemovedSignal); } void ThumbnailView::setThumbnailProvider(ThumbnailProvider* thumbnailProvider) { GV_RETURN_IF_FAIL(d->mThumbnailProvider != thumbnailProvider); if (thumbnailProvider) { connect(thumbnailProvider, &ThumbnailProvider::thumbnailLoaded, this, &ThumbnailView::setThumbnail); connect(thumbnailProvider, &ThumbnailProvider::thumbnailLoadingFailed, this, &ThumbnailView::setBrokenThumbnail); } else { disconnect(d->mThumbnailProvider, nullptr , this, nullptr); } d->mThumbnailProvider = thumbnailProvider; } void ThumbnailView::updateThumbnailSize() { QSize value = d->mThumbnailSize; // mWaitingThumbnail + const auto dpr = devicePixelRatioF(); int waitingThumbnailSize; - if (value.width() > 64) { - waitingThumbnailSize = 48; + if (value.width() > 64 * dpr) { + waitingThumbnailSize = qRound(48 * dpr); } else { - waitingThumbnailSize = 32; + waitingThumbnailSize = qRound(32 * dpr); } QPixmap icon = DesktopIcon(QStringLiteral("chronometer"), waitingThumbnailSize); QPixmap pix(value); pix.fill(Qt::transparent); QPainter painter(&pix); painter.setOpacity(0.5); painter.drawPixmap((value.width() - icon.width()) / 2, (value.height() - icon.height()) / 2, icon); painter.end(); d->mWaitingThumbnail = pix; + d->mWaitingThumbnail.setDevicePixelRatio(dpr); // Stop smoothing d->mSmoothThumbnailTimer.stop(); d->mSmoothThumbnailQueue.clear(); // Clear adjustedPixes ThumbnailForUrl::iterator it = d->mThumbnailForUrl.begin(), end = d->mThumbnailForUrl.end(); for (; it != end; ++it) { it.value().mAdjustedPix = QPixmap(); } - emit thumbnailSizeChanged(value); - emit thumbnailWidthChanged(value.width()); + emit thumbnailSizeChanged(value / dpr); + emit thumbnailWidthChanged(qRound(value.width() / dpr)); if (d->mScaleMode != ScaleToFit) { scheduleDelayedItemsLayout(); } d->scheduleThumbnailGeneration(); } void ThumbnailView::setThumbnailWidth(int width) { - if(d->mThumbnailSize.width() == width) { + const auto dpr = devicePixelRatioF(); + const qreal newWidthF = width * dpr; + const int newWidth = qRound(newWidthF); + if(d->mThumbnailSize.width() == newWidth) { return; } - int height = round((qreal)width / d->mThumbnailAspectRatio); - d->mThumbnailSize = QSize(width, height); + int height = qRound(newWidthF / d->mThumbnailAspectRatio); + d->mThumbnailSize = QSize(newWidth, height); updateThumbnailSize(); } void ThumbnailView::setThumbnailAspectRatio(qreal ratio) { if(d->mThumbnailAspectRatio == ratio) { return; } d->mThumbnailAspectRatio = ratio; int width = d->mThumbnailSize.width(); int height = round((qreal)width / d->mThumbnailAspectRatio); d->mThumbnailSize = QSize(width, height); updateThumbnailSize(); } qreal ThumbnailView::thumbnailAspectRatio() const { return d->mThumbnailAspectRatio; } QSize ThumbnailView::thumbnailSize() const { - return d->mThumbnailSize; + return d->mThumbnailSize / devicePixelRatioF(); } void ThumbnailView::setThumbnailViewHelper(AbstractThumbnailViewHelper* helper) { d->mThumbnailViewHelper = helper; } AbstractThumbnailViewHelper* ThumbnailView::thumbnailViewHelper() const { return d->mThumbnailViewHelper; } void ThumbnailView::setDocumentInfoProvider(AbstractDocumentInfoProvider* provider) { d->mDocumentInfoProvider = provider; if (provider) { connect(provider, &AbstractDocumentInfoProvider::busyStateChanged, this, &ThumbnailView::updateThumbnailBusyState); connect(provider, &AbstractDocumentInfoProvider::documentChanged, this, &ThumbnailView::updateThumbnail); } } AbstractDocumentInfoProvider* ThumbnailView::documentInfoProvider() const { return d->mDocumentInfoProvider; } void ThumbnailView::rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) { QListView::rowsAboutToBeRemoved(parent, start, end); // Remove references to removed items KFileItemList itemList; for (int pos = start; pos <= end; ++pos) { QModelIndex index = model()->index(pos, 0, parent); KFileItem item = fileItemForIndex(index); if (item.isNull()) { //qDebug() << "Skipping invalid item!" << index.data().toString(); continue; } QUrl url = item.url(); d->mThumbnailForUrl.remove(url); d->mSmoothThumbnailQueue.removeAll(url); itemList.append(item); } if (d->mThumbnailProvider) { d->mThumbnailProvider->removeItems(itemList); } // Removing rows might make new images visible, make sure their thumbnail // is generated d->mScheduledThumbnailGenerationTimer.start(); } void ThumbnailView::rowsInserted(const QModelIndex& parent, int start, int end) { QListView::rowsInserted(parent, start, end); d->mScheduledThumbnailGenerationTimer.start(); emit rowsInsertedSignal(parent, start, end); } void ThumbnailView::dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector &roles) { QListView::dataChanged(topLeft, bottomRight, roles); bool thumbnailsNeedRefresh = false; for (int row = topLeft.row(); row <= bottomRight.row(); ++row) { QModelIndex index = model()->index(row, 0); KFileItem item = fileItemForIndex(index); if (item.isNull()) { qWarning() << "Invalid item for index" << index << ". This should not happen!"; GV_FATAL_FAILS; continue; } ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(item.url()); if (it != d->mThumbnailForUrl.end()) { // All thumbnail views are connected to the model, so // ThumbnailView::dataChanged() is called for all of them. As a // result this method will also be called for views which are not // currently visible, and do not yet have a thumbnail for the // modified url. QDateTime mtime = item.time(KFileItem::ModificationTime); if (it->mModificationTime != mtime || it->mFileSize != item.size()) { // dataChanged() is called when the file changes but also when // the model fetched additional data such as semantic info. To // avoid needless refreshes, we only trigger a refresh if the // modification time changes. thumbnailsNeedRefresh = true; it->prepareForRefresh(mtime); } } } if (thumbnailsNeedRefresh) { d->mScheduledThumbnailGenerationTimer.start(); } } void ThumbnailView::showContextMenu() { d->mThumbnailViewHelper->showContextMenu(this); } void ThumbnailView::emitIndexActivatedIfNoModifiers(const QModelIndex& index) { if (QApplication::keyboardModifiers() == Qt::NoModifier) { emit indexActivated(index); } } void ThumbnailView::setThumbnail(const KFileItem& item, const QPixmap& pixmap, const QSize& size, qulonglong fileSize) { ThumbnailForUrl::iterator it = d->mThumbnailForUrl.find(item.url()); if (it == d->mThumbnailForUrl.end()) { return; } Thumbnail& thumbnail = it.value(); thumbnail.mGroupPix = pixmap; thumbnail.mAdjustedPix = QPixmap(); int largeGroupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::Large2x); thumbnail.mFullSize = size.isValid() ? size : QSize(largeGroupSize, largeGroupSize); thumbnail.mRealFullSize = size; thumbnail.mWaitingForThumbnail = false; thumbnail.mFileSize = fileSize; update(thumbnail.mIndex); if (d->mScaleMode != ScaleToFit) { scheduleDelayedItemsLayout(); } } void ThumbnailView::setBrokenThumbnail(const KFileItem& item) { ThumbnailForUrl::iterator it = d->mThumbnailForUrl.find(item.url()); if (it == d->mThumbnailForUrl.end()) { return; } Thumbnail& thumbnail = it.value(); MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item); if (kind == MimeTypeUtils::KIND_VIDEO) { // Special case for videos because our kde install may come without // support for video thumbnails so we show the mimetype icon instead of // a broken image icon const QPixmap pix = KIconLoader::global()->loadIcon(item.iconName(), KIconLoader::Desktop, d->mThumbnailSize.height()); thumbnail.initAsIcon(pix); } else if (kind == MimeTypeUtils::KIND_DIR) { // Special case for folders because ThumbnailProvider does not return a // thumbnail if there is no images thumbnail.mWaitingForThumbnail = false; return; } else { thumbnail.initAsIcon(DesktopIcon(QStringLiteral("image-missing"), 48)); thumbnail.mFullSize = thumbnail.mGroupPix.size(); } update(thumbnail.mIndex); } QPixmap ThumbnailView::thumbnailForIndex(const QModelIndex& index, QSize* fullSize) { KFileItem item = fileItemForIndex(index); if (item.isNull()) { LOG("Invalid item"); if (fullSize) { *fullSize = QSize(); } return QPixmap(); } QUrl url = item.url(); // Find or create Thumbnail instance ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url); if (it == d->mThumbnailForUrl.end()) { Thumbnail thumbnail = Thumbnail(QPersistentModelIndex(index), item.time(KFileItem::ModificationTime)); it = d->mThumbnailForUrl.insert(url, thumbnail); } Thumbnail& thumbnail = it.value(); // If dir or archive, generate a thumbnail from fileitem pixmap MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item); if (kind == MimeTypeUtils::KIND_ARCHIVE || kind == MimeTypeUtils::KIND_DIR) { int groupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::fromPixelSize(d->mThumbnailSize.height())); if (thumbnail.mGroupPix.isNull() || thumbnail.mGroupPix.height() < groupSize) { const QPixmap pix = KIconLoader::global()->loadIcon(item.iconName(), KIconLoader::Desktop, d->mThumbnailSize.height()); thumbnail.initAsIcon(pix); if (kind == MimeTypeUtils::KIND_ARCHIVE) { // No thumbnails for archives thumbnail.mWaitingForThumbnail = false; } else if (!d->mCreateThumbnailsForRemoteUrls && !UrlUtils::urlIsFastLocalFile(url)) { // If we don't want thumbnails for remote urls, use // "folder-remote" icon for remote folders, so that they do // not look like regular folders thumbnail.mWaitingForThumbnail = false; thumbnail.initAsIcon(DesktopIcon(QStringLiteral("folder-remote"), groupSize)); } else { // set mWaitingForThumbnail to true (necessary in the case // 'thumbnail' already existed before, but with a too small // mGroupPix) thumbnail.mWaitingForThumbnail = true; } } } if (thumbnail.mGroupPix.isNull()) { if (fullSize) { *fullSize = QSize(); } return d->mWaitingThumbnail; } // Adjust thumbnail if (thumbnail.mAdjustedPix.isNull()) { d->roughAdjustThumbnail(&thumbnail); } if (thumbnail.mRough && !d->mSmoothThumbnailQueue.contains(url)) { d->mSmoothThumbnailQueue.enqueue(url); if (!d->mSmoothThumbnailTimer.isActive()) { d->mSmoothThumbnailTimer.start(SMOOTH_DELAY); } } if (fullSize) { *fullSize = thumbnail.mRealFullSize; } + thumbnail.mAdjustedPix.setDevicePixelRatio(devicePixelRatioF()); return thumbnail.mAdjustedPix; } bool ThumbnailView::isModified(const QModelIndex& index) const { if (!d->mDocumentInfoProvider) { return false; } QUrl url = urlForIndex(index); return d->mDocumentInfoProvider->isModified(url); } bool ThumbnailView::isBusy(const QModelIndex& index) const { if (!d->mDocumentInfoProvider) { return false; } QUrl url = urlForIndex(index); return d->mDocumentInfoProvider->isBusy(url); } void ThumbnailView::startDrag(Qt::DropActions) { const QModelIndexList indexes = selectionModel()->selectedIndexes(); if (indexes.isEmpty()) { return; } KFileItemList selectedFiles; for (const auto &index : indexes) { selectedFiles << fileItemForIndex(index); } QDrag* drag = new QDrag(this); drag->setMimeData(MimeTypeUtils::selectionMimeData(selectedFiles, MimeTypeUtils::DropTarget)); d->initDragPixmap(drag, indexes); drag->exec(Qt::MoveAction | Qt::CopyAction | Qt::LinkAction, Qt::CopyAction); } void ThumbnailView::setZoomParameter() { const qreal sensitivityModifier = 0.25; d->mTouch->setZoomParameter(sensitivityModifier, d->mThumbnailSize.width()); } void ThumbnailView::zoomGesture(qreal newZoom, const QPoint&) { if (newZoom >= 0.0) { int width = qBound (int(MinThumbnailSize), static_cast(newZoom), int(MaxThumbnailSize)); setThumbnailWidth(width); } } void ThumbnailView::tapGesture(const QPoint& pos) { const QRect rect = QRect(pos, QSize(1, 1)); setSelection(rect, QItemSelectionModel::ClearAndSelect); emit activated(indexAt(pos)); } void ThumbnailView::startDragFromTouch(const QPoint& pos) { QModelIndex index = indexAt(pos); if (index.isValid()) { setCurrentIndex(index); d->mScroller->stop(); startDrag(Qt::CopyAction); } } void ThumbnailView::dragEnterEvent(QDragEnterEvent* event) { QAbstractItemView::dragEnterEvent(event); if (event->mimeData()->hasUrls()) { event->acceptProposedAction(); } } void ThumbnailView::dragMoveEvent(QDragMoveEvent* event) { // Necessary, otherwise we don't reach dropEvent() QAbstractItemView::dragMoveEvent(event); event->acceptProposedAction(); } void ThumbnailView::dropEvent(QDropEvent* event) { const QList urlList = KUrlMimeData::urlsFromMimeData(event->mimeData()); if (urlList.isEmpty()) { return; } QModelIndex destIndex = indexAt(event->pos()); if (destIndex.isValid()) { KFileItem item = fileItemForIndex(destIndex); if (item.isDir()) { QUrl destUrl = item.url(); d->mThumbnailViewHelper->showMenuForUrlDroppedOnDir(this, urlList, destUrl); return; } } d->mThumbnailViewHelper->showMenuForUrlDroppedOnViewport(this, urlList); event->acceptProposedAction(); } void ThumbnailView::keyPressEvent(QKeyEvent* event) { QListView::keyPressEvent(event); if (event->key() == Qt::Key_Return) { const QModelIndex index = selectionModel()->currentIndex(); if (index.isValid() && selectionModel()->selectedIndexes().count() == 1) { emit indexActivated(index); } } } void ThumbnailView::resizeEvent(QResizeEvent* event) { QListView::resizeEvent(event); d->scheduleThumbnailGeneration(); } void ThumbnailView::showEvent(QShowEvent* event) { QListView::showEvent(event); d->scheduleThumbnailGeneration(); QTimer::singleShot(0, this, &ThumbnailView::scrollToSelectedIndex); } void ThumbnailView::wheelEvent(QWheelEvent* event) { // If we don't adjust the single step, the wheel scroll exactly one item up // and down, giving the impression that the items do not move but only // their label changes. // For some reason it is necessary to set the step here: setting it in // setThumbnailSize() does not work //verticalScrollBar()->setSingleStep(d->mThumbnailSize / 5); if (event->modifiers() == Qt::ControlModifier) { - int width = d->mThumbnailSize.width() + (event->delta() > 0 ? 1 : -1) * WHEEL_ZOOM_MULTIPLIER; + int width = thumbnailSize().width() + (event->delta() > 0 ? 1 : -1) * WHEEL_ZOOM_MULTIPLIER; width = qMax(int(MinThumbnailSize), qMin(width, int(MaxThumbnailSize))); setThumbnailWidth(width); } else { QListView::wheelEvent(event); } } void ThumbnailView::mousePressEvent(QMouseEvent* event) { switch (event->button()) { case Qt::ForwardButton: case Qt::BackButton: return; default: QListView::mousePressEvent(event); } } void ThumbnailView::scrollToSelectedIndex() { QModelIndexList list = selectedIndexes(); if (list.count() >= 1) { scrollTo(list.first(), PositionAtCenter); } } void ThumbnailView::selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) { QListView::selectionChanged(selected, deselected); emit selectionChangedSignal(selected, deselected); } void ThumbnailView::scrollContentsBy(int dx, int dy) { QListView::scrollContentsBy(dx, dy); d->scheduleThumbnailGeneration(); } void ThumbnailView::generateThumbnailsForItems() { if (!isVisible() || !model()) { return; } const QRect visibleRect = viewport()->rect(); const int visibleSurface = visibleRect.width() * visibleRect.height(); const QPoint origin = visibleRect.center(); // distance => item QMultiMap itemMap; for (int row = 0; row < model()->rowCount(); ++row) { QModelIndex index = model()->index(row, 0); KFileItem item = fileItemForIndex(index); QUrl url = item.url(); // Filter out remote items if necessary if (!d->mCreateThumbnailsForRemoteUrls && !url.isLocalFile()) { continue; } // Filter out archives MimeTypeUtils::Kind kind = MimeTypeUtils::fileItemKind(item); if (kind == MimeTypeUtils::KIND_ARCHIVE) { continue; } // Immediately update modified items if (d->mDocumentInfoProvider && d->mDocumentInfoProvider->isModified(url)) { d->updateThumbnailForModifiedDocument(index); continue; } // Filter out items which already have a thumbnail ThumbnailForUrl::ConstIterator it = d->mThumbnailForUrl.constFind(url); if (it != d->mThumbnailForUrl.constEnd() && it.value().isGroupPixAdaptedForSize(d->mThumbnailSize.height())) { continue; } // Compute distance int distance; const QRect itemRect = visualRect(index); const qreal itemSurface = itemRect.width() * itemRect.height(); const QRect visibleItemRect = visibleRect.intersected(itemRect); qreal visibleItemFract = 0; if (itemSurface > 0) { visibleItemFract = visibleItemRect.width() * visibleItemRect.height() / itemSurface; } if (visibleItemFract > 0.7) { // Item is visible, order thumbnails from left to right, top to bottom // Distance is computed so that it is between 0 and visibleSurface distance = itemRect.top() * visibleRect.width() + itemRect.left(); // Make sure directory thumbnails are generated after image thumbnails: // Distance is between visibleSurface and 2 * visibleSurface if (kind == MimeTypeUtils::KIND_DIR) { distance = distance + visibleSurface; } } else { // Item is not visible, order thumbnails according to distance // Start at 2 * visibleSurface to ensure invisible thumbnails are // generated *after* visible thumbnails distance = 2 * visibleSurface + (itemRect.center() - origin).manhattanLength(); } // Add the item to our map itemMap.insert(distance, item); // Insert the thumbnail in mThumbnailForUrl, so that // setThumbnail() can find the item to update if (it == d->mThumbnailForUrl.constEnd()) { Thumbnail thumbnail = Thumbnail(QPersistentModelIndex(index), item.time(KFileItem::ModificationTime)); d->mThumbnailForUrl.insert(url, thumbnail); } } if (!itemMap.isEmpty()) { d->appendItemsToThumbnailProvider(itemMap.values()); } } void ThumbnailView::updateThumbnail(const QUrl& url) { const ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url); if (it == d->mThumbnailForUrl.end()) { return; } if (d->mDocumentInfoProvider) { d->updateThumbnailForModifiedDocument(it->mIndex); } else { const KFileItem item = fileItemForIndex(it->mIndex); d->appendItemsToThumbnailProvider(KFileItemList({ item })); } } void ThumbnailView::updateThumbnailBusyState(const QUrl& url, bool busy) { const ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url); if (it == d->mThumbnailForUrl.end()) { return; } QPersistentModelIndex index(it->mIndex); if (busy && !d->mBusyIndexSet.contains(index)) { d->mBusyIndexSet << index; update(index); if (d->mBusyAnimationTimeLine->state() != QTimeLine::Running) { d->mBusyAnimationTimeLine->start(); } } else if (!busy && d->mBusyIndexSet.remove(index)) { update(index); if (d->mBusyIndexSet.isEmpty()) { d->mBusyAnimationTimeLine->stop(); } } } void ThumbnailView::updateBusyIndexes() { Q_FOREACH(const QPersistentModelIndex & index, d->mBusyIndexSet) { update(index); } } QPixmap ThumbnailView::busySequenceCurrentPixmap() const { return d->mBusySequence.frameAt(d->mBusyAnimationTimeLine->currentFrame()); } void ThumbnailView::smoothNextThumbnail() { if (d->mSmoothThumbnailQueue.isEmpty()) { return; } if (d->mThumbnailProvider && d->mThumbnailProvider->isRunning()) { // give mThumbnailProvider priority over smoothing d->mSmoothThumbnailTimer.start(SMOOTH_DELAY); return; } QUrl url = d->mSmoothThumbnailQueue.dequeue(); ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url); GV_RETURN_IF_FAIL2(it != d->mThumbnailForUrl.end(), url << "not in mThumbnailForUrl."); Thumbnail& thumbnail = it.value(); thumbnail.mAdjustedPix = d->scale(thumbnail.mGroupPix, Qt::SmoothTransformation); thumbnail.mRough = false; GV_RETURN_IF_FAIL2(thumbnail.mIndex.isValid(), "index for" << url << "is invalid."); update(thumbnail.mIndex); if (!d->mSmoothThumbnailQueue.isEmpty()) { d->mSmoothThumbnailTimer.start(0); } } void ThumbnailView::reloadThumbnail(const QModelIndex& index) { QUrl url = urlForIndex(index); if (!url.isValid()) { qWarning() << "Invalid url for index" << index; return; } ThumbnailProvider::deleteImageThumbnail(url); ThumbnailForUrl::Iterator it = d->mThumbnailForUrl.find(url); if (it == d->mThumbnailForUrl.end()) { return; } d->mThumbnailForUrl.erase(it); generateThumbnailsForItems(); } void ThumbnailView::setCreateThumbnailsForRemoteUrls(bool createRemoteThumbs) { d->mCreateThumbnailsForRemoteUrls = createRemoteThumbs; } } // namespace