diff --git a/app/documentinfoprovider.cpp b/app/documentinfoprovider.cpp index 9888dc46..c1d8ba6d 100644 --- a/app/documentinfoprovider.cpp +++ b/app/documentinfoprovider.cpp @@ -1,91 +1,91 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2010 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 "documentinfoprovider.h" // Qt #include // KDE // Local #include #include namespace Gwenview { DocumentInfoProvider::DocumentInfoProvider(SortedDirModel* model) : AbstractDocumentInfoProvider(model) { mDirModel = model; connect(DocumentFactory::instance(), &DocumentFactory::documentBusyStateChanged, this, &AbstractDocumentInfoProvider::busyStateChanged); connect(DocumentFactory::instance(), &DocumentFactory::documentChanged, this, &AbstractDocumentInfoProvider::documentChanged); } -void DocumentInfoProvider::thumbnailForDocument(const QUrl &url, ThumbnailGroup::Enum group, QPixmap* outPix, QSize* outFullSize) const +void DocumentInfoProvider::thumbnailForDocument(const QUrl &url, ThumbnailGroup::Enum group, QImage *outPix, QSize* outFullSize) const { Q_ASSERT(outPix); Q_ASSERT(outFullSize); - *outPix = QPixmap(); + *outPix = QImage(); *outFullSize = QSize(); const int pixelSize = ThumbnailGroup::pixelSize(group); Document::Ptr doc = DocumentFactory::instance()->getCachedDocument(url); if (!doc) { return; } if (doc->loadingState() != Document::Loaded) { return; } QImage image = doc->image(); if (image.width() > pixelSize || image.height() > pixelSize) { image = image.scaled(pixelSize, pixelSize, Qt::KeepAspectRatio); } - *outPix = QPixmap::fromImage(image); + *outPix = image; *outFullSize = doc->size(); } bool DocumentInfoProvider::isModified(const QUrl &url) { Document::Ptr doc = DocumentFactory::instance()->getCachedDocument(url); if (doc) { return doc->loadingState() == Document::Loaded && doc->isModified(); } else { return false; } } bool DocumentInfoProvider::isBusy(const QUrl &url) { Document::Ptr doc = DocumentFactory::instance()->getCachedDocument(url); if (doc) { return doc->isBusy(); } else { return false; } } } // namespace diff --git a/app/documentinfoprovider.h b/app/documentinfoprovider.h index c17c2a24..c227c943 100644 --- a/app/documentinfoprovider.h +++ b/app/documentinfoprovider.h @@ -1,54 +1,54 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2010 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. */ #ifndef DOCUMENTINFOPROVIDER_H #define DOCUMENTINFOPROVIDER_H // Qt // KDE // Local #include namespace Gwenview { class SortedDirModel; class DocumentInfoProvider : public AbstractDocumentInfoProvider { Q_OBJECT public: DocumentInfoProvider(SortedDirModel* model); bool isBusy(const QUrl &url) override; bool isModified(const QUrl &url) override; - void thumbnailForDocument(const QUrl &url, ThumbnailGroup::Enum group, QPixmap* outPix, QSize* outFullSize) const override; + void thumbnailForDocument(const QUrl &url, ThumbnailGroup::Enum group, QImage* outPix, QSize* outFullSize) const override; private: SortedDirModel* mDirModel; }; } // namespace #endif /* DOCUMENTINFOPROVIDER_H */ diff --git a/app/filtercontroller.h b/app/filtercontroller.h index 9ea903b9..1e99e353 100644 --- a/app/filtercontroller.h +++ b/app/filtercontroller.h @@ -1,369 +1,370 @@ // 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. */ #ifndef FILTERCONTROLLER_H #define FILTERCONTROLLER_H #include // Qt #include #include #include #include // KDE #include // Local #include #include #include #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE // KDE #include // Local #include #include #endif class QAction; class QFrame; class QLineEdit; class QComboBox; class KComboBox; +class ThumbnailProvider; namespace Gwenview { class SortedDirModel; /** * An AbstractSortedDirModelFilter which filters on the file names */ class NameFilter : public AbstractSortedDirModelFilter { public: enum Mode { Contains, DoesNotContain }; NameFilter(SortedDirModel* model) : AbstractSortedDirModelFilter(model) , mText() , mMode(Contains) {} bool needsSemanticInfo() const override { return false; } bool acceptsIndex(const QModelIndex& index) const override { if (mText.isEmpty()) { return true; } switch (mMode) { case Contains: return index.data().toString().contains(mText, Qt::CaseInsensitive); default: /*DoesNotContain:*/ return !index.data().toString().contains(mText, Qt::CaseInsensitive); } } void setText(const QString& text) { mText = text; model()->applyFilters(); } void setMode(Mode mode) { mMode = mode; model()->applyFilters(); } private: QString mText; Mode mMode; }; class NameFilterWidget : public QWidget { Q_OBJECT public: NameFilterWidget(SortedDirModel*); ~NameFilterWidget() override; private Q_SLOTS: void applyNameFilter(); private: QPointer mFilter; KComboBox* mModeComboBox; QLineEdit* mLineEdit; }; /** * An AbstractSortedDirModelFilter which filters on the file dates */ class DateFilter : public AbstractSortedDirModelFilter { public: enum Mode { GreaterOrEqual, Equal, LessOrEqual }; DateFilter(SortedDirModel* model) : AbstractSortedDirModelFilter(model) , mMode(GreaterOrEqual) {} bool needsSemanticInfo() const override { return false; } bool acceptsIndex(const QModelIndex& index) const override { if (!mDate.isValid()) { return true; } KFileItem fileItem = model()->itemForSourceIndex(index); QDate date = TimeUtils::dateTimeForFileItem(fileItem).date(); switch (mMode) { case GreaterOrEqual: return date >= mDate; case Equal: return date == mDate; default: /* LessOrEqual */ return date <= mDate; } } void setDate(const QDate& date) { mDate = date; model()->applyFilters(); } void setMode(Mode mode) { mMode = mode; model()->applyFilters(); } private: QDate mDate; Mode mMode; }; class DateFilterWidget : public QWidget { Q_OBJECT public: DateFilterWidget(SortedDirModel*); ~DateFilterWidget() override; private Q_SLOTS: void applyDateFilter(); private: QPointer mFilter; KComboBox* mModeComboBox; DateWidget* mDateWidget; }; #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE /** * An AbstractSortedDirModelFilter which filters on file ratings */ class RatingFilter : public AbstractSortedDirModelFilter { public: enum Mode { GreaterOrEqual, Equal, LessOrEqual }; RatingFilter(SortedDirModel* model) : AbstractSortedDirModelFilter(model) , mRating(0) , mMode(GreaterOrEqual) {} bool needsSemanticInfo() const override { return true; } bool acceptsIndex(const QModelIndex& index) const override { SemanticInfo info = model()->semanticInfoForSourceIndex(index); switch (mMode) { case GreaterOrEqual: return info.mRating >= mRating; case Equal: return info.mRating == mRating; default: /* LessOrEqual */ return info.mRating <= mRating; } } void setRating(int value) { mRating = value; model()->applyFilters(); } void setMode(Mode mode) { mMode = mode; model()->applyFilters(); } private: int mRating; Mode mMode; }; class RatingFilterWidget : public QWidget { Q_OBJECT public: RatingFilterWidget(SortedDirModel*); ~RatingFilterWidget() override; private Q_SLOTS: void slotRatingChanged(int value); void updateFilterMode(); private: KComboBox* mModeComboBox; KRatingWidget* mRatingWidget; QPointer mFilter; }; /** * An AbstractSortedDirModelFilter which filters on associated tags */ class TagFilter : public AbstractSortedDirModelFilter { public: TagFilter(SortedDirModel* model) : AbstractSortedDirModelFilter(model) , mWantMatchingTag(true) {} bool needsSemanticInfo() const override { return true; } bool acceptsIndex(const QModelIndex& index) const override { if (mTag.isEmpty()) { return true; } SemanticInfo info = model()->semanticInfoForSourceIndex(index); if (mWantMatchingTag) { return info.mTags.contains(mTag); } else { return !info.mTags.contains(mTag); } } void setTag(const SemanticInfoTag& tag) { mTag = tag; model()->applyFilters(); } void setWantMatchingTag(bool value) { mWantMatchingTag = value; model()->applyFilters(); } private: SemanticInfoTag mTag; bool mWantMatchingTag; }; class TagFilterWidget : public QWidget { Q_OBJECT public: TagFilterWidget(SortedDirModel*); ~TagFilterWidget() override; private Q_SLOTS: void updateTagSetFilter(); private: KComboBox* mModeComboBox; QComboBox* mTagComboBox; QPointer mFilter; }; #endif /** * This class manages the filter widgets in the filter frame and assign the * corresponding filters to the SortedDirModel */ class FilterController : public QObject { Q_OBJECT public: FilterController(QFrame* filterFrame, SortedDirModel* model); QList actionList() const; private Q_SLOTS: void addFilterByName(); void addFilterByDate(); #ifndef GWENVIEW_SEMANTICINFO_BACKEND_NONE void addFilterByRating(); void addFilterByTag(); #endif void slotFilterWidgetClosed(); private: void addAction(const QString& text, const char* slot); void addFilter(QWidget* widget); FilterController* q; QFrame* mFrame; SortedDirModel* mDirModel; QList mActionList; int mFilterWidgetCount; /**< How many filter widgets are in mFrame */ }; } // namespace #endif /* FILTERCONTROLLER_H */ diff --git a/lib/documentview/documentview.cpp b/lib/documentview/documentview.cpp index 42e3a1fd..8ba437fb 100644 --- a/lib/documentview/documentview.cpp +++ b/lib/documentview/documentview.cpp @@ -1,1022 +1,1022 @@ // 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 "documentview.h" // C++ Standard library #include // Qt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE #include #include #include // Local #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 static const qreal REAL_DELTA = 0.001; static const qreal MAXIMUM_ZOOM_VALUE = qreal(DocumentView::MaximumZoom); static const auto MINSTEP = sqrt(0.5); static const auto MAXSTEP = sqrt(2.0); static const int COMPARE_MARGIN = 4; const int DocumentView::MaximumZoom = 16; struct DocumentViewPrivate { DocumentView* q; int mSortKey; // Used to sort views when displayed in compare mode HudWidget* mHud; BirdEyeView* mBirdEyeView; QPointer mMoveAnimation; QPointer mFadeAnimation; QGraphicsOpacityEffect* mOpacityEffect; LoadingIndicator* mLoadingIndicator; QScopedPointer mAdapter; QList mZoomSnapValues; Document::Ptr mDocument; DocumentView::Setup mSetup; bool mCurrent; bool mCompareMode; int controlWheelAccumulatedDelta; QPointF mDragStartPosition; QPointer mDragThumbnailProvider; QPointer mDrag; void setCurrentAdapter(AbstractDocumentViewAdapter* adapter) { Q_ASSERT(adapter); mAdapter.reset(adapter); adapter->widget()->setParentItem(q); resizeAdapterWidget(); if (adapter->canZoom()) { QObject::connect(adapter, SIGNAL(zoomChanged(qreal)), q, SLOT(slotZoomChanged(qreal))); QObject::connect(adapter, SIGNAL(zoomInRequested(QPointF)), q, SLOT(zoomIn(QPointF))); QObject::connect(adapter, SIGNAL(zoomOutRequested(QPointF)), q, SLOT(zoomOut(QPointF))); QObject::connect(adapter, SIGNAL(zoomToFitChanged(bool)), q, SIGNAL(zoomToFitChanged(bool))); QObject::connect(adapter, SIGNAL(zoomToFillChanged(bool)), q, SIGNAL(zoomToFillChanged(bool))); } QObject::connect(adapter, SIGNAL(scrollPosChanged()), q, SIGNAL(positionChanged())); QObject::connect(adapter, SIGNAL(previousImageRequested()), q, SIGNAL(previousImageRequested())); QObject::connect(adapter, SIGNAL(nextImageRequested()), q, SIGNAL(nextImageRequested())); QObject::connect(adapter, SIGNAL(toggleFullScreenRequested()), q, SIGNAL(toggleFullScreenRequested())); QObject::connect(adapter, SIGNAL(completed()), q, SLOT(slotCompleted())); adapter->loadConfig(); adapter->widget()->installSceneEventFilter(q); if (mCurrent) { adapter->widget()->setFocus(); } if (mSetup.valid && adapter->canZoom()) { adapter->setZoomToFit(mSetup.zoomToFit); adapter->setZoomToFill(mSetup.zoomToFill); if (!mSetup.zoomToFit && !mSetup.zoomToFill) { adapter->setZoom(mSetup.zoom); adapter->setScrollPos(mSetup.position); } } emit q->adapterChanged(); emit q->positionChanged(); if (adapter->canZoom()) { if (adapter->zoomToFit()) { emit q->zoomToFitChanged(true); } else if (adapter->zoomToFill()) { emit q->zoomToFillChanged(true); } else { emit q->zoomChanged(adapter->zoom()); } } if (adapter->rasterImageView()) { QObject::connect(adapter->rasterImageView(), SIGNAL(currentToolChanged(AbstractRasterImageViewTool*)), q, SIGNAL(currentToolChanged(AbstractRasterImageViewTool*))); } } void setupLoadingIndicator() { mLoadingIndicator = new LoadingIndicator(q); GraphicsWidgetFloater* floater = new GraphicsWidgetFloater(q); floater->setChildWidget(mLoadingIndicator); } HudButton* createHudButton(const QString& text, const QString &iconName, bool showText) { HudButton* button = new HudButton; if (showText) { button->setText(text); } else { button->setToolTip(text); } button->setIcon(QIcon::fromTheme(iconName)); return button; } void setupHud() { HudButton* trashButton = createHudButton(i18nc("@info:tooltip", "Trash"), QStringLiteral("user-trash"), false); HudButton* deselectButton = createHudButton(i18nc("@action:button", "Deselect"), QStringLiteral("list-remove"), true); QGraphicsWidget* content = new QGraphicsWidget; QGraphicsLinearLayout* layout = new QGraphicsLinearLayout(content); layout->addItem(trashButton); layout->addItem(deselectButton); mHud = new HudWidget(q); mHud->init(content, HudWidget::OptionNone); GraphicsWidgetFloater* floater = new GraphicsWidgetFloater(q); floater->setChildWidget(mHud); floater->setAlignment(Qt::AlignBottom | Qt::AlignHCenter); QObject::connect(trashButton, SIGNAL(clicked()), q, SLOT(emitHudTrashClicked())); QObject::connect(deselectButton, SIGNAL(clicked()), q, SLOT(emitHudDeselectClicked())); mHud->hide(); } void setupBirdEyeView() { if (mBirdEyeView) { delete mBirdEyeView; } mBirdEyeView = new BirdEyeView(q); mBirdEyeView->setZValue(1); } void updateCaption() { if (!mCurrent) { return; } QString caption; Document::Ptr doc = mAdapter->document(); if (!doc) { emit q->captionUpdateRequested(caption); return; } caption = doc->url().fileName(); QSize size = doc->size(); if (size.isValid()) { caption += QStringLiteral(" - %1x%2") .arg(size.width()) .arg(size.height()); if (mAdapter->canZoom()) { int intZoom = qRound(mAdapter->zoom() * 100); caption += QStringLiteral(" - %1%") .arg(intZoom); } } emit q->captionUpdateRequested(caption); } void uncheckZoomToFit() { if (mAdapter->zoomToFit()) { mAdapter->setZoomToFit(false); } } void uncheckZoomToFill() { if (mAdapter->zoomToFill()) { mAdapter->setZoomToFill(false); } } void setZoom(qreal zoom, const QPointF& center = QPointF(-1, -1)) { uncheckZoomToFit(); uncheckZoomToFill(); zoom = qBound(q->minimumZoom(), zoom, MAXIMUM_ZOOM_VALUE); mAdapter->setZoom(zoom, center); } void updateZoomSnapValues() { qreal min = q->minimumZoom(); mZoomSnapValues.clear(); for (qreal zoom = MINSTEP; zoom > min; zoom *= MINSTEP) { mZoomSnapValues << zoom; } mZoomSnapValues << min; std::reverse(mZoomSnapValues.begin(), mZoomSnapValues.end()); for (qreal zoom = 1; zoom < MAXIMUM_ZOOM_VALUE; zoom *= MAXSTEP) { mZoomSnapValues << zoom; } mZoomSnapValues << MAXIMUM_ZOOM_VALUE; emit q->minimumZoomChanged(min); } void showLoadingIndicator() { if (!mLoadingIndicator) { setupLoadingIndicator(); } mLoadingIndicator->show(); mLoadingIndicator->setZValue(1); } void hideLoadingIndicator() { if (!mLoadingIndicator) { return; } mLoadingIndicator->hide(); } void resizeAdapterWidget() { QRectF rect = QRectF(QPointF(0, 0), q->boundingRect().size()); if (mCompareMode) { rect.adjust(COMPARE_MARGIN, COMPARE_MARGIN, -COMPARE_MARGIN, -COMPARE_MARGIN); } mAdapter->widget()->setGeometry(rect); } void fadeTo(qreal value) { if (mFadeAnimation.data()) { qreal endValue = mFadeAnimation.data()->endValue().toReal(); if (qFuzzyCompare(value, endValue)) { // Same end value, don't change the actual animation return; } } // Create a new fade animation QPropertyAnimation* anim = new QPropertyAnimation(mOpacityEffect, "opacity"); anim->setStartValue(mOpacityEffect->opacity()); anim->setEndValue(value); if (qFuzzyCompare(value, 1)) { QObject::connect(anim, SIGNAL(finished()), q, SLOT(slotFadeInFinished())); } QObject::connect(anim, SIGNAL(finished()), q, SIGNAL(isAnimatedChanged())); anim->setDuration(q->style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, nullptr)); mFadeAnimation = anim; emit q->isAnimatedChanged(); anim->start(QAbstractAnimation::DeleteWhenStopped); } bool canPan() const { if (!q->canZoom()) { return false; } const QSize zoomedImageSize = mDocument->size() * q->zoom(); const QSize viewPortSize = q->boundingRect().size().toSize(); const bool imageWiderThanViewport = zoomedImageSize.width() > viewPortSize.width(); const bool imageTallerThanViewport = zoomedImageSize.height() > viewPortSize.height(); return (imageWiderThanViewport || imageTallerThanViewport); } void setDragPixmap(const QPixmap& pix) { if (mDrag) { DragPixmapGenerator::DragPixmap dragPixmap = DragPixmapGenerator::generate({pix}, 1); mDrag->setPixmap(dragPixmap.pix); mDrag->setHotSpot(dragPixmap.hotSpot); } } void executeDrag() { if (mDrag) { if (mAdapter->imageView()) { mAdapter->imageView()->resetDragCursor(); } mDrag->exec(Qt::MoveAction | Qt::CopyAction | Qt::LinkAction, Qt::CopyAction); } } void initDragThumbnailProvider() { mDragThumbnailProvider = new ThumbnailProvider(); QObject::connect(mDragThumbnailProvider, &ThumbnailProvider::thumbnailLoaded, q, &DocumentView::dragThumbnailLoaded); QObject::connect(mDragThumbnailProvider, &ThumbnailProvider::thumbnailLoadingFailed, q, &DocumentView::dragThumbnailLoadingFailed); } void startDragIfSensible() { if (q->document()->loadingState() == Document::LoadingFailed) { return; } if (q->currentTool()) { return; } if (mDrag) { mDrag->deleteLater(); } mDrag = new QDrag(q); const auto itemList = KFileItemList({q->document()->url()}); mDrag->setMimeData(MimeTypeUtils::selectionMimeData(itemList, MimeTypeUtils::DropTarget)); if (q->document()->isModified()) { setDragPixmap(QPixmap::fromImage(q->document()->image())); executeDrag(); } else { // Drag is triggered on success or failure of thumbnail generation if (mDragThumbnailProvider.isNull()) { initDragThumbnailProvider(); } mDragThumbnailProvider->appendItems(itemList); } } QPointF cursorPosition() { const QGraphicsScene* sc = q->scene(); if (sc) { const auto views = sc->views(); for (const QGraphicsView* view : views) { if (view->underMouse()) { return q->mapFromScene(view->mapFromGlobal(QCursor::pos())); } } } return QPointF(-1, -1); } }; DocumentView::DocumentView(QGraphicsScene* scene) : d(new DocumentViewPrivate) { setFlag(ItemIsFocusable); setFlag(ItemIsSelectable); setFlag(ItemClipsChildrenToShape); d->q = this; d->mLoadingIndicator = nullptr; d->mBirdEyeView = nullptr; d->mCurrent = false; d->mCompareMode = false; d->controlWheelAccumulatedDelta = 0; d->mDragStartPosition = QPointF(0, 0); d->mDrag = nullptr; // We use an opacity effect instead of using the opacity property directly, because the latter operates at // the painter level, which means if you draw multiple layers in paint(), all layers get the specified // opacity, resulting in all layers being visible when 0 < opacity < 1. // QGraphicsEffects on the other hand, operate after all painting is done, therefore 'flattening' all layers. // This is important for fade effects, where we don't want any background layers visible during the fade. d->mOpacityEffect = new QGraphicsOpacityEffect(this); d->mOpacityEffect->setOpacity(0); setGraphicsEffect(d->mOpacityEffect); scene->addItem(this); d->setupHud(); d->setCurrentAdapter(new EmptyAdapter); setAcceptDrops(true); connect(DocumentFactory::instance(), &DocumentFactory::documentChanged, this, [this]() { d->updateCaption(); }); } DocumentView::~DocumentView() { delete d->mDragThumbnailProvider; delete d->mDrag; delete d; } void DocumentView::createAdapterForDocument() { const MimeTypeUtils::Kind documentKind = d->mDocument->kind(); if (d->mAdapter && documentKind == d->mAdapter->kind() && documentKind != MimeTypeUtils::KIND_UNKNOWN) { // Do not reuse for KIND_UNKNOWN: we may need to change the message LOG("Reusing current adapter"); return; } AbstractDocumentViewAdapter* adapter = nullptr; switch (documentKind) { case MimeTypeUtils::KIND_RASTER_IMAGE: adapter = new RasterImageViewAdapter; break; case MimeTypeUtils::KIND_SVG_IMAGE: adapter = new SvgViewAdapter; break; case MimeTypeUtils::KIND_VIDEO: adapter = new VideoViewAdapter; connect(adapter, SIGNAL(videoFinished()), SIGNAL(videoFinished())); break; case MimeTypeUtils::KIND_UNKNOWN: adapter = new MessageViewAdapter; static_cast(adapter)->setErrorMessage(i18n("Gwenview does not know how to display this kind of document")); break; default: qWarning() << "should not be called for documentKind=" << documentKind; adapter = new MessageViewAdapter; break; } d->setCurrentAdapter(adapter); } void DocumentView::openUrl(const QUrl &url, const DocumentView::Setup& setup) { if (d->mDocument) { if (url == d->mDocument->url()) { return; } disconnect(d->mDocument.data(), nullptr, this, nullptr); } d->mSetup = setup; d->mDocument = DocumentFactory::instance()->load(url); connect(d->mDocument.data(), SIGNAL(busyChanged(QUrl,bool)), SLOT(slotBusyChanged(QUrl,bool))); connect(d->mDocument.data(), &Document::modified, this, [this]() { d->updateZoomSnapValues(); }); if (d->mDocument->loadingState() < Document::KindDetermined) { MessageViewAdapter* messageViewAdapter = qobject_cast(d->mAdapter.data()); if (messageViewAdapter) { messageViewAdapter->setInfoMessage(QString()); } d->showLoadingIndicator(); connect(d->mDocument.data(), SIGNAL(kindDetermined(QUrl)), SLOT(finishOpenUrl())); } else { QMetaObject::invokeMethod(this, "finishOpenUrl", Qt::QueuedConnection); } d->setupBirdEyeView(); } void DocumentView::finishOpenUrl() { disconnect(d->mDocument.data(), SIGNAL(kindDetermined(QUrl)), this, SLOT(finishOpenUrl())); GV_RETURN_IF_FAIL(d->mDocument->loadingState() >= Document::KindDetermined); if (d->mDocument->loadingState() == Document::LoadingFailed) { slotLoadingFailed(); return; } createAdapterForDocument(); connect(d->mDocument.data(), SIGNAL(loadingFailed(QUrl)), SLOT(slotLoadingFailed())); d->mAdapter->setDocument(d->mDocument); d->updateCaption(); } void DocumentView::loadAdapterConfig() { d->mAdapter->loadConfig(); } RasterImageView* DocumentView::imageView() const { return d->mAdapter->rasterImageView(); } void DocumentView::slotCompleted() { d->hideLoadingIndicator(); d->updateCaption(); d->updateZoomSnapValues(); if (!d->mAdapter->zoomToFit() || !d->mAdapter->zoomToFill()) { qreal min = minimumZoom(); if (d->mAdapter->zoom() < min) { d->mAdapter->setZoom(min); } } emit completed(); } DocumentView::Setup DocumentView::setup() const { Setup setup; if (d->mAdapter->canZoom()) { setup.valid = true; setup.zoomToFit = zoomToFit(); setup.zoomToFill = zoomToFill(); if (!setup.zoomToFit && !setup.zoomToFill) { setup.zoom = zoom(); setup.position = position(); } } return setup; } void DocumentView::slotLoadingFailed() { d->hideLoadingIndicator(); MessageViewAdapter* adapter = new MessageViewAdapter; adapter->setDocument(d->mDocument); QString message = xi18n("Loading %1 failed", d->mDocument->url().fileName()); adapter->setErrorMessage(message, d->mDocument->errorString()); d->setCurrentAdapter(adapter); emit completed(); } bool DocumentView::canZoom() const { return d->mAdapter->canZoom(); } void DocumentView::setZoomToFit(bool on) { if (on == d->mAdapter->zoomToFit()) { return; } d->mAdapter->setZoomToFit(on); } void DocumentView::toggleZoomToFit() { const bool zoomToFitOn = d->mAdapter->zoomToFit(); d->mAdapter->setZoomToFit(!zoomToFitOn); if (zoomToFitOn) { d->setZoom(1., d->cursorPosition()); } } void DocumentView::setZoomToFill(bool on) { if (on == d->mAdapter->zoomToFill()) { return; } d->mAdapter->setZoomToFill(on, d->cursorPosition()); } void DocumentView::toggleZoomToFill() { const bool zoomToFillOn = d->mAdapter->zoomToFill(); d->mAdapter->setZoomToFill(!zoomToFillOn, d->cursorPosition()); if (zoomToFillOn) { d->setZoom(1., d->cursorPosition()); } } bool DocumentView::zoomToFit() const { return d->mAdapter->zoomToFit(); } bool DocumentView::zoomToFill() const { return d->mAdapter->zoomToFill(); } void DocumentView::zoomActualSize() { d->uncheckZoomToFit(); d->uncheckZoomToFill(); d->mAdapter->setZoom(1., d->cursorPosition()); } void DocumentView::zoomIn(QPointF center) { if (center == QPointF(-1, -1)) { center = d->cursorPosition(); } qreal currentZoom = d->mAdapter->zoom(); Q_FOREACH(qreal zoom, d->mZoomSnapValues) { if (zoom > currentZoom + REAL_DELTA) { d->setZoom(zoom, center); return; } } } void DocumentView::zoomOut(QPointF center) { if (center == QPointF(-1, -1)) { center = d->cursorPosition(); } qreal currentZoom = d->mAdapter->zoom(); QListIterator it(d->mZoomSnapValues); it.toBack(); while (it.hasPrevious()) { qreal zoom = it.previous(); if (zoom < currentZoom - REAL_DELTA) { d->setZoom(zoom, center); return; } } } void DocumentView::slotZoomChanged(qreal zoom) { d->updateCaption(); emit zoomChanged(zoom); } void DocumentView::setZoom(qreal zoom) { d->setZoom(zoom); } qreal DocumentView::zoom() const { return d->mAdapter->zoom(); } void DocumentView::resizeEvent(QGraphicsSceneResizeEvent *event) { d->resizeAdapterWidget(); d->updateZoomSnapValues(); QGraphicsWidget::resizeEvent(event); } void DocumentView::mousePressEvent(QGraphicsSceneMouseEvent* event) { QGraphicsWidget::mousePressEvent(event); if (d->mAdapter->canZoom() && event->button() == Qt::MiddleButton) { if (event->modifiers() == Qt::NoModifier) { toggleZoomToFit(); } else if (event->modifiers() == Qt::SHIFT) { toggleZoomToFill(); } } } void DocumentView::wheelEvent(QGraphicsSceneWheelEvent* event) { if (d->mAdapter->canZoom()) { if ((event->modifiers() & Qt::ControlModifier) || (GwenviewConfig::mouseWheelBehavior() == MouseWheelBehavior::Zoom && event->modifiers() == Qt::NoModifier)) { d->controlWheelAccumulatedDelta += event->delta(); // Ctrl + wheel => zoom in or out if (d->controlWheelAccumulatedDelta >= QWheelEvent::DefaultDeltasPerStep) { zoomIn(event->pos()); d->controlWheelAccumulatedDelta = 0; } else if (d->controlWheelAccumulatedDelta <= -QWheelEvent::DefaultDeltasPerStep) { zoomOut(event->pos()); d->controlWheelAccumulatedDelta = 0; } return; } } if (GwenviewConfig::mouseWheelBehavior() == MouseWheelBehavior::Browse && event->modifiers() == Qt::NoModifier) { d->controlWheelAccumulatedDelta += event->delta(); // Browse with mouse wheel if (d->controlWheelAccumulatedDelta >= QWheelEvent::DefaultDeltasPerStep) { emit previousImageRequested(); d->controlWheelAccumulatedDelta = 0; } else if (d->controlWheelAccumulatedDelta <= -QWheelEvent::DefaultDeltasPerStep) { emit nextImageRequested(); d->controlWheelAccumulatedDelta = 0; } return; } // Scroll qreal dx = 0; // 16 = pixels for one line // 120: see QWheelEvent::delta() doc qreal dy = -qApp->wheelScrollLines() * 16 * event->delta() / 120; if (event->orientation() == Qt::Horizontal) { qSwap(dx, dy); } d->mAdapter->setScrollPos(d->mAdapter->scrollPos() + QPointF(dx, dy)); } void DocumentView::contextMenuEvent(QGraphicsSceneContextMenuEvent* event) { // Filter out context menu if Ctrl is down to avoid showing it when // zooming out with Ctrl + Right button if (event->modifiers() != Qt::ControlModifier) { emit contextMenuRequested(); } } void DocumentView::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget* /*widget*/) { // Fill background manually, because setAutoFillBackground(true) fill with QPalette::Window, // but our palettes use QPalette::Base for the background color/texture painter->fillRect(rect(), palette().base()); // Selection indicator/highlight if (d->mCompareMode && d->mCurrent) { painter->save(); painter->setBrush(Qt::NoBrush); painter->setPen(QPen(palette().highlight().color(), 2)); painter->setRenderHint(QPainter::Antialiasing); const QRectF visibleRectF = mapRectFromItem(d->mAdapter->widget(), d->mAdapter->visibleDocumentRect()); // Round the point and size independently. This is different than calling toRect(), // and is necessary to keep consistent rects, otherwise the selection rect can be // drawn 1 pixel too big or small. const QRect visibleRect = QRect(visibleRectF.topLeft().toPoint(), visibleRectF.size().toSize()); const QRect selectionRect = visibleRect.adjusted(-1, -1, 1, 1); painter->drawRoundedRect(selectionRect, 3, 3); painter->restore(); } } void DocumentView::slotBusyChanged(const QUrl&, bool busy) { if (busy) { d->showLoadingIndicator(); } else { d->hideLoadingIndicator(); } } qreal DocumentView::minimumZoom() const { // There is no point zooming out less than zoomToFit, but make sure it does // not get too small either return qBound(qreal(0.001), d->mAdapter->computeZoomToFit(), qreal(1.)); } void DocumentView::setCompareMode(bool compare) { d->mCompareMode = compare; if (compare) { d->mHud->show(); d->mHud->setZValue(1); } else { d->mHud->hide(); } } void DocumentView::setCurrent(bool value) { d->mCurrent = value; if (value) { d->mAdapter->widget()->setFocus(); d->updateCaption(); } update(); } bool DocumentView::isCurrent() const { return d->mCurrent; } QPoint DocumentView::position() const { return d->mAdapter->scrollPos().toPoint(); } void DocumentView::setPosition(const QPoint& pos) { d->mAdapter->setScrollPos(pos); } Document::Ptr DocumentView::document() const { return d->mDocument; } QUrl DocumentView::url() const { Document::Ptr doc = d->mDocument; return doc ? doc->url() : QUrl(); } void DocumentView::emitHudDeselectClicked() { emit hudDeselectClicked(this); } void DocumentView::emitHudTrashClicked() { emit hudTrashClicked(this); } void DocumentView::emitFocused() { emit focused(this); } void DocumentView::setGeometry(const QRectF& rect) { QGraphicsWidget::setGeometry(rect); if (d->mBirdEyeView) { d->mBirdEyeView->slotZoomOrSizeChanged(); } } void DocumentView::moveTo(const QRect& rect) { if (d->mMoveAnimation) { d->mMoveAnimation.data()->setEndValue(rect); } else { setGeometry(rect); } } void DocumentView::moveToAnimated(const QRect& rect) { QPropertyAnimation* anim = new QPropertyAnimation(this, "geometry"); anim->setStartValue(geometry()); anim->setEndValue(rect); anim->setDuration(style()->styleHint(QStyle::SH_Widget_Animation_Duration, nullptr, nullptr)); connect(anim, SIGNAL(finished()), SIGNAL(isAnimatedChanged())); d->mMoveAnimation = anim; emit isAnimatedChanged(); anim->start(QAbstractAnimation::DeleteWhenStopped); } QPropertyAnimation* DocumentView::fadeIn() { d->fadeTo(1); return d->mFadeAnimation.data(); } void DocumentView::fadeOut() { d->fadeTo(0); } void DocumentView::slotFadeInFinished() { emit fadeInFinished(this); } bool DocumentView::isAnimated() const { return d->mMoveAnimation || d->mFadeAnimation; } bool DocumentView::sceneEventFilter(QGraphicsItem*, QEvent* event) { if (event->type() == QEvent::GraphicsSceneMousePress) { const QGraphicsSceneMouseEvent* mouseEvent = static_cast(event); if (mouseEvent->button() == Qt::LeftButton) { d->mDragStartPosition = mouseEvent->pos(); } QMetaObject::invokeMethod(this, "emitFocused", Qt::QueuedConnection); } else if (event->type() == QEvent::GraphicsSceneHoverMove) { if (d->mBirdEyeView) { d->mBirdEyeView->onMouseMoved(); } } else if (event->type() == QEvent::GraphicsSceneMouseMove) { const QGraphicsSceneMouseEvent* mouseEvent = static_cast(event); const qreal dragDistance = (mouseEvent->pos() - d->mDragStartPosition).manhattanLength(); const qreal minDistanceToStartDrag = QGuiApplication::styleHints()->startDragDistance(); if (!d->canPan() && dragDistance >= minDistanceToStartDrag) { d->startDragIfSensible(); } } return false; } AbstractRasterImageViewTool* DocumentView::currentTool() const { return imageView() ? imageView()->currentTool() : nullptr; } int DocumentView::sortKey() const { return d->mSortKey; } void DocumentView::setSortKey(int sortKey) { d->mSortKey = sortKey; } void DocumentView::hideAndDeleteLater() { hide(); deleteLater(); } void DocumentView::setGraphicsEffectOpacity(qreal opacity) { d->mOpacityEffect->setOpacity(opacity); } void DocumentView::dragEnterEvent(QGraphicsSceneDragDropEvent* event) { QGraphicsWidget::dragEnterEvent(event); const auto urls = KUrlMimeData::urlsFromMimeData(event->mimeData()); bool acceptDrag = !urls.isEmpty(); if (urls.size() == 1 && urls.first() == url()) { // Do not allow dragging a single image onto itself acceptDrag = false; } event->setAccepted(acceptDrag); } void DocumentView::dropEvent(QGraphicsSceneDragDropEvent* event) { QGraphicsWidget::dropEvent(event); // Since we're capturing drops in View mode, we only support one url const QUrl url = event->mimeData()->urls().first(); if (UrlUtils::urlIsDirectory(url)) { emit openDirUrlRequested(url); } else { emit openUrlRequested(url); } } -void DocumentView::dragThumbnailLoaded(const KFileItem& item, const QPixmap& pix) +void DocumentView::dragThumbnailLoaded(const KFileItem& item, const QImage& pix) { - d->setDragPixmap(pix); + d->setDragPixmap(QPixmap::fromImage(pix)); d->executeDrag(); d->mDragThumbnailProvider->removeItems(KFileItemList({item})); } void DocumentView::dragThumbnailLoadingFailed(const KFileItem& item) { d->executeDrag(); d->mDragThumbnailProvider->removeItems(KFileItemList({item})); } } // namespace diff --git a/lib/documentview/documentview.h b/lib/documentview/documentview.h index 4db6659e..4d7a10ad 100644 --- a/lib/documentview/documentview.h +++ b/lib/documentview/documentview.h @@ -1,245 +1,245 @@ // 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. */ #ifndef DOCUMENTVIEW_H #define DOCUMENTVIEW_H #include // Qt #include // KDE // Local #include class QPropertyAnimation; class QUrl; namespace Gwenview { class AbstractRasterImageViewTool; class RasterImageView; struct DocumentViewPrivate; /** * This widget can display various documents, using an instance of * AbstractDocumentViewAdapter */ class GWENVIEWLIB_EXPORT DocumentView : public QGraphicsWidget { Q_OBJECT Q_PROPERTY(qreal zoom READ zoom WRITE setZoom NOTIFY zoomChanged) Q_PROPERTY(bool zoomToFit READ zoomToFit WRITE setZoomToFit NOTIFY zoomToFitChanged) Q_PROPERTY(bool zoomToFill READ zoomToFill WRITE setZoomToFill NOTIFY zoomToFillChanged) Q_PROPERTY(QPoint position READ position WRITE setPosition NOTIFY positionChanged) public: static const int MaximumZoom; struct Setup { Setup() : valid(false) , zoomToFit(true) , zoomToFill(false) , zoom(0) {} bool valid:1; bool zoomToFit:1; bool zoomToFill:1; qreal zoom; QPointF position; }; enum AnimationMethod { NoAnimation, SoftwareAnimation, GLAnimation }; /** * Create a new view attached to scene. We need the scene to be able to * install scene event filters. */ explicit DocumentView(QGraphicsScene* scene); ~DocumentView() override; Document::Ptr document() const; QUrl url() const; void openUrl(const QUrl&, const Setup&); Setup setup() const; /** * Tells the current adapter to load its config. Used when the user changed * the config while the view was visible. */ void loadAdapterConfig(); bool canZoom() const; qreal minimumZoom() const; qreal zoom() const; bool isCurrent() const; void setCurrent(bool); void setCompareMode(bool); bool zoomToFit() const; bool zoomToFill() const; QPoint position() const; /** * Returns the RasterImageView of the current adapter, if it has one */ RasterImageView* imageView() const; AbstractRasterImageViewTool* currentTool() const; void moveTo(const QRect&); void moveToAnimated(const QRect&); QPropertyAnimation* fadeIn(); void fadeOut(); void fakeFadeOut(); void setGeometry(const QRectF& rect) override; int sortKey() const; void setSortKey(int sortKey); bool isAnimated() const; /** * Sets the opacity on the installed QGraphicsOpacityEffect. * Use this instead of setOpacity(). */ void setGraphicsEffectOpacity(qreal opacity); public Q_SLOTS: void setZoom(qreal); void setZoomToFit(bool); void toggleZoomToFit(); void setZoomToFill(bool); void toggleZoomToFill(); void setPosition(const QPoint&); void hideAndDeleteLater(); Q_SIGNALS: /** * Emitted when the part has finished loading */ void completed(); void previousImageRequested(); void nextImageRequested(); void openUrlRequested(const QUrl&); void openDirUrlRequested(const QUrl&); void captionUpdateRequested(const QString&); void toggleFullScreenRequested(); void videoFinished(); void minimumZoomChanged(qreal); void zoomChanged(qreal); void adapterChanged(); void focused(DocumentView*); void zoomToFitChanged(bool); void zoomToFillChanged(bool); void positionChanged(); void hudTrashClicked(DocumentView*); void hudDeselectClicked(DocumentView*); void fadeInFinished(DocumentView*); void contextMenuRequested(); void currentToolChanged(AbstractRasterImageViewTool*); void isAnimatedChanged(); protected: void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = nullptr) override; void resizeEvent(QGraphicsSceneResizeEvent* event) override; void mousePressEvent(QGraphicsSceneMouseEvent* event) override; void wheelEvent(QGraphicsSceneWheelEvent* event) override; void contextMenuEvent(QGraphicsSceneContextMenuEvent* event) override; bool sceneEventFilter(QGraphicsItem*, QEvent*) override; void dragEnterEvent(QGraphicsSceneDragDropEvent* event) override; void dropEvent(QGraphicsSceneDragDropEvent* event) override; private Q_SLOTS: void finishOpenUrl(); void slotCompleted(); void slotLoadingFailed(); void zoomActualSize(); void zoomIn(QPointF center = QPointF(-1, -1)); void zoomOut(QPointF center = QPointF(-1, -1)); void slotZoomChanged(qreal); void slotBusyChanged(const QUrl&, bool); void emitHudTrashClicked(); void emitHudDeselectClicked(); void emitFocused(); void slotFadeInFinished(); - void dragThumbnailLoaded(const KFileItem&, const QPixmap&); + void dragThumbnailLoaded(const KFileItem&, const QImage &); void dragThumbnailLoadingFailed(const KFileItem&); private: friend struct DocumentViewPrivate; DocumentViewPrivate* const d; void createAdapterForDocument(); }; } // namespace #endif /* DOCUMENTVIEW_H */ diff --git a/lib/thumbnailprovider/thumbnailprovider.cpp b/lib/thumbnailprovider/thumbnailprovider.cpp index 63464ef6..e77aa274 100644 --- a/lib/thumbnailprovider/thumbnailprovider.cpp +++ b/lib/thumbnailprovider/thumbnailprovider.cpp @@ -1,577 +1,575 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview - A simple image viewer for KDE Copyright 2000-2007 Aurélien Gâteau This class is based on the ImagePreviewJob class from Konqueror. */ /* This file is part of the KDE project Copyright (C) 2000 David Faure 2000 Carsten Pfeiffer 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 "thumbnailprovider.h" #include #include #include // Qt #include #include #include -#include #include #include #include #include #include // KDE #include #include #include // Local #include "mimetypeutils.h" #include "thumbnailwriter.h" #include "thumbnailgenerator.h" #include "urlutils.h" namespace Gwenview { #undef ENABLE_LOG #undef LOG //#define ENABLE_LOG #ifdef ENABLE_LOG #define LOG(x) qDebug() << x #else #define LOG(x) ; #endif Q_GLOBAL_STATIC(ThumbnailWriter, sThumbnailWriter) static QString generateOriginalUri(const QUrl &url_) { QUrl url = url_; return url.adjusted(QUrl::RemovePassword).url(); } static QString generateThumbnailPath(const QString& uri, ThumbnailGroup::Enum group) { QString baseDir = ThumbnailProvider::thumbnailBaseDir(group); QCryptographicHash md5(QCryptographicHash::Md5); md5.addData(QFile::encodeName(uri)); return baseDir + QFile::encodeName(QString::fromLatin1(md5.result().toHex())) + QStringLiteral(".png"); } //------------------------------------------------------------------------ // // ThumbnailProvider static methods // //------------------------------------------------------------------------ static QString sThumbnailBaseDir; QString ThumbnailProvider::thumbnailBaseDir() { if (sThumbnailBaseDir.isEmpty()) { const QByteArray customDir = qgetenv("GV_THUMBNAIL_DIR"); if (customDir.isEmpty()) { sThumbnailBaseDir = QStandardPaths::writableLocation(QStandardPaths::GenericCacheLocation) + QStringLiteral("/thumbnails/"); } else { sThumbnailBaseDir = QFile::decodeName(customDir) + QLatin1Char('/'); } } return sThumbnailBaseDir; } void ThumbnailProvider::setThumbnailBaseDir(const QString& dir) { sThumbnailBaseDir = dir; } QString ThumbnailProvider::thumbnailBaseDir(ThumbnailGroup::Enum group) { QString dir = thumbnailBaseDir(); switch (group) { case ThumbnailGroup::Normal: dir += QStringLiteral("normal/"); break; case ThumbnailGroup::Large: dir += QStringLiteral("large/"); break; } return dir; } void ThumbnailProvider::deleteImageThumbnail(const QUrl &url) { QString uri = generateOriginalUri(url); QFile::remove(generateThumbnailPath(uri, ThumbnailGroup::Normal)); QFile::remove(generateThumbnailPath(uri, ThumbnailGroup::Large)); } static void moveThumbnailHelper(const QString& oldUri, const QString& newUri, ThumbnailGroup::Enum group) { QString oldPath = generateThumbnailPath(oldUri, group); QString newPath = generateThumbnailPath(newUri, group); QImage thumb; if (!thumb.load(oldPath)) { return; } thumb.setText(QStringLiteral("Thumb::URI"), newUri); thumb.save(newPath, "png"); QFile::remove(QFile::encodeName(oldPath)); } void ThumbnailProvider::moveThumbnail(const QUrl &oldUrl, const QUrl& newUrl) { QString oldUri = generateOriginalUri(oldUrl); QString newUri = generateOriginalUri(newUrl); moveThumbnailHelper(oldUri, newUri, ThumbnailGroup::Normal); moveThumbnailHelper(oldUri, newUri, ThumbnailGroup::Large); } //------------------------------------------------------------------------ // // ThumbnailProvider implementation // //------------------------------------------------------------------------ ThumbnailProvider::ThumbnailProvider() : KIO::Job() , mState(STATE_NEXTTHUMB) , mOriginalTime(0) { LOG(this); // Make sure we have a place to store our thumbnails QString thumbnailDirNormal = ThumbnailProvider::thumbnailBaseDir(ThumbnailGroup::Normal); QString thumbnailDirLarge = ThumbnailProvider::thumbnailBaseDir(ThumbnailGroup::Large); QDir().mkpath(thumbnailDirNormal); QDir().mkpath(thumbnailDirLarge); QFile::setPermissions(thumbnailDirNormal, QFileDevice::WriteOwner | QFileDevice::ReadOwner | QFileDevice::ExeOwner); QFile::setPermissions(thumbnailDirLarge, QFileDevice::WriteOwner | QFileDevice::ReadOwner | QFileDevice::ExeOwner); // Look for images and store the items in our todo list mCurrentItem = KFileItem(); mThumbnailGroup = ThumbnailGroup::Large; createNewThumbnailGenerator(); } ThumbnailProvider::~ThumbnailProvider() { LOG(this); abortSubjob(); mThumbnailGenerator->cancel(); disconnect(mThumbnailGenerator, nullptr, this, nullptr); disconnect(mThumbnailGenerator, nullptr, sThumbnailWriter, nullptr); connect(mThumbnailGenerator, SIGNAL(finished()), mThumbnailGenerator, SLOT(deleteLater())); if (mPreviousThumbnailGenerator) { disconnect(mPreviousThumbnailGenerator, nullptr, sThumbnailWriter, nullptr); } sThumbnailWriter->wait(); } void ThumbnailProvider::stop() { // Clear mItems and create a new ThumbnailGenerator if mThumbnailGenerator is running, // but also make sure that at most two ThumbnailGenerators are running. // startCreatingThumbnail() will take care that these two threads won't work on the same item. mItems.clear(); abortSubjob(); if (mThumbnailGenerator->isRunning() && !mPreviousThumbnailGenerator) { mPreviousThumbnailGenerator = mThumbnailGenerator; mPreviousThumbnailGenerator->cancel(); disconnect(mPreviousThumbnailGenerator, nullptr, this, nullptr); connect(mPreviousThumbnailGenerator, SIGNAL(finished()), mPreviousThumbnailGenerator, SLOT(deleteLater())); createNewThumbnailGenerator(); mCurrentItem = KFileItem(); } } const KFileItemList& ThumbnailProvider::pendingItems() const { return mItems; } void ThumbnailProvider::setThumbnailGroup(ThumbnailGroup::Enum group) { mThumbnailGroup = group; } void ThumbnailProvider::appendItems(const KFileItemList& items) { if (!mItems.isEmpty()) { QSet itemSet; Q_FOREACH(const KFileItem & item, mItems) { itemSet.insert(item.url().url()); } Q_FOREACH(const KFileItem & item, items) { if (!itemSet.contains(item.url().url())) { mItems.append(item); } } } else { mItems = items; } if (mCurrentItem.isNull()) { determineNextIcon(); } } void ThumbnailProvider::removeItems(const KFileItemList& itemList) { if (mItems.isEmpty()) { return; } Q_FOREACH(const KFileItem & item, itemList) { // If we are removing the next item, update to be the item after or the // first if we removed the last item mItems.removeAll(item); if (item == mCurrentItem) { abortSubjob(); } } // No more current item, carry on to the next remaining item if (mCurrentItem.isNull()) { determineNextIcon(); } } void ThumbnailProvider::removePendingItems() { mItems.clear(); } bool ThumbnailProvider::isRunning() const { return !mCurrentItem.isNull(); } //-Internal-------------------------------------------------------------- void ThumbnailProvider::createNewThumbnailGenerator() { mThumbnailGenerator = new ThumbnailGenerator; connect(mThumbnailGenerator, SIGNAL(done(QImage,QSize)), SLOT(thumbnailReady(QImage,QSize)), Qt::QueuedConnection); connect(mThumbnailGenerator, SIGNAL(thumbnailReadyToBeCached(QString,QImage)), sThumbnailWriter, SLOT(queueThumbnail(QString,QImage)), Qt::QueuedConnection); } void ThumbnailProvider::abortSubjob() { if (hasSubjobs()) { LOG("Killing subjob"); KJob* job = subjobs().first(); job->kill(); removeSubjob(job); mCurrentItem = KFileItem(); } } void ThumbnailProvider::determineNextIcon() { LOG(this); mState = STATE_NEXTTHUMB; // No more items ? if (mItems.isEmpty()) { LOG("No more items. Nothing to do"); mCurrentItem = KFileItem(); emit finished(); return; } mCurrentItem = mItems.takeFirst(); LOG("mCurrentItem.url=" << mCurrentItem.url()); // First, stat the orig file mState = STATE_STATORIG; mCurrentUrl = mCurrentItem.url().adjusted(QUrl::NormalizePathSegments); mOriginalFileSize = mCurrentItem.size(); // Do direct stat instead of using KIO if the file is local (faster) if (UrlUtils::urlIsFastLocalFile(mCurrentUrl)) { QFileInfo fileInfo(mCurrentUrl.toLocalFile()); mOriginalTime = fileInfo.lastModified().toTime_t(); QMetaObject::invokeMethod(this, "checkThumbnail", Qt::QueuedConnection); } else { KIO::Job* job = KIO::stat(mCurrentUrl, KIO::HideProgressInfo); KJobWidgets::setWindow(job, qApp->activeWindow()); LOG("KIO::stat orig" << mCurrentUrl.url()); addSubjob(job); } LOG("/determineNextIcon" << this); } void ThumbnailProvider::slotResult(KJob * job) { LOG(mState); removeSubjob(job); Q_ASSERT(subjobs().isEmpty()); // We should have only one job at a time switch (mState) { case STATE_NEXTTHUMB: Q_ASSERT(false); determineNextIcon(); return; case STATE_STATORIG: { // Could not stat original, drop this one and move on to the next one if (job->error()) { emitThumbnailLoadingFailed(); determineNextIcon(); return; } // Get modification time of the original file KIO::UDSEntry entry = static_cast(job)->statResult(); mOriginalTime = entry.numberValue(KIO::UDSEntry::UDS_MODIFICATION_TIME, -1); checkThumbnail(); return; } case STATE_DOWNLOADORIG: if (job->error()) { emitThumbnailLoadingFailed(); LOG("Delete temp file" << mTempPath); QFile::remove(mTempPath); mTempPath.clear(); determineNextIcon(); } else { startCreatingThumbnail(mTempPath); } return; case STATE_PREVIEWJOB: determineNextIcon(); return; } } void ThumbnailProvider::thumbnailReady(const QImage& _img, const QSize& _size) { QImage img = _img; QSize size = _size; if (!img.isNull()) { emitThumbnailLoaded(img, size); } else { emitThumbnailLoadingFailed(); } if (!mTempPath.isEmpty()) { LOG("Delete temp file" << mTempPath); QFile::remove(mTempPath); mTempPath.clear(); } determineNextIcon(); } QImage ThumbnailProvider::loadThumbnailFromCache() const { QImage image = sThumbnailWriter->value(mThumbnailPath); if (!image.isNull()) { return image; } image = QImage(mThumbnailPath); if (image.isNull() && mThumbnailGroup == ThumbnailGroup::Normal) { // If there is a large-sized thumbnail, generate the normal-sized version from it QString largeThumbnailPath = generateThumbnailPath(mOriginalUri, ThumbnailGroup::Large); QImage largeImage(largeThumbnailPath); if (largeImage.isNull()) { return image; } int size = ThumbnailGroup::pixelSize(ThumbnailGroup::Normal); image = largeImage.scaled(size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation); Q_FOREACH(const QString& key, largeImage.textKeys()) { QString text = largeImage.text(key); image.setText(key, text); } sThumbnailWriter->queueThumbnail(mThumbnailPath, image); } return image; } void ThumbnailProvider::checkThumbnail() { if (mCurrentItem.isNull()) { // This can happen if current item has been removed by removeItems() determineNextIcon(); return; } // If we are in the thumbnail dir, just load the file if (mCurrentUrl.isLocalFile() && mCurrentUrl.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path().startsWith(thumbnailBaseDir())) { QImage image(mCurrentUrl.toLocalFile()); emitThumbnailLoaded(image, image.size()); determineNextIcon(); return; } mOriginalUri = generateOriginalUri(mCurrentUrl); mThumbnailPath = generateThumbnailPath(mOriginalUri, mThumbnailGroup); LOG("Stat thumb" << mThumbnailPath); QImage thumb = loadThumbnailFromCache(); KIO::filesize_t fileSize = thumb.text(QStringLiteral("Thumb::Size")).toULongLong(); const QString hashString = thumb.text(QStringLiteral("Thumb::X-KDE-VisualHash")); const bool isNormalImage = MimeTypeUtils::fileItemKind(mCurrentItem) == MimeTypeUtils::KIND_RASTER_IMAGE; if (isNormalImage && hashString.isEmpty()) { thumb = QImage(); QFile::remove(mThumbnailPath); } if (!thumb.isNull()) { if (thumb.text(QStringLiteral("Thumb::URI")) == mOriginalUri && thumb.text(QStringLiteral("Thumb::MTime")).toInt() == mOriginalTime && (fileSize == 0 || fileSize == mOriginalFileSize) && !thumb.text(QStringLiteral("Thumb::X-KDE-VisualHash")).isEmpty()) { int width = 0, height = 0; QSize size; bool ok; width = thumb.text(QStringLiteral("Thumb::Image::Width")).toInt(&ok); if (ok) height = thumb.text(QStringLiteral("Thumb::Image::Height")).toInt(&ok); if (ok) { size = QSize(width, height); } else { LOG("Thumbnail for" << mOriginalUri << "does not contain correct image size information"); // Don't try to determine the size of a video, it probably won't work and // will cause high I/O usage with big files (bug #307007). if (MimeTypeUtils::urlKind(mCurrentUrl) == MimeTypeUtils::KIND_VIDEO) { emitThumbnailLoaded(thumb, QSize()); determineNextIcon(); return; } } emitThumbnailLoaded(thumb, size); determineNextIcon(); return; } } // Thumbnail not found or not valid if (isNormalImage) { if (mCurrentUrl.isLocalFile()) { // Original is a local file, create the thumbnail startCreatingThumbnail(mCurrentUrl.toLocalFile()); } else { // Original is remote, download it mState = STATE_DOWNLOADORIG; QTemporaryFile tempFile; tempFile.setAutoRemove(false); if (!tempFile.open()) { qWarning() << "Couldn't create temp file to download " << mCurrentUrl.toDisplayString(); emitThumbnailLoadingFailed(); determineNextIcon(); return; } mTempPath = tempFile.fileName(); QUrl url = QUrl::fromLocalFile(mTempPath); KIO::Job* job = KIO::file_copy(mCurrentUrl, url, -1, KIO::Overwrite | KIO::HideProgressInfo); KJobWidgets::setWindow(job, qApp->activeWindow()); LOG("Download remote file" << mCurrentUrl.toDisplayString() << "to" << url.toDisplayString()); addSubjob(job); } } else { // Not a raster image, use a KPreviewJob LOG("Starting a KPreviewJob for" << mCurrentItem.url()); mState = STATE_PREVIEWJOB; KFileItemList list; list.append(mCurrentItem); const int pixelSize = ThumbnailGroup::pixelSize(mThumbnailGroup); if (mPreviewPlugins.isEmpty()) { mPreviewPlugins = KIO::PreviewJob::availablePlugins(); } KIO::Job* job = KIO::filePreview(list, QSize(pixelSize, pixelSize), &mPreviewPlugins); //KJobWidgets::setWindow(job, qApp->activeWindow()); connect(job, SIGNAL(gotPreview(KFileItem,QPixmap)), this, SLOT(slotGotPreview(KFileItem,QPixmap))); connect(job, SIGNAL(failed(KFileItem)), this, SLOT(emitThumbnailLoadingFailed())); addSubjob(job); } } void ThumbnailProvider::startCreatingThumbnail(const QString& pixPath) { LOG("Creating thumbnail from" << pixPath); // If mPreviousThumbnailGenerator is already working on our current item // its thumbnail will be passed to sThumbnailWriter when ready. So we // connect mPreviousThumbnailGenerator's signal "finished" to determineNextIcon // which will load the thumbnail from sThumbnailWriter or from disk // (because we re-add mCurrentItem to mItems). if (mPreviousThumbnailGenerator && mPreviousThumbnailGenerator->isRunning() && mOriginalUri == mPreviousThumbnailGenerator->originalUri() && mOriginalTime == mPreviousThumbnailGenerator->originalTime() && mOriginalFileSize == mPreviousThumbnailGenerator->originalFileSize() && mCurrentItem.mimetype() == mPreviousThumbnailGenerator->originalMimeType()) { connect(mPreviousThumbnailGenerator, SIGNAL(finished()), SLOT(determineNextIcon())); mItems.prepend(mCurrentItem); return; } mThumbnailGenerator->load(mOriginalUri, mOriginalTime, mOriginalFileSize, mCurrentItem.mimetype(), pixPath, mThumbnailPath, mThumbnailGroup); } -void ThumbnailProvider::slotGotPreview(const KFileItem& item, const QPixmap& pixmap) +void ThumbnailProvider::slotGotPreview(const KFileItem& item, const QPixmap &pixmap) { if (mCurrentItem.isNull()) { // This can happen if current item has been removed by removeItems() return; } LOG(mCurrentItem.url()); QSize size; - emit thumbnailLoaded(item, pixmap, size, mOriginalFileSize); + emit thumbnailLoaded(item, pixmap.toImage(), size, mOriginalFileSize); } void ThumbnailProvider::emitThumbnailLoaded(const QImage& img, const QSize& size) { if (mCurrentItem.isNull()) { // This can happen if current item has been removed by removeItems() return; } LOG(mCurrentItem.url()); - QPixmap thumb = QPixmap::fromImage(img); - emit thumbnailLoaded(mCurrentItem, thumb, size, mOriginalFileSize); + emit thumbnailLoaded(mCurrentItem, img, size, mOriginalFileSize); } void ThumbnailProvider::emitThumbnailLoadingFailed() { if (mCurrentItem.isNull()) { // This can happen if current item has been removed by removeItems() return; } LOG(mCurrentItem.url()); emit thumbnailLoadingFailed(mCurrentItem); } bool ThumbnailProvider::isThumbnailWriterEmpty() { return sThumbnailWriter->isEmpty(); } } // namespace diff --git a/lib/thumbnailprovider/thumbnailprovider.h b/lib/thumbnailprovider/thumbnailprovider.h index fedd1c3e..b3a59c2a 100644 --- a/lib/thumbnailprovider/thumbnailprovider.h +++ b/lib/thumbnailprovider/thumbnailprovider.h @@ -1,182 +1,181 @@ // vim: set tabstop=4 shiftwidth=4 expandtab /* Gwenview - A simple image viewer for KDE Copyright 2000-2004 Aurélien Gâteau This class is based on the ImagePreviewJob class from Konqueror. */ /* This file is part of the KDE project Copyright (C) 2000 David Faure 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. */ #ifndef THUMBNAILPROVIDER_H #define THUMBNAILPROVIDER_H #include // Qt #include -#include #include // KDE #include #include // Local #include namespace Gwenview { class ThumbnailGenerator; class ThumbnailWriter; /** * A job that determines the thumbnails for the images in the current directory */ class GWENVIEWLIB_EXPORT ThumbnailProvider : public KIO::Job { Q_OBJECT public: ThumbnailProvider(); ~ThumbnailProvider() override; void stop(); /** * To be called whenever items are removed from the view */ void removeItems(const KFileItemList& itemList); /** * Remove all pending items */ void removePendingItems(); /** * Returns the list of items waiting for a thumbnail */ const KFileItemList& pendingItems() const; /** * Add items to the job */ void appendItems(const KFileItemList& items); /** * Defines size of thumbnails to generate */ void setThumbnailGroup(ThumbnailGroup::Enum); bool isRunning() const; /** * Returns the thumbnail base dir, independent of the thumbnail size */ static QString thumbnailBaseDir(); /** * Sets the thumbnail base dir, useful for unit-testing */ static void setThumbnailBaseDir(const QString&); /** * Returns the thumbnail base dir, for the @p group */ static QString thumbnailBaseDir(ThumbnailGroup::Enum group); /** * Delete the thumbnail for the @p url */ static void deleteImageThumbnail(const QUrl &url); /** * Move a thumbnail to match a file move */ static void moveThumbnail(const QUrl &oldUrl, const QUrl& newUrl); /** * Returns true if all thumbnails have been written to disk. Useful for * unit-testing. */ static bool isThumbnailWriterEmpty(); Q_SIGNALS: /** * Emitted when the thumbnail for the @p item has been loaded */ - void thumbnailLoaded(const KFileItem& item, const QPixmap&, const QSize&, qulonglong); + void thumbnailLoaded(const KFileItem& item, const QImage&, const QSize&, qulonglong); void thumbnailLoadingFailed(const KFileItem& item); /** * Queue is empty */ void finished(); protected: void slotResult(KJob *job) override; private Q_SLOTS: void determineNextIcon(); - void slotGotPreview(const KFileItem&, const QPixmap&); + void slotGotPreview(const KFileItem&, const QPixmap &); void checkThumbnail(); void thumbnailReady(const QImage&, const QSize&); void emitThumbnailLoadingFailed(); private: enum { STATE_STATORIG, STATE_DOWNLOADORIG, STATE_PREVIEWJOB, STATE_NEXTTHUMB } mState; KFileItemList mItems; KFileItem mCurrentItem; // The Url of the current item (always equivalent to m_items.first()->item()->url()) QUrl mCurrentUrl; // The Uri of the original image (might be different from mCurrentUrl.url()) QString mOriginalUri; // The modification time of the original image time_t mOriginalTime; // The file size of the original image KIO::filesize_t mOriginalFileSize; // The thumbnail path QString mThumbnailPath; // The temporary path for remote urls QString mTempPath; // Thumbnail group ThumbnailGroup::Enum mThumbnailGroup; ThumbnailGenerator* mThumbnailGenerator; QPointer mPreviousThumbnailGenerator; QStringList mPreviewPlugins; void createNewThumbnailGenerator(); void abortSubjob(); void startCreatingThumbnail(const QString& path); void emitThumbnailLoaded(const QImage& img, const QSize& size); QImage loadThumbnailFromCache() const; }; } // namespace #endif /* THUMBNAILPROVIDER_H */ diff --git a/lib/thumbnailview/thumbnailview.cpp b/lib/thumbnailview/thumbnailview.cpp index 76bf1d70..7dcba3c1 100644 --- a/lib/thumbnailview/thumbnailview.cpp +++ b/lib/thumbnailview/thumbnailview.cpp @@ -1,979 +1,984 @@ /* 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 // 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 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; /// Visual hash QBitArray mHash; }; 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; 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()); QImage 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); } ThumbnailView::~ThumbnailView() { 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(), SIGNAL(rowsRemoved(QModelIndex,int,int)), SIGNAL(rowsRemovedSignal(QModelIndex,int,int))); } void ThumbnailView::setThumbnailProvider(ThumbnailProvider* thumbnailProvider) { GV_RETURN_IF_FAIL(d->mThumbnailProvider != thumbnailProvider); if (thumbnailProvider) { - connect(thumbnailProvider, SIGNAL(thumbnailLoaded(KFileItem,QPixmap,QSize,qulonglong)), - SLOT(setThumbnail(KFileItem,QPixmap,QSize,qulonglong))); + connect(thumbnailProvider, SIGNAL(thumbnailLoaded(KFileItem,QImage,QSize,qulonglong)), + SLOT(setThumbnail(KFileItem,QImage,QSize,qulonglong))); connect(thumbnailProvider, SIGNAL(thumbnailLoadingFailed(KFileItem)), SLOT(setBrokenThumbnail(KFileItem))); } else { disconnect(d->mThumbnailProvider, nullptr , this, nullptr); } d->mThumbnailProvider = thumbnailProvider; } void ThumbnailView::updateThumbnailSize() { QSize value = d->mThumbnailSize; // mWaitingThumbnail int waitingThumbnailSize; if (value.width() > 64) { waitingThumbnailSize = 48; } else { waitingThumbnailSize = 32; } 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; // 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()); if (d->mScaleMode != ScaleToFit) { scheduleDelayedItemsLayout(); } d->scheduleThumbnailGeneration(); } void ThumbnailView::setThumbnailWidth(int width) { if(d->mThumbnailSize.width() == width) { return; } int height = round((qreal)width / d->mThumbnailAspectRatio); d->mThumbnailSize = QSize(width, 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; } 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 QImage& 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 = QPixmap::fromImage(pixmap); thumbnail.mAdjustedPix = QPixmap(); int largeGroupSize = ThumbnailGroup::pixelSize(ThumbnailGroup::Large); thumbnail.mFullSize = size.isValid() ? size : QSize(largeGroupSize, largeGroupSize); thumbnail.mRealFullSize = size; thumbnail.mWaitingForThumbnail = false; thumbnail.mFileSize = fileSize; - const QByteArray encodedHash = thumbnail.text(QStringLiteral("Thumb::X-KDE-VisualHash")).tolat; - thumbnail.mHash; + const QByteArray visualHash = QByteArray::fromHex(pixmap.text(QStringLiteral("Thumb::X-KDE-VisualHash")).toLatin1()); + if (!visualHash.isEmpty()) { + thumbnail.mHash = QBitArray::fromBits(visualHash.constData(), visualHash.size() * 8); + } else { + qWarning() << "Missing hash"; + } + qDebug() << thumbnail.mHash; 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; } 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::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, SLOT(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; width = qMax(int(MinThumbnailSize), qMin(width, int(MaxThumbnailSize))); setThumbnailWidth(width); } else { QListView::wheelEvent(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 diff --git a/lib/thumbnailview/thumbnailview.h b/lib/thumbnailview/thumbnailview.h index 34de32d3..2fac511f 100644 --- a/lib/thumbnailview/thumbnailview.h +++ b/lib/thumbnailview/thumbnailview.h @@ -1,216 +1,216 @@ /* 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. */ #ifndef THUMBNAILVIEW_H #define THUMBNAILVIEW_H #include // Qt #include // KDE #include class KFileItem; class QDragEnterEvent; class QDragMoveEvent; class QDropEvent; class QPixmap; namespace Gwenview { class AbstractDocumentInfoProvider; class AbstractThumbnailViewHelper; class ThumbnailProvider; struct ThumbnailViewPrivate; class GWENVIEWLIB_EXPORT ThumbnailView : public QListView { Q_OBJECT public: enum { MinThumbnailSize = 48, MaxThumbnailSize = 256 }; enum ThumbnailScaleMode { ScaleToSquare, ScaleToHeight, ScaleToWidth, ScaleToFit }; explicit ThumbnailView(QWidget* parent); ~ThumbnailView() override; void setThumbnailViewHelper(AbstractThumbnailViewHelper* helper); AbstractThumbnailViewHelper* thumbnailViewHelper() const; void setDocumentInfoProvider(AbstractDocumentInfoProvider* provider); AbstractDocumentInfoProvider* documentInfoProvider() const; ThumbnailScaleMode thumbnailScaleMode() const; void setThumbnailScaleMode(ThumbnailScaleMode); /** * Returns the thumbnail size. */ QSize thumbnailSize() const; /** * Returns the aspect ratio of the thumbnail. */ qreal thumbnailAspectRatio() const; QPixmap thumbnailForIndex(const QModelIndex&, QSize* fullSize = nullptr); /** * Returns true if the document pointed by the index has been modified * inside Gwenview. */ bool isModified(const QModelIndex&) const; /** * Returns true if the document pointed by the index is currently busy * (loading, saving, rotating...) */ bool isBusy(const QModelIndex& index) const; void setModel(QAbstractItemModel* model) override; void setThumbnailProvider(ThumbnailProvider* thumbnailProvider); /** * Publish this method so that delegates can call it. */ using QListView::scheduleDelayedItemsLayout; /** * Returns the current pixmap to paint when drawing a busy index. */ QPixmap busySequenceCurrentPixmap() const; void reloadThumbnail(const QModelIndex&); void updateThumbnailSize(); void setCreateThumbnailsForRemoteUrls(bool createRemoteThumbs); Q_SIGNALS: /** * It seems we can't use the 'activated()' signal for now because it does * not know about KDE single vs doubleclick settings. The indexActivated() * signal replaces it. */ void indexActivated(const QModelIndex&); void urlListDropped(const QList& lst, const QUrl &destination); void thumbnailSizeChanged(const QSize&); void thumbnailWidthChanged(int); /** * Emitted whenever selectionChanged() is called. * This signal is suffixed with "Signal" because * QAbstractItemView::selectionChanged() is a slot. */ void selectionChangedSignal(const QItemSelection&, const QItemSelection&); /** * Forward some signals from model, so that the delegate can use them */ void rowsRemovedSignal(const QModelIndex& parent, int start, int end); void rowsInsertedSignal(const QModelIndex& parent, int start, int end); public Q_SLOTS: /** * Sets the thumbnail's width, in pixels. Keeps aspect ratio unchanged. */ void setThumbnailWidth(int width); /** * Sets the thumbnail's aspect ratio. Keeps width unchanged. */ void setThumbnailAspectRatio(qreal ratio); void scrollToSelectedIndex(); void generateThumbnailsForItems(); protected: void dragEnterEvent(QDragEnterEvent*) override; void dragMoveEvent(QDragMoveEvent*) override; void dropEvent(QDropEvent*) override; void keyPressEvent(QKeyEvent*) override; void resizeEvent(QResizeEvent*) override; void scrollContentsBy(int dx, int dy) override; void showEvent(QShowEvent*) override; void wheelEvent(QWheelEvent*) override; void startDrag(Qt::DropActions) override; protected Q_SLOTS: void rowsAboutToBeRemoved(const QModelIndex& parent, int start, int end) override; void rowsInserted(const QModelIndex& parent, int start, int end) override; void selectionChanged(const QItemSelection& selected, const QItemSelection& deselected) override; void dataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QVector &roles = QVector()) override; private Q_SLOTS: void showContextMenu(); void emitIndexActivatedIfNoModifiers(const QModelIndex&); - void setThumbnail(const KFileItem&, const QPixmap&, const QSize&, qulonglong fileSize); + void setThumbnail(const KFileItem&, const QImage&, const QSize&, qulonglong fileSize); void setBrokenThumbnail(const KFileItem&); /** * Generate thumbnail for url. */ void updateThumbnail(const QUrl& url); /** * Tells the view the busy state of the document pointed by the url has changed. */ void updateThumbnailBusyState(const QUrl& url, bool); /* * Cause a repaint of all busy indexes */ void updateBusyIndexes(); void smoothNextThumbnail(); private: friend struct ThumbnailViewPrivate; ThumbnailViewPrivate * const d; }; } // namespace #endif /* THUMBNAILVIEW_H */