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