651 lines
18 KiB
C++

// vim: set tabstop=4 shiftwidth=4 expandtab:
/*
Gwenview: an image viewer
Copyright 2011 Aurélien Gâteau <agateau@kde.org>
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.
*/
// Self
#include "abstractimageview.h"
// Local
#include "alphabackgrounditem.h"
// KF
// Qt
#include <QApplication>
#include <QCursor>
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsView>
#include <QGuiApplication>
#include <QStandardPaths>
namespace Gwenview
{
static const int UNIT_STEP = 16;
struct AbstractImageViewPrivate {
enum Verbosity {
Silent,
Notify,
};
AbstractImageView *q = nullptr;
QCursor mZoomCursor;
Document::Ptr mDocument;
bool mControlKeyIsDown;
bool mEnlargeSmallerImages;
qreal mZoom;
bool mZoomToFit;
bool mZoomToFill;
QPointF mImageOffset;
QPointF mScrollPos;
QPointF mLastDragPos;
QSizeF mDocumentSize;
AlphaBackgroundItem *mBackgroundItem;
void adjustImageOffset(Verbosity verbosity = Notify)
{
QSizeF zoomedDocSize = q->dipDocumentSize() * mZoom;
QSizeF viewSize = q->boundingRect().size();
QPointF offset(qMax((viewSize.width() - zoomedDocSize.width()) / 2, qreal(0.)), qMax((viewSize.height() - zoomedDocSize.height()) / 2, qreal(0.)));
if (offset != mImageOffset) {
mImageOffset = offset;
if (verbosity == Notify) {
q->onImageOffsetChanged();
}
}
}
void adjustScrollPos(Verbosity verbosity = Notify)
{
setScrollPos(mScrollPos, verbosity);
}
void setScrollPos(const QPointF &_newPos, Verbosity verbosity = Notify)
{
if (!mDocument) {
mScrollPos = _newPos;
return;
}
const QSizeF zoomedDocSize = q->dipDocumentSize() * mZoom;
const QSizeF viewSize = q->boundingRect().size();
const QPointF newPos(qBound(qreal(0.), _newPos.x(), std::max(0., zoomedDocSize.width() - viewSize.width())),
qBound(qreal(0.), _newPos.y(), std::max(0., zoomedDocSize.height() - viewSize.height())));
if (newPos != mScrollPos) {
const QPointF oldPos = mScrollPos;
mScrollPos = newPos;
if (verbosity == Notify) {
q->onScrollPosChanged(oldPos);
}
// No verbosity test: we always notify the outside world about
// scrollPos changes
Q_EMIT q->scrollPosChanged();
}
}
void setupZoomCursor()
{
// We do not use "appdata" here because that does not work when this
// code is called from a KPart.
const QString path = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("gwenview/cursors/zoom.png"));
QPixmap cursorPixmap = QPixmap(path);
mZoomCursor = QCursor(cursorPixmap, 11, 11);
}
AbstractImageViewPrivate(AbstractImageView *parent)
: q(parent)
, mBackgroundItem(new AlphaBackgroundItem{q})
{
mBackgroundItem->setVisible(false);
}
void checkAndRequestZoomAction(const QGraphicsSceneMouseEvent *event)
{
if (event->modifiers() & Qt::ControlModifier) {
if (event->button() == Qt::LeftButton) {
Q_EMIT q->zoomInRequested(event->pos());
} else if (event->button() == Qt::RightButton) {
Q_EMIT q->zoomOutRequested(event->pos());
}
}
}
};
AbstractImageView::AbstractImageView(QGraphicsItem *parent)
: QGraphicsWidget(parent)
, d(new AbstractImageViewPrivate(this))
{
d->mControlKeyIsDown = false;
d->mEnlargeSmallerImages = false;
d->mZoom = 1;
d->mZoomToFit = true;
d->mZoomToFill = false;
d->mImageOffset = QPointF(0, 0);
d->mScrollPos = QPointF(0, 0);
setFocusPolicy(Qt::WheelFocus);
setFlag(ItemIsSelectable);
setFlag(ItemClipsChildrenToShape);
setAcceptHoverEvents(true);
d->setupZoomCursor();
updateCursor();
}
AbstractImageView::~AbstractImageView()
{
if (d->mDocument) {
d->mDocument->stopAnimation();
}
delete d;
}
Document::Ptr AbstractImageView::document() const
{
return d->mDocument;
}
void AbstractImageView::setDocument(const Document::Ptr &doc)
{
if (d->mDocument) {
disconnect(d->mDocument.data(), nullptr, this, nullptr);
}
d->mDocument = doc;
if (d->mDocument) {
connect(d->mDocument.data(), &Document::imageRectUpdated, this, &AbstractImageView::onImageRectUpdated);
}
loadFromDocument();
}
QSizeF AbstractImageView::documentSize() const
{
return d->mDocument ? d->mDocument->size() : QSizeF();
}
QSizeF AbstractImageView::dipDocumentSize() const
{
return d->mDocument ? d->mDocument->size() / devicePixelRatio() : QSizeF();
}
qreal AbstractImageView::zoom() const
{
return d->mZoom;
}
void AbstractImageView::setZoom(qreal zoom, const QPointF &_center, AbstractImageView::UpdateType updateType)
{
if (!d->mDocument) {
d->mZoom = zoom;
return;
}
if (updateType == UpdateIfNecessary && qFuzzyCompare(zoom, d->mZoom) && documentSize() == d->mDocumentSize) {
return;
}
qreal oldZoom = d->mZoom;
d->mZoom = zoom;
d->mDocumentSize = documentSize();
QPointF center;
if (_center == QPointF(-1, -1)) {
center = boundingRect().center();
} else {
center = _center;
}
/*
We want to keep the point at viewport coordinates "center" at the same
position after zooming. The coordinates of this point in image coordinates
can be expressed like this:
oldScroll + center
imagePointAtOldZoom = ------------------
oldZoom
scroll + center
imagePointAtZoom = ---------------
zoom
So we want:
imagePointAtOldZoom = imagePointAtZoom
oldScroll + center scroll + center
<=> ------------------ = ---------------
oldZoom zoom
zoom
<=> scroll = ------- (oldScroll + center) - center
oldZoom
*/
/*
Compute oldScroll
It's useless to take the new offset in consideration because if a direction
of the new offset is not 0, we won't be able to center on a specific point
in that direction.
*/
QPointF oldScroll = scrollPos() - imageOffset();
QPointF scroll = (zoom / oldZoom) * (oldScroll + center) - center;
d->adjustImageOffset(AbstractImageViewPrivate::Silent);
d->setScrollPos(scroll, AbstractImageViewPrivate::Silent);
onZoomChanged();
Q_EMIT zoomChanged(d->mZoom);
}
bool AbstractImageView::zoomToFit() const
{
return d->mZoomToFit;
}
bool AbstractImageView::zoomToFill() const
{
return d->mZoomToFill;
}
void AbstractImageView::setZoomToFit(bool on)
{
if (d->mZoomToFit == on) {
return;
}
d->mZoomToFit = on;
if (on) {
d->mZoomToFill = false;
setZoom(computeZoomToFit());
}
// We do not set zoom to 1 if zoomToFit is off, this is up to the code
// calling us. It may went to zoom to some other level and/or to zoom on
// a particular position
Q_EMIT zoomToFitChanged(d->mZoomToFit);
}
void AbstractImageView::setZoomToFill(bool on, const QPointF &center)
{
if (d->mZoomToFill == on) {
return;
}
d->mZoomToFill = on;
if (on) {
d->mZoomToFit = false;
setZoom(computeZoomToFill(), center);
}
// We do not set zoom to 1 if zoomToFit is off, this is up to the code
// calling us. It may went to zoom to some other level and/or to zoom on
// a particular position
Q_EMIT zoomToFillChanged(d->mZoomToFill);
}
void AbstractImageView::resizeEvent(QGraphicsSceneResizeEvent *event)
{
QGraphicsWidget::resizeEvent(event);
if (d->mZoomToFit) {
// setZoom() calls adjustImageOffset(), but only if the zoom changes.
// If the view is resized but does not cause a zoom change, we call
// adjustImageOffset() ourself.
const qreal newZoom = computeZoomToFit();
if (qFuzzyCompare(zoom(), newZoom)) {
d->adjustImageOffset(AbstractImageViewPrivate::Notify);
} else {
setZoom(newZoom);
}
} else if (d->mZoomToFill) {
const qreal newZoom = computeZoomToFill();
if (qFuzzyCompare(zoom(), newZoom)) {
d->adjustImageOffset(AbstractImageViewPrivate::Notify);
} else {
setZoom(newZoom);
}
} else {
d->adjustImageOffset();
d->adjustScrollPos();
}
}
void AbstractImageView::focusInEvent(QFocusEvent *event)
{
QGraphicsWidget::focusInEvent(event);
// We might have missed a keyReleaseEvent for the control key, e.g. for Ctrl+O
const bool controlKeyIsCurrentlyDown = QGuiApplication::queryKeyboardModifiers() & Qt::ControlModifier;
if (d->mControlKeyIsDown != controlKeyIsCurrentlyDown) {
d->mControlKeyIsDown = controlKeyIsCurrentlyDown;
updateCursor();
}
}
qreal AbstractImageView::computeZoomToFit() const
{
const QSizeF docSize = dipDocumentSize();
if (docSize.isEmpty()) {
return 1;
}
const QSizeF viewSize = boundingRect().size();
const qreal fitWidth = viewSize.width() / docSize.width();
const qreal fitHeight = viewSize.height() / docSize.height();
qreal fit = qMin(fitWidth, fitHeight);
if (!d->mEnlargeSmallerImages) {
fit = qMin(fit, qreal(1.));
}
return fit;
}
qreal AbstractImageView::computeZoomToFill() const
{
const QSizeF docSize = dipDocumentSize();
if (docSize.isEmpty()) {
return 1;
}
const QSizeF viewSize = boundingRect().size();
const qreal fitWidth = viewSize.width() / docSize.width();
const qreal fitHeight = viewSize.height() / docSize.height();
qreal fill = qMax(fitWidth, fitHeight);
if (!d->mEnlargeSmallerImages) {
fill = qMin(fill, qreal(1.));
}
return fill;
}
void AbstractImageView::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
QGraphicsItem::mousePressEvent(event);
d->checkAndRequestZoomAction(event);
// Prepare for panning or dragging
if (event->button() == Qt::LeftButton) {
d->mLastDragPos = event->pos();
updateCursor();
}
}
void AbstractImageView::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
QGraphicsItem::mouseMoveEvent(event);
QPointF mousePos = event->pos();
QPointF newScrollPos = d->mScrollPos + d->mLastDragPos - mousePos;
#if 0 // commented out due to mouse pointer warping around, bug in Qt?
// Wrap mouse pos
qreal maxWidth = boundingRect().width();
qreal maxHeight = boundingRect().height();
// We need a margin because if the window is maximized, the mouse may not
// be able to go past the bounding rect.
// The mouse get placed 1 pixel before/after the margin to avoid getting
// considered as needing to wrap the other way in next mouseMoveEvent
// (because we don't check the move vector)
const int margin = 5;
if (mousePos.x() <= margin) {
mousePos.setX(maxWidth - margin - 1);
} else if (mousePos.x() >= maxWidth - margin) {
mousePos.setX(margin + 1);
}
if (mousePos.y() <= margin) {
mousePos.setY(maxHeight - margin - 1);
} else if (mousePos.y() >= maxHeight - margin) {
mousePos.setY(margin + 1);
}
// Set mouse pos (Hackish translation to screen coords!)
QPointF screenDelta = event->screenPos() - event->pos();
QCursor::setPos((mousePos + screenDelta).toPoint());
#endif
d->mLastDragPos = mousePos;
d->setScrollPos(newScrollPos);
}
void AbstractImageView::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
QGraphicsItem::mouseReleaseEvent(event);
if (!d->mLastDragPos.isNull()) {
d->mLastDragPos = QPointF();
}
updateCursor();
}
void AbstractImageView::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Control) {
d->mControlKeyIsDown = true;
updateCursor();
return;
}
if (zoomToFit() || qFuzzyCompare(computeZoomToFit(), zoom())) {
if (event->modifiers() != Qt::NoModifier) {
return;
}
switch (event->key()) {
case Qt::Key_Left:
if (QApplication::isRightToLeft()) {
Q_EMIT nextImageRequested();
} else {
Q_EMIT previousImageRequested();
}
break;
case Qt::Key_Up:
Q_EMIT previousImageRequested();
break;
case Qt::Key_Right:
if (QApplication::isRightToLeft()) {
Q_EMIT previousImageRequested();
} else {
Q_EMIT nextImageRequested();
}
break;
case Qt::Key_Down:
Q_EMIT nextImageRequested();
break;
default:
break;
}
return;
}
QPointF delta(0, 0);
const qreal pageStep = boundingRect().height();
qreal unitStep;
if (event->modifiers() & Qt::ShiftModifier) {
unitStep = pageStep / 2;
} else {
unitStep = UNIT_STEP;
}
switch (event->key()) {
case Qt::Key_Left:
delta.setX(-unitStep);
break;
case Qt::Key_Right:
delta.setX(unitStep);
break;
case Qt::Key_Up:
delta.setY(-unitStep);
break;
case Qt::Key_Down:
delta.setY(unitStep);
break;
case Qt::Key_PageUp:
delta.setY(-pageStep);
break;
case Qt::Key_PageDown:
delta.setY(pageStep);
break;
case Qt::Key_Home:
d->setScrollPos(QPointF(d->mScrollPos.x(), 0));
return;
case Qt::Key_End:
d->setScrollPos(QPointF(d->mScrollPos.x(), dipDocumentSize().height() * zoom()));
return;
default:
return;
}
d->setScrollPos(d->mScrollPos + delta);
}
void AbstractImageView::keyReleaseEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Control) {
d->mControlKeyIsDown = false;
updateCursor();
}
}
void AbstractImageView::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
{
if (event->modifiers() == Qt::NoModifier && event->button() == Qt::LeftButton) {
Q_EMIT toggleFullScreenRequested();
}
d->checkAndRequestZoomAction(event);
}
QPointF AbstractImageView::imageOffset() const
{
return d->mImageOffset;
}
QPointF AbstractImageView::scrollPos() const
{
return d->mScrollPos;
}
void AbstractImageView::setScrollPos(const QPointF &pos)
{
d->setScrollPos(pos);
}
qreal AbstractImageView::devicePixelRatio() const
{
if (!scene()->views().isEmpty()) {
return scene()->views().constFirst()->devicePixelRatio();
} else {
return qApp->devicePixelRatio();
}
}
QPointF AbstractImageView::mapToView(const QPointF &imagePos) const
{
return imagePos / devicePixelRatio() * d->mZoom + d->mImageOffset - d->mScrollPos;
}
QPoint AbstractImageView::mapToView(const QPoint &imagePos) const
{
return mapToView(QPointF(imagePos)).toPoint();
}
QRectF AbstractImageView::mapToView(const QRectF &imageRect) const
{
return QRectF(mapToView(imageRect.topLeft()), imageRect.size() * zoom() / devicePixelRatio());
}
QRect AbstractImageView::mapToView(const QRect &imageRect) const
{
return QRect(mapToView(imageRect.topLeft()), imageRect.size() * zoom() / devicePixelRatio());
}
QPointF AbstractImageView::mapToImage(const QPointF &viewPos) const
{
return (viewPos - d->mImageOffset + d->mScrollPos) / d->mZoom * devicePixelRatio();
}
QPoint AbstractImageView::mapToImage(const QPoint &viewPos) const
{
return mapToImage(QPointF(viewPos)).toPoint();
}
QRectF AbstractImageView::mapToImage(const QRectF &viewRect) const
{
return QRectF(mapToImage(viewRect.topLeft()), viewRect.size() / zoom() * devicePixelRatio());
}
QRect AbstractImageView::mapToImage(const QRect &viewRect) const
{
return QRect(mapToImage(viewRect.topLeft()), viewRect.size() / zoom() * devicePixelRatio());
}
void AbstractImageView::setEnlargeSmallerImages(bool value)
{
d->mEnlargeSmallerImages = value;
if (zoomToFit()) {
setZoom(computeZoomToFit());
}
}
void AbstractImageView::updateCursor()
{
if (d->mControlKeyIsDown) {
setCursor(d->mZoomCursor);
} else {
if (d->mLastDragPos.isNull()) {
setCursor(Qt::OpenHandCursor);
} else {
setCursor(Qt::ClosedHandCursor);
}
}
}
QSizeF AbstractImageView::visibleImageSize() const
{
if (!document()) {
return QSizeF();
}
QSizeF size = dipDocumentSize() * zoom();
return size.boundedTo(boundingRect().size());
}
void AbstractImageView::applyPendingScrollPos()
{
d->adjustImageOffset();
d->adjustScrollPos();
}
void AbstractImageView::resetDragCursor()
{
d->mLastDragPos = QPointF();
updateCursor();
}
AlphaBackgroundItem *AbstractImageView::backgroundItem() const
{
return d->mBackgroundItem;
}
void AbstractImageView::onImageRectUpdated()
{
if (zoomToFit()) {
setZoom(computeZoomToFit());
} else if (zoomToFill()) {
setZoom(computeZoomToFill());
} else {
applyPendingScrollPos();
}
update();
}
} // namespace
#include "moc_abstractimageview.cpp"