File indexing completed on 2024-05-12 04:19:39

0001 // vim: set tabstop=4 shiftwidth=4 expandtab:
0002 /*
0003 Gwenview: an image viewer
0004 Copyright 2008 Aurélien Gâteau <agateau@kde.org>
0005 
0006 This program is free software; you can redistribute it and/or
0007 modify it under the terms of the GNU General Public License
0008 as published by the Free Software Foundation; either version 2
0009 of the License, or (at your option) any later version.
0010 
0011 This program is distributed in the hope that it will be useful,
0012 but WITHOUT ANY WARRANTY; without even the implied warranty of
0013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0014 GNU General Public License for more details.
0015 
0016 You should have received a copy of the GNU General Public License
0017 along with this program; if not, write to the Free Software
0018 Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.
0019 
0020 */
0021 // Self
0022 #include "documentview.h"
0023 
0024 // C++ Standard library
0025 #include <cmath>
0026 
0027 // Qt
0028 #include <QApplication>
0029 #include <QDrag>
0030 #include <QGestureEvent>
0031 #include <QGraphicsLinearLayout>
0032 #include <QGraphicsOpacityEffect>
0033 #include <QGraphicsProxyWidget>
0034 #include <QGraphicsScene>
0035 #include <QGraphicsSceneMouseEvent>
0036 #include <QGraphicsSceneWheelEvent>
0037 #include <QGraphicsView>
0038 #include <QIcon>
0039 #include <QLibraryInfo>
0040 #include <QMimeData>
0041 #include <QPainter>
0042 #include <QPointer>
0043 #include <QPropertyAnimation>
0044 #include <QStyleHints>
0045 #include <QTimer>
0046 #include <QUrl>
0047 
0048 // KF
0049 #include <KFileItem>
0050 #include <KLocalizedString>
0051 #include <KUrlMimeData>
0052 
0053 // Local
0054 #include "gwenview_lib_debug.h"
0055 #include <lib/document/documentfactory.h>
0056 #include <lib/documentview/abstractrasterimageviewtool.h>
0057 #include <lib/documentview/birdeyeview.h>
0058 #include <lib/documentview/loadingindicator.h>
0059 #include <lib/documentview/messageviewadapter.h>
0060 #include <lib/documentview/rasterimageview.h>
0061 #include <lib/documentview/rasterimageviewadapter.h>
0062 #include <lib/documentview/svgviewadapter.h>
0063 #include <lib/documentview/videoviewadapter.h>
0064 #include <lib/graphicswidgetfloater.h>
0065 #include <lib/gvdebug.h>
0066 #include <lib/gwenviewconfig.h>
0067 #include <lib/hud/hudbutton.h>
0068 #include <lib/hud/hudwidget.h>
0069 #include <lib/mimetypeutils.h>
0070 #include <lib/thumbnailprovider/thumbnailprovider.h>
0071 #include <lib/thumbnailview/dragpixmapgenerator.h>
0072 #include <lib/touch/touch.h>
0073 #ifndef GWENVIEW_NO_WAYLAND_GESTURES
0074 #include <lib/waylandgestures/waylandgestures.h>
0075 #endif
0076 #include <lib/urlutils.h>
0077 #include <transformimageoperation.h>
0078 
0079 namespace Gwenview
0080 {
0081 #undef ENABLE_LOG
0082 #undef LOG
0083 // #define ENABLE_LOG
0084 #ifdef ENABLE_LOG
0085 #define LOG(x) // qCDebug(GWENVIEW_LIB_LOG) << x
0086 #else
0087 #define LOG(x) ;
0088 #endif
0089 
0090 static const qreal REAL_DELTA = 0.001;
0091 static const qreal MAXIMUM_ZOOM_VALUE = qreal(DocumentView::MaximumZoom);
0092 static const auto MINSTEP = sqrt(0.5);
0093 static const auto MAXSTEP = sqrt(2.0);
0094 
0095 static const int COMPARE_MARGIN = 4;
0096 
0097 const int DocumentView::MaximumZoom = 16;
0098 const int DocumentView::AnimDuration = 250;
0099 
0100 struct DocumentViewPrivate {
0101     DocumentView *q = nullptr;
0102     int mSortKey; // Used to sort views when displayed in compare mode
0103     HudWidget *mHud = nullptr;
0104     BirdEyeView *mBirdEyeView = nullptr;
0105     QPointer<QPropertyAnimation> mMoveAnimation;
0106     QPointer<QPropertyAnimation> mFadeAnimation;
0107     QGraphicsOpacityEffect *mOpacityEffect = nullptr;
0108 
0109     LoadingIndicator *mLoadingIndicator = nullptr;
0110     /** Delays showing the loading indicator. This is to avoid that we show a few annoying frames of
0111      * a loading indicator even though the loading might be pretty much instantaneous. */
0112     QTimer *mLoadingIndicatorDelay = nullptr;
0113 
0114     QScopedPointer<AbstractDocumentViewAdapter> mAdapter;
0115     QList<qreal> mZoomSnapValues;
0116     Document::Ptr mDocument;
0117     DocumentView::Setup mSetup;
0118     bool mCurrent;
0119     bool mCompareMode;
0120     int controlWheelAccumulatedDelta;
0121 
0122     QPointF mDragStartPosition;
0123     QPointer<ThumbnailProvider> mDragThumbnailProvider;
0124     QPointer<QDrag> mDrag;
0125 
0126     Touch *mTouch = nullptr;
0127 #ifndef GWENVIEW_NO_WAYLAND_GESTURES
0128     WaylandGestures *mWaylandGestures = nullptr;
0129 #endif
0130     int mMinTimeBetweenPinch;
0131 
0132     void setCurrentAdapter(AbstractDocumentViewAdapter *adapter)
0133     {
0134         Q_ASSERT(adapter);
0135         mAdapter.reset(adapter);
0136 
0137         adapter->widget()->setParentItem(q);
0138         resizeAdapterWidget();
0139 
0140         if (adapter->canZoom()) {
0141             QObject::connect(adapter, &AbstractDocumentViewAdapter::zoomChanged, q, &DocumentView::slotZoomChanged);
0142             QObject::connect(adapter, &AbstractDocumentViewAdapter::zoomInRequested, q, &DocumentView::zoomIn);
0143             QObject::connect(adapter, &AbstractDocumentViewAdapter::zoomOutRequested, q, &DocumentView::zoomOut);
0144             QObject::connect(adapter, &AbstractDocumentViewAdapter::zoomToFitChanged, q, &DocumentView::zoomToFitChanged);
0145             QObject::connect(adapter, &AbstractDocumentViewAdapter::zoomToFillChanged, q, &DocumentView::zoomToFillChanged);
0146         }
0147         QObject::connect(adapter, &AbstractDocumentViewAdapter::scrollPosChanged, q, &DocumentView::positionChanged);
0148         QObject::connect(adapter, &AbstractDocumentViewAdapter::previousImageRequested, q, &DocumentView::previousImageRequested);
0149         QObject::connect(adapter, &AbstractDocumentViewAdapter::nextImageRequested, q, &DocumentView::nextImageRequested);
0150         QObject::connect(adapter, &AbstractDocumentViewAdapter::toggleFullScreenRequested, q, &DocumentView::toggleFullScreenRequested);
0151         QObject::connect(adapter, &AbstractDocumentViewAdapter::completed, q, &DocumentView::slotCompleted);
0152 
0153         adapter->loadConfig();
0154 
0155         adapter->widget()->installSceneEventFilter(q);
0156         if (mCurrent) {
0157             adapter->widget()->setFocus();
0158         }
0159 
0160         if (mSetup.valid && adapter->canZoom()) {
0161             adapter->setZoomToFit(mSetup.zoomToFit);
0162             adapter->setZoomToFill(mSetup.zoomToFill);
0163             if (!mSetup.zoomToFit && !mSetup.zoomToFill) {
0164                 adapter->setZoom(mSetup.zoom);
0165                 adapter->setScrollPos(mSetup.position);
0166             }
0167         }
0168         Q_EMIT q->adapterChanged();
0169         Q_EMIT q->positionChanged();
0170         if (adapter->canZoom()) {
0171             if (adapter->zoomToFit()) {
0172                 Q_EMIT q->zoomToFitChanged(true);
0173             } else if (adapter->zoomToFill()) {
0174                 Q_EMIT q->zoomToFillChanged(true);
0175             } else {
0176                 Q_EMIT q->zoomChanged(adapter->zoom());
0177             }
0178         }
0179         if (adapter->rasterImageView()) {
0180             QObject::connect(adapter->rasterImageView(), &RasterImageView::currentToolChanged, q, &DocumentView::currentToolChanged);
0181         }
0182     }
0183 
0184     void setupLoadingIndicator()
0185     {
0186         mLoadingIndicator = new LoadingIndicator(q);
0187         auto floater = new GraphicsWidgetFloater(q);
0188         floater->setChildWidget(mLoadingIndicator);
0189         mLoadingIndicator->setZValue(1);
0190         mLoadingIndicator->hide();
0191 
0192         mLoadingIndicatorDelay = new QTimer(q);
0193         mLoadingIndicatorDelay->setSingleShot(true);
0194         QObject::connect(mLoadingIndicatorDelay, &QTimer::timeout, mLoadingIndicator, [this]() {
0195             mLoadingIndicator->show();
0196             Q_EMIT q->indicateLoadingToUser();
0197         });
0198     }
0199 
0200     HudButton *createHudButton(const QString &text, const QString &iconName, bool showText)
0201     {
0202         auto button = new HudButton;
0203         if (showText) {
0204             button->setText(text);
0205         } else {
0206             button->setToolTip(text);
0207         }
0208         button->setIcon(QIcon::fromTheme(iconName));
0209         return button;
0210     }
0211 
0212     void setupHud()
0213     {
0214         HudButton *trashButton = createHudButton(i18nc("@info:tooltip", "Trash"), QStringLiteral("user-trash"), false);
0215         HudButton *deselectButton = createHudButton(i18nc("@action:button", "Deselect"), QStringLiteral("list-remove"), true);
0216 
0217         auto content = new QGraphicsWidget;
0218         auto layout = new QGraphicsLinearLayout(content);
0219         layout->addItem(trashButton);
0220         layout->addItem(deselectButton);
0221 
0222         mHud = new HudWidget(q);
0223         mHud->init(content, HudWidget::OptionNone);
0224         auto floater = new GraphicsWidgetFloater(q);
0225         floater->setChildWidget(mHud);
0226         floater->setAlignment(Qt::AlignBottom | Qt::AlignHCenter);
0227 
0228         QObject::connect(trashButton, &HudButton::clicked, q, &DocumentView::emitHudTrashClicked);
0229         QObject::connect(deselectButton, &HudButton::clicked, q, &DocumentView::emitHudDeselectClicked);
0230 
0231         mHud->hide();
0232     }
0233 
0234     void setupBirdEyeView()
0235     {
0236         if (mBirdEyeView) {
0237             delete mBirdEyeView;
0238         }
0239         mBirdEyeView = new BirdEyeView(q);
0240         mBirdEyeView->setZValue(1);
0241     }
0242 
0243     void updateCaption()
0244     {
0245         if (!mCurrent) {
0246             return;
0247         }
0248         QString caption;
0249 
0250         Document::Ptr doc = mAdapter->document();
0251         if (!doc) {
0252             Q_EMIT q->captionUpdateRequested(caption);
0253             return;
0254         }
0255 
0256         caption = doc->url().fileName();
0257         QSize size = doc->size();
0258         if (size.isValid()) {
0259             caption += QStringLiteral(" - ");
0260             caption += i18nc("@item:intable %1 is image width, %2 is image height", "%1x%2", size.width(), size.height());
0261             if (mAdapter->canZoom()) {
0262                 int intZoom = qRound(mAdapter->zoom() * 100);
0263                 caption += QStringLiteral(" - ");
0264                 caption += i18nc("Percent value", "%1%", intZoom);
0265             }
0266         }
0267         Q_EMIT q->captionUpdateRequested(caption);
0268     }
0269 
0270     void uncheckZoomToFit()
0271     {
0272         if (mAdapter->zoomToFit()) {
0273             mAdapter->setZoomToFit(false);
0274         }
0275     }
0276 
0277     void uncheckZoomToFill()
0278     {
0279         if (mAdapter->zoomToFill()) {
0280             mAdapter->setZoomToFill(false);
0281         }
0282     }
0283 
0284     void setZoom(qreal zoom, const QPointF &center = QPointF(-1, -1))
0285     {
0286         uncheckZoomToFit();
0287         uncheckZoomToFill();
0288         zoom = qBound(q->minimumZoom(), zoom, MAXIMUM_ZOOM_VALUE);
0289         mAdapter->setZoom(zoom, center);
0290     }
0291 
0292     void updateZoomSnapValues()
0293     {
0294         const qreal min = q->minimumZoom();
0295 
0296         mZoomSnapValues.clear();
0297         for (qreal zoom = MINSTEP; zoom > min; zoom *= MINSTEP) {
0298             mZoomSnapValues << zoom;
0299         }
0300         mZoomSnapValues << min;
0301 
0302         std::reverse(mZoomSnapValues.begin(), mZoomSnapValues.end());
0303 
0304         for (qreal zoom = 1; zoom < MAXIMUM_ZOOM_VALUE; zoom *= MAXSTEP) {
0305             mZoomSnapValues << zoom;
0306         }
0307         mZoomSnapValues << MAXIMUM_ZOOM_VALUE;
0308 
0309         Q_EMIT q->minimumZoomChanged(min);
0310     }
0311 
0312     void showLoadingIndicator()
0313     {
0314         if (!mLoadingIndicator) {
0315             setupLoadingIndicator();
0316         }
0317         mLoadingIndicatorDelay->start(400);
0318     }
0319 
0320     void hideLoadingIndicator()
0321     {
0322         if (!mLoadingIndicator) {
0323             return;
0324         }
0325         mLoadingIndicatorDelay->stop();
0326         mLoadingIndicator->hide();
0327     }
0328 
0329     void resizeAdapterWidget()
0330     {
0331         QRectF rect = QRectF(QPointF(0, 0), q->boundingRect().size());
0332         if (mCompareMode) {
0333             rect.adjust(COMPARE_MARGIN, COMPARE_MARGIN, -COMPARE_MARGIN, -COMPARE_MARGIN);
0334         }
0335         mAdapter->widget()->setGeometry(rect);
0336     }
0337 
0338     void fadeTo(qreal value)
0339     {
0340         if (mFadeAnimation.data()) {
0341             qreal endValue = mFadeAnimation.data()->endValue().toReal();
0342             if (qFuzzyCompare(value, endValue)) {
0343                 // Same end value, don't change the actual animation
0344                 return;
0345             }
0346         }
0347         // Create a new fade animation
0348         auto anim = new QPropertyAnimation(mOpacityEffect, "opacity");
0349         anim->setStartValue(mOpacityEffect->opacity());
0350         anim->setEndValue(value);
0351         if (qFuzzyCompare(value, 1)) {
0352             QObject::connect(anim, &QAbstractAnimation::finished, q, &DocumentView::slotFadeInFinished);
0353         }
0354         QObject::connect(anim, &QAbstractAnimation::finished, q, &DocumentView::isAnimatedChanged);
0355         anim->setDuration(DocumentView::AnimDuration);
0356         mFadeAnimation = anim;
0357         Q_EMIT q->isAnimatedChanged();
0358         anim->start(QAbstractAnimation::DeleteWhenStopped);
0359     }
0360 
0361     bool canPan() const
0362     {
0363         if (!q->canZoom()) {
0364             return false;
0365         }
0366 
0367         const QSize zoomedImageSize = mDocument->size() * q->zoom();
0368         const QSize viewPortSize = q->boundingRect().size().toSize();
0369         const bool imageWiderThanViewport = zoomedImageSize.width() > viewPortSize.width();
0370         const bool imageTallerThanViewport = zoomedImageSize.height() > viewPortSize.height();
0371         return (imageWiderThanViewport || imageTallerThanViewport);
0372     }
0373 
0374     void setDragPixmap(const QPixmap &pix)
0375     {
0376         if (mDrag) {
0377             DragPixmapGenerator::DragPixmap dragPixmap = DragPixmapGenerator::generate({pix}, 1);
0378             mDrag->setPixmap(dragPixmap.pix);
0379             mDrag->setHotSpot(dragPixmap.hotSpot);
0380         }
0381     }
0382 
0383     void executeDrag()
0384     {
0385         if (mDrag) {
0386             if (mAdapter->imageView()) {
0387                 mAdapter->imageView()->resetDragCursor();
0388             }
0389             mDrag->exec(Qt::MoveAction | Qt::CopyAction | Qt::LinkAction, Qt::CopyAction);
0390         }
0391     }
0392 
0393     void initDragThumbnailProvider()
0394     {
0395         mDragThumbnailProvider = new ThumbnailProvider();
0396         QObject::connect(mDragThumbnailProvider, &ThumbnailProvider::thumbnailLoaded, q, &DocumentView::dragThumbnailLoaded);
0397         QObject::connect(mDragThumbnailProvider, &ThumbnailProvider::thumbnailLoadingFailed, q, &DocumentView::dragThumbnailLoadingFailed);
0398     }
0399 
0400     void startDragIfSensible()
0401     {
0402         if (q->document()->loadingState() == Document::LoadingFailed) {
0403             return;
0404         }
0405 
0406         if (q->currentTool()) {
0407             return;
0408         }
0409 
0410         if (mDrag) {
0411             mDrag->deleteLater();
0412         }
0413         mDrag = new QDrag(q);
0414         const auto itemList = KFileItemList({KFileItem(q->document()->url())});
0415         auto *mimeData = MimeTypeUtils::selectionMimeData(itemList, MimeTypeUtils::DropTarget);
0416         KUrlMimeData::exportUrlsToPortal(mimeData);
0417         mDrag->setMimeData(mimeData);
0418 
0419         if (q->document()->isModified()) {
0420             setDragPixmap(QPixmap::fromImage(q->document()->image()));
0421             executeDrag();
0422         } else {
0423             // Drag is triggered on success or failure of thumbnail generation
0424             if (mDragThumbnailProvider.isNull()) {
0425                 initDragThumbnailProvider();
0426             }
0427             mDragThumbnailProvider->appendItems(itemList);
0428         }
0429     }
0430 
0431     QPointF cursorPosition()
0432     {
0433         const QGraphicsScene *sc = q->scene();
0434         if (sc) {
0435             const auto views = sc->views();
0436             for (const QGraphicsView *view : views) {
0437                 if (view->underMouse()) {
0438                     return q->mapFromScene(view->mapFromGlobal(QCursor::pos()));
0439                 }
0440             }
0441         }
0442         return QPointF(-1, -1);
0443     }
0444 };
0445 
0446 DocumentView::DocumentView(QGraphicsScene *scene)
0447     : d(new DocumentViewPrivate)
0448 {
0449     setFlag(ItemIsFocusable);
0450     setFlag(ItemIsSelectable);
0451     setFlag(ItemClipsChildrenToShape);
0452 
0453     d->q = this;
0454     d->mLoadingIndicator = nullptr;
0455     d->mBirdEyeView = nullptr;
0456     d->mCurrent = false;
0457     d->mCompareMode = false;
0458     d->controlWheelAccumulatedDelta = 0;
0459     d->mDragStartPosition = QPointF(0, 0);
0460     d->mDrag = nullptr;
0461 
0462 #ifndef GWENVIEW_NO_WAYLAND_GESTURES
0463     if (QApplication::platformName() == QStringLiteral("wayland")) {
0464         d->mWaylandGestures = new WaylandGestures();
0465         connect(d->mWaylandGestures, &WaylandGestures::pinchGestureStarted, [this]() {
0466             d->mWaylandGestures->setStartZoom(zoom());
0467         });
0468         connect(d->mWaylandGestures, &WaylandGestures::pinchZoomChanged, [this](double zoom) {
0469             d->setZoom(zoom, d->cursorPosition());
0470         });
0471     }
0472 #endif
0473 
0474     d->mTouch = new Touch(this);
0475     setAcceptTouchEvents(true);
0476     connect(d->mTouch, &Touch::doubleTapTriggered, this, &DocumentView::toggleFullScreenRequested);
0477     connect(d->mTouch, &Touch::twoFingerTapTriggered, this, &DocumentView::contextMenuRequested);
0478     connect(d->mTouch, &Touch::pinchGestureStarted, this, &DocumentView::setPinchParameter);
0479     connect(d->mTouch, &Touch::pinchZoomTriggered, this, &DocumentView::zoomGesture);
0480     connect(d->mTouch, &Touch::pinchRotateTriggered, this, &DocumentView::rotationsGesture);
0481     connect(d->mTouch, &Touch::swipeRightTriggered, this, &DocumentView::swipeRight);
0482     connect(d->mTouch, &Touch::swipeLeftTriggered, this, &DocumentView::swipeLeft);
0483     connect(d->mTouch, &Touch::PanTriggered, this, &DocumentView::panGesture);
0484     connect(d->mTouch, &Touch::tapHoldAndMovingTriggered, this, &DocumentView::startDragFromTouch);
0485 
0486     // We use an opacity effect instead of using the opacity property directly, because the latter operates at
0487     // the painter level, which means if you draw multiple layers in paint(), all layers get the specified
0488     // opacity, resulting in all layers being visible when 0 < opacity < 1.
0489     // QGraphicsEffects on the other hand, operate after all painting is done, therefore 'flattening' all layers.
0490     // This is important for fade effects, where we don't want any background layers visible during the fade.
0491     d->mOpacityEffect = new QGraphicsOpacityEffect(this);
0492     d->mOpacityEffect->setOpacity(0);
0493 
0494     // QTBUG-74963. QGraphicsOpacityEffect cause painting an image as non-highdpi.
0495     if (qFuzzyCompare(qApp->devicePixelRatio(), 1.0) || QLibraryInfo::version() >= QVersionNumber(5, 12, 4))
0496         setGraphicsEffect(d->mOpacityEffect);
0497 
0498     scene->addItem(this);
0499 
0500     d->setupHud();
0501     d->setCurrentAdapter(new EmptyAdapter);
0502 
0503     setAcceptDrops(true);
0504 
0505     connect(DocumentFactory::instance(), &DocumentFactory::documentChanged, this, [this]() {
0506         d->updateCaption();
0507     });
0508 }
0509 
0510 DocumentView::~DocumentView()
0511 {
0512     delete d->mTouch;
0513 #ifndef GWENVIEW_NO_WAYLAND_GESTURES
0514     delete d->mWaylandGestures;
0515 #endif
0516     delete d->mDragThumbnailProvider;
0517     delete d->mDrag;
0518     delete d;
0519 }
0520 
0521 void DocumentView::createAdapterForDocument()
0522 {
0523     const MimeTypeUtils::Kind documentKind = d->mDocument->kind();
0524     if (d->mAdapter && documentKind == d->mAdapter->kind() && documentKind != MimeTypeUtils::KIND_UNKNOWN) {
0525         // Do not reuse for KIND_UNKNOWN: we may need to change the message
0526         LOG("Reusing current adapter");
0527         return;
0528     }
0529     AbstractDocumentViewAdapter *adapter = nullptr;
0530     switch (documentKind) {
0531     case MimeTypeUtils::KIND_RASTER_IMAGE:
0532         adapter = new RasterImageViewAdapter;
0533         break;
0534     case MimeTypeUtils::KIND_SVG_IMAGE:
0535         adapter = new SvgViewAdapter;
0536         break;
0537     case MimeTypeUtils::KIND_VIDEO:
0538         adapter = new VideoViewAdapter;
0539         connect(adapter, SIGNAL(videoFinished()), SIGNAL(videoFinished()));
0540         break;
0541     case MimeTypeUtils::KIND_UNKNOWN:
0542         adapter = new MessageViewAdapter;
0543         static_cast<MessageViewAdapter *>(adapter)->setErrorMessage(i18n("Gwenview does not know how to display this kind of document"));
0544         break;
0545     default:
0546         qCWarning(GWENVIEW_LIB_LOG) << "should not be called for documentKind=" << documentKind;
0547         adapter = new MessageViewAdapter;
0548         break;
0549     }
0550 
0551     d->setCurrentAdapter(adapter);
0552 }
0553 
0554 void DocumentView::openUrl(const QUrl &url, const DocumentView::Setup &setup)
0555 {
0556     if (d->mDocument) {
0557         if (url == d->mDocument->url()) {
0558             return;
0559         }
0560         disconnect(d->mDocument.data(), nullptr, this, nullptr);
0561     }
0562 
0563     // because some loading will be going on right now, also display the indicator after a small delay
0564     // it will be hidden again in slotBusyChanged()
0565     d->showLoadingIndicator();
0566 
0567     d->mSetup = setup;
0568     d->mDocument = DocumentFactory::instance()->load(url);
0569     connect(d->mDocument.data(), &Document::busyChanged, this, &DocumentView::slotBusyChanged);
0570     connect(d->mDocument.data(), &Document::modified, this, [this]() {
0571         d->updateZoomSnapValues();
0572     });
0573 
0574     if (d->mDocument->loadingState() < Document::KindDetermined) {
0575         auto messageViewAdapter = qobject_cast<MessageViewAdapter *>(d->mAdapter.data());
0576         if (messageViewAdapter) {
0577             messageViewAdapter->setInfoMessage(QString());
0578         }
0579         connect(d->mDocument.data(), &Document::kindDetermined, this, &DocumentView::finishOpenUrl);
0580     } else {
0581         QMetaObject::invokeMethod(this, &DocumentView::finishOpenUrl, Qt::QueuedConnection);
0582     }
0583 
0584     if (GwenviewConfig::birdEyeViewEnabled()) {
0585         d->setupBirdEyeView();
0586     }
0587 }
0588 
0589 void DocumentView::finishOpenUrl()
0590 {
0591     disconnect(d->mDocument.data(), &Document::kindDetermined, this, &DocumentView::finishOpenUrl);
0592     GV_RETURN_IF_FAIL(d->mDocument->loadingState() >= Document::KindDetermined);
0593 
0594     if (d->mDocument->loadingState() == Document::LoadingFailed) {
0595         slotLoadingFailed();
0596         return;
0597     }
0598     createAdapterForDocument();
0599 
0600     connect(d->mDocument.data(), &Document::loadingFailed, this, &DocumentView::slotLoadingFailed);
0601     d->mAdapter->setDocument(d->mDocument);
0602     d->updateCaption();
0603 }
0604 
0605 void DocumentView::loadAdapterConfig()
0606 {
0607     d->mAdapter->loadConfig();
0608 }
0609 
0610 RasterImageView *DocumentView::imageView() const
0611 {
0612     return d->mAdapter->rasterImageView();
0613 }
0614 
0615 void DocumentView::slotCompleted()
0616 {
0617     d->hideLoadingIndicator();
0618     d->updateCaption();
0619     d->updateZoomSnapValues();
0620     if (!d->mAdapter->zoomToFit() || !d->mAdapter->zoomToFill()) {
0621         qreal min = minimumZoom();
0622         if (d->mAdapter->zoom() < min) {
0623             d->mAdapter->setZoom(min);
0624         }
0625     }
0626     Q_EMIT completed();
0627 }
0628 
0629 DocumentView::Setup DocumentView::setup() const
0630 {
0631     Setup setup;
0632     if (d->mAdapter->canZoom()) {
0633         setup.valid = true;
0634         setup.zoomToFit = zoomToFit();
0635         setup.zoomToFill = zoomToFill();
0636         if (!setup.zoomToFit && !setup.zoomToFill) {
0637             setup.zoom = zoom();
0638             setup.position = position();
0639         }
0640     }
0641     return setup;
0642 }
0643 
0644 void DocumentView::slotLoadingFailed()
0645 {
0646     d->hideLoadingIndicator();
0647     auto adapter = new MessageViewAdapter;
0648     adapter->setDocument(d->mDocument);
0649     QString message = xi18n("Loading <filename>%1</filename> failed", d->mDocument->url().fileName());
0650     adapter->setErrorMessage(message, d->mDocument->errorString());
0651     d->setCurrentAdapter(adapter);
0652     Q_EMIT completed();
0653 }
0654 
0655 bool DocumentView::canZoom() const
0656 {
0657     return d->mAdapter->canZoom();
0658 }
0659 
0660 void DocumentView::setZoomToFit(bool on)
0661 {
0662     if (on == d->mAdapter->zoomToFit()) {
0663         return;
0664     }
0665     d->mAdapter->setZoomToFit(on);
0666 }
0667 
0668 void DocumentView::toggleZoomToFit()
0669 {
0670     const bool zoomToFitOn = d->mAdapter->zoomToFit();
0671     d->mAdapter->setZoomToFit(!zoomToFitOn);
0672     if (zoomToFitOn) {
0673         d->setZoom(1., d->cursorPosition());
0674     }
0675 }
0676 
0677 void DocumentView::setZoomToFill(bool on)
0678 {
0679     if (on == d->mAdapter->zoomToFill()) {
0680         return;
0681     }
0682     d->mAdapter->setZoomToFill(on, d->cursorPosition());
0683 }
0684 
0685 void DocumentView::toggleZoomToFill()
0686 {
0687     const bool zoomToFillOn = d->mAdapter->zoomToFill();
0688     d->mAdapter->setZoomToFill(!zoomToFillOn, d->cursorPosition());
0689     if (zoomToFillOn) {
0690         d->setZoom(1., d->cursorPosition());
0691     }
0692 }
0693 
0694 void DocumentView::toggleBirdEyeView()
0695 {
0696     if (d->mBirdEyeView) {
0697         BirdEyeView *tmp = d->mBirdEyeView;
0698         d->mBirdEyeView = nullptr;
0699         delete tmp;
0700     } else {
0701         d->setupBirdEyeView();
0702     }
0703 
0704     GwenviewConfig::setBirdEyeViewEnabled(!GwenviewConfig::birdEyeViewEnabled());
0705 }
0706 
0707 void DocumentView::setBackgroundColorMode(BackgroundColorMode colorMode)
0708 {
0709     GwenviewConfig::setBackgroundColorMode(colorMode);
0710     Q_EMIT backgroundColorModeChanged(colorMode);
0711 }
0712 
0713 bool DocumentView::zoomToFit() const
0714 {
0715     return d->mAdapter->zoomToFit();
0716 }
0717 
0718 bool DocumentView::zoomToFill() const
0719 {
0720     return d->mAdapter->zoomToFill();
0721 }
0722 
0723 void DocumentView::zoomActualSize()
0724 {
0725     d->uncheckZoomToFit();
0726     d->uncheckZoomToFill();
0727     d->mAdapter->setZoom(1., d->cursorPosition());
0728 }
0729 
0730 void DocumentView::zoomIn(QPointF center)
0731 {
0732     if (center == QPointF(-1, -1)) {
0733         center = d->cursorPosition();
0734     }
0735     qreal currentZoom = d->mAdapter->zoom();
0736 
0737     for (qreal zoom : qAsConst(d->mZoomSnapValues)) {
0738         if (zoom > currentZoom + REAL_DELTA) {
0739             d->setZoom(zoom, center);
0740             return;
0741         }
0742     }
0743 }
0744 
0745 void DocumentView::zoomContinuous(int delta, QPointF center)
0746 {
0747     if (center == QPointF(-1, -1)) {
0748         center = d->cursorPosition();
0749     }
0750     const qreal currentZoom = d->mAdapter->zoom();
0751 
0752     // multiplies by sqrt(2) for every mouse wheel step
0753     const qreal newZoom = currentZoom * pow(2, 0.5 * float(delta) / QWheelEvent::DefaultDeltasPerStep);
0754     d->setZoom(newZoom, center);
0755     return;
0756 }
0757 
0758 void DocumentView::zoomOut(QPointF center)
0759 {
0760     if (center == QPointF(-1, -1)) {
0761         center = d->cursorPosition();
0762     }
0763     qreal currentZoom = d->mAdapter->zoom();
0764 
0765     QListIterator<qreal> it(d->mZoomSnapValues);
0766     it.toBack();
0767     while (it.hasPrevious()) {
0768         qreal zoom = it.previous();
0769         if (zoom < currentZoom - REAL_DELTA) {
0770             d->setZoom(zoom, center);
0771             return;
0772         }
0773     }
0774 }
0775 
0776 void DocumentView::slotZoomChanged(qreal zoom)
0777 {
0778     d->updateCaption();
0779     Q_EMIT zoomChanged(zoom);
0780 }
0781 
0782 void DocumentView::setZoom(qreal zoom)
0783 {
0784     d->setZoom(zoom);
0785 }
0786 
0787 qreal DocumentView::zoom() const
0788 {
0789     return d->mAdapter->zoom();
0790 }
0791 
0792 void DocumentView::setPinchParameter(qint64 timeStamp)
0793 {
0794     Q_UNUSED(timeStamp);
0795     const qreal sensitivityModifier = 0.85;
0796     const qreal rotationThreshold = 40;
0797     d->mTouch->setZoomParameter(sensitivityModifier, zoom());
0798     d->mTouch->setRotationThreshold(rotationThreshold);
0799     d->mMinTimeBetweenPinch = 0;
0800 }
0801 
0802 void DocumentView::zoomGesture(qreal zoom, const QPoint &zoomCenter, qint64 timeStamp)
0803 {
0804     qint64 now = QDateTime::currentMSecsSinceEpoch();
0805     const qint64 diff = now - timeStamp;
0806 
0807     // in Wayland we can get the gesture event more frequently, to reduce CPU power we don't use every event
0808     // to calculate and paint a new image (mMinTimeBetweenPinch).To determine the exact minimum waiting time between two
0809     // pinch events, we use the difference between the time stamps. If the difference is too high we increase the minimum waiting time.
0810     // The maximal waiting time is 40 milliseconds, this is equal to 25 frames per second.
0811     if (diff > 40) {
0812         d->mMinTimeBetweenPinch = (d->mMinTimeBetweenPinch * 2) + 1;
0813         if (d->mMinTimeBetweenPinch > 40) {
0814             d->mMinTimeBetweenPinch = 40;
0815         }
0816     }
0817 
0818     if (diff > d->mMinTimeBetweenPinch) {
0819         if (zoom >= 0.0 && d->mAdapter->canZoom()) {
0820             d->setZoom(zoom, zoomCenter);
0821         }
0822     }
0823 }
0824 
0825 void DocumentView::rotationsGesture(qreal rotation)
0826 {
0827     if (rotation > 0.0) {
0828         auto op = new TransformImageOperation(ROT_90);
0829         op->applyToDocument(d->mDocument);
0830     } else if (rotation < 0.0) {
0831         auto op = new TransformImageOperation(ROT_270);
0832         op->applyToDocument(d->mDocument);
0833     }
0834 }
0835 
0836 void DocumentView::swipeRight()
0837 {
0838     const QPoint scrollPos = d->mAdapter->scrollPos().toPoint();
0839     if (scrollPos.x() <= 1) {
0840         Q_EMIT d->mAdapter->previousImageRequested();
0841     }
0842 }
0843 
0844 void DocumentView::swipeLeft()
0845 {
0846     const QSizeF dipSize = d->mAdapter->imageView()->dipDocumentSize();
0847     const QPoint scrollPos = d->mAdapter->scrollPos().toPoint();
0848     const int width = dipSize.width() * d->mAdapter->zoom();
0849     const QRect visibleRect = d->mAdapter->visibleDocumentRect().toRect();
0850     const int x = scrollPos.x() + visibleRect.width();
0851     if (x >= (width - 1)) {
0852         Q_EMIT d->mAdapter->nextImageRequested();
0853     }
0854 }
0855 
0856 void DocumentView::panGesture(const QPointF &delta)
0857 {
0858     d->mAdapter->setScrollPos(d->mAdapter->scrollPos() + delta);
0859 }
0860 
0861 void DocumentView::startDragFromTouch(const QPoint &)
0862 {
0863     d->startDragIfSensible();
0864 }
0865 
0866 void DocumentView::resizeEvent(QGraphicsSceneResizeEvent *event)
0867 {
0868     d->resizeAdapterWidget();
0869     d->updateZoomSnapValues();
0870     QGraphicsWidget::resizeEvent(event);
0871 }
0872 
0873 void DocumentView::mousePressEvent(QGraphicsSceneMouseEvent *event)
0874 {
0875     // Don't let (presumably double click handling in) the superclass swallow the second of two
0876     // quickly following middle/side clicks, preventing fast toggle & navigation. We wouldn't even
0877     // get a double click event - handling that could be somewhat cleaner.
0878     if (d->mAdapter->canZoom() && event->button() == Qt::MiddleButton) {
0879         if (event->modifiers() == Qt::NoModifier) {
0880             event->accept();
0881             toggleZoomToFit();
0882             return;
0883         } else if (event->modifiers() == Qt::SHIFT) {
0884             event->accept();
0885             toggleZoomToFill();
0886             return;
0887         }
0888     }
0889     else if (event->button() == Qt::BackButton) {
0890         event->accept();
0891         Q_EMIT previousImageRequested();
0892         return;
0893     }
0894     else if (event->button() == Qt::ForwardButton) {
0895         event->accept();
0896         Q_EMIT nextImageRequested();
0897         return;
0898     }
0899 
0900     QGraphicsWidget::mousePressEvent(event);
0901 }
0902 
0903 void DocumentView::wheelEvent(QGraphicsSceneWheelEvent *event)
0904 {
0905     if (d->mAdapter->canZoom()) {
0906         if ((event->modifiers() & Qt::ControlModifier)
0907             || (GwenviewConfig::mouseWheelBehavior() == MouseWheelBehavior::Zoom && event->modifiers() == Qt::NoModifier)) {
0908             zoomContinuous(event->delta(), event->pos());
0909             // Ctrl + wheel => zoom in or out
0910             return;
0911         }
0912     }
0913     if (GwenviewConfig::mouseWheelBehavior() == MouseWheelBehavior::Browse && event->modifiers() == Qt::NoModifier) {
0914         d->controlWheelAccumulatedDelta += event->delta();
0915         // Browse with mouse wheel
0916         if (d->controlWheelAccumulatedDelta >= QWheelEvent::DefaultDeltasPerStep) {
0917             Q_EMIT previousImageRequested();
0918             d->controlWheelAccumulatedDelta = 0;
0919         } else if (d->controlWheelAccumulatedDelta <= -QWheelEvent::DefaultDeltasPerStep) {
0920             Q_EMIT nextImageRequested();
0921             d->controlWheelAccumulatedDelta = 0;
0922         }
0923         return;
0924     }
0925     // Scroll
0926     qreal dx = 0;
0927     // 16 = pixels for one line
0928     // 120: see QWheelEvent::angleDelta().y() doc
0929     qreal dy = -qApp->wheelScrollLines() * 16 * event->delta() / 120;
0930     if (event->orientation() == Qt::Horizontal) {
0931         std::swap(dx, dy);
0932     }
0933     d->mAdapter->setScrollPos(d->mAdapter->scrollPos() + QPointF(dx, dy));
0934 }
0935 
0936 void DocumentView::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
0937 {
0938     // Filter out context menu if Ctrl is down to avoid showing it when
0939     // zooming out with Ctrl + Right button
0940     if (event->modifiers() != Qt::ControlModifier) {
0941         Q_EMIT contextMenuRequested();
0942     }
0943 }
0944 
0945 void DocumentView::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
0946 {
0947     // Fill background manually, because setAutoFillBackground(true) fill with QPalette::Window,
0948     // but our palettes use QPalette::Base for the background color/texture
0949     painter->fillRect(rect(), palette().base());
0950 
0951     // Selection indicator/highlight
0952     if (d->mCompareMode && d->mCurrent) {
0953         painter->save();
0954         painter->setBrush(Qt::NoBrush);
0955         painter->setPen(QPen(palette().highlight().color(), 2));
0956         painter->setRenderHint(QPainter::Antialiasing);
0957         const QRectF visibleRectF = mapRectFromItem(d->mAdapter->widget(), d->mAdapter->visibleDocumentRect());
0958         // Round the point and size independently. This is different than calling toRect(),
0959         // and is necessary to keep consistent rects, otherwise the selection rect can be
0960         // drawn 1 pixel too big or small.
0961         const QRect visibleRect = QRect(visibleRectF.topLeft().toPoint(), visibleRectF.size().toSize());
0962         const QRect selectionRect = visibleRect.adjusted(-1, -1, 1, 1);
0963         painter->drawRoundedRect(selectionRect, 3, 3);
0964         painter->restore();
0965     }
0966 }
0967 
0968 void DocumentView::slotBusyChanged(const QUrl &, bool busy)
0969 {
0970     if (busy) {
0971         d->showLoadingIndicator();
0972     } else {
0973         d->hideLoadingIndicator();
0974     }
0975 }
0976 
0977 qreal DocumentView::minimumZoom() const
0978 {
0979     // There is no point zooming out less than zoomToFit, but make sure it does
0980     // not get too small either
0981     return qBound(qreal(0.001), d->mAdapter->computeZoomToFit(), qreal(1.));
0982 }
0983 
0984 void DocumentView::setCompareMode(bool compare)
0985 {
0986     d->mCompareMode = compare;
0987     if (compare) {
0988         d->mHud->show();
0989         d->mHud->setZValue(1);
0990     } else {
0991         d->mHud->hide();
0992     }
0993 }
0994 
0995 void DocumentView::setCurrent(bool value)
0996 {
0997     d->mCurrent = value;
0998     if (value) {
0999         d->mAdapter->widget()->setFocus();
1000         d->updateCaption();
1001     }
1002     update();
1003 }
1004 
1005 bool DocumentView::isCurrent() const
1006 {
1007     return d->mCurrent;
1008 }
1009 
1010 QPoint DocumentView::position() const
1011 {
1012     return d->mAdapter->scrollPos().toPoint();
1013 }
1014 
1015 void DocumentView::setPosition(const QPoint &pos)
1016 {
1017     d->mAdapter->setScrollPos(pos);
1018 }
1019 
1020 Document::Ptr DocumentView::document() const
1021 {
1022     return d->mDocument;
1023 }
1024 
1025 QUrl DocumentView::url() const
1026 {
1027     Document::Ptr doc = d->mDocument;
1028     return doc ? doc->url() : QUrl();
1029 }
1030 
1031 void DocumentView::emitHudDeselectClicked()
1032 {
1033     Q_EMIT hudDeselectClicked(this);
1034 }
1035 
1036 void DocumentView::emitHudTrashClicked()
1037 {
1038     Q_EMIT hudTrashClicked(this);
1039 }
1040 
1041 void DocumentView::emitFocused()
1042 {
1043     Q_EMIT focused(this);
1044 }
1045 
1046 void DocumentView::setGeometry(const QRectF &rect)
1047 {
1048     QGraphicsWidget::setGeometry(rect);
1049     if (d->mBirdEyeView) {
1050         d->mBirdEyeView->slotZoomOrSizeChanged();
1051     }
1052 }
1053 
1054 void DocumentView::moveTo(const QRect &rect)
1055 {
1056     if (d->mMoveAnimation) {
1057         d->mMoveAnimation.data()->setEndValue(rect);
1058     } else {
1059         setGeometry(rect);
1060     }
1061 }
1062 
1063 void DocumentView::moveToAnimated(const QRect &rect)
1064 {
1065     auto anim = new QPropertyAnimation(this, "geometry");
1066     anim->setStartValue(geometry());
1067     anim->setEndValue(rect);
1068     anim->setDuration(DocumentView::AnimDuration);
1069     connect(anim, &QAbstractAnimation::finished, this, &DocumentView::isAnimatedChanged);
1070     d->mMoveAnimation = anim;
1071     Q_EMIT isAnimatedChanged();
1072     anim->start(QAbstractAnimation::DeleteWhenStopped);
1073 }
1074 
1075 QPropertyAnimation *DocumentView::fadeIn()
1076 {
1077     d->fadeTo(1);
1078     return d->mFadeAnimation.data();
1079 }
1080 
1081 void DocumentView::fadeOut()
1082 {
1083     d->fadeTo(0);
1084 }
1085 
1086 void DocumentView::slotFadeInFinished()
1087 {
1088     Q_EMIT fadeInFinished(this);
1089 }
1090 
1091 bool DocumentView::isAnimated() const
1092 {
1093     return d->mMoveAnimation || d->mFadeAnimation;
1094 }
1095 
1096 bool DocumentView::sceneEventFilter(QGraphicsItem *, QEvent *event)
1097 {
1098     if (event->type() == QEvent::GraphicsSceneMousePress) {
1099         const QGraphicsSceneMouseEvent *mouseEvent = static_cast<QGraphicsSceneMouseEvent *>(event);
1100         if (mouseEvent->button() == Qt::LeftButton) {
1101             d->mDragStartPosition = mouseEvent->pos();
1102         }
1103         QMetaObject::invokeMethod(this, &DocumentView::emitFocused, Qt::QueuedConnection);
1104     } else if (event->type() == QEvent::GraphicsSceneHoverMove) {
1105         if (d->mBirdEyeView) {
1106             d->mBirdEyeView->onMouseMoved();
1107         }
1108     } else if (event->type() == QEvent::GraphicsSceneMouseMove) {
1109         const QGraphicsSceneMouseEvent *mouseEvent = static_cast<QGraphicsSceneMouseEvent *>(event);
1110         // in some older version of Qt, Qt synthesize a mouse event from the touch event
1111         // we need to suppress this.
1112         // I need this for my working system (OpenSUSE Leap 15.0, Qt 5.9.4)
1113         if (mouseEvent->source() == Qt::MouseEventSynthesizedByQt) {
1114             return true;
1115         }
1116         // We need to check if the Left mouse button is pressed, otherwise this can lead
1117         // to starting a drag & drop sequence using the Forward/Backward mouse buttons
1118         if (!mouseEvent->buttons().testFlag(Qt::LeftButton)) {
1119             return false;
1120         }
1121         const qreal dragDistance = (mouseEvent->pos() - d->mDragStartPosition).manhattanLength();
1122         const qreal minDistanceToStartDrag = QGuiApplication::styleHints()->startDragDistance();
1123         if (!d->canPan() && dragDistance >= minDistanceToStartDrag) {
1124             d->startDragIfSensible();
1125         }
1126     }
1127     return false;
1128 }
1129 
1130 AbstractRasterImageViewTool *DocumentView::currentTool() const
1131 {
1132     return imageView() ? imageView()->currentTool() : nullptr;
1133 }
1134 
1135 int DocumentView::sortKey() const
1136 {
1137     return d->mSortKey;
1138 }
1139 
1140 void DocumentView::setSortKey(int sortKey)
1141 {
1142     d->mSortKey = sortKey;
1143 }
1144 
1145 void DocumentView::hideAndDeleteLater()
1146 {
1147     hide();
1148     deleteLater();
1149 }
1150 
1151 void DocumentView::setGraphicsEffectOpacity(qreal opacity)
1152 {
1153     d->mOpacityEffect->setOpacity(opacity);
1154 }
1155 
1156 void DocumentView::dragEnterEvent(QGraphicsSceneDragDropEvent *event)
1157 {
1158     QGraphicsWidget::dragEnterEvent(event);
1159 
1160     const auto urls = KUrlMimeData::urlsFromMimeData(event->mimeData());
1161     bool acceptDrag = !urls.isEmpty();
1162     if (urls.size() == 1 && urls.first() == url()) {
1163         // Do not allow dragging a single image onto itself
1164         acceptDrag = false;
1165     }
1166     event->setAccepted(acceptDrag);
1167 }
1168 
1169 void DocumentView::dropEvent(QGraphicsSceneDragDropEvent *event)
1170 {
1171     QGraphicsWidget::dropEvent(event);
1172     // Since we're capturing drops in View mode, we only support one url
1173     const QUrl url = KUrlMimeData::urlsFromMimeData(event->mimeData()).first();
1174     if (UrlUtils::urlIsDirectory(url)) {
1175         Q_EMIT openDirUrlRequested(url);
1176     } else {
1177         Q_EMIT openUrlRequested(url);
1178     }
1179 }
1180 
1181 void DocumentView::dragThumbnailLoaded(const KFileItem &item, const QPixmap &pix)
1182 {
1183     d->setDragPixmap(pix);
1184     d->executeDrag();
1185     d->mDragThumbnailProvider->removeItems(KFileItemList({item}));
1186 }
1187 
1188 void DocumentView::dragThumbnailLoadingFailed(const KFileItem &item)
1189 {
1190     d->executeDrag();
1191     d->mDragThumbnailProvider->removeItems(KFileItemList({item}));
1192 }
1193 
1194 } // namespace
1195 
1196 #include "moc_documentview.cpp"