|
|
// vim:set tabstop=4 shiftwidth=4 noexpandtab:
|
|
|
/*
|
|
|
Gwenview - A simple image viewer for KDE
|
|
|
Copyright 2000-2004 Aur<75>ien G<>eau
|
|
|
|
|
|
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 "config.h"
|
|
|
|
|
|
#include "imageview.moc"
|
|
|
|
|
|
#include <assert.h>
|
|
|
#include <math.h>
|
|
|
|
|
|
// Qt
|
|
|
#include <qcolor.h>
|
|
|
#include <qcombobox.h>
|
|
|
#include <qcursor.h>
|
|
|
#include <qdatetime.h>
|
|
|
#include <qevent.h>
|
|
|
#include <qpainter.h>
|
|
|
#include <qpixmap.h>
|
|
|
#include <qlabel.h>
|
|
|
#include <qtimer.h>
|
|
|
#include <qvaluevector.h>
|
|
|
|
|
|
// KDE
|
|
|
#include <kaction.h>
|
|
|
#include <kconfig.h>
|
|
|
#include <kdebug.h>
|
|
|
#include <kdeversion.h>
|
|
|
#include <klocale.h>
|
|
|
#include <kstandarddirs.h>
|
|
|
#include <kstdaction.h>
|
|
|
#include <kurldrag.h>
|
|
|
#include <kapplication.h>
|
|
|
|
|
|
// Local
|
|
|
#include "document.h"
|
|
|
#include "imageutils/imageutils.h"
|
|
|
#include "bcgdialog.h"
|
|
|
#include "busylevelmanager.h"
|
|
|
#include "imageviewtools.h"
|
|
|
#include "imageutils/croppedqimage.h"
|
|
|
#include "imageviewconfig.h"
|
|
|
|
|
|
namespace Gwenview {
|
|
|
|
|
|
/*
|
|
|
|
|
|
Coordinates:
|
|
|
|
|
|
The image can be zoomed, can have a position offset, and additionally there is
|
|
|
QScrollView's viewport. This means there are several coordinate systems.
|
|
|
|
|
|
|
|
|
|
|
|
Let's start from simple things. Viewport ignored, zoom ignored:
|
|
|
|
|
|
A-----------------------------------
|
|
|
| |
|
|
|
| |
|
|
|
| B--------------------- |
|
|
|
| | | |
|
|
|
| | | |
|
|
|
| | | |
|
|
|
| | | |
|
|
|
| | | |
|
|
|
| | | |
|
|
|
| ---------------------C |
|
|
|
| |
|
|
|
| |
|
|
|
------------------------------------
|
|
|
|
|
|
|
|
|
The inner rectangle is the image, outer rectangle is the widget.
|
|
|
A = [ 0, 0 ]
|
|
|
B = [ mXOffset, mYOffset ]
|
|
|
C = B + [ mDocument->width(), mDocument->height() ]
|
|
|
|
|
|
|
|
|
|
|
|
The same, additionally the image is zoomed.
|
|
|
|
|
|
A = [ 0, 0 ]
|
|
|
B = [ mXOffset, mYOffset ]
|
|
|
C = [ mZoom * mDocument->width(), mZoom * mDocument->height()) ]
|
|
|
|
|
|
The groups of functions imageToWidget() and widgetToImage() do conversions
|
|
|
between the image and widget coordinates, i.e. imageToWidget() accepts coordinates
|
|
|
in the image (original,not zoomed,image's topleft corner is [0,0]) and returns
|
|
|
coordinates in the picture above, widgetToImage() works the other way around.
|
|
|
|
|
|
There's no bounds checking, so widgetToImage( A ) in the example above would
|
|
|
return image coordinate with negative x,y.
|
|
|
|
|
|
The widgetToImage() functions round the values (in order to have the conversion
|
|
|
as approximate as possible). However when converting from widget to image and back
|
|
|
this can result in the final rectangle being smaller than the original.
|
|
|
The widgetToImageBounding() function converts from widget to image coordinates
|
|
|
in a way which makes sure the reverse conversion will be at least as large
|
|
|
as the original geometry.
|
|
|
|
|
|
There are no conversion functions for only width/height, as their conversion
|
|
|
depends on the position (because of the rounding etc.). For similar reasons
|
|
|
conversions should not be done with the bottomright corner of a rectangle,
|
|
|
but with the point next to it.
|
|
|
|
|
|
|
|
|
|
|
|
For conversions from/to QScrollView's viewport, usually QScrollView methods should
|
|
|
be used: contentsX(), contentsY(), contentsWidth(), contentsHeight(), visibleWidth(),
|
|
|
visibleHeight(), contentsToViewport() and viewportToContents().
|
|
|
|
|
|
*/
|
|
|
|
|
|
const double MAX_ZOOM=16.0; // Same value as GIMP
|
|
|
|
|
|
const int DEFAULT_MAX_REPAINT_SIZE = 10000;
|
|
|
const int LIMIT_MAX_REPAINT_SIZE = 10000000;
|
|
|
|
|
|
#ifndef HAVE_LROUND
|
|
|
inline
|
|
|
long int lround( double x ) {
|
|
|
return static_cast< long int >( x >= 0 ? x + 0.5 : x - 0.5 );
|
|
|
}
|
|
|
#endif
|
|
|
|
|
|
|
|
|
struct ImageView::Private {
|
|
|
Document* mDocument;
|
|
|
|
|
|
Tools mTools;
|
|
|
|
|
|
ToolID mToolID;
|
|
|
|
|
|
// Offset to center images
|
|
|
int mXOffset, mYOffset;
|
|
|
|
|
|
// Zoom info
|
|
|
ZoomMode mZoomMode;
|
|
|
double mZoom;
|
|
|
|
|
|
// Gamma, brightness, contrast - multiplied by 100
|
|
|
int mGamma, mBrightness, mContrast;
|
|
|
|
|
|
// Our actions
|
|
|
QComboBox* mZoomCombo;
|
|
|
// We do not use KSelectAction because it's not possible to set the combo text
|
|
|
KWidgetAction* mZoomComboAction;
|
|
|
KToggleAction* mZoomToFit;
|
|
|
KToggleAction* mZoomToWidth;
|
|
|
KToggleAction* mZoomToHeight;
|
|
|
QValueVector<KToggleAction*> mZoomComboActions;
|
|
|
KAction* mZoomIn;
|
|
|
KAction* mZoomOut;
|
|
|
KAction* mResetZoom;
|
|
|
KToggleAction* mLockZoom;
|
|
|
KAction* mAdjustBCG;
|
|
|
KAction* mIncreaseGamma;
|
|
|
KAction* mDecreaseGamma;
|
|
|
KAction* mIncreaseBrightness;
|
|
|
KAction* mDecreaseBrightness;
|
|
|
KAction* mIncreaseContrast;
|
|
|
KAction* mDecreaseContrast;
|
|
|
KActionCollection* mActionCollection;
|
|
|
BCGDialog* mBCGDialog;
|
|
|
|
|
|
// Fullscreen stuff
|
|
|
bool mFullScreen;
|
|
|
|
|
|
// Object state info
|
|
|
bool mOperaLikePrevious; // Flag to avoid showing the popup menu on Opera like previous
|
|
|
double mZoomBeforeAuto;
|
|
|
int mXCenterBeforeAuto, mYCenterBeforeAuto;
|
|
|
|
|
|
QMap< long long, PendingPaint > mPendingPaints;
|
|
|
QRegion mPendingNormalRegion;
|
|
|
QRegion mPendingSmoothRegion;
|
|
|
int mPendingOperations;
|
|
|
QTimer mPendingPaintTimer;
|
|
|
bool mSmoothingSuspended;
|
|
|
QRegion mValidImageArea;
|
|
|
|
|
|
int imageToWidgetX( int x ) const {
|
|
|
if( mZoom == 1.0 ) return x + mXOffset;
|
|
|
return lround( x * mZoom ) + mXOffset;
|
|
|
}
|
|
|
|
|
|
int imageToWidgetY( int y ) const {
|
|
|
if( mZoom == 1.0 ) return y + mYOffset;
|
|
|
return lround( y * mZoom ) + mYOffset;
|
|
|
}
|
|
|
|
|
|
QPoint imageToWidget( const QPoint& p ) const {
|
|
|
return QPoint( imageToWidgetX( p.x()), imageToWidgetY( p.y()));
|
|
|
}
|
|
|
|
|
|
QRect imageToWidget( const QRect& r ) const {
|
|
|
return QRect( imageToWidget( r.topLeft()),
|
|
|
// don't use bottomright corner for conversion, but the one next to it
|
|
|
imageToWidget( r.bottomRight() + QPoint( 1, 1 )) - QPoint( 1, 1 ));
|
|
|
}
|
|
|
|
|
|
int widgetToImageX( int x ) const {
|
|
|
if( mZoom == 1.0 ) return x - mXOffset;
|
|
|
return lround( ( x - mXOffset ) / mZoom );
|
|
|
}
|
|
|
|
|
|
int widgetToImageY( int y ) const {
|
|
|
if( mZoom == 1.0 ) return y - mYOffset;
|
|
|
return lround( ( y - mYOffset ) / mZoom );
|
|
|
}
|
|
|
|
|
|
QPoint widgetToImage( const QPoint& p ) const {
|
|
|
return QPoint( widgetToImageX( p.x()), widgetToImageY( p.y()));
|
|
|
}
|
|
|
|
|
|
QRect widgetToImage( const QRect& r ) const {
|
|
|
return QRect( widgetToImage( r.topLeft()),
|
|
|
// don't use bottomright corner for conversion, but the one next to it
|
|
|
widgetToImage( r.bottomRight() + QPoint( 1, 1 )) - QPoint( 1, 1 ));
|
|
|
}
|
|
|
|
|
|
QRect widgetToImageBounding( const QRect& r, int extra ) const {
|
|
|
QRect ret = widgetToImage( r );
|
|
|
// make sure converting to image and back always returns QRect at least as large as 'r'
|
|
|
extra += mZoom == 1.0 ? 0 : int( ceil( 1 / mZoom ));
|
|
|
ret.addCoords( -extra, -extra, extra, extra );
|
|
|
return ret;
|
|
|
}
|
|
|
|
|
|
void initZoomCombo() {
|
|
|
mZoomCombo->clear();
|
|
|
for (QValueVector<KToggleAction*>::iterator it=mZoomComboActions.begin();
|
|
|
it!=mZoomComboActions.end();
|
|
|
++it)
|
|
|
{
|
|
|
QString txt=(*it)->plainText();
|
|
|
mZoomCombo->insertItem(txt);
|
|
|
}
|
|
|
|
|
|
const double zoomValues[] = { 0.5, 1, 2 };
|
|
|
int nbValues=sizeof(zoomValues) / sizeof(double);
|
|
|
for (int pos=0; pos<nbValues; ++pos) {
|
|
|
QString txt=QString("%1%").arg( int(zoomValues[pos]*100) );
|
|
|
mZoomCombo->insertItem(txt);
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
inline bool doDelayedSmoothing() {
|
|
|
return ImageViewConfig::delayedSmoothing()
|
|
|
&& ImageViewConfig::smoothAlgorithm()!=ImageUtils::SMOOTH_NONE;
|
|
|
}
|
|
|
|
|
|
|
|
|
class ImageView::EventFilter : public QObject {
|
|
|
public:
|
|
|
EventFilter(ImageView* parent)
|
|
|
: QObject(parent) {}
|
|
|
|
|
|
bool eventFilter(QObject*, QEvent* event) {
|
|
|
switch (event->type()) {
|
|
|
case QEvent::KeyPress:
|
|
|
case QEvent::KeyRelease:
|
|
|
case QEvent::AccelOverride:
|
|
|
return static_cast< ImageView* >( parent())
|
|
|
->viewportKeyEvent(static_cast<QKeyEvent*>(event));
|
|
|
default:
|
|
|
break;
|
|
|
}
|
|
|
return false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
ImageView::ImageView(QWidget* parent,Document* document, KActionCollection* actionCollection)
|
|
|
: QScrollView(parent,0L,WResizeNoErase|WRepaintNoErase|WPaintClever)
|
|
|
{
|
|
|
d=new Private;
|
|
|
d->mDocument=document;
|
|
|
d->mToolID=SCROLL;
|
|
|
d->mXOffset=0;
|
|
|
d->mYOffset=0;
|
|
|
d->mZoomMode=static_cast<ZoomMode>( ImageViewConfig::zoomMode() );
|
|
|
d->mZoom=1;
|
|
|
d->mActionCollection=actionCollection;
|
|
|
d->mFullScreen=false;
|
|
|
d->mOperaLikePrevious=false;
|
|
|
d->mZoomBeforeAuto=1;
|
|
|
d->mPendingOperations= 0 ;
|
|
|
d->mSmoothingSuspended= false ;
|
|
|
d->mGamma = 100;
|
|
|
d->mBrightness = 0;
|
|
|
d->mContrast = 100;
|
|
|
d->mBCGDialog = 0;
|
|
|
|
|
|
viewport()->setFocusPolicy(WheelFocus);
|
|
|
setFrameStyle(NoFrame);
|
|
|
setAcceptDrops( true );
|
|
|
viewport()->setAcceptDrops( true );
|
|
|
|
|
|
updateScrollBarMode();
|
|
|
viewport()->setBackgroundColor(ImageViewConfig::backgroundColor() );
|
|
|
|
|
|
d->mTools[SCROLL]=new ScrollTool(this);
|
|
|
d->mTools[ZOOM]=new ZoomTool(this);
|
|
|
d->mTools[d->mToolID]->updateCursor();
|
|
|
|
|
|
// Create actions
|
|
|
d->mZoomToFit=new KToggleAction(i18n("Fit to &Window"),"viewmagfit",0,d->mActionCollection,"view_zoom_to_fit");
|
|
|
connect(d->mZoomToFit,SIGNAL(toggled(bool)),
|
|
|
this,SLOT(setZoomToFit(bool)) );
|
|
|
d->mZoomToWidth=new KToggleAction(i18n("Fit to &Width"),0,0,d->mActionCollection,"view_zoom_to_width");
|
|
|
connect(d->mZoomToWidth,SIGNAL(toggled(bool)),
|
|
|
this,SLOT(setZoomToWidth(bool)) );
|
|
|
d->mZoomToHeight=new KToggleAction(i18n("Fit to &Height"),0,0,d->mActionCollection,"view_zoom_to_height");
|
|
|
connect(d->mZoomToHeight,SIGNAL(toggled(bool)),
|
|
|
this,SLOT(setZoomToHeight(bool)) );
|
|
|
|
|
|
d->mZoomIn=KStdAction::zoomIn(this,SLOT(slotZoomIn()),d->mActionCollection);
|
|
|
|
|
|
d->mZoomOut=KStdAction::zoomOut(this,SLOT(slotZoomOut()),d->mActionCollection);
|
|
|
|
|
|
d->mResetZoom=KStdAction::actualSize(this,SLOT(slotResetZoom()),d->mActionCollection);
|
|
|
d->mResetZoom->setIcon("viewmag1");
|
|
|
|
|
|
d->mLockZoom=new KToggleAction(i18n("&Lock Zoom"),"lock",0,d->mActionCollection,"view_zoom_lock");
|
|
|
d->mLockZoom->setChecked(ImageViewConfig::lockZoom());
|
|
|
connect(d->mLockZoom,SIGNAL(toggled(bool)),
|
|
|
this,SLOT(setLockZoom(bool)) );
|
|
|
|
|
|
d->mZoomCombo=new QComboBox(true);
|
|
|
// Avoid stealing focus
|
|
|
d->mZoomCombo->setFocusPolicy(ClickFocus);
|
|
|
connect(d->mZoomCombo, SIGNAL(activated(int)),
|
|
|
this, SLOT(slotSelectZoom()) );
|
|
|
|
|
|
d->mZoomComboAction=new KWidgetAction(d->mZoomCombo, i18n("Zoom"), 0, 0, 0, d->mActionCollection, "view_zoom_to");
|
|
|
|
|
|
d->mZoomComboActions.append(d->mZoomToFit);
|
|
|
d->mZoomComboActions.append(d->mZoomToWidth);
|
|
|
d->mZoomComboActions.append(d->mZoomToHeight);
|
|
|
if (d->mZoomMode!=ZOOM_FREE) {
|
|
|
d->mZoomComboActions[d->mZoomMode]->setChecked(true);
|
|
|
}
|
|
|
d->initZoomCombo();
|
|
|
|
|
|
d->mAdjustBCG=new KAction(i18n("Adjust Brightness/Contrast/Gamma"), "colorize", 0,
|
|
|
this, SLOT(showBCGDialog()), d->mActionCollection, "adjust_bcg");
|
|
|
d->mIncreaseGamma=new KAction(i18n("Increase Gamma"),0,CTRL+Key_G,
|
|
|
this,SLOT(increaseGamma()),d->mActionCollection,"increase_gamma");
|
|
|
d->mDecreaseGamma=new KAction(i18n("Decrease Gamma"),0,SHIFT+CTRL+Key_G,
|
|
|
this,SLOT(decreaseGamma()),d->mActionCollection,"decrease_gamma");
|
|
|
d->mIncreaseBrightness=new KAction(i18n("Increase Brightness" ),0,CTRL+Key_B,
|
|
|
this,SLOT(increaseBrightness()),d->mActionCollection,"increase_brightness");
|
|
|
d->mDecreaseBrightness=new KAction(i18n("Decrease Brightness" ),0,SHIFT+CTRL+Key_B,
|
|
|
this,SLOT(decreaseBrightness()),d->mActionCollection,"decrease_brightness");
|
|
|
d->mIncreaseContrast=new KAction(i18n("Increase Contrast" ),0,CTRL+Key_C,
|
|
|
this,SLOT(increaseContrast()),d->mActionCollection,"increase_contrast");
|
|
|
d->mDecreaseContrast=new KAction(i18n("Decrease Contrast" ),0,SHIFT+CTRL+Key_C,
|
|
|
this,SLOT(decreaseContrast()),d->mActionCollection,"decrease_contrast");
|
|
|
|
|
|
// Connect to some interesting signals
|
|
|
connect(d->mDocument,SIGNAL(loaded(const KURL&)),
|
|
|
this,SLOT(slotLoaded()) );
|
|
|
|
|
|
connect(d->mDocument,SIGNAL(loading()),
|
|
|
this,SLOT( loadingStarted()) );
|
|
|
|
|
|
connect(d->mDocument,SIGNAL(modified()),
|
|
|
this,SLOT(slotModified()) );
|
|
|
|
|
|
connect(d->mDocument, SIGNAL(sizeUpdated()),
|
|
|
this, SLOT(slotImageSizeUpdated()) );
|
|
|
|
|
|
connect(d->mDocument, SIGNAL(rectUpdated(const QRect&)),
|
|
|
this, SLOT(slotImageRectUpdated(const QRect&)) );
|
|
|
|
|
|
connect(&d->mPendingPaintTimer,SIGNAL(timeout()),
|
|
|
this,SLOT(checkPendingOperations()) );
|
|
|
|
|
|
connect(BusyLevelManager::instance(),SIGNAL(busyLevelChanged(BusyLevel)),
|
|
|
this,SLOT(slotBusyLevelChanged(BusyLevel) ));
|
|
|
|
|
|
// This event filter is here to make sure the pixmap view is aware of the changes
|
|
|
// in the keyboard modifiers, even if it isn't focused. However, making this widget
|
|
|
// itself the filter would lead to doubled paint events, because QScrollView
|
|
|
// installs an event filter on its viewport, and doesn't filter out the paint
|
|
|
// events -> it'd get it twice, first from app filter, second from viewport filter.
|
|
|
EventFilter* filter=new EventFilter(this);
|
|
|
kapp->installEventFilter(filter);
|
|
|
}
|
|
|
|
|
|
|
|
|
ImageView::~ImageView() {
|
|
|
ImageViewConfig::setZoomMode(d->mZoomMode);
|
|
|
ImageViewConfig::setLockZoom(d->mLockZoom->isChecked());
|
|
|
ImageViewConfig::self()->writeConfig();
|
|
|
delete d->mTools[SCROLL];
|
|
|
delete d->mTools[ZOOM];
|
|
|
delete d;
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::slotLoaded() {
|
|
|
if (d->mDocument->isNull()) {
|
|
|
resizeContents(0,0);
|
|
|
viewport()->repaint(false);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (doDelayedSmoothing()) scheduleOperation( SMOOTH_PASS );
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::slotModified() {
|
|
|
if (d->mZoomMode!=ZOOM_FREE) {
|
|
|
updateZoom(d->mZoomMode);
|
|
|
} else {
|
|
|
updateContentSize();
|
|
|
updateImageOffset();
|
|
|
updateZoomActions();
|
|
|
fullRepaint();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::loadingStarted() {
|
|
|
cancelPending();
|
|
|
d->mSmoothingSuspended = true;
|
|
|
d->mValidImageArea = QRegion();
|
|
|
d->mGamma = 100;
|
|
|
d->mBrightness = 0;
|
|
|
d->mContrast = 100;
|
|
|
|
|
|
if (!d->mLockZoom->isChecked()) {
|
|
|
d->mZoomBeforeAuto = 1.;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//------------------------------------------------------------------------
|
|
|
//
|
|
|
// Properties
|
|
|
//
|
|
|
//------------------------------------------------------------------------
|
|
|
double ImageView::zoom() const {
|
|
|
return d->mZoom;
|
|
|
}
|
|
|
|
|
|
|
|
|
bool ImageView::fullScreen() const {
|
|
|
return d->mFullScreen;
|
|
|
}
|
|
|
|
|
|
|
|
|
QPoint ImageView::offset() const {
|
|
|
return QPoint(d->mXOffset, d->mYOffset);
|
|
|
}
|
|
|
|
|
|
|
|
|
bool ImageView::canZoom(bool in) const {
|
|
|
KAction* zoomAction=in ? d->mZoomIn : d->mZoomOut;
|
|
|
return zoomAction->isEnabled();
|
|
|
}
|
|
|
|
|
|
|
|
|
KToggleAction* ImageView::zoomToFit() const {
|
|
|
return d->mZoomToFit;
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::updateFromSettings() {
|
|
|
// Reset, so that next repaint doesn't possibly take longer because of
|
|
|
// smoothing
|
|
|
ImageViewConfig::setMaxRepaintSize(DEFAULT_MAX_REPAINT_SIZE);
|
|
|
ImageViewConfig::setMaxScaleRepaintSize(DEFAULT_MAX_REPAINT_SIZE);
|
|
|
ImageViewConfig::setMaxSmoothRepaintSize(DEFAULT_MAX_REPAINT_SIZE);
|
|
|
|
|
|
if( doDelayedSmoothing() ) {
|
|
|
scheduleOperation( SMOOTH_PASS );
|
|
|
} else {
|
|
|
fullRepaint();
|
|
|
}
|
|
|
|
|
|
// If enlargeSmallImage changed
|
|
|
if (d->mZoomMode!=ZOOM_FREE) {
|
|
|
updateZoom(d->mZoomMode);
|
|
|
}
|
|
|
|
|
|
updateScrollBarMode();
|
|
|
|
|
|
if (!d->mFullScreen) {
|
|
|
viewport()->setBackgroundColor(ImageViewConfig::backgroundColor() );
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::setZoom(double zoom, int centerX, int centerY) {
|
|
|
updateZoom(ZOOM_FREE, zoom, centerX, centerY);
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::updateZoom(ZoomMode zoomMode, double value, int centerX, int centerY) {
|
|
|
ZoomMode oldZoomMode = d->mZoomMode;
|
|
|
double oldZoom=d->mZoom;
|
|
|
d->mZoomMode=zoomMode;
|
|
|
KAction* checkedZoomAction=0;
|
|
|
|
|
|
viewport()->setUpdatesEnabled(false);
|
|
|
|
|
|
if (zoomMode==ZOOM_FREE) {
|
|
|
Q_ASSERT(value!=0);
|
|
|
d->mZoom=value;
|
|
|
} else {
|
|
|
if (oldZoomMode == ZOOM_FREE) {
|
|
|
// Only store zoom before auto if we were in ZOOM_FREE mode, otherwise
|
|
|
// we will store the computed auto zoom value (Bug 134590)
|
|
|
d->mZoomBeforeAuto = d->mZoom;
|
|
|
}
|
|
|
d->mXCenterBeforeAuto=width()/2 + contentsX() + d->mXOffset;
|
|
|
d->mYCenterBeforeAuto=height()/2 + contentsY() + d->mYOffset;
|
|
|
|
|
|
if (zoomMode==ZOOM_FIT) {
|
|
|
d->mZoom=computeZoomToFit();
|
|
|
checkedZoomAction=d->mZoomToFit;
|
|
|
|
|
|
} else if (zoomMode==ZOOM_FIT_WIDTH) {
|
|
|
d->mZoom=computeZoomToWidth();
|
|
|
checkedZoomAction=d->mZoomToWidth;
|
|
|
|
|
|
} else {
|
|
|
d->mZoom=computeZoomToHeight();
|
|
|
checkedZoomAction=d->mZoomToHeight;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Make sure only one zoom action is toggled on
|
|
|
d->mZoomToFit->setChecked( checkedZoomAction==d->mZoomToFit);
|
|
|
d->mZoomToWidth->setChecked( checkedZoomAction==d->mZoomToWidth);
|
|
|
d->mZoomToHeight->setChecked(checkedZoomAction==d->mZoomToHeight);
|
|
|
|
|
|
updateContentSize();
|
|
|
|
|
|
// Find the coordinate of the center of the image
|
|
|
// and center the view on it
|
|
|
if (centerX==-1) {
|
|
|
centerX=int( ((visibleWidth()/2+contentsX()-d->mXOffset)/oldZoom)*d->mZoom );
|
|
|
}
|
|
|
if (centerY==-1) {
|
|
|
centerY=int( ((visibleHeight()/2+contentsY()-d->mYOffset)/oldZoom)*d->mZoom );
|
|
|
}
|
|
|
center(centerX,centerY);
|
|
|
|
|
|
updateScrollBarMode();
|
|
|
updateImageOffset();
|
|
|
updateZoomActions();
|
|
|
|
|
|
viewport()->setUpdatesEnabled(true);
|
|
|
fullRepaint();
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::setFullScreen(bool fullScreen) {
|
|
|
d->mFullScreen=fullScreen;
|
|
|
|
|
|
if (d->mFullScreen) {
|
|
|
viewport()->setBackgroundColor(black);
|
|
|
} else {
|
|
|
viewport()->setBackgroundColor(ImageViewConfig::backgroundColor() );
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
//------------------------------------------------------------------------
|
|
|
//
|
|
|
// Overloaded methods
|
|
|
//
|
|
|
//------------------------------------------------------------------------
|
|
|
void ImageView::resizeEvent(QResizeEvent* event) {
|
|
|
QScrollView::resizeEvent(event);
|
|
|
if (d->mZoomMode!=ZOOM_FREE) {
|
|
|
updateZoom(d->mZoomMode);
|
|
|
} else {
|
|
|
updateContentSize();
|
|
|
updateImageOffset();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
inline void composite(uint* rgba,uint value) {
|
|
|
uint alpha=(*rgba) >> 24;
|
|
|
if (alpha<255) {
|
|
|
uint alphaValue=(255-alpha)*value;
|
|
|
|
|
|
uint c1=( ( (*rgba & 0xFF0000) >> 16 ) * alpha + alphaValue ) >> 8;
|
|
|
uint c2=( ( (*rgba & 0x00FF00) >> 8 ) * alpha + alphaValue ) >> 8;
|
|
|
uint c3=( ( (*rgba & 0x0000FF) >> 0 ) * alpha + alphaValue ) >> 8;
|
|
|
*rgba=0xFF000000 + (c1<<16) + (c2<<8) + c3;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void ImageView::drawContents(QPainter* painter,int clipx,int clipy,int clipw,int cliph) {
|
|
|
// Erase borders
|
|
|
QRect imageRect(0, 0, d->mDocument->width(), d->mDocument->height());
|
|
|
imageRect = d->imageToWidget(imageRect);
|
|
|
|
|
|
QRect widgetRect = QRect(0, 0, visibleWidth(), visibleHeight());
|
|
|
|
|
|
QRegion region = QRegion(widgetRect) - imageRect;
|
|
|
QMemArray<QRect> rects = region.rects();
|
|
|
for(unsigned int pos = 0; pos < rects.count(); ++pos ) {
|
|
|
painter->eraseRect(rects[pos]);
|
|
|
}
|
|
|
|
|
|
// Repaint
|
|
|
if( !d->mValidImageArea.isEmpty()) {
|
|
|
addPendingPaint( false, QRect( clipx, clipy, clipw, cliph ));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// How this pending stuff works:
|
|
|
// There's a queue of areas to paint (each with bool saying whether it's smooth pass).
|
|
|
// Also, there's a bitfield of pending operations, operations are handled only after
|
|
|
// there's nothing more to paint (so that smooth pass is started).
|
|
|
void ImageView::addPendingPaint( bool smooth, QRect rect ) {
|
|
|
if( d->mSmoothingSuspended && smooth ) return;
|
|
|
|
|
|
// try to avoid scheduling already scheduled areas
|
|
|
QRegion& region = smooth ? d->mPendingSmoothRegion : d->mPendingNormalRegion;
|
|
|
if( region.intersect( rect ) == QRegion( rect ))
|
|
|
return; // whole rect has already pending paints
|
|
|
// at least try to remove the part that's already scheduled
|
|
|
rect = ( QRegion( rect ) - region ).boundingRect();
|
|
|
region += rect;
|
|
|
if( rect.isEmpty())
|
|
|
return;
|
|
|
addPendingPaintInternal( smooth, rect );
|
|
|
}
|
|
|
|
|
|
void ImageView::addPendingPaintInternal( bool smooth, QRect rect ) {
|
|
|
const long long MAX_DIM = 1000000; // if monitors get larger than this, we're in trouble :)
|
|
|
// QMap will ensure ordering (non-smooth first, top-to-bottom, left-to-right)
|
|
|
long long key = ( smooth ? MAX_DIM * MAX_DIM : 0 ) + rect.y() * MAX_DIM + rect.x();
|
|
|
// handle the case of two different paints at the same position (just in case)
|
|
|
key *= 100;
|
|
|
bool insert = true;
|
|
|
while( d->mPendingPaints.contains( key )) {
|
|
|
if( d->mPendingPaints[ key ].rect.contains( rect )) {
|
|
|
insert = false;
|
|
|
break;
|
|
|
}
|
|
|
if( rect.contains( d->mPendingPaints[ key ].rect )) {
|
|
|
break;
|
|
|
}
|
|
|
++key;
|
|
|
}
|
|
|
if( insert ) {
|
|
|
d->mPendingPaints[ key ] = PendingPaint( smooth, rect );
|
|
|
}
|
|
|
scheduleOperation( CHECK_OPERATIONS );
|
|
|
}
|
|
|
|
|
|
void ImageView::checkPendingOperations() {
|
|
|
checkPendingOperationsInternal();
|
|
|
if( d->mPendingPaints.isEmpty() && d->mPendingOperations == 0 ) {
|
|
|
d->mPendingPaintTimer.stop();
|
|
|
}
|
|
|
updateBusyLevels();
|
|
|
}
|
|
|
|
|
|
void ImageView::limitPaintSize( PendingPaint& paint ) {
|
|
|
// The only thing that makes time spent in performPaint() vary
|
|
|
// is whether there will be scaling and whether there will be smoothing.
|
|
|
// So there are three max sizes for each mode.
|
|
|
int maxSize = ImageViewConfig::maxRepaintSize();
|
|
|
if( d->mZoom != 1.0 ) {
|
|
|
if( paint.smooth || !doDelayedSmoothing() ) {
|
|
|
maxSize = ImageViewConfig::maxSmoothRepaintSize();
|
|
|
} else {
|
|
|
maxSize = ImageViewConfig::maxScaleRepaintSize();
|
|
|
}
|
|
|
}
|
|
|
// don't paint more than max_size pixels at a time
|
|
|
int maxHeight = ( maxSize + paint.rect.width() - 1 ) / paint.rect.width(); // round up
|
|
|
maxHeight = QMAX( maxHeight, 5 ); // at least 5 lines together
|
|
|
// can't repaint whole paint at once, adjust height and schedule the rest
|
|
|
if( maxHeight < paint.rect.height()) {
|
|
|
QRect remaining = paint.rect;
|
|
|
remaining.setTop( remaining.top() + maxHeight );
|
|
|
addPendingPaintInternal( paint.smooth, remaining );
|
|
|
paint.rect.setHeight( maxHeight );
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::checkPendingOperationsInternal() {
|
|
|
if( !d->mPendingPaintTimer.isActive()) // suspended
|
|
|
return;
|
|
|
while( !d->mPendingPaints.isEmpty()) {
|
|
|
PendingPaint paint = *d->mPendingPaints.begin();
|
|
|
d->mPendingPaints.remove( d->mPendingPaints.begin());
|
|
|
limitPaintSize( paint ); // modifies paint.rect if necessary
|
|
|
QRegion& region = paint.smooth ? d->mPendingSmoothRegion : d->mPendingNormalRegion;
|
|
|
region -= paint.rect;
|
|
|
QRect visibleRect( contentsX(), contentsY(), visibleWidth(), visibleHeight());
|
|
|
QRect paintRect = paint.rect.intersect( visibleRect );
|
|
|
if( !paintRect.isEmpty()) {
|
|
|
QPainter painter( viewport());
|
|
|
painter.translate( -contentsX(), -contentsY());
|
|
|
performPaint( &painter, paintRect.x(), paintRect.y(),
|
|
|
paintRect.width(), paintRect.height(), paint.smooth );
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
if( d->mPendingOperations & SMOOTH_PASS ) {
|
|
|
d->mSmoothingSuspended = false;
|
|
|
if( doDelayedSmoothing() ) {
|
|
|
QRect visibleRect( contentsX(), contentsY(), visibleWidth(), visibleHeight());
|
|
|
addPendingPaint( true, visibleRect );
|
|
|
}
|
|
|
d->mPendingOperations &= ~SMOOTH_PASS;
|
|
|
return;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void ImageView::scheduleOperation( Operation operation )
|
|
|
{
|
|
|
d->mPendingOperations |= operation;
|
|
|
slotBusyLevelChanged( BusyLevelManager::instance()->busyLevel());
|
|
|
updateBusyLevels();
|
|
|
}
|
|
|
|
|
|
void ImageView::updateBusyLevels() {
|
|
|
if( !d->mPendingPaintTimer.isActive()) {
|
|
|
BusyLevelManager::instance()->setBusyLevel( this, BUSY_NONE );
|
|
|
} else if( !d->mPendingPaints.isEmpty() && !(*d->mPendingPaints.begin()).smooth ) {
|
|
|
BusyLevelManager::instance()->setBusyLevel( this, BUSY_PAINTING );
|
|
|
} else if(( d->mPendingOperations & SMOOTH_PASS )
|
|
|
|| ( !d->mPendingPaints.isEmpty() && (*d->mPendingPaints.begin()).smooth )) {
|
|
|
BusyLevelManager::instance()->setBusyLevel( this, BUSY_SMOOTHING );
|
|
|
} else {
|
|
|
assert( false );
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void ImageView::slotBusyLevelChanged( BusyLevel level ) {
|
|
|
bool resume = false;
|
|
|
if( level <= BUSY_PAINTING
|
|
|
&& !d->mPendingPaints.isEmpty() && !(*d->mPendingPaints.begin()).smooth ) {
|
|
|
resume = true;
|
|
|
} else if( level <= BUSY_SMOOTHING
|
|
|
&& (( d->mPendingOperations & SMOOTH_PASS )
|
|
|
|| ( !d->mPendingPaints.isEmpty() && (*d->mPendingPaints.begin()).smooth ))) {
|
|
|
resume = true;
|
|
|
}
|
|
|
if( resume ) {
|
|
|
d->mPendingPaintTimer.start( 0 );
|
|
|
} else {
|
|
|
d->mPendingPaintTimer.stop();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// How to do painting:
|
|
|
// When something needs to be erased: QPainter on viewport and eraseRect()
|
|
|
// When whole picture needs to be repainted: fullRepaint()
|
|
|
// When a part of the picture needs to be updated: viewport()->repaint(area,false)
|
|
|
// All other paints will be changed to progressive painting.
|
|
|
void ImageView::fullRepaint() {
|
|
|
if( !viewport()->isUpdatesEnabled()) return;
|
|
|
cancelPending();
|
|
|
viewport()->repaint(false);
|
|
|
}
|
|
|
|
|
|
void ImageView::cancelPending() {
|
|
|
d->mPendingPaints.clear();
|
|
|
d->mPendingNormalRegion = QRegion();
|
|
|
d->mPendingSmoothRegion = QRegion();
|
|
|
d->mPendingPaintTimer.stop();
|
|
|
d->mPendingOperations = 0;
|
|
|
updateBusyLevels();
|
|
|
}
|
|
|
|
|
|
//#define DEBUG_RECTS
|
|
|
|
|
|
// do the actual painting
|
|
|
void ImageView::performPaint( QPainter* painter, int clipx, int clipy, int clipw, int cliph, bool secondPass ) {
|
|
|
#ifdef DEBUG_RECTS
|
|
|
static QColor colors[4]={QColor(255,0,0),QColor(0,255,0),QColor(0,0,255),QColor(255,255,0) };
|
|
|
static int numColor=0;
|
|
|
#endif
|
|
|
|
|
|
QTime t;
|
|
|
t.start();
|
|
|
|
|
|
if (d->mDocument->isNull()) {
|
|
|
painter->eraseRect(clipx,clipy,clipw,cliph);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// True if another pass will follow
|
|
|
bool fastpass = doDelayedSmoothing() && zoom() != 1.0 && !secondPass;
|
|
|
|
|
|
ImageUtils::SmoothAlgorithm smoothAlgo = ImageUtils::SMOOTH_NONE;
|
|
|
if( zoom() != 1.0 ) {
|
|
|
if (doDelayedSmoothing() && !secondPass) {
|
|
|
// Add a second, smoothing pass
|
|
|
addPendingPaint( true, QRect( clipx, clipy, clipw, cliph ));
|
|
|
} else {
|
|
|
// We need to smooth now
|
|
|
smoothAlgo = static_cast<ImageUtils::SmoothAlgorithm>( ImageViewConfig::smoothAlgorithm() );
|
|
|
}
|
|
|
}
|
|
|
|
|
|
int extraPixels = ImageUtils::extraScalePixels( smoothAlgo, zoom());
|
|
|
QRect imageRect = d->widgetToImageBounding( QRect(clipx,clipy,clipw,cliph), extraPixels );
|
|
|
imageRect = imageRect.intersect( QRect( 0, 0, d->mDocument->width(), d->mDocument->height()));
|
|
|
QMemArray< QRect > rects = d->mValidImageArea.intersect( imageRect ).rects();
|
|
|
for( unsigned int i = 1; i < rects.count(); ++i ) {
|
|
|
addPendingPaint( secondPass, d->imageToWidget( rects[ i ] ));
|
|
|
}
|
|
|
imageRect = rects.count() > 0 ? rects[ 0 ] : QRect();
|
|
|
if (imageRect.isEmpty()) {
|
|
|
painter->eraseRect(clipx,clipy,clipw,cliph);
|
|
|
return;
|
|
|
}
|
|
|
QRect widgetRect = d->imageToWidget( imageRect );
|
|
|
if (widgetRect.isEmpty() || imageRect.isEmpty()) {
|
|
|
painter->eraseRect(clipx,clipy,clipw,cliph);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// With very large images, just getting a subimage using QImage::copy( QRect ) takes a significant
|
|
|
// portion of time here (even though it's just copying of data - probably because it's a lot of data).
|
|
|
// So don't do any subimage copying but instead use CroppedQImage which just manipulates scanline
|
|
|
// pointers. Note however that it's a bit hackish and there may be trouble if any code accesses
|
|
|
// the image data directly as a whole. See CroppedQImage for details.
|
|
|
|
|
|
// QImage image = d->mDocument->image().copy( imageRect );
|
|
|
ImageUtils::CroppedQImage image( d->mDocument->image(), imageRect );
|
|
|
|
|
|
if( zoom() != 1.0 ) {
|
|
|
image=ImageUtils::scale(image,widgetRect.width(),widgetRect.height(), smoothAlgo );
|
|
|
}
|
|
|
|
|
|
if( d->mBrightness != 0 ) {
|
|
|
image.normalize(); // needed, it will be modified
|
|
|
image = ImageUtils::changeBrightness( image, d->mBrightness );
|
|
|
}
|
|
|
|
|
|
if( d->mContrast != 100 ) { // != 1.0
|
|
|
image.normalize(); // needed, it will be modified
|
|
|
image = ImageUtils::changeContrast( image, d->mContrast );
|
|
|
}
|
|
|
|
|
|
if( d->mGamma != 100 ) { // != 1.0
|
|
|
image.normalize(); // needed, it will be modified
|
|
|
image = ImageUtils::changeGamma( image, d->mGamma );
|
|
|
}
|
|
|
|
|
|
// Calling normalize() here would make image to be a proper QImage without modified scanlines,
|
|
|
// so that even calling QImage::copy() would work. However, it seems it's not necessary to call
|
|
|
// it here. The code above checks that QImage::copy() or similar doesn't occur (that zoom() != 1.0
|
|
|
// is there primarily to avoid that). If any kind of redraw trouble occurs, try uncommenting this
|
|
|
// line below first.
|
|
|
// image.normalize(); // make it use its own data, if needed
|
|
|
|
|
|
if (image.hasAlphaBuffer()) {
|
|
|
image.normalize(); // needed, it will be modified
|
|
|
if (image.depth()!=32) {
|
|
|
image=image.convertDepth(32);
|
|
|
}
|
|
|
|
|
|
bool light;
|
|
|
|
|
|
int imageXOffset=widgetRect.x()-d->mXOffset;
|
|
|
int imageYOffset=widgetRect.y()-d->mYOffset;
|
|
|
int imageWidth=image.width();
|
|
|
int imageHeight=image.height();
|
|
|
for (int y=0;y<imageHeight;++y) {
|
|
|
uint* rgba=(uint*)(image.scanLine(y));
|
|
|
for(int x=0;x<imageWidth;x++) {
|
|
|
light= ((x+imageXOffset) & 16) ^ ((y+imageYOffset) & 16);
|
|
|
composite(rgba,light?192:128);
|
|
|
rgba++;
|
|
|
}
|
|
|
}
|
|
|
image.setAlphaBuffer(false);
|
|
|
}
|
|
|
|
|
|
QRect paintRect( clipx, clipy, clipw, cliph );
|
|
|
QPixmap buffer( paintRect.size());
|
|
|
{
|
|
|
QPainter bufferPainter(&buffer);
|
|
|
bufferPainter.setBackgroundColor(painter->backgroundColor());
|
|
|
bufferPainter.eraseRect(0,0,paintRect.width(),paintRect.height());
|
|
|
bufferPainter.drawImage(widgetRect.topLeft()-paintRect.topLeft(),image,
|
|
|
fastpass?ThresholdDither:0);
|
|
|
}
|
|
|
painter->drawPixmap(paintRect.topLeft(),buffer);
|
|
|
|
|
|
if( paintRect.width() * paintRect.height() >= 10000 ) { // ignore small repaints
|
|
|
// try to do one step in 0.1sec
|
|
|
int size = paintRect.width() * paintRect.height() * 100 / QMAX( t.elapsed(), 1 );
|
|
|
|
|
|
int maxRepaintSize;
|
|
|
if (zoom() == 1.0) {
|
|
|
maxRepaintSize=ImageViewConfig::maxRepaintSize();
|
|
|
} else {
|
|
|
if (smoothAlgo!=ImageUtils::SMOOTH_NONE) {
|
|
|
maxRepaintSize=ImageViewConfig::maxSmoothRepaintSize();
|
|
|
} else {
|
|
|
maxRepaintSize=ImageViewConfig::maxScaleRepaintSize();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
maxRepaintSize = KCLAMP(
|
|
|
( size + maxRepaintSize ) / 2,
|
|
|
10000, LIMIT_MAX_REPAINT_SIZE);
|
|
|
|
|
|
if (zoom() == 1.0) {
|
|
|
ImageViewConfig::setMaxRepaintSize(maxRepaintSize);
|
|
|
} else {
|
|
|
if (smoothAlgo!=ImageUtils::SMOOTH_NONE) {
|
|
|
ImageViewConfig::setMaxSmoothRepaintSize(maxRepaintSize);
|
|
|
} else {
|
|
|
ImageViewConfig::setMaxScaleRepaintSize(maxRepaintSize);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
#ifdef DEBUG_RECTS
|
|
|
painter->setPen(colors[numColor]);
|
|
|
numColor=(numColor+1)%4;
|
|
|
painter->drawRect(paintRect);
|
|
|
#endif
|
|
|
|
|
|
QApplication::flushX();
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::viewportMousePressEvent(QMouseEvent* event) {
|
|
|
viewport()->setFocus();
|
|
|
switch (event->button()) {
|
|
|
case Qt::LeftButton:
|
|
|
d->mTools[d->mToolID]->leftButtonPressEvent(event);
|
|
|
break;
|
|
|
case Qt::RightButton:
|
|
|
d->mTools[d->mToolID]->rightButtonPressEvent(event);
|
|
|
break;
|
|
|
default: // Avoid compiler complain
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::viewportMouseMoveEvent(QMouseEvent* event) {
|
|
|
selectTool(event->state(), true);
|
|
|
d->mTools[d->mToolID]->mouseMoveEvent(event);
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::viewportMouseReleaseEvent(QMouseEvent* event) {
|
|
|
switch (event->button()) {
|
|
|
case Qt::LeftButton:
|
|
|
if (event->stateAfter() & Qt::RightButton) {
|
|
|
d->mOperaLikePrevious=true;
|
|
|
emit selectPrevious();
|
|
|
return;
|
|
|
}
|
|
|
d->mTools[d->mToolID]->leftButtonReleaseEvent(event);
|
|
|
break;
|
|
|
|
|
|
case Qt::MidButton:
|
|
|
d->mTools[d->mToolID]->midButtonReleaseEvent(event);
|
|
|
break;
|
|
|
|
|
|
case Qt::RightButton:
|
|
|
if (event->stateAfter() & Qt::LeftButton) {
|
|
|
emit selectNext();
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (d->mOperaLikePrevious) { // Avoid showing the popup menu after Opera like previous
|
|
|
d->mOperaLikePrevious=false;
|
|
|
} else {
|
|
|
d->mTools[d->mToolID]->rightButtonReleaseEvent(event);
|
|
|
}
|
|
|
break;
|
|
|
|
|
|
default: // Avoid compiler complain
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
bool ImageView::eventFilter(QObject* obj, QEvent* event) {
|
|
|
switch (event->type()) {
|
|
|
case QEvent::KeyPress:
|
|
|
case QEvent::KeyRelease:
|
|
|
case QEvent::AccelOverride:
|
|
|
return viewportKeyEvent(static_cast<QKeyEvent*>(event));
|
|
|
|
|
|
case QEvent::MouseButtonDblClick:
|
|
|
if (d->mToolID==ZOOM) return false;
|
|
|
emit doubleClicked();
|
|
|
return true;
|
|
|
|
|
|
// Getting/loosing focus causes repaints, but repainting here is expensive,
|
|
|
// and there's no need to repaint on focus changes, as the focus is not
|
|
|
// indicated.
|
|
|
case QEvent::FocusIn:
|
|
|
case QEvent::FocusOut:
|
|
|
return true;
|
|
|
|
|
|
case QEvent::Enter:
|
|
|
selectTool( kapp->keyboardMouseState(), true );
|
|
|
emitRequestHintDisplay();
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
break;
|
|
|
}
|
|
|
return QScrollView::eventFilter(obj,event);
|
|
|
}
|
|
|
|
|
|
|
|
|
bool ImageView::viewportKeyEvent(QKeyEvent* event) {
|
|
|
selectTool(event->stateAfter(), false);
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::contentsDragEnterEvent(QDragEnterEvent* event) {
|
|
|
event->accept( QUriDrag::canDecode( event ));
|
|
|
}
|
|
|
|
|
|
void ImageView::contentsDropEvent(QDropEvent* event) {
|
|
|
KURL::List list;
|
|
|
if( KURLDrag::decode( event, list )) {
|
|
|
d->mDocument->setURL( list.first());
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void ImageView::keyPressEvent( QKeyEvent *event ) {
|
|
|
QScrollView::keyPressEvent( event );
|
|
|
int deltaX, deltaY;
|
|
|
|
|
|
if (event->state() != Qt::NoButton) {
|
|
|
return;
|
|
|
}
|
|
|
switch (event->key()) {
|
|
|
case Key_Up:
|
|
|
deltaX = 0;
|
|
|
deltaY = -1;
|
|
|
break;
|
|
|
case Key_Down:
|
|
|
deltaX = 0;
|
|
|
deltaY = 1;
|
|
|
break;
|
|
|
case Key_Left:
|
|
|
deltaX = -1;
|
|
|
deltaY = 0;
|
|
|
break;
|
|
|
case Key_Right:
|
|
|
deltaX = 1;
|
|
|
deltaY = 0;
|
|
|
break;
|
|
|
default:
|
|
|
return;
|
|
|
}
|
|
|
deltaX *= width() / 2;
|
|
|
deltaY *= height() / 2;
|
|
|
scrollBy (deltaX, deltaY);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* If force is set, the cursor will be updated even if the tool is not
|
|
|
* different from the current one.
|
|
|
*/
|
|
|
void ImageView::selectTool(ButtonState state, bool force) {
|
|
|
ToolID oldToolID=d->mToolID;
|
|
|
if (state & ControlButton) {
|
|
|
d->mToolID=ZOOM;
|
|
|
if (d->mToolID!=oldToolID) {
|
|
|
emitRequestHintDisplay();
|
|
|
}
|
|
|
} else {
|
|
|
d->mToolID=SCROLL;
|
|
|
}
|
|
|
|
|
|
if (d->mToolID!=oldToolID || force) {
|
|
|
d->mTools[d->mToolID]->updateCursor();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::wheelEvent(QWheelEvent* event) {
|
|
|
d->mTools[d->mToolID]->wheelEvent(event);
|
|
|
}
|
|
|
|
|
|
|
|
|
//------------------------------------------------------------------------
|
|
|
//
|
|
|
// Slots
|
|
|
//
|
|
|
//------------------------------------------------------------------------
|
|
|
void ImageView::slotZoomIn() {
|
|
|
updateZoom(ZOOM_FREE, computeZoom(true));
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::slotZoomOut() {
|
|
|
updateZoom(ZOOM_FREE, computeZoom(false));
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::slotResetZoom() {
|
|
|
updateZoom(ZOOM_FREE, 1.0);
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::slotSelectZoom() {
|
|
|
int currentItem=d->mZoomCombo->currentItem();
|
|
|
|
|
|
if (currentItem>=int(d->mZoomComboActions.count()) ) {
|
|
|
QString txt=d->mZoomCombo->currentText();
|
|
|
txt=txt.left(txt.find('%'));
|
|
|
double value=KGlobal::locale()->readNumber(txt) / 100.0;
|
|
|
updateZoom(ZOOM_FREE, value);
|
|
|
} else {
|
|
|
d->mZoomComboActions[currentItem]->activate();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::setZoomToFit(bool on) {
|
|
|
if (on) {
|
|
|
updateZoom(ZOOM_FIT);
|
|
|
} else {
|
|
|
updateZoom(ZOOM_FREE, d->mZoomBeforeAuto, d->mXCenterBeforeAuto, d->mYCenterBeforeAuto);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::setZoomToWidth(bool on) {
|
|
|
if (on) {
|
|
|
updateZoom(ZOOM_FIT_WIDTH);
|
|
|
} else {
|
|
|
updateZoom(ZOOM_FREE, d->mZoomBeforeAuto, d->mXCenterBeforeAuto, d->mYCenterBeforeAuto);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::setZoomToHeight(bool on) {
|
|
|
if (on) {
|
|
|
updateZoom(ZOOM_FIT_HEIGHT);
|
|
|
} else {
|
|
|
updateZoom(ZOOM_FREE, d->mZoomBeforeAuto, d->mXCenterBeforeAuto, d->mYCenterBeforeAuto);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::setLockZoom(bool value) {
|
|
|
if( value ) {
|
|
|
d->mZoomToFit->setChecked( false );
|
|
|
d->mZoomToWidth->setChecked( false );
|
|
|
d->mZoomToHeight->setChecked( false );
|
|
|
}
|
|
|
// don't change zoom here, keep it even if it was from some auto zoom mode
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::showBCGDialog() {
|
|
|
if (!d->mBCGDialog) {
|
|
|
d->mBCGDialog=new BCGDialog(this);
|
|
|
}
|
|
|
d->mBCGDialog->show();
|
|
|
}
|
|
|
|
|
|
|
|
|
int ImageView::brightness() const {
|
|
|
return d->mBrightness;
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::setBrightness(int value) {
|
|
|
d->mBrightness=value;
|
|
|
fullRepaint();
|
|
|
}
|
|
|
|
|
|
void ImageView::increaseBrightness() {
|
|
|
d->mBrightness = KCLAMP( d->mBrightness + 5, -100, 100 );
|
|
|
emit bcgChanged();
|
|
|
fullRepaint();
|
|
|
}
|
|
|
|
|
|
void ImageView::decreaseBrightness() {
|
|
|
d->mBrightness = KCLAMP( d->mBrightness - 5, -100, 100 );
|
|
|
emit bcgChanged();
|
|
|
fullRepaint();
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int ImageView::contrast() const {
|
|
|
return d->mContrast - 100;
|
|
|
}
|
|
|
|
|
|
void ImageView::setContrast(int value) {
|
|
|
d->mContrast=value + 100;
|
|
|
fullRepaint();
|
|
|
}
|
|
|
|
|
|
void ImageView::increaseContrast() {
|
|
|
d->mContrast = KCLAMP( d->mContrast + 10, 0, 500 );
|
|
|
emit bcgChanged();
|
|
|
fullRepaint();
|
|
|
}
|
|
|
|
|
|
void ImageView::decreaseContrast() {
|
|
|
d->mContrast = KCLAMP( d->mContrast - 10, 0, 500 );
|
|
|
emit bcgChanged();
|
|
|
fullRepaint();
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int ImageView::gamma() const {
|
|
|
return d->mGamma - 100;
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::setGamma(int value) {
|
|
|
d->mGamma=value + 100;
|
|
|
fullRepaint();
|
|
|
}
|
|
|
|
|
|
void ImageView::increaseGamma() {
|
|
|
d->mGamma = KCLAMP( d->mGamma + 10, 10, 500 );
|
|
|
emit bcgChanged();
|
|
|
fullRepaint();
|
|
|
}
|
|
|
|
|
|
void ImageView::decreaseGamma() {
|
|
|
d->mGamma = KCLAMP( d->mGamma - 10, 10, 500 );
|
|
|
emit bcgChanged();
|
|
|
fullRepaint();
|
|
|
}
|
|
|
//------------------------------------------------------------------------
|
|
|
//
|
|
|
// Private
|
|
|
//
|
|
|
//------------------------------------------------------------------------
|
|
|
void ImageView::emitRequestHintDisplay() {
|
|
|
if (d->mDocument->isNull()) return;
|
|
|
|
|
|
emit requestHintDisplay( d->mTools[d->mToolID]->hint() );
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::slotImageSizeUpdated() {
|
|
|
d->mXOffset=0;
|
|
|
d->mYOffset=0;
|
|
|
|
|
|
d->mValidImageArea = QRegion();
|
|
|
if (d->mZoomMode!=ZOOM_FREE) {
|
|
|
d->mXCenterBeforeAuto=0;
|
|
|
d->mYCenterBeforeAuto=0;
|
|
|
} else {
|
|
|
horizontalScrollBar()->setValue(0);
|
|
|
verticalScrollBar()->setValue(0);
|
|
|
}
|
|
|
if (d->mZoomMode!=ZOOM_FREE) {
|
|
|
updateZoom(d->mZoomMode);
|
|
|
} else {
|
|
|
if( !d->mLockZoom->isChecked()) {
|
|
|
setZoom( 1.0 );
|
|
|
}
|
|
|
}
|
|
|
|
|
|
updateZoomActions();
|
|
|
d->mAdjustBCG->setEnabled(!d->mDocument->isNull());
|
|
|
d->mIncreaseGamma->setEnabled(!d->mDocument->isNull());
|
|
|
d->mDecreaseGamma->setEnabled(!d->mDocument->isNull());
|
|
|
d->mIncreaseBrightness->setEnabled(!d->mDocument->isNull());
|
|
|
d->mDecreaseBrightness->setEnabled(!d->mDocument->isNull());
|
|
|
d->mIncreaseContrast->setEnabled(!d->mDocument->isNull());
|
|
|
d->mDecreaseContrast->setEnabled(!d->mDocument->isNull());
|
|
|
|
|
|
updateContentSize();
|
|
|
updateImageOffset();
|
|
|
updateScrollBarMode();
|
|
|
fullRepaint();
|
|
|
}
|
|
|
|
|
|
void ImageView::slotImageRectUpdated(const QRect& imageRect) {
|
|
|
d->mValidImageArea += imageRect;
|
|
|
viewport()->repaint( d->imageToWidget( imageRect ), false );
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::updateScrollBarMode() {
|
|
|
if (d->mZoomMode==ZOOM_FIT || !ImageViewConfig::showScrollBars()) {
|
|
|
setVScrollBarMode(AlwaysOff);
|
|
|
setHScrollBarMode(AlwaysOff);
|
|
|
} else {
|
|
|
setVScrollBarMode(Auto);
|
|
|
setHScrollBarMode(Auto);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::updateContentSize() {
|
|
|
resizeContents(
|
|
|
int(d->mDocument->width()*d->mZoom),
|
|
|
int(d->mDocument->height()*d->mZoom) );
|
|
|
}
|
|
|
|
|
|
double ImageView::computeZoomToFit() const {
|
|
|
if (d->mDocument->isNull()) {
|
|
|
return 1.0;
|
|
|
}
|
|
|
QSize size=d->mDocument->image().size();
|
|
|
size.scale(width(),height(),QSize::ScaleMin);
|
|
|
|
|
|
double zoom=double(size.width())/d->mDocument->width();
|
|
|
if (zoom>1.0 && !ImageViewConfig::enlargeSmallImages()) return 1.0;
|
|
|
return zoom;
|
|
|
}
|
|
|
|
|
|
double ImageView::computeZoomToWidth() const {
|
|
|
if (d->mDocument->isNull()) {
|
|
|
return 1.0;
|
|
|
}
|
|
|
int sw = verticalScrollBar()->sizeHint().width(); // geometry is not valid before first show()
|
|
|
int w = width();
|
|
|
int dw = d->mDocument->width();
|
|
|
switch( vScrollBarMode()) {
|
|
|
case AlwaysOff:
|
|
|
return double(w)/dw;
|
|
|
case AlwaysOn:
|
|
|
return double(w-sw)/dw;
|
|
|
case Auto:
|
|
|
default:
|
|
|
// there will be a vertical scrollbar if the image's height will be too large
|
|
|
if( d->mDocument->height() * (double(w)/dw) > height()) return double(w-sw)/dw;
|
|
|
return double(w)/dw;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
double ImageView::computeZoomToHeight() const {
|
|
|
if (d->mDocument->isNull()) {
|
|
|
return 1.0;
|
|
|
}
|
|
|
int sh = horizontalScrollBar()->sizeHint().height();
|
|
|
int h = height();
|
|
|
int dh = d->mDocument->height();
|
|
|
switch( vScrollBarMode()) {
|
|
|
case AlwaysOff:
|
|
|
return double(h)/dh;
|
|
|
case AlwaysOn:
|
|
|
return double(h-sh)/dh;
|
|
|
case Auto:
|
|
|
default:
|
|
|
if( d->mDocument->width() * (double(h)/dh) > width()) return double(h-sh)/dh;
|
|
|
return double(h)/dh;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
double ImageView::computeZoom(bool in) const {
|
|
|
const double F = 0.5; // change in 0.5 steps
|
|
|
double zoomtofit = computeZoomToFit();
|
|
|
double zoomtowidth = computeZoomToWidth();
|
|
|
double zoomtoheight = computeZoomToHeight();
|
|
|
if (in) {
|
|
|
double newzoom;
|
|
|
if (d->mZoom>=1.0) {
|
|
|
newzoom = (floor(d->mZoom/F)+1.0)*F;
|
|
|
} else {
|
|
|
newzoom = 1/(( ceil(1/d->mZoom/F)-1.0 )*F);
|
|
|
}
|
|
|
if( d->mZoom < zoomtofit && zoomtofit < newzoom ) newzoom = zoomtofit;
|
|
|
if( d->mZoom < zoomtowidth && zoomtowidth < newzoom ) newzoom = zoomtowidth;
|
|
|
if( d->mZoom < zoomtoheight && zoomtoheight < newzoom ) newzoom = zoomtoheight;
|
|
|
return newzoom;
|
|
|
} else {
|
|
|
double newzoom;
|
|
|
if (d->mZoom>1.0) {
|
|
|
newzoom = (ceil(d->mZoom/F)-1.0)*F;
|
|
|
} else {
|
|
|
newzoom = 1/(( floor(1/d->mZoom/F)+1.0 )*F);
|
|
|
}
|
|
|
if( d->mZoom > zoomtofit && zoomtofit > newzoom ) newzoom = zoomtofit;
|
|
|
if( d->mZoom > zoomtowidth && zoomtowidth > newzoom ) newzoom = zoomtowidth;
|
|
|
if( d->mZoom > zoomtoheight && zoomtoheight > newzoom ) newzoom = zoomtoheight;
|
|
|
return newzoom;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
void ImageView::updateImageOffset() {
|
|
|
int viewWidth=width();
|
|
|
int viewHeight=height();
|
|
|
|
|
|
// Compute d->mXOffset and d->mYOffset in case the image does not fit
|
|
|
// the view width or height
|
|
|
int zpixWidth=int(d->mDocument->width() * d->mZoom);
|
|
|
int zpixHeight=int(d->mDocument->height() * d->mZoom);
|
|
|
|
|
|
if (zpixWidth>viewWidth && hScrollBarMode()!=AlwaysOff) {
|
|
|
// use sizeHint() - geometry is not valid before first show()
|
|
|
viewHeight-=horizontalScrollBar()->sizeHint().height();
|
|
|
}
|
|
|
if (zpixHeight>viewHeight && vScrollBarMode()!=AlwaysOff) {
|
|
|
viewWidth-=verticalScrollBar()->sizeHint().width();
|
|
|
}
|
|
|
|
|
|
d->mXOffset=QMAX(0,(viewWidth-zpixWidth)/2);
|
|
|
d->mYOffset=QMAX(0,(viewHeight-zpixHeight)/2);
|
|
|
}
|
|
|
|
|
|
|
|
|
void ImageView::updateZoomActions() {
|
|
|
// Disable most actions if there's no image
|
|
|
if (d->mDocument->isNull()) {
|
|
|
d->mZoomComboAction->setEnabled(false);
|
|
|
d->mZoomIn->setEnabled(false);
|
|
|
d->mZoomOut->setEnabled(false);
|
|
|
d->mResetZoom->setEnabled(false);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
d->mZoomComboAction->setEnabled(true);
|
|
|
d->mZoomToFit->setEnabled(true);
|
|
|
d->mZoomToWidth->setEnabled(true);
|
|
|
d->mZoomToHeight->setEnabled(true);
|
|
|
d->mResetZoom->setEnabled(true);
|
|
|
|
|
|
|
|
|
if (d->mZoomMode==ZOOM_FREE) {
|
|
|
d->mZoomIn->setEnabled(d->mZoom<MAX_ZOOM);
|
|
|
d->mZoomOut->setEnabled(d->mZoom>1/MAX_ZOOM);
|
|
|
QString zoomText=QString("%1%").arg(int(d->mZoom*100));
|
|
|
d->mZoomCombo->setCurrentText(zoomText);
|
|
|
} else {
|
|
|
d->mZoomIn->setEnabled(true);
|
|
|
d->mZoomOut->setEnabled(true);
|
|
|
d->mZoomCombo->setCurrentItem(d->mZoomMode);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
} // namespace
|