2
Gwenview: an image viewer
3
Copyright 2007 AurĆ©lien GĆ¢teau <agateau@kde.org>
5
This program is free software; you can redistribute it and/or
6
modify it under the terms of the GNU General Public License
7
as published by the Free Software Foundation; either version 2
8
of the License, or (at your option) any later version.
10
This program is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
GNU General Public License for more details.
15
You should have received a copy of the GNU General Public License
16
along with this program; if not, write to the Free Software
17
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20
#include "imageview.moc"
23
#include <QApplication>
25
#include <QPaintEvent>
33
#include "abstractimageviewtool.h"
34
#include "imagescaler.h"
43
#define LOG(x) kDebug() << x
48
struct ImageViewPrivate {
50
QPixmap mBackgroundTexture;
52
ImageView::AlphaBackgroundMode mAlphaBackgroundMode;
53
QColor mAlphaBackgroundColor;
54
bool mEnlargeSmallerImages;
55
Document::Ptr mDocument;
58
QPixmap mCurrentBuffer;
59
QPixmap mAlternateBuffer;
61
QPointer<AbstractImageViewTool> mTool;
62
QPointer<AbstractImageViewTool> mDefaultTool;
66
void createBackgroundTexture() {
67
mBackgroundTexture = QPixmap(32, 32);
68
QPainter painter(&mBackgroundTexture);
69
painter.fillRect(mBackgroundTexture.rect(), QColor(128, 128, 128));
70
QColor light = QColor(192, 192, 192);
71
painter.fillRect(0, 0, 16, 16, light);
72
painter.fillRect(16, 16, 16, 16, light);
76
QSize visibleImageSize() const {
83
zoom = mView->computeZoomToFit();
88
size = mDocument->size() * zoom;
89
size = size.boundedTo(mViewport->size());
95
void drawAlphaBackground(QPainter* painter, const QRect& viewportRect, const QPoint& zoomedImageTopLeft) {
96
if (mAlphaBackgroundMode == ImageView::AlphaBackgroundCheckBoard) {
98
zoomedImageTopLeft.x() % mBackgroundTexture.width(),
99
zoomedImageTopLeft.y() % mBackgroundTexture.height()
101
painter->drawTiledPixmap(
106
painter->fillRect(viewportRect, mAlphaBackgroundColor);
110
void createBuffer() {
111
QSize size = mView->size();
112
if (size == mCurrentBuffer.size()) {
115
if (!size.isValid()) {
116
mAlternateBuffer = QPixmap();
117
mCurrentBuffer = QPixmap();
121
mAlternateBuffer = QPixmap(size);
122
QPainter painter(&mAlternateBuffer);
123
QColor bgColor = mViewport->palette().color(mViewport->backgroundRole());
124
painter.fillRect(mAlternateBuffer.rect(), bgColor);
125
painter.drawPixmap(0, 0, mCurrentBuffer);
126
qSwap(mAlternateBuffer, mCurrentBuffer);
128
mAlternateBuffer = QPixmap();
132
int hScroll() const {
136
return mView->horizontalScrollBar()->value();
140
int vScroll() const {
144
return mView->verticalScrollBar()->value();
148
QRect mapViewportToZoomedImage(const QRect& viewportRect) {
149
QPoint offset = mView->imageOffset();
151
viewportRect.x() + hScroll() - offset.x(),
152
viewportRect.y() + vScroll() - offset.y(),
153
viewportRect.width(),
154
viewportRect.height()
161
void setScalerRegionToVisibleRect() {
162
QRect rect = mapViewportToZoomedImage(mViewport->rect());
163
mScaler->setDestinationRegion(QRegion(rect));
167
void forceBufferRecreation() {
168
mCurrentBuffer = QPixmap();
170
setScalerRegionToVisibleRect();
174
void startAnimationIfNecessary() {
175
if (mDocument && mView->isVisible()) {
176
mDocument->startAnimation();
181
// At least gcc 3.4.6 on FreeBSD requires a default constructor.
182
ImageViewPrivate() { }
186
ImageView::ImageView(QWidget* parent)
187
: QAbstractScrollArea(parent)
188
, d(new ImageViewPrivate)
190
d->mAlphaBackgroundMode = AlphaBackgroundCheckBoard;
191
d->mAlphaBackgroundColor = Qt::black;
195
d->mZoomToFit = true;
196
d->createBackgroundTexture();
197
setFrameShape(QFrame::NoFrame);
198
setBackgroundRole(QPalette::Base);
199
d->mViewport = new QWidget();
200
setViewport(d->mViewport);
201
d->mViewport->setMouseTracking(true);
202
horizontalScrollBar()->setSingleStep(16);
203
verticalScrollBar()->setSingleStep(16);
204
d->mScaler = new ImageScaler(this);
205
d->mInsideSetZoom = false;
207
if (QApplication::isRightToLeft()) {
208
// Ensure we don't get weird behavior in RightToleft mode
210
horizontalScrollBar()->setLayoutDirection(Qt::LeftToRight);
212
connect(d->mScaler, SIGNAL(scaledRect(int, int, const QImage&)),
213
SLOT(updateFromScaler(int, int, const QImage&)) );
216
ImageView::~ImageView() {
221
void ImageView::setAlphaBackgroundMode(AlphaBackgroundMode mode) {
222
d->mAlphaBackgroundMode = mode;
223
if (d->mDocument && d->mDocument->hasAlphaChannel()) {
224
d->forceBufferRecreation();
229
void ImageView::setAlphaBackgroundColor(const QColor& color) {
230
d->mAlphaBackgroundColor = color;
231
if (d->mDocument && d->mDocument->hasAlphaChannel()) {
232
d->forceBufferRecreation();
237
void ImageView::setEnlargeSmallerImages(bool value) {
238
d->mEnlargeSmallerImages = value;
240
setZoom(computeZoomToFit());
245
void ImageView::setDocument(Document::Ptr document) {
247
d->mDocument->stopAnimation();
248
disconnect(d->mDocument.data(), 0, this, 0);
250
d->mDocument = document;
252
d->mViewport->update();
256
connect(d->mDocument.data(), SIGNAL(metaInfoLoaded(const KUrl&)),
257
SLOT(slotDocumentMetaInfoLoaded()) );
258
connect(d->mDocument.data(), SIGNAL(isAnimatedUpdated()),
259
SLOT(slotDocumentIsAnimatedUpdated()) );
261
const Document::LoadingState state = d->mDocument->loadingState();
262
if (state == Document::MetaInfoLoaded || state == Document::Loaded) {
263
slotDocumentMetaInfoLoaded();
268
void ImageView::slotDocumentMetaInfoLoaded() {
269
if (d->mDocument->size().isValid()) {
272
// Could not retrieve image size from meta info, we need to load the
274
connect(d->mDocument.data(), SIGNAL(loaded(const KUrl&)),
275
SLOT(finishSetDocument()) );
276
d->mDocument->startLoadingFullImage();
281
void ImageView::finishSetDocument() {
282
if (!d->mDocument->size().isValid()) {
283
kError() << "No valid image size available, this should not happen!";
288
d->mScaler->setDocument(d->mDocument);
290
connect(d->mDocument.data(), SIGNAL(imageRectUpdated(const QRect&)),
291
SLOT(updateImageRect(const QRect&)) );
294
// Set the zoom to an invalid value to make sure setZoom() does not
295
// return early because the new zoom is the same as the old zoom.
297
setZoom(computeZoomToFit());
299
QRect rect(QPoint(0, 0), d->mDocument->size());
300
updateImageRect(rect);
304
d->startAnimationIfNecessary();
305
d->mViewport->update();
309
Document::Ptr ImageView::document() const {
314
void ImageView::slotDocumentIsAnimatedUpdated() {
315
d->startAnimationIfNecessary();
319
void ImageView::updateImageRect(const QRect& imageRect) {
320
LOG("imageRect" << imageRect);
321
QRect viewportRect = mapToViewport(imageRect);
322
viewportRect = viewportRect.intersected(d->mViewport->rect());
323
if (viewportRect.isEmpty()) {
328
setZoom(computeZoomToFit());
330
d->setScalerRegionToVisibleRect();
331
d->mViewport->update();
335
void ImageView::paintEvent(QPaintEvent* event) {
336
QPainter painter(d->mViewport);
337
painter.setClipRect(event->rect());
339
painter.setCompositionMode(QPainter::CompositionMode_Source);
340
painter.drawPixmap(0, 0, d->mCurrentBuffer);
342
painter.setCompositionMode(QPainter::CompositionMode_SourceOver);
344
d->mTool->paint(&painter);
348
void ImageView::resizeEvent(QResizeEvent*) {
350
// Bypass "same zoom" test: even if the zoom is the same we want a new
351
// buffer of the correct size (and if switching from/to fullscreen, using
352
// the correct background) to be created
354
setZoom(computeZoomToFit());
355
// Make sure one can't use mousewheel in zoom-to-fit mode
356
horizontalScrollBar()->setRange(0, 0);
357
verticalScrollBar()->setRange(0, 0);
361
d->setScalerRegionToVisibleRect();
365
QPoint ImageView::imageOffset() const {
366
QSize size = d->visibleImageSize();
367
int left = qMax( (d->mViewport->width() - size.width()) / 2, 0);
368
int top = qMax( (d->mViewport->height() - size.height()) / 2, 0);
370
return QPoint(left, top);
374
void ImageView::setZoom(qreal zoom, const QPoint& _center) {
379
qreal oldZoom = d->mZoom;
380
if (qAbs(zoom - oldZoom) < 0.001) {
383
// Get offset *before* setting mZoom, otherwise we get the new offset
384
QPoint oldOffset = imageOffset();
388
if (_center == QPoint(-1, -1)) {
389
center = QPoint(d->mViewport->width() / 2, d->mViewport->height() / 2);
394
// If we zoom more than twice, then assume the user wants to see the real
395
// pixels, for example to fine tune a crop operation
397
d->mScaler->setTransformationMode(Qt::SmoothTransformation);
399
d->mScaler->setTransformationMode(Qt::FastTransformation);
403
d->mInsideSetZoom = true;
406
We want to keep the point at viewport coordinates "center" at the same
407
position after zooming. The coordinates of this point in image coordinates
408
can be expressed like this:
411
imagePointAtOldZoom = ------------------
415
imagePointAtZoom = ---------------
420
imagePointAtOldZoom = imagePointAtZoom
422
oldScroll + center scroll + center
423
<=> ------------------ = ---------------
427
<=> scroll = ------- (oldScroll + center) - center
433
It's useless to take the new offset in consideration because if a direction
434
of the new offset is not 0, we won't be able to center on a specific point
437
QPointF oldScroll = QPointF(d->hScroll(), d->vScroll()) - oldOffset;
439
QPointF scroll = (zoom / oldZoom) * (oldScroll + center) - center;
442
horizontalScrollBar()->setValue(int(scroll.x()));
443
verticalScrollBar()->setValue(int(scroll.y()));
444
d->mInsideSetZoom = false;
446
d->mScaler->setZoom(d->mZoom);
447
d->setScalerRegionToVisibleRect();
448
emit zoomChanged(d->mZoom);
451
qreal ImageView::zoom() const {
455
bool ImageView::zoomToFit() const {
456
return d->mZoomToFit;
459
void ImageView::setZoomToFit(bool on) {
462
setZoom(computeZoomToFit());
466
void ImageView::updateScrollBars() {
467
if (!d->mDocument || d->mZoomToFit) {
468
setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
469
setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
472
setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
473
setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
476
int width = d->mViewport->width();
477
int height = d->mViewport->height();
479
max = qMax(0, int(d->mDocument->width() * d->mZoom) - width);
480
horizontalScrollBar()->setRange(0, max);
481
horizontalScrollBar()->setPageStep(width);
483
max = qMax(0, int(d->mDocument->height() * d->mZoom) - height);
484
verticalScrollBar()->setRange(0, max);
485
verticalScrollBar()->setPageStep(height);
489
void ImageView::scrollContentsBy(int dx, int dy) {
490
if (d->mInsideSetZoom) {
491
// Do not scroll anything: since we are zooming the whole viewport will
492
// eventually be repainted
497
if (d->mAlternateBuffer.size() != d->mCurrentBuffer.size()) {
498
d->mAlternateBuffer = QPixmap(d->mCurrentBuffer.size());
500
QPainter painter(&d->mAlternateBuffer);
501
painter.drawPixmap(dx, dy, d->mCurrentBuffer);
503
qSwap(d->mCurrentBuffer, d->mAlternateBuffer);
505
// Scale missing parts
507
int posX = d->hScroll();
508
int posY = d->vScroll();
509
int width = d->mViewport->width();
510
int height = d->mViewport->height();
514
rect = QRect(posX, posY, dx, height);
516
rect = QRect(posX + width + dx, posY, -dx, height);
521
rect = QRect(posX, posY, width, dy);
523
rect = QRect(posX, posY + height + dy, width, -dy);
527
d->mScaler->setDestinationRegion(region);
528
d->mViewport->update();
532
void ImageView::updateFromScaler(int zoomedImageLeft, int zoomedImageTop, const QImage& image) {
534
QPoint offset = imageOffset();
535
int viewportLeft = offset.x() + zoomedImageLeft - d->hScroll();
536
int viewportTop = offset.y() + zoomedImageTop - d->vScroll();
539
QPainter painter(&d->mCurrentBuffer);
540
if (d->mDocument->hasAlphaChannel()) {
541
d->drawAlphaBackground(
542
&painter, QRect(viewportLeft, viewportTop, image.width(), image.height()),
543
QPoint(zoomedImageLeft, zoomedImageTop)
546
painter.setCompositionMode(QPainter::CompositionMode_Source);
548
painter.drawImage(viewportLeft, viewportTop, image);
550
// Clear borders. We do it here which is quite late, but this way we ensure
551
// borders are not cleared until the new image is ready. Meanwhile we
552
// continue to show the previous image. If we cleared the borders earlier
553
// we could end up clearing the borders on the previous image, leading to
554
// a temporary cropped result
555
QColor bgColor = d->mViewport->palette().color(d->mViewport->backgroundRole());
556
QRegion region = d->mCurrentBuffer.rect();
557
region -= QRect(imageOffset(), d->mDocument->size() * d->mZoom);
558
Q_FOREACH(const QRect& rect, region.rects()) {
559
painter.fillRect(rect, bgColor);
564
pen.setStyle(Qt::DotLine);
566
painter.drawRect(viewportLeft, viewportTop, image.width() - 1, image.height() - 1);
569
d->mViewport->update();
573
void ImageView::setCurrentTool(AbstractImageViewTool* tool) {
575
d->mTool->toolDeactivated();
577
d->mTool = tool ? QPointer<AbstractImageViewTool>(tool) : d->mDefaultTool;
579
d->mTool->toolActivated();
581
d->mViewport->update();
585
AbstractImageViewTool* ImageView::currentTool() const {
590
void ImageView::setDefaultTool(AbstractImageViewTool* tool) {
591
d->mDefaultTool = tool;
592
if (!d->mTool && tool) {
593
setCurrentTool(tool);
598
AbstractImageViewTool* ImageView::defaultTool() const {
599
return d->mDefaultTool;
603
QPoint ImageView::mapToViewport(const QPoint& src) const {
604
QPoint dst(int(src.x() * d->mZoom), int(src.y() * d->mZoom));
606
dst += imageOffset();
608
dst.rx() -= d->hScroll();
609
dst.ry() -= d->vScroll();
615
QPointF ImageView::mapToViewportF(const QPointF& src) const {
616
QPointF dst(src.x() * d->mZoom, src.y() * d->mZoom);
618
dst += imageOffset();
620
dst.rx() -= d->hScroll();
621
dst.ry() -= d->vScroll();
627
QRect ImageView::mapToViewport(const QRect& src) const {
629
mapToViewport(src.topLeft()),
630
mapToViewport(src.bottomRight())
636
QRectF ImageView::mapToViewportF(const QRectF& src) const {
638
mapToViewportF(src.topLeft()),
639
mapToViewportF(src.bottomRight())
645
QPoint ImageView::mapToImage(const QPoint& src) const {
648
dst.rx() += d->hScroll();
649
dst.ry() += d->vScroll();
651
dst -= imageOffset();
653
return QPoint(int(dst.x() / d->mZoom), int(dst.y() / d->mZoom));
657
QPointF ImageView::mapToImageF(const QPointF& src) const {
660
dst.rx() += d->hScroll();
661
dst.ry() += d->vScroll();
663
dst -= imageOffset();
665
return dst / d->mZoom;
669
QRect ImageView::mapToImage(const QRect& src) const {
671
mapToImage(src.topLeft()),
672
mapToImage(src.bottomRight())
678
QRectF ImageView::mapToImageF(const QRectF& src) const {
680
mapToImageF(src.topLeft()),
681
mapToImageF(src.bottomRight())
687
qreal ImageView::computeZoomToFit() const {
688
qreal zoom = qMin(computeZoomToFitWidth(), computeZoomToFitHeight());
690
if (!d->mEnlargeSmallerImages) {
691
zoom = qMin(zoom, qreal(1.0));
698
qreal ImageView::computeZoomToFitWidth() const {
699
if (!d->mDocument || !d->mDocument->size().isValid()) {
702
return qreal(d->mViewport->width()) / d->mDocument->width();
706
qreal ImageView::computeZoomToFitHeight() const {
707
if (!d->mDocument || !d->mDocument->size().isValid()) {
710
return qreal(d->mViewport->height()) / d->mDocument->height();
714
void ImageView::showEvent(QShowEvent* event) {
715
QAbstractScrollArea::showEvent(event);
716
d->startAnimationIfNecessary();
720
void ImageView::hideEvent(QHideEvent* event) {
721
QAbstractScrollArea::hideEvent(event);
723
d->mDocument->stopAnimation();
728
void ImageView::mousePressEvent(QMouseEvent* event) {
730
d->mTool->mousePressEvent(event);
735
void ImageView::mouseMoveEvent(QMouseEvent* event) {
737
d->mTool->mouseMoveEvent(event);
742
void ImageView::mouseReleaseEvent(QMouseEvent* event) {
744
d->mTool->mouseReleaseEvent(event);
749
void ImageView::wheelEvent(QWheelEvent* event) {
751
d->mTool->wheelEvent(event);
756
void ImageView::keyPressEvent(QKeyEvent* event) {
758
d->mTool->keyPressEvent(event);
760
QAbstractScrollArea::keyPressEvent(event);
764
void ImageView::keyReleaseEvent(QKeyEvent* event) {
766
d->mTool->keyReleaseEvent(event);
768
QAbstractScrollArea::keyReleaseEvent(event);