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