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 ¢er = 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