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

0001 // vim: set tabstop=4 shiftwidth=4 expandtab:
0002 /*
0003 Gwenview: an image viewer
0004 Copyright 2011 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 "abstractimageview.h"
0023 
0024 // Local
0025 #include "alphabackgrounditem.h"
0026 
0027 // KF
0028 
0029 // Qt
0030 #include <QApplication>
0031 #include <QCursor>
0032 #include <QGraphicsScene>
0033 #include <QGraphicsSceneMouseEvent>
0034 #include <QGraphicsView>
0035 #include <QGuiApplication>
0036 #include <QStandardPaths>
0037 namespace Gwenview
0038 {
0039 static const int UNIT_STEP = 16;
0040 
0041 struct AbstractImageViewPrivate {
0042     enum Verbosity {
0043         Silent,
0044         Notify,
0045     };
0046     AbstractImageView *q = nullptr;
0047     QCursor mZoomCursor;
0048     Document::Ptr mDocument;
0049 
0050     bool mControlKeyIsDown;
0051     bool mEnlargeSmallerImages;
0052 
0053     qreal mZoom;
0054     bool mZoomToFit;
0055     bool mZoomToFill;
0056     QPointF mImageOffset;
0057     QPointF mScrollPos;
0058     QPointF mLastDragPos;
0059     QSizeF mDocumentSize;
0060 
0061     AlphaBackgroundItem *mBackgroundItem;
0062 
0063     void adjustImageOffset(Verbosity verbosity = Notify)
0064     {
0065         QSizeF zoomedDocSize = q->dipDocumentSize() * mZoom;
0066         QSizeF viewSize = q->boundingRect().size();
0067         QPointF offset(qMax((viewSize.width() - zoomedDocSize.width()) / 2, qreal(0.)), qMax((viewSize.height() - zoomedDocSize.height()) / 2, qreal(0.)));
0068 
0069         if (offset != mImageOffset) {
0070             mImageOffset = offset;
0071 
0072             if (verbosity == Notify) {
0073                 q->onImageOffsetChanged();
0074             }
0075         }
0076     }
0077 
0078     void adjustScrollPos(Verbosity verbosity = Notify)
0079     {
0080         setScrollPos(mScrollPos, verbosity);
0081     }
0082 
0083     void setScrollPos(const QPointF &_newPos, Verbosity verbosity = Notify)
0084     {
0085         if (!mDocument) {
0086             mScrollPos = _newPos;
0087             return;
0088         }
0089         const QSizeF zoomedDocSize = q->dipDocumentSize() * mZoom;
0090         const QSizeF viewSize = q->boundingRect().size();
0091         const QPointF newPos(qBound(qreal(0.), _newPos.x(), std::max(0., zoomedDocSize.width() - viewSize.width())),
0092                              qBound(qreal(0.), _newPos.y(), std::max(0., zoomedDocSize.height() - viewSize.height())));
0093         if (newPos != mScrollPos) {
0094             const QPointF oldPos = mScrollPos;
0095             mScrollPos = newPos;
0096 
0097             if (verbosity == Notify) {
0098                 q->onScrollPosChanged(oldPos);
0099             }
0100             // No verbosity test: we always notify the outside world about
0101             // scrollPos changes
0102             Q_EMIT q->scrollPosChanged();
0103         }
0104     }
0105 
0106     void setupZoomCursor()
0107     {
0108         // We do not use "appdata" here because that does not work when this
0109         // code is called from a KPart.
0110         const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("gwenview/cursors/zoom.png"));
0111         QPixmap cursorPixmap = QPixmap(path);
0112         mZoomCursor = QCursor(cursorPixmap, 11, 11);
0113     }
0114 
0115     AbstractImageViewPrivate(AbstractImageView *parent)
0116         : q(parent)
0117         , mBackgroundItem(new AlphaBackgroundItem{q})
0118     {
0119         mBackgroundItem->setVisible(false);
0120     }
0121 
0122     void checkAndRequestZoomAction(const QGraphicsSceneMouseEvent *event)
0123     {
0124         if (event->modifiers() & Qt::ControlModifier) {
0125             if (event->button() == Qt::LeftButton) {
0126                 Q_EMIT q->zoomInRequested(event->pos());
0127             } else if (event->button() == Qt::RightButton) {
0128                 Q_EMIT q->zoomOutRequested(event->pos());
0129             }
0130         }
0131     }
0132 };
0133 
0134 AbstractImageView::AbstractImageView(QGraphicsItem *parent)
0135     : QGraphicsWidget(parent)
0136     , d(new AbstractImageViewPrivate(this))
0137 {
0138     d->mControlKeyIsDown = false;
0139     d->mEnlargeSmallerImages = false;
0140     d->mZoom = 1;
0141     d->mZoomToFit = true;
0142     d->mZoomToFill = false;
0143     d->mImageOffset = QPointF(0, 0);
0144     d->mScrollPos = QPointF(0, 0);
0145     setFocusPolicy(Qt::WheelFocus);
0146     setFlag(ItemIsSelectable);
0147     setFlag(ItemClipsChildrenToShape);
0148     setAcceptHoverEvents(true);
0149     d->setupZoomCursor();
0150     updateCursor();
0151 }
0152 
0153 AbstractImageView::~AbstractImageView()
0154 {
0155     if (d->mDocument) {
0156         d->mDocument->stopAnimation();
0157     }
0158     delete d;
0159 }
0160 
0161 Document::Ptr AbstractImageView::document() const
0162 {
0163     return d->mDocument;
0164 }
0165 
0166 void AbstractImageView::setDocument(const Document::Ptr &doc)
0167 {
0168     if (d->mDocument) {
0169         disconnect(d->mDocument.data(), nullptr, this, nullptr);
0170     }
0171     d->mDocument = doc;
0172     if (d->mDocument) {
0173         connect(d->mDocument.data(), &Document::imageRectUpdated, this, &AbstractImageView::onImageRectUpdated);
0174     }
0175 
0176     loadFromDocument();
0177 }
0178 
0179 QSizeF AbstractImageView::documentSize() const
0180 {
0181     return d->mDocument ? d->mDocument->size() : QSizeF();
0182 }
0183 
0184 QSizeF AbstractImageView::dipDocumentSize() const
0185 {
0186     return d->mDocument ? d->mDocument->size() / devicePixelRatio() : QSizeF();
0187 }
0188 
0189 qreal AbstractImageView::zoom() const
0190 {
0191     return d->mZoom;
0192 }
0193 
0194 void AbstractImageView::setZoom(qreal zoom, const QPointF &_center, AbstractImageView::UpdateType updateType)
0195 {
0196     if (!d->mDocument) {
0197         d->mZoom = zoom;
0198         return;
0199     }
0200 
0201     if (updateType == UpdateIfNecessary && qFuzzyCompare(zoom, d->mZoom) && documentSize() == d->mDocumentSize) {
0202         return;
0203     }
0204     qreal oldZoom = d->mZoom;
0205     d->mZoom = zoom;
0206     d->mDocumentSize = documentSize();
0207 
0208     QPointF center;
0209     if (_center == QPointF(-1, -1)) {
0210         center = boundingRect().center();
0211     } else {
0212         center = _center;
0213     }
0214 
0215     /*
0216     We want to keep the point at viewport coordinates "center" at the same
0217     position after zooming. The coordinates of this point in image coordinates
0218     can be expressed like this:
0219 
0220                           oldScroll + center
0221     imagePointAtOldZoom = ------------------
0222                                oldZoom
0223 
0224                        scroll + center
0225     imagePointAtZoom = ---------------
0226                             zoom
0227 
0228     So we want:
0229 
0230         imagePointAtOldZoom = imagePointAtZoom
0231 
0232         oldScroll + center   scroll + center
0233     <=> ------------------ = ---------------
0234               oldZoom             zoom
0235 
0236                   zoom
0237     <=> scroll = ------- (oldScroll + center) - center
0238                  oldZoom
0239     */
0240 
0241     /*
0242     Compute oldScroll
0243     It's useless to take the new offset in consideration because if a direction
0244     of the new offset is not 0, we won't be able to center on a specific point
0245     in that direction.
0246     */
0247     QPointF oldScroll = scrollPos() - imageOffset();
0248 
0249     QPointF scroll = (zoom / oldZoom) * (oldScroll + center) - center;
0250 
0251     d->adjustImageOffset(AbstractImageViewPrivate::Silent);
0252     d->setScrollPos(scroll, AbstractImageViewPrivate::Silent);
0253     onZoomChanged();
0254     Q_EMIT zoomChanged(d->mZoom);
0255 }
0256 
0257 bool AbstractImageView::zoomToFit() const
0258 {
0259     return d->mZoomToFit;
0260 }
0261 
0262 bool AbstractImageView::zoomToFill() const
0263 {
0264     return d->mZoomToFill;
0265 }
0266 
0267 void AbstractImageView::setZoomToFit(bool on)
0268 {
0269     if (d->mZoomToFit == on) {
0270         return;
0271     }
0272     d->mZoomToFit = on;
0273     if (on) {
0274         d->mZoomToFill = false;
0275         setZoom(computeZoomToFit());
0276     }
0277     // We do not set zoom to 1 if zoomToFit is off, this is up to the code
0278     // calling us. It may went to zoom to some other level and/or to zoom on
0279     // a particular position
0280     Q_EMIT zoomToFitChanged(d->mZoomToFit);
0281 }
0282 
0283 void AbstractImageView::setZoomToFill(bool on, const QPointF &center)
0284 {
0285     if (d->mZoomToFill == on) {
0286         return;
0287     }
0288     d->mZoomToFill = on;
0289     if (on) {
0290         d->mZoomToFit = false;
0291         setZoom(computeZoomToFill(), center);
0292     }
0293     // We do not set zoom to 1 if zoomToFit is off, this is up to the code
0294     // calling us. It may went to zoom to some other level and/or to zoom on
0295     // a particular position
0296     Q_EMIT zoomToFillChanged(d->mZoomToFill);
0297 }
0298 
0299 void AbstractImageView::resizeEvent(QGraphicsSceneResizeEvent *event)
0300 {
0301     QGraphicsWidget::resizeEvent(event);
0302     if (d->mZoomToFit) {
0303         // setZoom() calls adjustImageOffset(), but only if the zoom changes.
0304         // If the view is resized but does not cause a zoom change, we call
0305         // adjustImageOffset() ourself.
0306         const qreal newZoom = computeZoomToFit();
0307         if (qFuzzyCompare(zoom(), newZoom)) {
0308             d->adjustImageOffset(AbstractImageViewPrivate::Notify);
0309         } else {
0310             setZoom(newZoom);
0311         }
0312     } else if (d->mZoomToFill) {
0313         const qreal newZoom = computeZoomToFill();
0314         if (qFuzzyCompare(zoom(), newZoom)) {
0315             d->adjustImageOffset(AbstractImageViewPrivate::Notify);
0316         } else {
0317             setZoom(newZoom);
0318         }
0319     } else {
0320         d->adjustImageOffset();
0321         d->adjustScrollPos();
0322     }
0323 }
0324 
0325 void AbstractImageView::focusInEvent(QFocusEvent *event)
0326 {
0327     QGraphicsWidget::focusInEvent(event);
0328 
0329     // We might have missed a keyReleaseEvent for the control key, e.g. for Ctrl+O
0330     const bool controlKeyIsCurrentlyDown = QGuiApplication::queryKeyboardModifiers() & Qt::ControlModifier;
0331     if (d->mControlKeyIsDown != controlKeyIsCurrentlyDown) {
0332         d->mControlKeyIsDown = controlKeyIsCurrentlyDown;
0333         updateCursor();
0334     }
0335 }
0336 
0337 qreal AbstractImageView::computeZoomToFit() const
0338 {
0339     const QSizeF docSize = dipDocumentSize();
0340     if (docSize.isEmpty()) {
0341         return 1;
0342     }
0343     const QSizeF viewSize = boundingRect().size();
0344     const qreal fitWidth = viewSize.width() / docSize.width();
0345     const qreal fitHeight = viewSize.height() / docSize.height();
0346     qreal fit = qMin(fitWidth, fitHeight);
0347     if (!d->mEnlargeSmallerImages) {
0348         fit = qMin(fit, qreal(1.));
0349     }
0350     return fit;
0351 }
0352 
0353 qreal AbstractImageView::computeZoomToFill() const
0354 {
0355     const QSizeF docSize = dipDocumentSize();
0356     if (docSize.isEmpty()) {
0357         return 1;
0358     }
0359     const QSizeF viewSize = boundingRect().size();
0360     const qreal fitWidth = viewSize.width() / docSize.width();
0361     const qreal fitHeight = viewSize.height() / docSize.height();
0362     qreal fill = qMax(fitWidth, fitHeight);
0363     if (!d->mEnlargeSmallerImages) {
0364         fill = qMin(fill, qreal(1.));
0365     }
0366     return fill;
0367 }
0368 
0369 void AbstractImageView::mousePressEvent(QGraphicsSceneMouseEvent *event)
0370 {
0371     QGraphicsItem::mousePressEvent(event);
0372 
0373     d->checkAndRequestZoomAction(event);
0374 
0375     // Prepare for panning or dragging
0376     if (event->button() == Qt::LeftButton) {
0377         d->mLastDragPos = event->pos();
0378         updateCursor();
0379     }
0380 }
0381 
0382 void AbstractImageView::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
0383 {
0384     QGraphicsItem::mouseMoveEvent(event);
0385 
0386     QPointF mousePos = event->pos();
0387     QPointF newScrollPos = d->mScrollPos + d->mLastDragPos - mousePos;
0388 
0389 #if 0 // commented out due to mouse pointer warping around, bug in Qt?
0390     // Wrap mouse pos
0391     qreal maxWidth = boundingRect().width();
0392     qreal maxHeight = boundingRect().height();
0393     // We need a margin because if the window is maximized, the mouse may not
0394     // be able to go past the bounding rect.
0395     // The mouse get placed 1 pixel before/after the margin to avoid getting
0396     // considered as needing to wrap the other way in next mouseMoveEvent
0397     // (because we don't check the move vector)
0398     const int margin = 5;
0399     if (mousePos.x() <= margin) {
0400         mousePos.setX(maxWidth - margin - 1);
0401     } else if (mousePos.x() >= maxWidth - margin) {
0402         mousePos.setX(margin + 1);
0403     }
0404     if (mousePos.y() <= margin) {
0405         mousePos.setY(maxHeight - margin - 1);
0406     } else if (mousePos.y() >= maxHeight - margin) {
0407         mousePos.setY(margin + 1);
0408     }
0409 
0410     // Set mouse pos (Hackish translation to screen coords!)
0411     QPointF screenDelta = event->screenPos() - event->pos();
0412     QCursor::setPos((mousePos + screenDelta).toPoint());
0413 #endif
0414 
0415     d->mLastDragPos = mousePos;
0416     d->setScrollPos(newScrollPos);
0417 }
0418 
0419 void AbstractImageView::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
0420 {
0421     QGraphicsItem::mouseReleaseEvent(event);
0422     if (!d->mLastDragPos.isNull()) {
0423         d->mLastDragPos = QPointF();
0424     }
0425     updateCursor();
0426 }
0427 
0428 void AbstractImageView::keyPressEvent(QKeyEvent *event)
0429 {
0430     if (event->key() == Qt::Key_Control) {
0431         d->mControlKeyIsDown = true;
0432         updateCursor();
0433         return;
0434     }
0435     if (zoomToFit() || qFuzzyCompare(computeZoomToFit(), zoom())) {
0436         if (event->modifiers() != Qt::NoModifier) {
0437             return;
0438         }
0439 
0440         switch (event->key()) {
0441         case Qt::Key_Left:
0442             if (QApplication::isRightToLeft()) {
0443                 Q_EMIT nextImageRequested();
0444             } else {
0445                 Q_EMIT previousImageRequested();
0446             }
0447             break;
0448         case Qt::Key_Up:
0449             Q_EMIT previousImageRequested();
0450             break;
0451         case Qt::Key_Right:
0452             if (QApplication::isRightToLeft()) {
0453                 Q_EMIT previousImageRequested();
0454             } else {
0455                 Q_EMIT nextImageRequested();
0456             }
0457             break;
0458         case Qt::Key_Down:
0459             Q_EMIT nextImageRequested();
0460             break;
0461         default:
0462             break;
0463         }
0464         return;
0465     }
0466 
0467     QPointF delta(0, 0);
0468     const qreal pageStep = boundingRect().height();
0469     qreal unitStep;
0470 
0471     if (event->modifiers() & Qt::ShiftModifier) {
0472         unitStep = pageStep / 2;
0473     } else {
0474         unitStep = UNIT_STEP;
0475     }
0476     switch (event->key()) {
0477     case Qt::Key_Left:
0478         delta.setX(-unitStep);
0479         break;
0480     case Qt::Key_Right:
0481         delta.setX(unitStep);
0482         break;
0483     case Qt::Key_Up:
0484         delta.setY(-unitStep);
0485         break;
0486     case Qt::Key_Down:
0487         delta.setY(unitStep);
0488         break;
0489     case Qt::Key_PageUp:
0490         delta.setY(-pageStep);
0491         break;
0492     case Qt::Key_PageDown:
0493         delta.setY(pageStep);
0494         break;
0495     case Qt::Key_Home:
0496         d->setScrollPos(QPointF(d->mScrollPos.x(), 0));
0497         return;
0498     case Qt::Key_End:
0499         d->setScrollPos(QPointF(d->mScrollPos.x(), dipDocumentSize().height() * zoom()));
0500         return;
0501     default:
0502         return;
0503     }
0504     d->setScrollPos(d->mScrollPos + delta);
0505 }
0506 
0507 void AbstractImageView::keyReleaseEvent(QKeyEvent *event)
0508 {
0509     if (event->key() == Qt::Key_Control) {
0510         d->mControlKeyIsDown = false;
0511         updateCursor();
0512     }
0513 }
0514 
0515 void AbstractImageView::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
0516 {
0517     if (event->modifiers() == Qt::NoModifier && event->button() == Qt::LeftButton) {
0518         Q_EMIT toggleFullScreenRequested();
0519     }
0520 
0521     d->checkAndRequestZoomAction(event);
0522 }
0523 
0524 QPointF AbstractImageView::imageOffset() const
0525 {
0526     return d->mImageOffset;
0527 }
0528 
0529 QPointF AbstractImageView::scrollPos() const
0530 {
0531     return d->mScrollPos;
0532 }
0533 
0534 void AbstractImageView::setScrollPos(const QPointF &pos)
0535 {
0536     d->setScrollPos(pos);
0537 }
0538 
0539 qreal AbstractImageView::devicePixelRatio() const
0540 {
0541     if (!scene()->views().isEmpty()) {
0542         return scene()->views().constFirst()->devicePixelRatio();
0543     } else {
0544         return qApp->devicePixelRatio();
0545     }
0546 }
0547 
0548 QPointF AbstractImageView::mapToView(const QPointF &imagePos) const
0549 {
0550     return imagePos / devicePixelRatio() * d->mZoom + d->mImageOffset - d->mScrollPos;
0551 }
0552 
0553 QPoint AbstractImageView::mapToView(const QPoint &imagePos) const
0554 {
0555     return mapToView(QPointF(imagePos)).toPoint();
0556 }
0557 
0558 QRectF AbstractImageView::mapToView(const QRectF &imageRect) const
0559 {
0560     return QRectF(mapToView(imageRect.topLeft()), imageRect.size() * zoom() / devicePixelRatio());
0561 }
0562 
0563 QRect AbstractImageView::mapToView(const QRect &imageRect) const
0564 {
0565     return QRect(mapToView(imageRect.topLeft()), imageRect.size() * zoom() / devicePixelRatio());
0566 }
0567 
0568 QPointF AbstractImageView::mapToImage(const QPointF &viewPos) const
0569 {
0570     return (viewPos - d->mImageOffset + d->mScrollPos) / d->mZoom * devicePixelRatio();
0571 }
0572 
0573 QPoint AbstractImageView::mapToImage(const QPoint &viewPos) const
0574 {
0575     return mapToImage(QPointF(viewPos)).toPoint();
0576 }
0577 
0578 QRectF AbstractImageView::mapToImage(const QRectF &viewRect) const
0579 {
0580     return QRectF(mapToImage(viewRect.topLeft()), viewRect.size() / zoom() * devicePixelRatio());
0581 }
0582 
0583 QRect AbstractImageView::mapToImage(const QRect &viewRect) const
0584 {
0585     return QRect(mapToImage(viewRect.topLeft()), viewRect.size() / zoom() * devicePixelRatio());
0586 }
0587 
0588 void AbstractImageView::setEnlargeSmallerImages(bool value)
0589 {
0590     d->mEnlargeSmallerImages = value;
0591     if (zoomToFit()) {
0592         setZoom(computeZoomToFit());
0593     }
0594 }
0595 
0596 void AbstractImageView::updateCursor()
0597 {
0598     if (d->mControlKeyIsDown) {
0599         setCursor(d->mZoomCursor);
0600     } else {
0601         if (d->mLastDragPos.isNull()) {
0602             setCursor(Qt::OpenHandCursor);
0603         } else {
0604             setCursor(Qt::ClosedHandCursor);
0605         }
0606     }
0607 }
0608 
0609 QSizeF AbstractImageView::visibleImageSize() const
0610 {
0611     if (!document()) {
0612         return QSizeF();
0613     }
0614     QSizeF size = dipDocumentSize() * zoom();
0615     return size.boundedTo(boundingRect().size());
0616 }
0617 
0618 void AbstractImageView::applyPendingScrollPos()
0619 {
0620     d->adjustImageOffset();
0621     d->adjustScrollPos();
0622 }
0623 
0624 void AbstractImageView::resetDragCursor()
0625 {
0626     d->mLastDragPos = QPointF();
0627     updateCursor();
0628 }
0629 
0630 AlphaBackgroundItem *AbstractImageView::backgroundItem() const
0631 {
0632     return d->mBackgroundItem;
0633 }
0634 
0635 void AbstractImageView::onImageRectUpdated()
0636 {
0637     if (zoomToFit()) {
0638         setZoom(computeZoomToFit());
0639     } else if (zoomToFill()) {
0640         setZoom(computeZoomToFill());
0641     } else {
0642         applyPendingScrollPos();
0643     }
0644 
0645     update();
0646 }
0647 
0648 } // namespace
0649 
0650 #include "moc_abstractimageview.cpp"