1 // vim: set tabstop=4 shiftwidth=4 expandtab:
2 /*
3 Gwenview: an image viewer
4 Copyright 2008 Aurélien Gâteau <agateau@kde.org>
5 
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation; either version 2
9 of the License, or (at your option) any later version.
10 
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 GNU General Public License for more details.
15 
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.
19 
20 */
21 // Self
22 #include "documentview.h"
23 
24 // C++ Standard library
25 #include <cmath>
26 
27 // Qt
28 #include <QApplication>
29 #include <QDrag>
30 #include <QGestureEvent>
31 #include <QGraphicsLinearLayout>
32 #include <QGraphicsOpacityEffect>
33 #include <QGraphicsProxyWidget>
34 #include <QGraphicsScene>
35 #include <QGraphicsSceneMouseEvent>
36 #include <QGraphicsSceneWheelEvent>
37 #include <QGraphicsView>
38 #include <QIcon>
39 #include <QLibraryInfo>
40 #include <QMimeData>
41 #include <QPainter>
42 #include <QPointer>
43 #include <QPropertyAnimation>
44 #include <QStyleHints>
45 #include <QUrl>
46 
47 // KF
48 #include <KFileItem>
49 #include <KLocalizedString>
50 #include <KUrlMimeData>
51 
52 // Local
53 #include "gwenview_lib_debug.h"
54 #include <lib/document/documentfactory.h>
55 #include <lib/documentview/abstractrasterimageviewtool.h>
56 #include <lib/documentview/birdeyeview.h>
57 #include <lib/documentview/loadingindicator.h>
58 #include <lib/documentview/messageviewadapter.h>
59 #include <lib/documentview/rasterimageview.h>
60 #include <lib/documentview/rasterimageviewadapter.h>
61 #include <lib/documentview/svgviewadapter.h>
62 #include <lib/documentview/videoviewadapter.h>
63 #include <lib/graphicswidgetfloater.h>
64 #include <lib/gvdebug.h>
65 #include <lib/gwenviewconfig.h>
66 #include <lib/hud/hudbutton.h>
67 #include <lib/hud/hudwidget.h>
68 #include <lib/mimetypeutils.h>
69 #include <lib/thumbnailprovider/thumbnailprovider.h>
70 #include <lib/thumbnailview/dragpixmapgenerator.h>
71 #include <lib/touch/touch.h>
72 #include <lib/urlutils.h>
73 #include <transformimageoperation.h>
74 
75 namespace Gwenview
76 {
77 #undef ENABLE_LOG
78 #undef LOG
79 //#define ENABLE_LOG
80 #ifdef ENABLE_LOG
81 #define LOG(x) // qCDebug(GWENVIEW_LIB_LOG) << x
82 #else
83 #define LOG(x) ;
84 #endif
85 
86 static const qreal REAL_DELTA = 0.001;
87 static const qreal MAXIMUM_ZOOM_VALUE = qreal(DocumentView::MaximumZoom);
88 static const auto MINSTEP = sqrt(0.5);
89 static const auto MAXSTEP = sqrt(2.0);
90 
91 static const int COMPARE_MARGIN = 4;
92 
93 const int DocumentView::MaximumZoom = 16;
94 const int DocumentView::AnimDuration = 250;
95 
96 struct DocumentViewPrivate {
97     DocumentView *q;
98     int mSortKey; // Used to sort views when displayed in compare mode
99     HudWidget *mHud;
100     BirdEyeView *mBirdEyeView;
101     QPointer<QPropertyAnimation> mMoveAnimation;
102     QPointer<QPropertyAnimation> mFadeAnimation;
103     QGraphicsOpacityEffect *mOpacityEffect;
104 
105     LoadingIndicator *mLoadingIndicator;
106 
107     QScopedPointer<AbstractDocumentViewAdapter> mAdapter;
108     QList<qreal> mZoomSnapValues;
109     Document::Ptr mDocument;
110     DocumentView::Setup mSetup;
111     bool mCurrent;
112     bool mCompareMode;
113     int controlWheelAccumulatedDelta;
114 
115     QPointF mDragStartPosition;
116     QPointer<ThumbnailProvider> mDragThumbnailProvider;
117     QPointer<QDrag> mDrag;
118 
119     Touch *mTouch;
120     int mMinTimeBetweenPinch;
121 
setCurrentAdapterGwenview::DocumentViewPrivate122     void setCurrentAdapter(AbstractDocumentViewAdapter *adapter)
123     {
124         Q_ASSERT(adapter);
125         mAdapter.reset(adapter);
126 
127         adapter->widget()->setParentItem(q);
128         resizeAdapterWidget();
129 
130         if (adapter->canZoom()) {
131             QObject::connect(adapter, &AbstractDocumentViewAdapter::zoomChanged, q, &DocumentView::slotZoomChanged);
132             QObject::connect(adapter, &AbstractDocumentViewAdapter::zoomInRequested, q, &DocumentView::zoomIn);
133             QObject::connect(adapter, &AbstractDocumentViewAdapter::zoomOutRequested, q, &DocumentView::zoomOut);
134             QObject::connect(adapter, &AbstractDocumentViewAdapter::zoomToFitChanged, q, &DocumentView::zoomToFitChanged);
135             QObject::connect(adapter, &AbstractDocumentViewAdapter::zoomToFillChanged, q, &DocumentView::zoomToFillChanged);
136         }
137         QObject::connect(adapter, &AbstractDocumentViewAdapter::scrollPosChanged, q, &DocumentView::positionChanged);
138         QObject::connect(adapter, &AbstractDocumentViewAdapter::previousImageRequested, q, &DocumentView::previousImageRequested);
139         QObject::connect(adapter, &AbstractDocumentViewAdapter::nextImageRequested, q, &DocumentView::nextImageRequested);
140         QObject::connect(adapter, &AbstractDocumentViewAdapter::toggleFullScreenRequested, q, &DocumentView::toggleFullScreenRequested);
141         QObject::connect(adapter, &AbstractDocumentViewAdapter::completed, q, &DocumentView::slotCompleted);
142 
143         adapter->loadConfig();
144 
145         adapter->widget()->installSceneEventFilter(q);
146         if (mCurrent) {
147             adapter->widget()->setFocus();
148         }
149 
150         if (mSetup.valid && adapter->canZoom()) {
151             adapter->setZoomToFit(mSetup.zoomToFit);
152             adapter->setZoomToFill(mSetup.zoomToFill);
153             if (!mSetup.zoomToFit && !mSetup.zoomToFill) {
154                 adapter->setZoom(mSetup.zoom);
155                 adapter->setScrollPos(mSetup.position);
156             }
157         }
158         Q_EMIT q->adapterChanged();
159         Q_EMIT q->positionChanged();
160         if (adapter->canZoom()) {
161             if (adapter->zoomToFit()) {
162                 Q_EMIT q->zoomToFitChanged(true);
163             } else if (adapter->zoomToFill()) {
164                 Q_EMIT q->zoomToFillChanged(true);
165             } else {
166                 Q_EMIT q->zoomChanged(adapter->zoom());
167             }
168         }
169         if (adapter->rasterImageView()) {
170             QObject::connect(adapter->rasterImageView(), &RasterImageView::currentToolChanged, q, &DocumentView::currentToolChanged);
171         }
172     }
173 
setupLoadingIndicatorGwenview::DocumentViewPrivate174     void setupLoadingIndicator()
175     {
176         mLoadingIndicator = new LoadingIndicator(q);
177         auto *floater = new GraphicsWidgetFloater(q);
178         floater->setChildWidget(mLoadingIndicator);
179     }
180 
createHudButtonGwenview::DocumentViewPrivate181     HudButton *createHudButton(const QString &text, const QString &iconName, bool showText)
182     {
183         auto *button = new HudButton;
184         if (showText) {
185             button->setText(text);
186         } else {
187             button->setToolTip(text);
188         }
189         button->setIcon(QIcon::fromTheme(iconName));
190         return button;
191     }
192 
setupHudGwenview::DocumentViewPrivate193     void setupHud()
194     {
195         HudButton *trashButton = createHudButton(i18nc("@info:tooltip", "Trash"), QStringLiteral("user-trash"), false);
196         HudButton *deselectButton = createHudButton(i18nc("@action:button", "Deselect"), QStringLiteral("list-remove"), true);
197 
198         auto *content = new QGraphicsWidget;
199         auto *layout = new QGraphicsLinearLayout(content);
200         layout->addItem(trashButton);
201         layout->addItem(deselectButton);
202 
203         mHud = new HudWidget(q);
204         mHud->init(content, HudWidget::OptionNone);
205         auto *floater = new GraphicsWidgetFloater(q);
206         floater->setChildWidget(mHud);
207         floater->setAlignment(Qt::AlignBottom | Qt::AlignHCenter);
208 
209         QObject::connect(trashButton, &HudButton::clicked, q, &DocumentView::emitHudTrashClicked);
210         QObject::connect(deselectButton, &HudButton::clicked, q, &DocumentView::emitHudDeselectClicked);
211 
212         mHud->hide();
213     }
214 
setupBirdEyeViewGwenview::DocumentViewPrivate215     void setupBirdEyeView()
216     {
217         if (mBirdEyeView) {
218             delete mBirdEyeView;
219         }
220         mBirdEyeView = new BirdEyeView(q);
221         mBirdEyeView->setZValue(1);
222     }
223 
updateCaptionGwenview::DocumentViewPrivate224     void updateCaption()
225     {
226         if (!mCurrent) {
227             return;
228         }
229         QString caption;
230 
231         Document::Ptr doc = mAdapter->document();
232         if (!doc) {
233             Q_EMIT q->captionUpdateRequested(caption);
234             return;
235         }
236 
237         caption = doc->url().fileName();
238         QSize size = doc->size();
239         if (size.isValid()) {
240             caption += QStringLiteral(" - %1x%2").arg(size.width()).arg(size.height());
241             if (mAdapter->canZoom()) {
242                 int intZoom = qRound(mAdapter->zoom() * 100);
243                 caption += QStringLiteral(" - %1%").arg(intZoom);
244             }
245         }
246         Q_EMIT q->captionUpdateRequested(caption);
247     }
248 
uncheckZoomToFitGwenview::DocumentViewPrivate249     void uncheckZoomToFit()
250     {
251         if (mAdapter->zoomToFit()) {
252             mAdapter->setZoomToFit(false);
253         }
254     }
255 
uncheckZoomToFillGwenview::DocumentViewPrivate256     void uncheckZoomToFill()
257     {
258         if (mAdapter->zoomToFill()) {
259             mAdapter->setZoomToFill(false);
260         }
261     }
262 
setZoomGwenview::DocumentViewPrivate263     void setZoom(qreal zoom, const QPointF &center = QPointF(-1, -1))
264     {
265         uncheckZoomToFit();
266         uncheckZoomToFill();
267         zoom = qBound(q->minimumZoom(), zoom, MAXIMUM_ZOOM_VALUE);
268         mAdapter->setZoom(zoom, center);
269     }
270 
updateZoomSnapValuesGwenview::DocumentViewPrivate271     void updateZoomSnapValues()
272     {
273         qreal min = q->minimumZoom();
274 
275         mZoomSnapValues.clear();
276         for (qreal zoom = MINSTEP; zoom > min; zoom *= MINSTEP) {
277             mZoomSnapValues << zoom;
278         }
279         mZoomSnapValues << min;
280 
281         std::reverse(mZoomSnapValues.begin(), mZoomSnapValues.end());
282 
283         for (qreal zoom = 1; zoom < MAXIMUM_ZOOM_VALUE; zoom *= MAXSTEP) {
284             mZoomSnapValues << zoom;
285         }
286         mZoomSnapValues << MAXIMUM_ZOOM_VALUE;
287 
288         Q_EMIT q->minimumZoomChanged(min);
289     }
290 
showLoadingIndicatorGwenview::DocumentViewPrivate291     void showLoadingIndicator()
292     {
293         if (!mLoadingIndicator) {
294             setupLoadingIndicator();
295         }
296         mLoadingIndicator->show();
297         mLoadingIndicator->setZValue(1);
298     }
299 
hideLoadingIndicatorGwenview::DocumentViewPrivate300     void hideLoadingIndicator()
301     {
302         if (!mLoadingIndicator) {
303             return;
304         }
305         mLoadingIndicator->hide();
306     }
307 
resizeAdapterWidgetGwenview::DocumentViewPrivate308     void resizeAdapterWidget()
309     {
310         QRectF rect = QRectF(QPointF(0, 0), q->boundingRect().size());
311         if (mCompareMode) {
312             rect.adjust(COMPARE_MARGIN, COMPARE_MARGIN, -COMPARE_MARGIN, -COMPARE_MARGIN);
313         }
314         mAdapter->widget()->setGeometry(rect);
315     }
316 
fadeToGwenview::DocumentViewPrivate317     void fadeTo(qreal value)
318     {
319         if (mFadeAnimation.data()) {
320             qreal endValue = mFadeAnimation.data()->endValue().toReal();
321             if (qFuzzyCompare(value, endValue)) {
322                 // Same end value, don't change the actual animation
323                 return;
324             }
325         }
326         // Create a new fade animation
327         auto *anim = new QPropertyAnimation(mOpacityEffect, "opacity");
328         anim->setStartValue(mOpacityEffect->opacity());
329         anim->setEndValue(value);
330         if (qFuzzyCompare(value, 1)) {
331             QObject::connect(anim, &QAbstractAnimation::finished, q, &DocumentView::slotFadeInFinished);
332         }
333         QObject::connect(anim, &QAbstractAnimation::finished, q, &DocumentView::isAnimatedChanged);
334         anim->setDuration(DocumentView::AnimDuration);
335         mFadeAnimation = anim;
336         Q_EMIT q->isAnimatedChanged();
337         anim->start(QAbstractAnimation::DeleteWhenStopped);
338     }
339 
canPanGwenview::DocumentViewPrivate340     bool canPan() const
341     {
342         if (!q->canZoom()) {
343             return false;
344         }
345 
346         const QSize zoomedImageSize = mDocument->size() * q->zoom();
347         const QSize viewPortSize = q->boundingRect().size().toSize();
348         const bool imageWiderThanViewport = zoomedImageSize.width() > viewPortSize.width();
349         const bool imageTallerThanViewport = zoomedImageSize.height() > viewPortSize.height();
350         return (imageWiderThanViewport || imageTallerThanViewport);
351     }
352 
setDragPixmapGwenview::DocumentViewPrivate353     void setDragPixmap(const QPixmap &pix)
354     {
355         if (mDrag) {
356             DragPixmapGenerator::DragPixmap dragPixmap = DragPixmapGenerator::generate({pix}, 1);
357             mDrag->setPixmap(dragPixmap.pix);
358             mDrag->setHotSpot(dragPixmap.hotSpot);
359         }
360     }
361 
executeDragGwenview::DocumentViewPrivate362     void executeDrag()
363     {
364         if (mDrag) {
365             if (mAdapter->imageView()) {
366                 mAdapter->imageView()->resetDragCursor();
367             }
368             mDrag->exec(Qt::MoveAction | Qt::CopyAction | Qt::LinkAction, Qt::CopyAction);
369         }
370     }
371 
initDragThumbnailProviderGwenview::DocumentViewPrivate372     void initDragThumbnailProvider()
373     {
374         mDragThumbnailProvider = new ThumbnailProvider();
375         QObject::connect(mDragThumbnailProvider, &ThumbnailProvider::thumbnailLoaded, q, &DocumentView::dragThumbnailLoaded);
376         QObject::connect(mDragThumbnailProvider, &ThumbnailProvider::thumbnailLoadingFailed, q, &DocumentView::dragThumbnailLoadingFailed);
377     }
378 
startDragIfSensibleGwenview::DocumentViewPrivate379     void startDragIfSensible()
380     {
381         if (q->document()->loadingState() == Document::LoadingFailed) {
382             return;
383         }
384 
385         if (q->currentTool()) {
386             return;
387         }
388 
389         if (mDrag) {
390             mDrag->deleteLater();
391         }
392         mDrag = new QDrag(q);
393         const auto itemList = KFileItemList({q->document()->url()});
394         mDrag->setMimeData(MimeTypeUtils::selectionMimeData(itemList, MimeTypeUtils::DropTarget));
395 
396         if (q->document()->isModified()) {
397             setDragPixmap(QPixmap::fromImage(q->document()->image()));
398             executeDrag();
399         } else {
400             // Drag is triggered on success or failure of thumbnail generation
401             if (mDragThumbnailProvider.isNull()) {
402                 initDragThumbnailProvider();
403             }
404             mDragThumbnailProvider->appendItems(itemList);
405         }
406     }
407 
cursorPositionGwenview::DocumentViewPrivate408     QPointF cursorPosition()
409     {
410         const QGraphicsScene *sc = q->scene();
411         if (sc) {
412             const auto views = sc->views();
413             for (const QGraphicsView *view : views) {
414                 if (view->underMouse()) {
415                     return q->mapFromScene(view->mapFromGlobal(QCursor::pos()));
416                 }
417             }
418         }
419         return QPointF(-1, -1);
420     }
421 };
422 
DocumentView(QGraphicsScene * scene)423 DocumentView::DocumentView(QGraphicsScene *scene)
424     : d(new DocumentViewPrivate)
425 {
426     setFlag(ItemIsFocusable);
427     setFlag(ItemIsSelectable);
428     setFlag(ItemClipsChildrenToShape);
429 
430     d->q = this;
431     d->mLoadingIndicator = nullptr;
432     d->mBirdEyeView = nullptr;
433     d->mCurrent = false;
434     d->mCompareMode = false;
435     d->controlWheelAccumulatedDelta = 0;
436     d->mDragStartPosition = QPointF(0, 0);
437     d->mDrag = nullptr;
438 
439     d->mTouch = new Touch(this);
440     setAcceptTouchEvents(true);
441     connect(d->mTouch, &Touch::doubleTapTriggered, this, &DocumentView::toggleFullScreenRequested);
442     connect(d->mTouch, &Touch::twoFingerTapTriggered, this, &DocumentView::contextMenuRequested);
443     connect(d->mTouch, &Touch::pinchGestureStarted, this, &DocumentView::setPinchParameter);
444     connect(d->mTouch, &Touch::pinchZoomTriggered, this, &DocumentView::zoomGesture);
445     connect(d->mTouch, &Touch::pinchRotateTriggered, this, &DocumentView::rotationsGesture);
446     connect(d->mTouch, &Touch::swipeRightTriggered, this, &DocumentView::swipeRight);
447     connect(d->mTouch, &Touch::swipeLeftTriggered, this, &DocumentView::swipeLeft);
448     connect(d->mTouch, &Touch::PanTriggered, this, &DocumentView::panGesture);
449     connect(d->mTouch, &Touch::tapHoldAndMovingTriggered, this, &DocumentView::startDragFromTouch);
450 
451     // We use an opacity effect instead of using the opacity property directly, because the latter operates at
452     // the painter level, which means if you draw multiple layers in paint(), all layers get the specified
453     // opacity, resulting in all layers being visible when 0 < opacity < 1.
454     // QGraphicsEffects on the other hand, operate after all painting is done, therefore 'flattening' all layers.
455     // This is important for fade effects, where we don't want any background layers visible during the fade.
456     d->mOpacityEffect = new QGraphicsOpacityEffect(this);
457     d->mOpacityEffect->setOpacity(0);
458 
459     // QTBUG-74963. QGraphicsOpacityEffect cause painting an image as non-highdpi.
460     if (qFuzzyCompare(qApp->devicePixelRatio(), 1.0) || QLibraryInfo::version() >= QVersionNumber(5, 12, 4))
461         setGraphicsEffect(d->mOpacityEffect);
462 
463     scene->addItem(this);
464 
465     d->setupHud();
466     d->setCurrentAdapter(new EmptyAdapter);
467 
468     setAcceptDrops(true);
469 
470     connect(DocumentFactory::instance(), &DocumentFactory::documentChanged, this, [this]() {
471         d->updateCaption();
472     });
473 }
474 
~DocumentView()475 DocumentView::~DocumentView()
476 {
477     delete d->mTouch;
478     delete d->mDragThumbnailProvider;
479     delete d->mDrag;
480     delete d;
481 }
482 
createAdapterForDocument()483 void DocumentView::createAdapterForDocument()
484 {
485     const MimeTypeUtils::Kind documentKind = d->mDocument->kind();
486     if (d->mAdapter && documentKind == d->mAdapter->kind() && documentKind != MimeTypeUtils::KIND_UNKNOWN) {
487         // Do not reuse for KIND_UNKNOWN: we may need to change the message
488         LOG("Reusing current adapter");
489         return;
490     }
491     AbstractDocumentViewAdapter *adapter = nullptr;
492     switch (documentKind) {
493     case MimeTypeUtils::KIND_RASTER_IMAGE:
494         adapter = new RasterImageViewAdapter;
495         break;
496     case MimeTypeUtils::KIND_SVG_IMAGE:
497         adapter = new SvgViewAdapter;
498         break;
499     case MimeTypeUtils::KIND_VIDEO:
500         adapter = new VideoViewAdapter;
501         connect(adapter, SIGNAL(videoFinished()), SIGNAL(videoFinished()));
502         break;
503     case MimeTypeUtils::KIND_UNKNOWN:
504         adapter = new MessageViewAdapter;
505         static_cast<MessageViewAdapter *>(adapter)->setErrorMessage(i18n("Gwenview does not know how to display this kind of document"));
506         break;
507     default:
508         qCWarning(GWENVIEW_LIB_LOG) << "should not be called for documentKind=" << documentKind;
509         adapter = new MessageViewAdapter;
510         break;
511     }
512 
513     d->setCurrentAdapter(adapter);
514 }
515 
openUrl(const QUrl & url,const DocumentView::Setup & setup)516 void DocumentView::openUrl(const QUrl &url, const DocumentView::Setup &setup)
517 {
518     if (d->mDocument) {
519         if (url == d->mDocument->url()) {
520             return;
521         }
522         disconnect(d->mDocument.data(), nullptr, this, nullptr);
523     }
524 
525     // because some loading will be going on right now, also display the indicator right now
526     // it will be hidden again in slotBusyChanged()
527     d->showLoadingIndicator();
528 
529     d->mSetup = setup;
530     d->mDocument = DocumentFactory::instance()->load(url);
531     connect(d->mDocument.data(), &Document::busyChanged, this, &DocumentView::slotBusyChanged);
532     connect(d->mDocument.data(), &Document::modified, this, [this]() {
533         d->updateZoomSnapValues();
534     });
535 
536     if (d->mDocument->loadingState() < Document::KindDetermined) {
537         auto *messageViewAdapter = qobject_cast<MessageViewAdapter *>(d->mAdapter.data());
538         if (messageViewAdapter) {
539             messageViewAdapter->setInfoMessage(QString());
540         }
541         connect(d->mDocument.data(), &Document::kindDetermined, this, &DocumentView::finishOpenUrl);
542     } else {
543         QMetaObject::invokeMethod(this, &DocumentView::finishOpenUrl, Qt::QueuedConnection);
544     }
545 
546     if (GwenviewConfig::birdEyeViewEnabled()) {
547         d->setupBirdEyeView();
548     }
549 }
550 
finishOpenUrl()551 void DocumentView::finishOpenUrl()
552 {
553     disconnect(d->mDocument.data(), &Document::kindDetermined, this, &DocumentView::finishOpenUrl);
554     GV_RETURN_IF_FAIL(d->mDocument->loadingState() >= Document::KindDetermined);
555 
556     if (d->mDocument->loadingState() == Document::LoadingFailed) {
557         slotLoadingFailed();
558         return;
559     }
560     createAdapterForDocument();
561 
562     connect(d->mDocument.data(), &Document::loadingFailed, this, &DocumentView::slotLoadingFailed);
563     d->mAdapter->setDocument(d->mDocument);
564     d->updateCaption();
565 }
566 
loadAdapterConfig()567 void DocumentView::loadAdapterConfig()
568 {
569     d->mAdapter->loadConfig();
570 }
571 
imageView() const572 RasterImageView *DocumentView::imageView() const
573 {
574     return d->mAdapter->rasterImageView();
575 }
576 
slotCompleted()577 void DocumentView::slotCompleted()
578 {
579     d->hideLoadingIndicator();
580     d->updateCaption();
581     d->updateZoomSnapValues();
582     if (!d->mAdapter->zoomToFit() || !d->mAdapter->zoomToFill()) {
583         qreal min = minimumZoom();
584         if (d->mAdapter->zoom() < min) {
585             d->mAdapter->setZoom(min);
586         }
587     }
588     Q_EMIT completed();
589 }
590 
setup() const591 DocumentView::Setup DocumentView::setup() const
592 {
593     Setup setup;
594     if (d->mAdapter->canZoom()) {
595         setup.valid = true;
596         setup.zoomToFit = zoomToFit();
597         setup.zoomToFill = zoomToFill();
598         if (!setup.zoomToFit && !setup.zoomToFill) {
599             setup.zoom = zoom();
600             setup.position = position();
601         }
602     }
603     return setup;
604 }
605 
slotLoadingFailed()606 void DocumentView::slotLoadingFailed()
607 {
608     d->hideLoadingIndicator();
609     auto *adapter = new MessageViewAdapter;
610     adapter->setDocument(d->mDocument);
611     QString message = xi18n("Loading <filename>%1</filename> failed", d->mDocument->url().fileName());
612     adapter->setErrorMessage(message, d->mDocument->errorString());
613     d->setCurrentAdapter(adapter);
614     Q_EMIT completed();
615 }
616 
canZoom() const617 bool DocumentView::canZoom() const
618 {
619     return d->mAdapter->canZoom();
620 }
621 
setZoomToFit(bool on)622 void DocumentView::setZoomToFit(bool on)
623 {
624     if (on == d->mAdapter->zoomToFit()) {
625         return;
626     }
627     d->mAdapter->setZoomToFit(on);
628 }
629 
toggleZoomToFit()630 void DocumentView::toggleZoomToFit()
631 {
632     const bool zoomToFitOn = d->mAdapter->zoomToFit();
633     d->mAdapter->setZoomToFit(!zoomToFitOn);
634     if (zoomToFitOn) {
635         d->setZoom(1., d->cursorPosition());
636     }
637 }
638 
setZoomToFill(bool on)639 void DocumentView::setZoomToFill(bool on)
640 {
641     if (on == d->mAdapter->zoomToFill()) {
642         return;
643     }
644     d->mAdapter->setZoomToFill(on, d->cursorPosition());
645 }
646 
toggleZoomToFill()647 void DocumentView::toggleZoomToFill()
648 {
649     const bool zoomToFillOn = d->mAdapter->zoomToFill();
650     d->mAdapter->setZoomToFill(!zoomToFillOn, d->cursorPosition());
651     if (zoomToFillOn) {
652         d->setZoom(1., d->cursorPosition());
653     }
654 }
655 
toggleBirdEyeView()656 void DocumentView::toggleBirdEyeView()
657 {
658     if (d->mBirdEyeView) {
659         BirdEyeView *tmp = d->mBirdEyeView;
660         d->mBirdEyeView = nullptr;
661         delete tmp;
662     } else {
663         d->setupBirdEyeView();
664     }
665 
666     GwenviewConfig::setBirdEyeViewEnabled(!GwenviewConfig::birdEyeViewEnabled());
667 }
668 
setBackgroundColorMode(BackgroundColorWidget::ColorMode colorMode)669 void DocumentView::setBackgroundColorMode(BackgroundColorWidget::ColorMode colorMode)
670 {
671     GwenviewConfig::setBackgroundColorMode(colorMode);
672     Q_EMIT backgroundColorModeChanged(colorMode);
673 }
674 
zoomToFit() const675 bool DocumentView::zoomToFit() const
676 {
677     return d->mAdapter->zoomToFit();
678 }
679 
zoomToFill() const680 bool DocumentView::zoomToFill() const
681 {
682     return d->mAdapter->zoomToFill();
683 }
684 
zoomActualSize()685 void DocumentView::zoomActualSize()
686 {
687     d->uncheckZoomToFit();
688     d->uncheckZoomToFill();
689     d->mAdapter->setZoom(1., d->cursorPosition());
690 }
691 
zoomIn(QPointF center)692 void DocumentView::zoomIn(QPointF center)
693 {
694     if (center == QPointF(-1, -1)) {
695         center = d->cursorPosition();
696     }
697     qreal currentZoom = d->mAdapter->zoom();
698 
699     for (qreal zoom : qAsConst(d->mZoomSnapValues)) {
700         if (zoom > currentZoom + REAL_DELTA) {
701             d->setZoom(zoom, center);
702             return;
703         }
704     }
705 }
706 
zoomOut(QPointF center)707 void DocumentView::zoomOut(QPointF center)
708 {
709     if (center == QPointF(-1, -1)) {
710         center = d->cursorPosition();
711     }
712     qreal currentZoom = d->mAdapter->zoom();
713 
714     QListIterator<qreal> it(d->mZoomSnapValues);
715     it.toBack();
716     while (it.hasPrevious()) {
717         qreal zoom = it.previous();
718         if (zoom < currentZoom - REAL_DELTA) {
719             d->setZoom(zoom, center);
720             return;
721         }
722     }
723 }
724 
slotZoomChanged(qreal zoom)725 void DocumentView::slotZoomChanged(qreal zoom)
726 {
727     d->updateCaption();
728     Q_EMIT zoomChanged(zoom);
729 }
730 
setZoom(qreal zoom)731 void DocumentView::setZoom(qreal zoom)
732 {
733     d->setZoom(zoom);
734 }
735 
zoom() const736 qreal DocumentView::zoom() const
737 {
738     return d->mAdapter->zoom();
739 }
740 
setPinchParameter(qint64 timeStamp)741 void DocumentView::setPinchParameter(qint64 timeStamp)
742 {
743     Q_UNUSED(timeStamp);
744     const qreal sensitivityModifier = 0.85;
745     const qreal rotationThreshold = 40;
746     d->mTouch->setZoomParameter(sensitivityModifier, zoom());
747     d->mTouch->setRotationThreshold(rotationThreshold);
748     d->mMinTimeBetweenPinch = 0;
749 }
750 
zoomGesture(qreal zoom,const QPoint & zoomCenter,qint64 timeStamp)751 void DocumentView::zoomGesture(qreal zoom, const QPoint &zoomCenter, qint64 timeStamp)
752 {
753     qint64 now = QDateTime::currentMSecsSinceEpoch();
754     const qint64 diff = now - timeStamp;
755 
756     // in Wayland we can get the gesture event more frequently, to reduce CPU power we don't use every event
757     // to calculate and paint a new image (mMinTimeBetweenPinch).To determine the exact minimum waiting time between two
758     // pinch events, we use the difference between the time stamps. If the difference is too high we increase the minimum waiting time.
759     // The maximal waiting time is 40 milliseconds, this is equal to 25 frames per second.
760     if (diff > 40) {
761         d->mMinTimeBetweenPinch = (d->mMinTimeBetweenPinch * 2) + 1;
762         if (d->mMinTimeBetweenPinch > 40) {
763             d->mMinTimeBetweenPinch = 40;
764         }
765     }
766 
767     if (diff > d->mMinTimeBetweenPinch) {
768         if (zoom >= 0.0 && d->mAdapter->canZoom()) {
769             d->setZoom(zoom, zoomCenter);
770         }
771     }
772 }
773 
rotationsGesture(qreal rotation)774 void DocumentView::rotationsGesture(qreal rotation)
775 {
776     if (rotation > 0.0) {
777         auto *op = new TransformImageOperation(ROT_90);
778         op->applyToDocument(d->mDocument);
779     } else if (rotation < 0.0) {
780         auto *op = new TransformImageOperation(ROT_270);
781         op->applyToDocument(d->mDocument);
782     }
783 }
784 
swipeRight()785 void DocumentView::swipeRight()
786 {
787     const QPoint scrollPos = d->mAdapter->scrollPos().toPoint();
788     if (scrollPos.x() <= 1) {
789         Q_EMIT d->mAdapter->previousImageRequested();
790     }
791 }
792 
swipeLeft()793 void DocumentView::swipeLeft()
794 {
795     const QSizeF dipSize = d->mAdapter->imageView()->dipDocumentSize();
796     const QPoint scrollPos = d->mAdapter->scrollPos().toPoint();
797     const int width = dipSize.width() * d->mAdapter->zoom();
798     const QRect visibleRect = d->mAdapter->visibleDocumentRect().toRect();
799     const int x = scrollPos.x() + visibleRect.width();
800     if (x >= (width - 1)) {
801         Q_EMIT d->mAdapter->nextImageRequested();
802     }
803 }
804 
panGesture(const QPointF & delta)805 void DocumentView::panGesture(const QPointF &delta)
806 {
807     d->mAdapter->setScrollPos(d->mAdapter->scrollPos() + delta);
808 }
809 
startDragFromTouch(const QPoint &)810 void DocumentView::startDragFromTouch(const QPoint &)
811 {
812     d->startDragIfSensible();
813 }
814 
resizeEvent(QGraphicsSceneResizeEvent * event)815 void DocumentView::resizeEvent(QGraphicsSceneResizeEvent *event)
816 {
817     d->resizeAdapterWidget();
818     d->updateZoomSnapValues();
819     QGraphicsWidget::resizeEvent(event);
820 }
821 
mousePressEvent(QGraphicsSceneMouseEvent * event)822 void DocumentView::mousePressEvent(QGraphicsSceneMouseEvent *event)
823 {
824     QGraphicsWidget::mousePressEvent(event);
825 
826     if (d->mAdapter->canZoom() && event->button() == Qt::MiddleButton) {
827         if (event->modifiers() == Qt::NoModifier) {
828             toggleZoomToFit();
829         } else if (event->modifiers() == Qt::SHIFT) {
830             toggleZoomToFill();
831         }
832     }
833 }
834 
wheelEvent(QGraphicsSceneWheelEvent * event)835 void DocumentView::wheelEvent(QGraphicsSceneWheelEvent *event)
836 {
837     if (d->mAdapter->canZoom()) {
838         if ((event->modifiers() & Qt::ControlModifier)
839             || (GwenviewConfig::mouseWheelBehavior() == MouseWheelBehavior::Zoom && event->modifiers() == Qt::NoModifier)) {
840             d->controlWheelAccumulatedDelta += event->delta();
841             // Ctrl + wheel => zoom in or out
842             if (d->controlWheelAccumulatedDelta >= QWheelEvent::DefaultDeltasPerStep) {
843                 zoomIn(event->pos());
844                 d->controlWheelAccumulatedDelta = 0;
845             } else if (d->controlWheelAccumulatedDelta <= -QWheelEvent::DefaultDeltasPerStep) {
846                 zoomOut(event->pos());
847                 d->controlWheelAccumulatedDelta = 0;
848             }
849             return;
850         }
851     }
852     if (GwenviewConfig::mouseWheelBehavior() == MouseWheelBehavior::Browse && event->modifiers() == Qt::NoModifier) {
853         d->controlWheelAccumulatedDelta += event->delta();
854         // Browse with mouse wheel
855         if (d->controlWheelAccumulatedDelta >= QWheelEvent::DefaultDeltasPerStep) {
856             Q_EMIT previousImageRequested();
857             d->controlWheelAccumulatedDelta = 0;
858         } else if (d->controlWheelAccumulatedDelta <= -QWheelEvent::DefaultDeltasPerStep) {
859             Q_EMIT nextImageRequested();
860             d->controlWheelAccumulatedDelta = 0;
861         }
862         return;
863     }
864     // Scroll
865     qreal dx = 0;
866     // 16 = pixels for one line
867     // 120: see QWheelEvent::angleDelta().y() doc
868     qreal dy = -qApp->wheelScrollLines() * 16 * event->delta() / 120;
869     if (event->orientation() == Qt::Horizontal) {
870         qSwap(dx, dy);
871     }
872     d->mAdapter->setScrollPos(d->mAdapter->scrollPos() + QPointF(dx, dy));
873 }
874 
contextMenuEvent(QGraphicsSceneContextMenuEvent * event)875 void DocumentView::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
876 {
877     // Filter out context menu if Ctrl is down to avoid showing it when
878     // zooming out with Ctrl + Right button
879     if (event->modifiers() != Qt::ControlModifier) {
880         Q_EMIT contextMenuRequested();
881     }
882 }
883 
paint(QPainter * painter,const QStyleOptionGraphicsItem *,QWidget *)884 void DocumentView::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/)
885 {
886     // Fill background manually, because setAutoFillBackground(true) fill with QPalette::Window,
887     // but our palettes use QPalette::Base for the background color/texture
888     painter->fillRect(rect(), palette().base());
889 
890     // Selection indicator/highlight
891     if (d->mCompareMode && d->mCurrent) {
892         painter->save();
893         painter->setBrush(Qt::NoBrush);
894         painter->setPen(QPen(palette().highlight().color(), 2));
895         painter->setRenderHint(QPainter::Antialiasing);
896         const QRectF visibleRectF = mapRectFromItem(d->mAdapter->widget(), d->mAdapter->visibleDocumentRect());
897         // Round the point and size independently. This is different than calling toRect(),
898         // and is necessary to keep consistent rects, otherwise the selection rect can be
899         // drawn 1 pixel too big or small.
900         const QRect visibleRect = QRect(visibleRectF.topLeft().toPoint(), visibleRectF.size().toSize());
901         const QRect selectionRect = visibleRect.adjusted(-1, -1, 1, 1);
902         painter->drawRoundedRect(selectionRect, 3, 3);
903         painter->restore();
904     }
905 }
906 
slotBusyChanged(const QUrl &,bool busy)907 void DocumentView::slotBusyChanged(const QUrl &, bool busy)
908 {
909     if (busy) {
910         d->showLoadingIndicator();
911     } else {
912         d->hideLoadingIndicator();
913     }
914 }
915 
minimumZoom() const916 qreal DocumentView::minimumZoom() const
917 {
918     // There is no point zooming out less than zoomToFit, but make sure it does
919     // not get too small either
920     return qBound(qreal(0.001), d->mAdapter->computeZoomToFit(), qreal(1.));
921 }
922 
setCompareMode(bool compare)923 void DocumentView::setCompareMode(bool compare)
924 {
925     d->mCompareMode = compare;
926     if (compare) {
927         d->mHud->show();
928         d->mHud->setZValue(1);
929     } else {
930         d->mHud->hide();
931     }
932 }
933 
setCurrent(bool value)934 void DocumentView::setCurrent(bool value)
935 {
936     d->mCurrent = value;
937     if (value) {
938         d->mAdapter->widget()->setFocus();
939         d->updateCaption();
940     }
941     update();
942 }
943 
isCurrent() const944 bool DocumentView::isCurrent() const
945 {
946     return d->mCurrent;
947 }
948 
position() const949 QPoint DocumentView::position() const
950 {
951     return d->mAdapter->scrollPos().toPoint();
952 }
953 
setPosition(const QPoint & pos)954 void DocumentView::setPosition(const QPoint &pos)
955 {
956     d->mAdapter->setScrollPos(pos);
957 }
958 
document() const959 Document::Ptr DocumentView::document() const
960 {
961     return d->mDocument;
962 }
963 
url() const964 QUrl DocumentView::url() const
965 {
966     Document::Ptr doc = d->mDocument;
967     return doc ? doc->url() : QUrl();
968 }
969 
emitHudDeselectClicked()970 void DocumentView::emitHudDeselectClicked()
971 {
972     Q_EMIT hudDeselectClicked(this);
973 }
974 
emitHudTrashClicked()975 void DocumentView::emitHudTrashClicked()
976 {
977     Q_EMIT hudTrashClicked(this);
978 }
979 
emitFocused()980 void DocumentView::emitFocused()
981 {
982     Q_EMIT focused(this);
983 }
984 
setGeometry(const QRectF & rect)985 void DocumentView::setGeometry(const QRectF &rect)
986 {
987     QGraphicsWidget::setGeometry(rect);
988     if (d->mBirdEyeView) {
989         d->mBirdEyeView->slotZoomOrSizeChanged();
990     }
991 }
992 
moveTo(const QRect & rect)993 void DocumentView::moveTo(const QRect &rect)
994 {
995     if (d->mMoveAnimation) {
996         d->mMoveAnimation.data()->setEndValue(rect);
997     } else {
998         setGeometry(rect);
999     }
1000 }
1001 
moveToAnimated(const QRect & rect)1002 void DocumentView::moveToAnimated(const QRect &rect)
1003 {
1004     auto *anim = new QPropertyAnimation(this, "geometry");
1005     anim->setStartValue(geometry());
1006     anim->setEndValue(rect);
1007     anim->setDuration(DocumentView::AnimDuration);
1008     connect(anim, &QAbstractAnimation::finished, this, &DocumentView::isAnimatedChanged);
1009     d->mMoveAnimation = anim;
1010     Q_EMIT isAnimatedChanged();
1011     anim->start(QAbstractAnimation::DeleteWhenStopped);
1012 }
1013 
fadeIn()1014 QPropertyAnimation *DocumentView::fadeIn()
1015 {
1016     d->fadeTo(1);
1017     return d->mFadeAnimation.data();
1018 }
1019 
fadeOut()1020 void DocumentView::fadeOut()
1021 {
1022     d->fadeTo(0);
1023 }
1024 
slotFadeInFinished()1025 void DocumentView::slotFadeInFinished()
1026 {
1027     Q_EMIT fadeInFinished(this);
1028 }
1029 
isAnimated() const1030 bool DocumentView::isAnimated() const
1031 {
1032     return d->mMoveAnimation || d->mFadeAnimation;
1033 }
1034 
sceneEventFilter(QGraphicsItem *,QEvent * event)1035 bool DocumentView::sceneEventFilter(QGraphicsItem *, QEvent *event)
1036 {
1037     if (event->type() == QEvent::GraphicsSceneMousePress) {
1038         const QGraphicsSceneMouseEvent *mouseEvent = static_cast<QGraphicsSceneMouseEvent *>(event);
1039         if (mouseEvent->button() == Qt::LeftButton) {
1040             d->mDragStartPosition = mouseEvent->pos();
1041         }
1042         QMetaObject::invokeMethod(this, &DocumentView::emitFocused, Qt::QueuedConnection);
1043     } else if (event->type() == QEvent::GraphicsSceneHoverMove) {
1044         if (d->mBirdEyeView) {
1045             d->mBirdEyeView->onMouseMoved();
1046         }
1047     } else if (event->type() == QEvent::GraphicsSceneMouseMove) {
1048         const QGraphicsSceneMouseEvent *mouseEvent = static_cast<QGraphicsSceneMouseEvent *>(event);
1049         // in some older version of Qt, Qt synthesize a mouse event from the touch event
1050         // we need to suppress this.
1051         // I need this for my working system (OpenSUSE Leap 15.0, Qt 5.9.4)
1052         if (mouseEvent->source() == Qt::MouseEventSynthesizedByQt) {
1053             return true;
1054         }
1055         // We need to check if the Left mouse button is pressed, otherwise this can lead
1056         // to starting a drag & drop sequence using the Forward/Backward mouse buttons
1057         if (!mouseEvent->buttons().testFlag(Qt::LeftButton)) {
1058             return false;
1059         }
1060         const qreal dragDistance = (mouseEvent->pos() - d->mDragStartPosition).manhattanLength();
1061         const qreal minDistanceToStartDrag = QGuiApplication::styleHints()->startDragDistance();
1062         if (!d->canPan() && dragDistance >= minDistanceToStartDrag) {
1063             d->startDragIfSensible();
1064         }
1065     }
1066     return false;
1067 }
1068 
currentTool() const1069 AbstractRasterImageViewTool *DocumentView::currentTool() const
1070 {
1071     return imageView() ? imageView()->currentTool() : nullptr;
1072 }
1073 
sortKey() const1074 int DocumentView::sortKey() const
1075 {
1076     return d->mSortKey;
1077 }
1078 
setSortKey(int sortKey)1079 void DocumentView::setSortKey(int sortKey)
1080 {
1081     d->mSortKey = sortKey;
1082 }
1083 
hideAndDeleteLater()1084 void DocumentView::hideAndDeleteLater()
1085 {
1086     hide();
1087     deleteLater();
1088 }
1089 
setGraphicsEffectOpacity(qreal opacity)1090 void DocumentView::setGraphicsEffectOpacity(qreal opacity)
1091 {
1092     d->mOpacityEffect->setOpacity(opacity);
1093 }
1094 
dragEnterEvent(QGraphicsSceneDragDropEvent * event)1095 void DocumentView::dragEnterEvent(QGraphicsSceneDragDropEvent *event)
1096 {
1097     QGraphicsWidget::dragEnterEvent(event);
1098 
1099     const auto urls = KUrlMimeData::urlsFromMimeData(event->mimeData());
1100     bool acceptDrag = !urls.isEmpty();
1101     if (urls.size() == 1 && urls.first() == url()) {
1102         // Do not allow dragging a single image onto itself
1103         acceptDrag = false;
1104     }
1105     event->setAccepted(acceptDrag);
1106 }
1107 
dropEvent(QGraphicsSceneDragDropEvent * event)1108 void DocumentView::dropEvent(QGraphicsSceneDragDropEvent *event)
1109 {
1110     QGraphicsWidget::dropEvent(event);
1111     // Since we're capturing drops in View mode, we only support one url
1112     const QUrl url = event->mimeData()->urls().first();
1113     if (UrlUtils::urlIsDirectory(url)) {
1114         Q_EMIT openDirUrlRequested(url);
1115     } else {
1116         Q_EMIT openUrlRequested(url);
1117     }
1118 }
1119 
dragThumbnailLoaded(const KFileItem & item,const QPixmap & pix)1120 void DocumentView::dragThumbnailLoaded(const KFileItem &item, const QPixmap &pix)
1121 {
1122     d->setDragPixmap(pix);
1123     d->executeDrag();
1124     d->mDragThumbnailProvider->removeItems(KFileItemList({item}));
1125 }
1126 
dragThumbnailLoadingFailed(const KFileItem & item)1127 void DocumentView::dragThumbnailLoadingFailed(const KFileItem &item)
1128 {
1129     d->executeDrag();
1130     d->mDragThumbnailProvider->removeItems(KFileItemList({item}));
1131 }
1132 
1133 } // namespace
1134