diff --git a/lib/document/document.cpp b/lib/document/document.cpp index f7a81bea..d8642cf7 100644 --- a/lib/document/document.cpp +++ b/lib/document/document.cpp @@ -1,573 +1,578 @@ /* 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() / qApp->devicePixelRatio()) / invertedZoom, Qt::KeepAspectRatio, Qt::FastTransformation); + mDownSampledImageMap[invertedZoom] = mImage.scaled(mImage.size() / 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() / qApp->devicePixelRatio()) / invertedZoom; + const QSize downSampledSize = d->mImage.size() / 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()); } 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; } +QSize Document::scaledSize() const +{ + return d->mSize / qApp->devicePixelRatio(); +} + 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/document.h b/lib/document/document.h index 0fed7af3..d51758ba 100644 --- a/lib/document/document.h +++ b/lib/document/document.h @@ -1,264 +1,271 @@ /* 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. */ #ifndef DOCUMENT_H #define DOCUMENT_H #include #include #include // Qt #include #include #include // Local #include #include class QImage; class QRect; class QSize; class QSvgRenderer; class QUndoStack; class KJob; class QUrl; namespace Gwenview { class AbstractDocumentEditor; class AbstractDocumentImpl; class DocumentJob; class DocumentFactory; struct DocumentPrivate; class ImageMetaInfoModel; /** * This class represents an image. * * It handles loading and saving the image, applying operations and maintaining * the document undo stack. * * It is capable of loading down sampled versions of an image using * prepareDownSampledImageForZoom() and downSampledImageForZoom(). Down sampled * images load much faster than the full image but you need to load the full * image to manipulate it (use startLoadingFullImage() to do so). * * To get a Document instance for url, ask for one with * DocumentFactory::instance()->load(url); */ class GWENVIEWLIB_EXPORT Document : public QObject, public QSharedData { Q_OBJECT public: /** * Document won't produce down sampled images for any zoom value higher than maxDownSampledZoom(). * * Note: We can't use the enum {} trick to declare this constant, that's * why it's defined as a static method */ static qreal maxDownSampledZoom(); enum LoadingState { Loading, ///< Image is loading KindDetermined, ///< Image is still loading, but kind has been determined MetaInfoLoaded, ///< Image is still loading, but meta info has been loaded Loaded, ///< Full image has been loaded LoadingFailed ///< Image loading has failed }; typedef QExplicitlySharedDataPointer Ptr; ~Document(); /** * Returns a message for the last error which happened */ QString errorString() const; void reload(); void startLoadingFullImage(); /** * Prepare a version of the image down sampled to be a bit bigger than * size() * @a zoom. * Do not ask for a down sampled image for @a zoom >= to MaxDownSampledZoom. * * @return true if the image is ready, false if not. In this case the * downSampledImageReady() signal will be emitted. */ bool prepareDownSampledImageForZoom(qreal zoom); LoadingState loadingState() const; MimeTypeUtils::Kind kind() const; bool isModified() const; const QImage& image() const; const QImage& downSampledImageForZoom(qreal zoom) const; /** * Returns an implementation of AbstractDocumentEditor if this document can * be edited. */ AbstractDocumentEditor* editor(); QUrl url() const; DocumentJob* save(const QUrl &url, const QByteArray& format); QByteArray format() const; void waitUntilLoaded(); /** * Returns the size of the loaded document * This value is NOT adjusted for HiDPI rendering - * meaning that the real size of the image will be returned */ QSize size() const; + /** + * Returns the size of the loaded document + * This value is adjusted for HiDPI rendering - + * meaning that the scaled size of the image will be returned + */ + QSize scaledSize() const; + /** * Returns the width of the loaded document * This value is NOT adjusted for HiDPI rendering - * meaning that the real size of the image will be returned */ int width() const { return size().width(); } /** * Returns the height of the loaded document * This value is NOT adjusted for HiDPI rendering - * meaning that the real size of the image will be returned */ int height() const { return size().height(); } bool hasAlphaChannel() const; ImageMetaInfoModel* metaInfo() const; QUndoStack* undoStack() const; void setKeepRawData(bool); bool keepRawData() const; /** * Returns how much bytes the document is using */ int memoryUsage() const; /** * Returns the compressed version of the document, if it is still * available. */ QByteArray rawData() const; Cms::Profile::Ptr cmsProfile() const; /** * Returns a QSvgRenderer which can be used to render this document if it is * an SVG image. Returns a NULL pointer otherwise. */ QSvgRenderer* svgRenderer() const; /** * Returns true if the image can be edited. * You must ensure it has been fully loaded with startLoadingFullImage() first. */ bool isEditable() const; /** * Returns true if the image is animated (eg: gif or mng format) */ bool isAnimated() const; /** * Starts animation. Only sensible if isAnimated() returns true. */ void startAnimation(); /** * Stops animation. Only sensible if isAnimated() returns true. */ void stopAnimation(); void enqueueJob(DocumentJob*); /** * Returns true if there are queued tasks for this document. */ bool isBusy() const; Q_SIGNALS: void downSampledImageReady(); void imageRectUpdated(const QRect&); void kindDetermined(const QUrl&); void metaInfoLoaded(const QUrl&); void loaded(const QUrl&); void loadingFailed(const QUrl&); void saved(const QUrl &oldUrl, const QUrl& newUrl); void modified(const QUrl&); void metaInfoUpdated(); void isAnimatedUpdated(); void busyChanged(const QUrl&, bool); void allTasksDone(); private Q_SLOTS: void emitMetaInfoLoaded(); void emitLoaded(); void emitLoadingFailed(); void slotUndoIndexChanged(); void slotSaveResult(KJob*); void slotJobFinished(KJob*); private: friend class AbstractDocumentImpl; friend class DocumentFactory; friend struct DocumentPrivate; friend class DownSamplingJob; void setImageInternal(const QImage&); void setKind(MimeTypeUtils::Kind); void setFormat(const QByteArray&); void setSize(const QSize&); void setExiv2Image(Exiv2::Image::AutoPtr); void setDownSampledImage(const QImage&, int invertedZoom); void switchToImpl(AbstractDocumentImpl* impl); void setErrorString(const QString&); void setCmsProfile(Cms::Profile::Ptr); Document(const QUrl&); DocumentPrivate * const d; }; } // namespace #endif /* DOCUMENT_H */ diff --git a/lib/documentview/abstractimageview.cpp b/lib/documentview/abstractimageview.cpp index 49eda351..e0c3728b 100644 --- a/lib/documentview/abstractimageview.cpp +++ b/lib/documentview/abstractimageview.cpp @@ -1,579 +1,584 @@ // 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() / qApp->devicePixelRatio()) * mZoom; + QSizeF zoomedDocSize = q->scaledDocumentSize() * 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() / qApp->devicePixelRatio()) * mZoom; + QSizeF zoomedDocSize = q->scaledDocumentSize() * 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(); } +QSizeF AbstractImageView::scaledDocumentSize() const +{ + return d->mDocument ? d->mDocument->scaledSize() : 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() / qApp->devicePixelRatio()); 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() / qApp->devicePixelRatio(); + QSizeF docSize = scaledDocumentSize(); if (docSize.isEmpty()) { return 1; } QSizeF viewSize = boundingRect().size(); qDebug() << "Scaler documentSize" << documentSize() << "; docSize" << docSize << "; viewSize" << viewSize; 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.)); } qDebug() << "Scaler computeZoomToFit is" << fit; return fit; } qreal AbstractImageView::computeZoomToFitWidth() const { - QSizeF docSize = documentSize(); + QSizeF docSize = scaledDocumentSize(); if (docSize.isEmpty()) { return 1; } QSizeF viewSize = boundingRect().size(); - qreal fitWidth = viewSize.width() / (docSize.width() / qApp->devicePixelRatio()); + 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())); + d->setScrollPos(QPointF(d->mScrollPos.x(), scaledDocumentSize().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 / 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() / qApp->devicePixelRatio() ); } QRect AbstractImageView::mapToView(const QRect& imageRect) const { return QRect( mapToView(imageRect.topLeft()), imageRect.size() / qApp->devicePixelRatio() * zoom() ); } QPointF AbstractImageView::mapToImage(const QPointF& viewPos) const { return (viewPos - d->mImageOffset + d->mScrollPos) / d->mZoom * qApp->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() * qApp->devicePixelRatio() ); } QRect AbstractImageView::mapToImage(const QRect& viewRect) const { return QRect( mapToImage(viewRect.topLeft()), viewRect.size() / zoom() * qApp->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 = (documentSize() / qApp->devicePixelRatio()) * zoom(); + QSizeF size = scaledDocumentSize() * zoom(); return size.boundedTo(boundingRect().size()); } void AbstractImageView::applyPendingScrollPos() { d->adjustImageOffset(); d->adjustScrollPos(); } } // namespace diff --git a/lib/documentview/abstractimageview.h b/lib/documentview/abstractimageview.h index e68d5043..3bf13471 100644 --- a/lib/documentview/abstractimageview.h +++ b/lib/documentview/abstractimageview.h @@ -1,156 +1,163 @@ // 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. */ #ifndef ABSTRACTIMAGEVIEW_H #define ABSTRACTIMAGEVIEW_H // Local #include // KDE // Qt #include namespace Gwenview { struct AbstractImageViewPrivate; /** * */ class AbstractImageView : public QGraphicsWidget { Q_OBJECT public: enum UpdateType { UpdateIfNecessary, ForceUpdate }; AbstractImageView(QGraphicsItem* parent); ~AbstractImageView(); qreal zoom() const; virtual void setZoom(qreal zoom, const QPointF& center = QPointF(-1, -1), UpdateType updateType = UpdateIfNecessary); bool zoomToFit() const; bool zoomToFitWidth() const; virtual void setZoomToFit(bool value); virtual void setZoomToFitWidth(bool value); virtual void setDocument(Document::Ptr doc); Document::Ptr document() const; qreal computeZoomToFit() const; qreal computeZoomToFitWidth() const; /** * Returns the size of the loaded document * This value is NOT adjusted for HiDPI rendering - * meaning that the real size of the image will be returned */ QSizeF documentSize() const; + /** + * Returns the size of the loaded document + * This value is adjusted for HiDPI rendering - + * meaning that the scaled size of the image will be returned + */ + QSizeF scaledDocumentSize() const; + /* * Is adjusted for HiDPI */ QSizeF visibleImageSize() const; /** * If the image is smaller than the view, imageOffset is the distance from * the topleft corner of the view to the topleft corner of the image. * Neither x nor y can be negative. * This is in image coordinats and NOT adjusted for HiDPI rendering. */ QPointF imageOffset() const; /** * The scroll position, in zoomed image coordinates. * x and y are always between 0 and (docsize * zoom - viewsize) */ QPointF scrollPos() const; void setScrollPos(const QPointF& pos); QPointF mapToView(const QPointF& imagePos) const; QPoint mapToView(const QPoint& imagePos) const; QRectF mapToView(const QRectF& imageRect) const; QRect mapToView(const QRect& imageRect) const; QPointF mapToImage(const QPointF& viewPos) const; QPoint mapToImage(const QPoint& viewPos) const; QRectF mapToImage(const QRectF& viewRect) const; QRect mapToImage(const QRect& viewRect) const; void setEnlargeSmallerImages(bool value); void applyPendingScrollPos(); public Q_SLOTS: void updateCursor(); Q_SIGNALS: void zoomToFitChanged(bool); void zoomToFitWidthChanged(bool); void zoomChanged(qreal); void zoomInRequested(const QPointF&); void zoomOutRequested(const QPointF&); void scrollPosChanged(); void completed(); void previousImageRequested(); void nextImageRequested(); void toggleFullScreenRequested(); protected: virtual void loadFromDocument() = 0; virtual void onZoomChanged() = 0; /** * Called when the offset changes. * Note: to avoid multiple adjustments, this is not called if zoom changes! */ virtual void onImageOffsetChanged() = 0; /** * Called when the scrollPos changes. * Note: to avoid multiple adjustments, this is not called if zoom changes! */ virtual void onScrollPosChanged(const QPointF& oldPos) = 0; void resizeEvent(QGraphicsSceneResizeEvent* event) Q_DECL_OVERRIDE; void keyPressEvent(QKeyEvent* event) Q_DECL_OVERRIDE; void keyReleaseEvent(QKeyEvent* event) Q_DECL_OVERRIDE; void mousePressEvent(QGraphicsSceneMouseEvent* event) Q_DECL_OVERRIDE; void mouseMoveEvent(QGraphicsSceneMouseEvent* event) Q_DECL_OVERRIDE; void mouseReleaseEvent(QGraphicsSceneMouseEvent* event) Q_DECL_OVERRIDE; void mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event) Q_DECL_OVERRIDE; private: friend struct AbstractImageViewPrivate; AbstractImageViewPrivate* const d; }; } // namespace #endif /* ABSTRACTIMAGEVIEW_H */ diff --git a/lib/documentview/rasterimageview.cpp b/lib/documentview/rasterimageview.cpp index 65f33f48..937a3270 100644 --- a/lib/documentview/rasterimageview.cpp +++ b/lib/documentview/rasterimageview.cpp @@ -1,563 +1,563 @@ // 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 "rasterimageview.h" // Local #include #include #include #include // KDE // Qt #include #include #include #include #include #include // LCMS2 #include namespace Gwenview { struct RasterImageViewPrivate { RasterImageView* q; ImageScaler* mScaler; QPixmap mBackgroundTexture; bool mEmittedCompleted; // Config RasterImageView::AlphaBackgroundMode mAlphaBackgroundMode; QColor mAlphaBackgroundColor; bool mEnlargeSmallerImages; // /Config bool mBufferIsEmpty; QPixmap mCurrentBuffer; // The alternate buffer is useful when scrolling: existing content is copied // to mAlternateBuffer and buffers are swapped. This avoids allocating a new // QPixmap every time the image is scrolled. QPixmap mAlternateBuffer; QTimer* mUpdateTimer; QPointer mTool; bool mApplyDisplayTransform; // Defaults to true. Can be set to false if there is no need or no way to apply color profile cmsHTRANSFORM mDisplayTransform; void updateDisplayTransform(QImage::Format format) { GV_RETURN_IF_FAIL(format != QImage::Format_Invalid); mApplyDisplayTransform = false; if (mDisplayTransform) { cmsDeleteTransform(mDisplayTransform); } mDisplayTransform = 0; Cms::Profile::Ptr profile = q->document()->cmsProfile(); if (!profile) { // The assumption that something unmarked is *probably* sRGB is better than failing to apply any transform when one // has a wide-gamut screen. profile = Cms::Profile::getSRgbProfile(); } Cms::Profile::Ptr monitorProfile = Cms::Profile::getMonitorProfile(); if (!monitorProfile) { qWarning() << "Could not get monitor color profile"; return; } cmsUInt32Number cmsFormat = 0; switch (format) { case QImage::Format_RGB32: case QImage::Format_ARGB32: cmsFormat = TYPE_BGRA_8; break; #if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) case QImage::Format_Grayscale8: cmsFormat = TYPE_GRAY_8; break; #endif default: qWarning() << "Gwenview can only apply color profile on RGB32 or ARGB32 images"; return; } mDisplayTransform = cmsCreateTransform(profile->handle(), cmsFormat, monitorProfile->handle(), cmsFormat, INTENT_PERCEPTUAL, cmsFLAGS_BLACKPOINTCOMPENSATION); mApplyDisplayTransform = true; } void createBackgroundTexture() { mBackgroundTexture = QPixmap(32, 32); QPainter painter(&mBackgroundTexture); painter.fillRect(mBackgroundTexture.rect(), QColor(128, 128, 128)); QColor light = QColor(192, 192, 192); painter.fillRect(0, 0, 16, 16, light); painter.fillRect(16, 16, 16, 16, light); } void setupUpdateTimer() { mUpdateTimer = new QTimer(q); mUpdateTimer->setInterval(500); mUpdateTimer->setSingleShot(true); QObject::connect(mUpdateTimer, SIGNAL(timeout()), q, SLOT(updateBuffer())); } void startAnimationIfNecessary() { if (q->document() && q->isVisible()) { q->document()->startAnimation(); } } QRectF mapViewportToZoomedImage(const QRectF& viewportRect) const { return QRectF( viewportRect.topLeft() - (q->imageOffset() / qApp->devicePixelRatio()) + q->scrollPos(), viewportRect.size() ); } void setScalerRegionToVisibleRect() { QRectF rect = mapViewportToZoomedImage(q->boundingRect()); mScaler->setDestinationRegion(QRegion(rect.toRect())); } void resizeBuffer() { QSize size = q->visibleImageSize().toSize(); if (size == mCurrentBuffer.size()) { return; } if (!size.isValid()) { mAlternateBuffer = QPixmap(); mCurrentBuffer = QPixmap(); return; } mAlternateBuffer = QPixmap(size * qApp->devicePixelRatio()); mAlternateBuffer.setDevicePixelRatio(qApp->devicePixelRatio()); mAlternateBuffer.fill(Qt::transparent); { QPainter painter(&mAlternateBuffer); painter.drawPixmap(0, 0, mCurrentBuffer); } qSwap(mAlternateBuffer, mCurrentBuffer); mAlternateBuffer = QPixmap(); } void drawAlphaBackground(QPainter* painter, const QRect& viewportRect, const QPoint& zoomedImageTopLeft) { if (mAlphaBackgroundMode == RasterImageView::AlphaBackgroundCheckBoard) { QPoint textureOffset( zoomedImageTopLeft.x() % mBackgroundTexture.width(), zoomedImageTopLeft.y() % mBackgroundTexture.height() ); painter->drawTiledPixmap( viewportRect, mBackgroundTexture, textureOffset); } else { painter->fillRect(viewportRect, mAlphaBackgroundColor); } } }; RasterImageView::RasterImageView(QGraphicsItem* parent) : AbstractImageView(parent) , d(new RasterImageViewPrivate) { d->q = this; d->mEmittedCompleted = false; d->mApplyDisplayTransform = true; d->mDisplayTransform = 0; d->mAlphaBackgroundMode = AlphaBackgroundCheckBoard; d->mAlphaBackgroundColor = Qt::black; d->mEnlargeSmallerImages = false; d->mBufferIsEmpty = true; d->mScaler = new ImageScaler(this); connect(d->mScaler, &ImageScaler::scaledRect, this, &RasterImageView::updateFromScaler); d->createBackgroundTexture(); d->setupUpdateTimer(); } RasterImageView::~RasterImageView() { if (d->mDisplayTransform) { cmsDeleteTransform(d->mDisplayTransform); } delete d; } void RasterImageView::setAlphaBackgroundMode(AlphaBackgroundMode mode) { d->mAlphaBackgroundMode = mode; if (document() && document()->hasAlphaChannel()) { d->mCurrentBuffer = QPixmap(); updateBuffer(); } } void RasterImageView::setAlphaBackgroundColor(const QColor& color) { d->mAlphaBackgroundColor = color; if (document() && document()->hasAlphaChannel()) { d->mCurrentBuffer = QPixmap(); updateBuffer(); } } void RasterImageView::loadFromDocument() { Document::Ptr doc = document(); if (!doc) { return; } connect(doc.data(), SIGNAL(metaInfoLoaded(QUrl)), SLOT(slotDocumentMetaInfoLoaded())); connect(doc.data(), SIGNAL(isAnimatedUpdated()), SLOT(slotDocumentIsAnimatedUpdated())); const Document::LoadingState state = doc->loadingState(); if (state == Document::MetaInfoLoaded || state == Document::Loaded) { slotDocumentMetaInfoLoaded(); } } void RasterImageView::slotDocumentMetaInfoLoaded() { if (document()->size().isValid()) { QMetaObject::invokeMethod(this, "finishSetDocument", Qt::QueuedConnection); } else { // Could not retrieve image size from meta info, we need to load the // full image now. connect(document().data(), SIGNAL(loaded(QUrl)), SLOT(finishSetDocument())); document()->startLoadingFullImage(); } } void RasterImageView::finishSetDocument() { GV_RETURN_IF_FAIL(document()->size().isValid()); d->mScaler->setDocument(document()); d->resizeBuffer(); applyPendingScrollPos(); connect(document().data(), SIGNAL(imageRectUpdated(QRect)), SLOT(updateImageRect(QRect))); if (zoomToFit()) { // Force the update otherwise if computeZoomToFit() returns 1, setZoom() // will think zoom has not changed and won't update the image setZoom(computeZoomToFit(), QPointF(-1, -1), ForceUpdate); } else if (zoomToFitWidth()) { setZoom(computeZoomToFitWidth(), QPointF(-1, -1), ForceUpdate); } else { updateBuffer(); } d->startAnimationIfNecessary(); update(); } void RasterImageView::updateImageRect(const QRect& imageRect) { QRectF viewRect = mapToView(imageRect); if (!viewRect.intersects(boundingRect())) { return; } if (zoomToFit()) { setZoom(computeZoomToFit()); } else if (zoomToFitWidth()) { setZoom(computeZoomToFitWidth()); } d->setScalerRegionToVisibleRect(); update(); } void RasterImageView::slotDocumentIsAnimatedUpdated() { d->startAnimationIfNecessary(); } void RasterImageView::updateFromScaler(int zoomedImageLeft, int zoomedImageTop, const QImage& image) { if (d->mApplyDisplayTransform) { d->updateDisplayTransform(image.format()); if (d->mDisplayTransform) { quint8 *bytes = const_cast(image.bits()); cmsDoTransform(d->mDisplayTransform, bytes, bytes, image.width() * image.height()); } } d->resizeBuffer(); int viewportLeft = zoomedImageLeft - scrollPos().x(); int viewportTop = zoomedImageTop - scrollPos().y(); d->mBufferIsEmpty = false; { QPainter painter(&d->mCurrentBuffer); if (document()->hasAlphaChannel()) { d->drawAlphaBackground( &painter, QRect(viewportLeft, viewportTop, image.width(), image.height()), QPoint(zoomedImageLeft, zoomedImageTop) ); } else { painter.setCompositionMode(QPainter::CompositionMode_Source); } painter.drawImage(viewportLeft, viewportTop, image); } update(); if (!d->mEmittedCompleted) { d->mEmittedCompleted = true; completed(); } } void RasterImageView::onZoomChanged() { // If we zoom more than twice, then assume the user wants to see the real // pixels, for example to fine tune a crop operation if (zoom() < 2.) { d->mScaler->setTransformationMode(Qt::SmoothTransformation); } else { d->mScaler->setTransformationMode(Qt::FastTransformation); } if (!d->mUpdateTimer->isActive()) { updateBuffer(); } } void RasterImageView::onImageOffsetChanged() { update(); } void RasterImageView::onScrollPosChanged(const QPointF& oldPos) { QPointF delta = scrollPos() - oldPos; // Scroll existing { if (d->mAlternateBuffer.size() != d->mCurrentBuffer.size()) { d->mAlternateBuffer = QPixmap(d->mCurrentBuffer.size()); d->mAlternateBuffer.setDevicePixelRatio(d->mCurrentBuffer.devicePixelRatio()); } QPainter painter(&d->mAlternateBuffer); painter.drawPixmap(-delta, d->mCurrentBuffer); } qSwap(d->mCurrentBuffer, d->mAlternateBuffer); // Scale missing parts QRect nCurrentBufferRect(QRectF(d->mCurrentBuffer.rect().x() / qApp->devicePixelRatio(), d->mCurrentBuffer.rect().y() / qApp->devicePixelRatio(), d->mCurrentBuffer.rect().width() / qApp->devicePixelRatio(), d->mCurrentBuffer.rect().height() / qApp->devicePixelRatio()).toAlignedRect()); QRegion bufferRegion = QRegion(nCurrentBufferRect.translated(scrollPos().toPoint())); QRegion updateRegion = bufferRegion - bufferRegion.translated(-delta.toPoint()); updateBuffer(updateRegion); update(); } void RasterImageView::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget* /*widget*/) { d->mCurrentBuffer.setDevicePixelRatio(qApp->devicePixelRatio()); qDebug() << "d->mCurrentBuffer.setDevicePixelRatio" << qApp->devicePixelRatio() << "; dpr" << painter->device()->devicePixelRatioF(); //d->mCurrentBuffer.save("/tmp/buffer.png"); QPointF topLeft = imageOffset(); if (zoomToFit()) { // In zoomToFit mode, scale crudely the buffer to fit the screen. This // provide an approximate rendered which will be replaced when the scheduled // proper scale is ready. - QSizeF size = documentSize() * zoom() / qApp->devicePixelRatio(); + QSizeF size = scaledDocumentSize() * zoom(); painter->drawPixmap(topLeft.x(), topLeft.y(), size.width(), size.height(), d->mCurrentBuffer); } else { painter->drawPixmap(topLeft, d->mCurrentBuffer); } if (d->mTool) { d->mTool.data()->paint(painter); } // Debug #if 0 QSizeF visibleSize = documentSize() * zoom(); painter->setPen(Qt::red); painter->drawRect(topLeft.x(), topLeft.y(), visibleSize.width() - 1, visibleSize.height() - 1); painter->setPen(Qt::blue); painter->drawRect(topLeft.x(), topLeft.y(), d->mCurrentBuffer.width() - 1, d->mCurrentBuffer.height() - 1); #endif } void RasterImageView::resizeEvent(QGraphicsSceneResizeEvent* event) { // If we are in zoomToFit mode and have something in our buffer, delay the // update: paint() will paint a scaled version of the buffer until resizing // is done. This is much faster than rescaling the whole image for each // resize event we receive. // mUpdateTimer must be started before calling AbstractImageView::resizeEvent() // because AbstractImageView::resizeEvent() will call onZoomChanged(), which // will trigger an immediate update unless the mUpdateTimer is active. if (zoomToFit() && !d->mBufferIsEmpty) { d->mUpdateTimer->start(); } else if (zoomToFitWidth() && !d->mBufferIsEmpty) { d->mUpdateTimer->start(); } AbstractImageView::resizeEvent(event); if (!zoomToFit()) { // Only update buffer if we are not in zoomToFit mode: if we are // onZoomChanged() will have already updated the buffer. updateBuffer(); } else if (!zoomToFitWidth()) { updateBuffer(); } } void RasterImageView::updateBuffer(const QRegion& region) { d->mUpdateTimer->stop(); d->mScaler->setZoom(zoom()); if (region.isEmpty()) { d->setScalerRegionToVisibleRect(); } else { d->mScaler->setDestinationRegion(region); } } void RasterImageView::setCurrentTool(AbstractRasterImageViewTool* tool) { if (d->mTool) { d->mTool.data()->toolDeactivated(); d->mTool.data()->deleteLater(); } d->mTool = tool; if (d->mTool) { d->mTool.data()->toolActivated(); } updateCursor(); currentToolChanged(tool); update(); } AbstractRasterImageViewTool* RasterImageView::currentTool() const { return d->mTool.data(); } void RasterImageView::mousePressEvent(QGraphicsSceneMouseEvent* event) { if (d->mTool) { d->mTool.data()->mousePressEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::mousePressEvent(event); } void RasterImageView::mouseMoveEvent(QGraphicsSceneMouseEvent* event) { if (d->mTool) { d->mTool.data()->mouseMoveEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::mouseMoveEvent(event); } void RasterImageView::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) { if (d->mTool) { d->mTool.data()->mouseReleaseEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::mouseReleaseEvent(event); } void RasterImageView::wheelEvent(QGraphicsSceneWheelEvent* event) { if (d->mTool) { d->mTool.data()->wheelEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::wheelEvent(event); } void RasterImageView::keyPressEvent(QKeyEvent* event) { if (d->mTool) { d->mTool.data()->keyPressEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::keyPressEvent(event); } void RasterImageView::keyReleaseEvent(QKeyEvent* event) { if (d->mTool) { d->mTool.data()->keyReleaseEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::keyReleaseEvent(event); } void RasterImageView::hoverMoveEvent(QGraphicsSceneHoverEvent* event) { if (d->mTool) { d->mTool.data()->hoverMoveEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::hoverMoveEvent(event); } } // namespace