diff --git a/lib/document/document.cpp b/lib/document/document.cpp index 86ee4569..3204ab0d 100644 --- a/lib/document/document.cpp +++ b/lib/document/document.cpp @@ -1,573 +1,573 @@ /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau 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, Boston, MA 02110-1301, USA. */ #include "document.h" #include "document_p.h" // Qt #include #include #include #include #include // KDE #include #include // Local #include "documentjob.h" #include "emptydocumentimpl.h" #include "gvdebug.h" #include "imagemetainfomodel.h" #include "loadingdocumentimpl.h" #include "loadingjob.h" #include "savejob.h" namespace Gwenview { #undef ENABLE_LOG #undef LOG //#define ENABLE_LOG #ifdef ENABLE_LOG #define LOG(x) //qDebug() << x #else #define LOG(x) ; #endif #ifdef ENABLE_LOG static void logQueue(DocumentPrivate* d) { #define PREFIX " QUEUE: " if (!d->mCurrentJob) { Q_ASSERT(d->mJobQueue.isEmpty()); qDebug(PREFIX "No current job, no pending jobs"); return; } qDebug() << PREFIX "Current job:" << d->mCurrentJob.data(); if (d->mJobQueue.isEmpty()) { qDebug(PREFIX "No pending jobs"); return; } qDebug(PREFIX "%d pending job(s):", d->mJobQueue.size()); Q_FOREACH(DocumentJob* job, d->mJobQueue) { Q_ASSERT(job); qDebug() << PREFIX "-" << job; } #undef PREFIX } #define LOG_QUEUE(msg, d) \ LOG(msg); \ logQueue(d) #else #define LOG_QUEUE(msg, d) #endif //- DocumentPrivate --------------------------------------- void DocumentPrivate::scheduleImageLoading(int invertedZoom) { LoadingDocumentImpl* impl = qobject_cast(mImpl); Q_ASSERT(impl); impl->loadImage(invertedZoom); } void DocumentPrivate::scheduleImageDownSampling(int invertedZoom) { LOG("invertedZoom=" << invertedZoom); DownSamplingJob* job = qobject_cast(mCurrentJob.data()); if (job && job->mInvertedZoom == invertedZoom) { LOG("Current job is already doing it"); return; } // Remove any previously scheduled downsampling job DocumentJobQueue::Iterator it; for (it = mJobQueue.begin(); it != mJobQueue.end(); ++it) { DownSamplingJob* job = qobject_cast(*it); if (!job) { continue; } if (job->mInvertedZoom == invertedZoom) { // Already scheduled, nothing to do LOG("Already scheduled"); return; } else { LOG("Removing downsampling job"); mJobQueue.erase(it); delete job; } } q->enqueueJob(new DownSamplingJob(invertedZoom)); } void DocumentPrivate::downSampleImage(int invertedZoom) { - mDownSampledImageMap[invertedZoom] = mImage.scaled(mImage.size() / invertedZoom, Qt::KeepAspectRatio, Qt::FastTransformation); + mDownSampledImageMap[invertedZoom] = mImage.scaled((mImage.size() / qApp->devicePixelRatio()) / invertedZoom, Qt::KeepAspectRatio, Qt::FastTransformation); if (mDownSampledImageMap[invertedZoom].size().isEmpty()) { mDownSampledImageMap[invertedZoom] = mImage; } q->downSampledImageReady(); } //- DownSamplingJob --------------------------------------- void DownSamplingJob::doStart() { DocumentPrivate* d = document()->d; d->downSampleImage(mInvertedZoom); setError(NoError); emitResult(); } //- Document ---------------------------------------------- qreal Document::maxDownSampledZoom() { return 0.5; } Document::Document(const QUrl &url) : QObject() , d(new DocumentPrivate) { d->q = this; d->mImpl = 0; d->mUrl = url; d->mKeepRawData = false; connect(&d->mUndoStack, SIGNAL(indexChanged(int)), SLOT(slotUndoIndexChanged())); reload(); } Document::~Document() { // We do not want undo stack to emit signals, forcing us to emit signals // ourself while we are being destroyed. disconnect(&d->mUndoStack, 0, this, 0); delete d->mImpl; delete d; } void Document::reload() { d->mSize = QSize(); d->mImage = QImage(); d->mDownSampledImageMap.clear(); d->mExiv2Image.reset(); d->mKind = MimeTypeUtils::KIND_UNKNOWN; d->mFormat = QByteArray(); d->mImageMetaInfoModel.setUrl(d->mUrl); d->mUndoStack.clear(); d->mErrorString.clear(); d->mCmsProfile = 0; switchToImpl(new LoadingDocumentImpl(this)); } const QImage& Document::image() const { return d->mImage; } /** * invertedZoom is the biggest power of 2 for which zoom < 1/invertedZoom. * Example: * zoom = 0.4 == 1/2.5 => invertedZoom = 2 (1/2.5 < 1/2) * zoom = 0.2 == 1/5 => invertedZoom = 4 (1/5 < 1/4) */ inline int invertedZoomForZoom(qreal zoom) { int invertedZoom; for (invertedZoom = 1; zoom < 1. / (invertedZoom * 4); invertedZoom *= 2) {} return invertedZoom; } const QImage& Document::downSampledImageForZoom(qreal zoom) const { static const QImage sNullImage; int invertedZoom = invertedZoomForZoom(zoom); if (invertedZoom == 1) { return d->mImage; } if (!d->mDownSampledImageMap.contains(invertedZoom)) { if (!d->mImage.isNull()) { // Special case: if we have the full image and the down sampled // image would be too small, return the original image. - const QSize downSampledSize = d->mImage.size() / invertedZoom; + const QSize downSampledSize = (d->mImage.size() / qApp->devicePixelRatio()) / invertedZoom; if (downSampledSize.isEmpty()) { return d->mImage; } } return sNullImage; } return d->mDownSampledImageMap[invertedZoom]; } Document::LoadingState Document::loadingState() const { return d->mImpl->loadingState(); } void Document::switchToImpl(AbstractDocumentImpl* impl) { Q_ASSERT(impl); LOG("old impl:" << d->mImpl << "new impl:" << impl); if (d->mImpl) { d->mImpl->deleteLater(); } d->mImpl = impl; connect(d->mImpl, SIGNAL(metaInfoLoaded()), this, SLOT(emitMetaInfoLoaded())); connect(d->mImpl, SIGNAL(loaded()), this, SLOT(emitLoaded())); connect(d->mImpl, SIGNAL(loadingFailed()), this, SLOT(emitLoadingFailed())); connect(d->mImpl, SIGNAL(imageRectUpdated(QRect)), this, SIGNAL(imageRectUpdated(QRect))); connect(d->mImpl, SIGNAL(isAnimatedUpdated()), this, SIGNAL(isAnimatedUpdated())); d->mImpl->init(); } void Document::setImageInternal(const QImage& image) { d->mImage = image; d->mDownSampledImageMap.clear(); // If we didn't get the image size before decoding the full image, set it // now setSize(d->mImage.size() / qApp->devicePixelRatio()); } QUrl Document::url() const { return d->mUrl; } QByteArray Document::rawData() const { return d->mImpl->rawData(); } bool Document::keepRawData() const { return d->mKeepRawData; } void Document::setKeepRawData(bool value) { d->mKeepRawData = value; } void Document::waitUntilLoaded() { startLoadingFullImage(); while (true) { LoadingState state = loadingState(); if (state == Loaded || state == LoadingFailed) { return; } qApp->processEvents(QEventLoop::ExcludeUserInputEvents); } } DocumentJob* Document::save(const QUrl &url, const QByteArray& format) { waitUntilLoaded(); DocumentJob* job = d->mImpl->save(url, format); if (!job) { qWarning() << "Implementation does not support saving!"; setErrorString(i18nc("@info", "Gwenview cannot save this kind of documents.")); return 0; } job->setProperty("oldUrl", d->mUrl); job->setProperty("newUrl", url); connect(job, &DocumentJob::result, this, &Document::slotSaveResult); enqueueJob(job); return job; } void Document::slotSaveResult(KJob* job) { if (job->error()) { setErrorString(job->errorString()); } else { d->mUndoStack.setClean(); SaveJob* saveJob = static_cast(job); d->mUrl = saveJob->newUrl(); d->mImageMetaInfoModel.setUrl(d->mUrl); saved(saveJob->oldUrl(), d->mUrl); } } QByteArray Document::format() const { return d->mFormat; } void Document::setFormat(const QByteArray& format) { d->mFormat = format; emit metaInfoUpdated(); } MimeTypeUtils::Kind Document::kind() const { return d->mKind; } void Document::setKind(MimeTypeUtils::Kind kind) { d->mKind = kind; emit kindDetermined(d->mUrl); } QSize Document::size() const { return d->mSize; } bool Document::hasAlphaChannel() const { if (d->mImage.isNull()) { return false; } else { return d->mImage.hasAlphaChannel(); } } int Document::memoryUsage() const { // FIXME: Take undo stack into account int usage = d->mImage.byteCount(); usage += rawData().length(); return usage; } void Document::setSize(const QSize& size) { if (size == d->mSize) { return; } d->mSize = size; d->mImageMetaInfoModel.setImageSize(size); emit metaInfoUpdated(); } bool Document::isModified() const { return !d->mUndoStack.isClean(); } AbstractDocumentEditor* Document::editor() { return d->mImpl->editor(); } void Document::setExiv2Image(Exiv2::Image::AutoPtr image) { d->mExiv2Image = image; d->mImageMetaInfoModel.setExiv2Image(d->mExiv2Image.get()); emit metaInfoUpdated(); } void Document::setDownSampledImage(const QImage& image, int invertedZoom) { Q_ASSERT(!d->mDownSampledImageMap.contains(invertedZoom)); d->mDownSampledImageMap[invertedZoom] = image; emit downSampledImageReady(); } QString Document::errorString() const { return d->mErrorString; } void Document::setErrorString(const QString& string) { d->mErrorString = string; } ImageMetaInfoModel* Document::metaInfo() const { return &d->mImageMetaInfoModel; } void Document::startLoadingFullImage() { LoadingState state = loadingState(); if (state <= MetaInfoLoaded) { // Schedule full image loading LoadingJob* job = new LoadingJob; job->uiDelegate()->setAutoWarningHandlingEnabled(false); job->uiDelegate()->setAutoErrorHandlingEnabled(false); enqueueJob(job); d->scheduleImageLoading(1); } else if (state == Loaded) { return; } else if (state == LoadingFailed) { qWarning() << "Can't load full image: loading has already failed"; } } bool Document::prepareDownSampledImageForZoom(qreal zoom) { if (zoom >= maxDownSampledZoom()) { qWarning() << "No need to call prepareDownSampledImageForZoom if zoom >= " << maxDownSampledZoom(); return true; } int invertedZoom = invertedZoomForZoom(zoom); if (d->mDownSampledImageMap.contains(invertedZoom)) { LOG("downSampledImageForZoom=" << zoom << "invertedZoom=" << invertedZoom << "ready"); return true; } LOG("downSampledImageForZoom=" << zoom << "invertedZoom=" << invertedZoom << "not ready"); if (loadingState() == LoadingFailed) { qWarning() << "Image has failed to load, not doing anything"; return false; } else if (loadingState() == Loaded) { d->scheduleImageDownSampling(invertedZoom); return false; } // Schedule down sampled image loading d->scheduleImageLoading(invertedZoom); return false; } void Document::emitMetaInfoLoaded() { emit metaInfoLoaded(d->mUrl); } void Document::emitLoaded() { emit loaded(d->mUrl); } void Document::emitLoadingFailed() { emit loadingFailed(d->mUrl); } QUndoStack* Document::undoStack() const { return &d->mUndoStack; } void Document::slotUndoIndexChanged() { if (d->mUndoStack.isClean()) { // If user just undid all his changes this does not really correspond // to a save, but it's similar enough as far as Document users are // concerned saved(d->mUrl, d->mUrl); } else { modified(d->mUrl); } } bool Document::isEditable() const { return d->mImpl->isEditable(); } bool Document::isAnimated() const { return d->mImpl->isAnimated(); } void Document::startAnimation() { return d->mImpl->startAnimation(); } void Document::stopAnimation() { return d->mImpl->stopAnimation(); } void Document::enqueueJob(DocumentJob* job) { LOG("job=" << job); job->setDocument(Ptr(this)); connect(job, &LoadingJob::finished, this, &Document::slotJobFinished); if (d->mCurrentJob) { d->mJobQueue.enqueue(job); } else { d->mCurrentJob = job; LOG("Starting first job"); job->start(); busyChanged(d->mUrl, true); } LOG_QUEUE("Job added", d); } void Document::slotJobFinished(KJob* job) { LOG("job=" << job); GV_RETURN_IF_FAIL(job == d->mCurrentJob.data()); if (d->mJobQueue.isEmpty()) { LOG("All done"); d->mCurrentJob.clear(); busyChanged(d->mUrl, false); allTasksDone(); } else { LOG("Starting next job"); d->mCurrentJob = d->mJobQueue.dequeue(); GV_RETURN_IF_FAIL(d->mCurrentJob); d->mCurrentJob.data()->start(); } LOG_QUEUE("Removed done job", d); } bool Document::isBusy() const { return !d->mJobQueue.isEmpty(); } QSvgRenderer* Document::svgRenderer() const { return d->mImpl->svgRenderer(); } void Document::setCmsProfile(Cms::Profile::Ptr ptr) { d->mCmsProfile = ptr; } Cms::Profile::Ptr Document::cmsProfile() const { return d->mCmsProfile; } } // namespace diff --git a/lib/document/documentloadedimpl.cpp b/lib/document/documentloadedimpl.cpp index 6222c54d..f5e6c052 100644 --- a/lib/document/documentloadedimpl.cpp +++ b/lib/document/documentloadedimpl.cpp @@ -1,123 +1,130 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau 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, Boston, MA 02110-1301, USA. */ // Self #include "documentloadedimpl.h" // Qt #include #include #include #include #include #include +#include // KDE // Local #include "documentjob.h" #include "imageutils.h" #include "savejob.h" namespace Gwenview { struct DocumentLoadedImplPrivate { QByteArray mRawData; bool mQuietInit; }; DocumentLoadedImpl::DocumentLoadedImpl(Document* document, const QByteArray& rawData, bool quietInit) : AbstractDocumentImpl(document) , d(new DocumentLoadedImplPrivate) { if (document->keepRawData()) { d->mRawData = rawData; } d->mQuietInit = quietInit; } DocumentLoadedImpl::~DocumentLoadedImpl() { delete d; } void DocumentLoadedImpl::init() { if (!d->mQuietInit) { - emit imageRectUpdated(document()->image().rect()); + const qreal dpr = qApp->devicePixelRatio(); + QRect imageRect = QRectF(document()->image().rect().x() / dpr, document()->image().rect().y() / dpr, document()->image().rect().width() / dpr, document()->image().rect().height() / dpr).toAlignedRect(); + emit imageRectUpdated(imageRect); emit loaded(); } } bool DocumentLoadedImpl::isEditable() const { return true; } Document::LoadingState DocumentLoadedImpl::loadingState() const { return Document::Loaded; } bool DocumentLoadedImpl::saveInternal(QIODevice* device, const QByteArray& format) { QImageWriter writer(device, format); bool ok = writer.write(document()->image()); if (ok) { setDocumentFormat(format); } else { setDocumentErrorString(writer.errorString()); } return ok; } DocumentJob* DocumentLoadedImpl::save(const QUrl &url, const QByteArray& format) { return new SaveJob(this, url, format); } AbstractDocumentEditor* DocumentLoadedImpl::editor() { return this; } void DocumentLoadedImpl::setImage(const QImage& image) { setDocumentImage(image); - imageRectUpdated(image.rect()); + const qreal dpr = qApp->devicePixelRatio(); + QRect imageRect = QRectF(image.rect().x() / dpr, image.rect().y() / dpr, image.rect().width() / dpr, image.rect().height() / dpr).toAlignedRect(); + imageRectUpdated(imageRect); } void DocumentLoadedImpl::applyTransformation(Orientation orientation) { QImage image = document()->image(); QMatrix matrix = ImageUtils::transformMatrix(orientation); image = image.transformed(matrix); setDocumentImage(image); - imageRectUpdated(image.rect()); + const qreal dpr = qApp->devicePixelRatio(); + QRect imageRect = QRectF(image.rect().x() / dpr, image.rect().y() / dpr, image.rect().width() / dpr, image.rect().height() / dpr).toAlignedRect(); + imageRectUpdated(imageRect); } QByteArray DocumentLoadedImpl::rawData() const { return d->mRawData; } } // namespace diff --git a/lib/documentview/abstractimageview.cpp b/lib/documentview/abstractimageview.cpp index 85150266..f893bbd9 100644 --- a/lib/documentview/abstractimageview.cpp +++ b/lib/documentview/abstractimageview.cpp @@ -1,574 +1,574 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2011 Aurélien Gâteau 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 // KDE // Qt #include #include #include #include #include namespace Gwenview { static const int UNIT_STEP = 16; struct AbstractImageViewPrivate { enum Verbosity { Silent, Notify }; AbstractImageView* q; QCursor mZoomCursor; Document::Ptr mDocument; bool mControlKeyIsDown; bool mEnlargeSmallerImages; qreal mZoom; bool mZoomToFit; bool mZoomToFitWidth; QPointF mImageOffset; QPointF mScrollPos; QPointF mLastDragPos; void adjustImageOffset(Verbosity verbosity = Notify) { QSizeF zoomedDocSize = q->documentSize() * 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; } QSizeF zoomedDocSize = q->documentSize() * mZoom; QSizeF viewSize = q->boundingRect().size(); QPointF newPos( qBound(qreal(0.), _newPos.x(), zoomedDocSize.width() - viewSize.width()), qBound(qreal(0.), _newPos.y(), zoomedDocSize.height() - viewSize.height()) ); if (newPos != mScrollPos) { QPointF oldPos = mScrollPos; mScrollPos = newPos; if (verbosity == Notify) { q->onScrollPosChanged(oldPos); } // No verbosity test: we always notify the outside world about // scrollPos changes QMetaObject::invokeMethod(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); } }; AbstractImageView::AbstractImageView(QGraphicsItem* parent) : QGraphicsWidget(parent) , d(new AbstractImageViewPrivate) { d->q = this; d->mControlKeyIsDown = false; d->mEnlargeSmallerImages = false; d->mZoom = 1; d->mZoomToFit = true; d->mZoomToFitWidth = false; d->mImageOffset = QPointF(0, 0); d->mScrollPos = QPointF(0, 0); setFocusPolicy(Qt::WheelFocus); setFlag(ItemIsSelectable); 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(Document::Ptr doc) { if (d->mDocument) { disconnect(d->mDocument.data(), 0, this, 0); } d->mDocument = doc; loadFromDocument(); } QSizeF AbstractImageView::documentSize() const { return d->mDocument ? d->mDocument->size() : 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)) { return; } qreal oldZoom = d->mZoom; d->mZoom = zoom; 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(); zoomChanged(d->mZoom); } bool AbstractImageView::zoomToFit() const { return d->mZoomToFit; } bool AbstractImageView::zoomToFitWidth() const { return d->mZoomToFitWidth; } void AbstractImageView::setZoomToFit(bool on) { d->mZoomToFit = on; if (on) { 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 zoomToFitChanged(d->mZoomToFit); } void AbstractImageView::setZoomToFitWidth(bool on) { d->mZoomToFitWidth = on; if (on) { setZoom(computeZoomToFitWidth()); } // 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 zoomToFitWidthChanged(d->mZoomToFitWidth); } 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->mZoomToFitWidth) { const qreal newZoom = computeZoomToFitWidth(); if (qFuzzyCompare(zoom(), newZoom)) { d->adjustImageOffset(AbstractImageViewPrivate::Notify); } else { setZoom(newZoom); } } else { d->adjustImageOffset(); d->adjustScrollPos(); } } qreal AbstractImageView::computeZoomToFit() const { QSizeF docSize = documentSize(); if (docSize.isEmpty()) { return 1; } QSizeF viewSize = boundingRect().size(); qreal fitWidth = viewSize.width() / docSize.width(); qreal fitHeight = viewSize.height() / docSize.height(); qreal fit = qMin(fitWidth, fitHeight); if (!d->mEnlargeSmallerImages) { fit = qMin(fit, qreal(1.)); } return fit; } qreal AbstractImageView::computeZoomToFitWidth() const { QSizeF docSize = documentSize(); if (docSize.isEmpty()) { return 1; } QSizeF viewSize = boundingRect().size(); qreal fitWidth = viewSize.width() / docSize.width(); if (!d->mEnlargeSmallerImages) { fitWidth = qMin(fitWidth, qreal(1.)); } return fitWidth; } void AbstractImageView::mousePressEvent(QGraphicsSceneMouseEvent* event) { QGraphicsItem::mousePressEvent(event); if (event->button() == Qt::MiddleButton) { bool value = !zoomToFit(); setZoomToFit(value); if (!value) { setZoom(1.); } return; } if (event->modifiers() & Qt::ControlModifier) { if (event->button() == Qt::LeftButton) { zoomInRequested(event->pos()); return; } else if (event->button() == Qt::RightButton) { zoomOutRequested(event->pos()); return; } } 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: case Qt::Key_Up: previousImageRequested(); break; case Qt::Key_Right: case Qt::Key_Down: nextImageRequested(); break; default: break; } return; } QPointF delta(0, 0); 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(), documentSize().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) { toggleFullScreenRequested(); } } QPointF AbstractImageView::imageOffset() const { return d->mImageOffset; } QPointF AbstractImageView::scrollPos() const { return d->mScrollPos; } void AbstractImageView::setScrollPos(const QPointF& pos) { d->setScrollPos(pos); } QPointF AbstractImageView::mapToView(const QPointF& imagePos) const { - return imagePos * d->mZoom + d->mImageOffset - d->mScrollPos; + return (imagePos / qApp->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() + imageRect.size() * zoom() / qApp->devicePixelRatio() ); } QRect AbstractImageView::mapToView(const QRect& imageRect) const { return QRect( mapToView(imageRect.topLeft()), imageRect.size() * zoom() ); } QPointF AbstractImageView::mapToImage(const QPointF& viewPos) const { return (viewPos - d->mImageOffset + d->mScrollPos) / d->mZoom; } 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() ); } QRect AbstractImageView::mapToImage(const QRect& viewRect) const { return QRect( mapToImage(viewRect.topLeft()), viewRect.size() / zoom() ); } 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 = documentSize() * zoom(); - return size.boundedTo(boundingRect().size()); + return size.boundedTo(boundingRect().size()) * qApp->devicePixelRatio(); } void AbstractImageView::applyPendingScrollPos() { d->adjustImageOffset(); d->adjustScrollPos(); } } // namespace