File indexing completed on 2024-04-14 14:36:02

0001 /************************************************************************
0002  *                                  *
0003  *  This file is part of libkscan, a KDE scanning library.      *
0004  *                                  *
0005  *  Copyright (C) 2013 Jonathan Marten <jjm@keelhaul.me.uk>     *
0006  *  Copyright (C) 1999 Klaas Freitag <freitag@suse.de>          *
0007  *                                  *
0008  *  This library is free software; you can redistribute it and/or   *
0009  *  modify it under the terms of the GNU Library General Public     *
0010  *  License as published by the Free Software Foundation and appearing  *
0011  *  in the file COPYING included in the packaging of this file;     *
0012  *  either version 2 of the License, or (at your option) any later  *
0013  *  version.                                *
0014  *                                  *
0015  *  This program is distributed in the hope that it will be useful, *
0016  *  but WITHOUT ANY WARRANTY; without even the implied warranty of  *
0017  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the   *
0018  *  GNU General Public License for more details.            *
0019  *                                  *
0020  *  You should have received a copy of the GNU General Public License   *
0021  *  along with this program;  see the file COPYING.  If not, write to   *
0022  *  the Free Software Foundation, Inc., 51 Franklin Street,     *
0023  *  Fifth Floor, Boston, MA 02110-1301, USA.                *
0024  *                                  *
0025  ************************************************************************/
0026 
0027 #include "imagecanvas.h"
0028 
0029 #include <QGraphicsScene>
0030 #include <QGraphicsPixmapItem>
0031 
0032 #include <qtransform.h>
0033 #include <qimage.h>
0034 #include <qpainter.h>
0035 #include <qstyle.h>
0036 #include <qevent.h>
0037 #include <qapplication.h>
0038 #include <qmenu.h>
0039 
0040 #include <klocalizedstring.h>
0041 
0042 #include "imgscaledialog.h"
0043 #include "libkookascan_logging.h"
0044 
0045 
0046 //  Parameters for display and selection
0047 //  ------------------------------------
0048 
0049 #undef HOLD_SELECTION                   // hold selection over new image
0050 
0051 const int MIN_AREA_WIDTH = 2;               // minimum size of selection area
0052 const int MIN_AREA_HEIGHT = 2;
0053 const int DELTA = 3;                    // tolerance for mouse hits
0054 const int TIMER_INTERVAL = 100;             // animation timer interval
0055 
0056 const int DASH_DASH = 4;                // length of drawn dashes
0057 const int DASH_SPACE = 4;               // length of drawn spaces
0058 
0059 const int DASH_LENGTH = (DASH_DASH + DASH_SPACE);
0060 
0061 //  HighlightItem -- A graphics item to maintain and draw a single
0062 //  highlight rectangle.
0063 
0064 class HighlightItem : public QGraphicsItem
0065 {
0066 public:
0067     HighlightItem(const QRect &rect,
0068                   ImageCanvas::HighlightStyle style,
0069                   const QPen &pen, const QBrush &brush,
0070                   QGraphicsItem *parent = nullptr);
0071     virtual ~HighlightItem()                {}
0072 
0073     QRectF boundingRect() const override;
0074 
0075     virtual void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
0076                        QWidget *widget = nullptr) override;
0077 
0078 private:
0079     QRectF mRectangle;
0080 
0081     ImageCanvas::HighlightStyle mStyle;
0082     QPen mPen;
0083     QBrush mBrush;
0084 };
0085 
0086 HighlightItem::HighlightItem(const QRect &rect,
0087                              ImageCanvas::HighlightStyle style,
0088                              const QPen &pen, const QBrush &brush,
0089                              QGraphicsItem *parent)
0090     : QGraphicsItem(parent)
0091 {
0092     mRectangle = rect;
0093     mStyle = style;
0094     mPen = pen;
0095     mBrush = brush;
0096 
0097     mPen.setCosmetic(true);
0098 }
0099 
0100 QRectF HighlightItem::boundingRect() const
0101 {
0102     return (mRectangle);
0103 }
0104 
0105 void HighlightItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
0106 {
0107     painter->setPen(mPen);
0108     painter->setBrush(mBrush);
0109 
0110     if (mStyle == ImageCanvas::HighlightBox) {
0111         painter->drawRect(mRectangle);
0112     } else painter->drawLine(mRectangle.left(), mRectangle.bottom(),
0113                                  mRectangle.right(), mRectangle.bottom());
0114 }
0115 
0116 //  SelectionItem -- A graphics item to maintain and draw the
0117 //  selection rectangle.
0118 //
0119 //  There is only one of these items on the canvas.  Its bounding
0120 //  rectangle, stored here in scene coordinates (i.e. image pixels)
0121 //  is the master reference for the selection rectangle.
0122 
0123 class SelectionItem : public QGraphicsItem
0124 {
0125 public:
0126     explicit SelectionItem(QGraphicsItem *parent = nullptr);
0127     ~SelectionItem() override {}
0128 
0129     QRectF boundingRect() const override;
0130     void setRect(const QRectF &rect);
0131 
0132     void stepDashPattern();
0133     void resetDashPattern();
0134 
0135     void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
0136                        QWidget *widget = nullptr) override;
0137 
0138 private:
0139     QRectF mRectangle;
0140     int mDashOffset;
0141 };
0142 
0143 SelectionItem::SelectionItem(QGraphicsItem *parent)
0144     : QGraphicsItem(parent)
0145 {
0146     mDashOffset = 0;
0147 }
0148 
0149 QRectF SelectionItem::boundingRect() const
0150 {
0151     return (mRectangle);
0152 }
0153 
0154 void SelectionItem::setRect(const QRectF &rect)
0155 {
0156     prepareGeometryChange();
0157     mRectangle = rect;
0158 }
0159 
0160 void SelectionItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
0161 {
0162     painter->setBrush(QBrush());
0163 
0164     QPen pen1(Qt::white);               // solid white box behind
0165     painter->setPen(pen1);
0166     painter->drawRect(mRectangle);
0167 
0168     QPen pen2(Qt::CustomDashLine);          // dashed black box on top
0169     QVector<qreal> dashes(2);
0170     dashes[0] = DASH_DASH;
0171     dashes[1] = DASH_SPACE;
0172     pen2.setDashPattern(dashes);
0173     pen2.setDashOffset(mDashOffset);
0174     painter->setPen(pen2);
0175     painter->drawRect(mRectangle);
0176 }
0177 
0178 // Counting backwards so that the "ants" march clockwise
0179 // (at least with the current Qt painting implementation)
0180 void SelectionItem::stepDashPattern()
0181 {
0182     --mDashOffset;
0183     if (mDashOffset < 0) {
0184         mDashOffset = (DASH_LENGTH - 1);
0185     }
0186     update();
0187 }
0188 
0189 void SelectionItem::resetDashPattern()
0190 {
0191     mDashOffset = 0;
0192 }
0193 
0194 //  ImageCanvas -- Scrolling area containing the pixmap/highlight widget.
0195 //  Selected areas are drawn over the top of that.
0196 
0197 ImageCanvas::ImageCanvas(QWidget *parent)
0198     : QGraphicsView(parent)
0199 {
0200     setObjectName("ImageCanvas");
0201 
0202     mContextMenu = nullptr;
0203     mTimerId = 0;
0204 
0205     mKeepZoom = false;
0206     mReadOnly = false;
0207     mScaleType = ImageCanvas::ScaleUnspecified;
0208     mDefaultScaleType = ImageCanvas::ScaleOriginal;
0209     mScaleFactor = 100;                 // means original size
0210     mMaintainAspect = true;
0211 
0212     setAlignment(Qt::AlignLeft|Qt::AlignTop);
0213 
0214     mScene = new QGraphicsScene(this);
0215     setScene(mScene);
0216 
0217     mPixmapItem = new QGraphicsPixmapItem;
0218     mPixmapItem->setShapeMode(QGraphicsPixmapItem::BoundingRectShape);
0219     mScene->addItem(mPixmapItem);
0220 
0221     mSelectionItem = new SelectionItem;
0222     mSelectionItem->setVisible(false);          // no selection yet
0223     mScene->addItem(mSelectionItem);
0224 
0225     mMoving = ImageCanvas::MoveNone;
0226     mCurrentCursor = Qt::ArrowCursor;
0227 
0228     newImage(ScanImage::Ptr());             // no displayed image
0229 
0230     setCursorShape(Qt::CrossCursor);
0231     setMouseTracking(true);
0232 
0233     setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
0234     show();
0235 }
0236 
0237 
0238 ImageCanvas::~ImageCanvas()
0239 {
0240     stopMarqueeTimer();
0241 }
0242 
0243 
0244 //  Setting the image
0245 //  -----------------
0246 
0247 void ImageCanvas::newImage(ScanImage::Ptr newImage, bool holdZoom)
0248 {
0249     mImage = newImage;                  // take reference to shared pointer
0250 
0251 #ifdef HOLD_SELECTION
0252     QRect oldSelected = mSelected;
0253     qCDebug(LIBKOOKASCAN_LOG) << "original selection" << oldSelected << "null" << oldSelected.isEmpty();
0254     qCDebug(LIBKOOKASCAN_LOG) << "w" << mSelected.width() << "h" << mSelected.height();
0255 #endif
0256 
0257     stopMarqueeTimer();                 // also clears selection
0258 
0259     if (!mImage.isNull())               // handle the new image
0260     {
0261         //qCDebug(LIBKOOKASCAN_LOG) << "new image size is" << mImage->size();
0262         mPixmapItem->setPixmap(QPixmap::fromImage(*mImage.data()));
0263         setSceneRect(mPixmapItem->boundingRect());  // image always defines size
0264 
0265         if (!mKeepZoom && !holdZoom) setScaleType(defaultScaleType());
0266 
0267 #ifdef HOLD_SELECTION
0268         if (!oldSelected.isNull()) {
0269             mSelected = oldSelected;
0270             qCDebug(LIBKOOKASCAN_LOG) << "restored selection" << mSelected;
0271             startMarqueeTimer();
0272         }
0273 #endif
0274     }
0275     else mPixmapItem->setPixmap(QPixmap());     // no new image to show
0276 
0277     recalculateViewScale();
0278 }
0279 
0280 
0281 //  Context menu
0282 //  ------------
0283 //
0284 //  We don't actually populate the menu or handle its actions here.
0285 //  The menu is populated with the parent application's actions via
0286 //  KookaView::connectViewerAction(), and the application handles the
0287 //  triggered actions.  Those which need some work from us are passed to
0288 //  slotUserAction() with the appropriate UserAction key.
0289 
0290 QMenu *ImageCanvas::contextMenu()
0291 {
0292     if (mContextMenu == nullptr) {         // menu not created yet
0293         mContextMenu = new QMenu(this);
0294     }
0295 
0296     return (mContextMenu);
0297 }
0298 
0299 void ImageCanvas::contextMenuEvent(QContextMenuEvent *ev)
0300 {
0301     if (mContextMenu == nullptr) {
0302         return;    // menu not enabled
0303     }
0304     // but allowed if no image loaded
0305     mContextMenu->popup(ev->globalPos());
0306     ev->accept();
0307 }
0308 
0309 void ImageCanvas::performUserAction(ImageCanvas::UserAction act)
0310 {
0311     if (mImage.isNull()) return;            // no action if no image loaded
0312 
0313     switch (act) {
0314     case ImageCanvas::UserActionZoom: {
0315         ImgScaleDialog zoomDia(this, mScaleFactor);
0316         if (zoomDia.exec()) {
0317             int sf = zoomDia.getSelected();
0318             setScaleType(ImageCanvas::ScaleZoom);
0319             setScaleFactor(sf);
0320         }
0321     }
0322     break;
0323 
0324     case ImageCanvas::UserActionOrigSize:
0325         setScaleType(ImageCanvas::ScaleOriginal);
0326         break;
0327 
0328     case ImageCanvas::UserActionFitWidth:
0329         setScaleType(ImageCanvas::ScaleFitWidth);
0330         break;
0331 
0332     case ImageCanvas::UserActionFitHeight:
0333         setScaleType(ImageCanvas::ScaleFitHeight);
0334         break;
0335 
0336     case ImageCanvas::UserActionClose:
0337         emit closingRequested();
0338         return;
0339     }
0340 
0341     recalculateViewScale();
0342 }
0343 
0344 //  Selected rectangle
0345 //  ------------------
0346 
0347 bool ImageCanvas::hasSelectedRect() const
0348 {
0349     if (!hasImage()) {
0350         return (false);
0351     }
0352     return (mSelectionItem->isVisible() && mSelectionItem->boundingRect().isValid());
0353 }
0354 
0355 // Get the selected area in absolute image pixels.
0356 QRect ImageCanvas::selectedRect() const
0357 {
0358     if (!hasSelectedRect()) {
0359         return (QRect());
0360     }
0361     return (mSelectionItem->boundingRect().toRect());
0362 }
0363 
0364 // Get the selected area as a proportion of the image size.
0365 QRectF ImageCanvas::selectedRectF() const
0366 {
0367     if (!hasSelectedRect()) {
0368         return (QRectF());
0369     }
0370     const QRectF r = mSelectionItem->boundingRect();
0371 
0372     QRectF retval;
0373     retval.setLeft(r.left() / mImage->width());
0374     retval.setRight(r.right() / mImage->width());
0375     retval.setTop(r.top() / mImage->height());
0376     retval.setBottom(r.bottom() / mImage->height());
0377     return (retval);
0378 }
0379 
0380 // Set the selected area in absolute image pixels.
0381 void ImageCanvas::setSelectionRect(const QRect &rect)
0382 {
0383     //qCDebug(LIBKOOKASCAN_LOG) << "rect=" << rect;
0384     if (!hasImage()) {
0385         return;
0386     }
0387 
0388     if (!rect.isValid()) {
0389         stopMarqueeTimer();    // clear the selection
0390     } else {                     // set the selection
0391         mSelectionItem->setRect(rect);
0392         startMarqueeTimer();
0393     }
0394 }
0395 
0396 // Set the selected area as a proportion of the image size.
0397 void ImageCanvas::setSelectionRect(const QRectF &rect)
0398 {
0399     //qCDebug(LIBKOOKASCAN_LOG) << "rect=" << rect;
0400     if (!hasImage()) {
0401         return;
0402     }
0403 
0404     if (!rect.isValid()) {
0405         stopMarqueeTimer();    // clear the selection
0406     } else {                     // set the selection
0407         QRectF setval;
0408         setval.setLeft(rect.left()*mImage->width());
0409         setval.setRight(rect.right()*mImage->width());
0410         setval.setTop(rect.top()*mImage->height());
0411         setval.setBottom(rect.bottom()*mImage->height());
0412 
0413         mSelectionItem->setRect(setval);
0414         startMarqueeTimer();
0415     }
0416 }
0417 
0418 
0419 //  Selected image
0420 //  --------------
0421 
0422 ScanImage::Ptr ImageCanvas::selectedImage() const
0423 {
0424     if (!hasImage()) return (ScanImage::Ptr());     // no image available
0425     const QRect r = selectedRect();
0426     if (!r.isValid()) return (ScanImage::Ptr());    // no selection
0427 
0428     ScanImage *img = new ScanImage(mImage->copy(r));    // extract selection as new image
0429     return (ScanImage::Ptr(img));           // return shared pointer to it
0430 }
0431 
0432 
0433 //  Animation timer
0434 //  ---------------
0435 
0436 void ImageCanvas::timerEvent(QTimerEvent *)
0437 {
0438     if (!hasImage()) {
0439         return;    // no image acquired
0440     }
0441     if (!isVisible()) {
0442         return;    // we're not visible
0443     }
0444     if (mMoving != ImageCanvas::MoveNone) {
0445         return;    // mouse operation in progress
0446     }
0447 
0448     mSelectionItem->stepDashPattern();
0449 }
0450 
0451 void ImageCanvas::startMarqueeTimer()
0452 {
0453     if (mTimerId == 0) {
0454         mTimerId = startTimer(TIMER_INTERVAL);
0455     }
0456     mSelectionItem->setVisible(true);
0457 }
0458 
0459 void ImageCanvas::stopMarqueeTimer()
0460 {
0461     if (mTimerId != 0) {
0462         killTimer(mTimerId);
0463         mTimerId = 0;
0464     }
0465 
0466     mSelectionItem->setVisible(false);          // clear the selection
0467     mSelectionItem->resetDashPattern();
0468 }
0469 
0470 //  Mouse events
0471 //  ------------
0472 
0473 void ImageCanvas::mousePressEvent(QMouseEvent *ev)
0474 {
0475     if (mReadOnly) {
0476         return;    // only if permitted
0477     }
0478     if (ev->button() != Qt::LeftButton) {
0479         return;    // only action this button
0480     }
0481     if (mMoving != ImageCanvas::MoveNone) {
0482         return;    // something already in progress
0483     }
0484     if (!hasImage()) {
0485         return;    // no image displayed
0486     }
0487 
0488     const QList<QGraphicsItem *> its = items(ev->pos());
0489     if (its.isEmpty()) {
0490         return;    // not over any item
0491     }
0492     if (its.last() != mPixmapItem) {
0493         return;    // not within the image
0494     }
0495 
0496     mMoving = classifyPoint(ev->pos());         // see where hit happened
0497     if (mMoving == ImageCanvas::MoveNone) {     // starting new area
0498         QPoint p = mapToScene(ev->pos()).toPoint(); // position in scene coords
0499         mStartPoint = p;
0500         mLastPoint = p;
0501 
0502         mSelectionItem->setRect(QRectF(p.x(), p.y(), 0, 0));
0503         mSelectionItem->setVisible(true);
0504         mMoving = ImageCanvas::MoveNew;
0505     }
0506 }
0507 
0508 void ImageCanvas::mouseReleaseEvent(QMouseEvent *ev)
0509 {
0510     if (mReadOnly) {
0511         return;    // only if permitted
0512     }
0513     if (ev->button() != Qt::LeftButton) {
0514         return;    // only action this button
0515     }
0516     if (mMoving == ImageCanvas::MoveNone) {
0517         return;    // nothing in progress
0518     }
0519     if (!hasImage()) {
0520         return;    // no image displayed
0521     }
0522 
0523     mMoving = ImageCanvas::MoveNone;
0524 
0525     QRect selected = selectedRect();
0526     //qCDebug(LIBKOOKASCAN_LOG) << "selected rect" << selected;
0527     if (selected.width() < MIN_AREA_WIDTH || selected.height() < MIN_AREA_HEIGHT) {
0528         //qCDebug(LIBKOOKASCAN_LOG) << "no selection";
0529         stopMarqueeTimer();             // also hides selection
0530         emit newRect(QRect());
0531         emit newRect(QRectF());
0532     } else {
0533         //qCDebug(LIBKOOKASCAN_LOG) << "have selection";
0534         startMarqueeTimer();
0535         emit newRect(selectedRect());
0536         emit newRect(selectedRectF());
0537     }
0538 
0539     mouseMoveEvent(ev);                 // update cursor shape
0540 }
0541 
0542 void ImageCanvas::mouseMoveEvent(QMouseEvent *ev)
0543 {
0544     if (mReadOnly) {
0545         return;    // only if permitted
0546     }
0547     if (!hasImage()) {
0548         return;    // no image displayed
0549     }
0550 
0551     int x = ev->pos().x();              // mouse position
0552     int y = ev->pos().y();              // in view coordinates
0553 
0554     int lx = mImage->width();               // limits for moved rectangle
0555     int ly = mImage->height();              // in scene coordinates
0556 
0557     QPoint pixExtent = mapFromScene(lx, ly);        // those same limits
0558     int ix = pixExtent.x();             // in view coordinates
0559     int iy = pixExtent.y();
0560 
0561     // Limit drag rectangle to the scaled pixmap
0562     if (x < 0) {
0563         x = 0;
0564     }
0565     if (x >= ix) {
0566         x = ix - 1;
0567     }
0568     if (y < 0) {
0569         y = 0;
0570     }
0571     if (y >= iy) {
0572         y = iy - 1;
0573     }
0574 
0575     QPoint scenePos = mapToScene(x, y).toPoint();   // that limited position
0576     int sx = scenePos.x();              // in scene coordinates
0577     int sy = scenePos.y();
0578 
0579     //qCDebug(LIBKOOKASCAN_LOG) << x << y << "moving" << mMoving;
0580 
0581     // Set the cursor shape appropriately
0582     switch (mMoving != ImageCanvas::MoveNone ? mMoving : classifyPoint(QPoint(x, y))) {
0583     case ImageCanvas::MoveNone:
0584         setCursorShape(Qt::CrossCursor);
0585         break;
0586 
0587     case ImageCanvas::MoveLeft:
0588     case ImageCanvas::MoveRight:
0589         setCursorShape(Qt::SizeHorCursor);
0590         break;
0591 
0592     case ImageCanvas::MoveTop:
0593     case ImageCanvas::MoveBottom:
0594         setCursorShape(Qt::SizeVerCursor);
0595         break;
0596 
0597     case ImageCanvas::MoveTopLeft:
0598     case ImageCanvas::MoveBottomRight:
0599         setCursorShape(Qt::SizeFDiagCursor);
0600         break;
0601 
0602     case ImageCanvas::MoveTopRight:
0603     case ImageCanvas::MoveBottomLeft:
0604         setCursorShape(Qt::SizeBDiagCursor);
0605         break;
0606 
0607     case ImageCanvas::MoveWhole:
0608         setCursorShape(Qt::SizeAllCursor);
0609         break;
0610 
0611     case ImageCanvas::MoveNew:
0612         setCursorShape(Qt::ArrowCursor);
0613         break;
0614     }
0615 
0616     // Update the selection rectangle
0617     if (mMoving != ImageCanvas::MoveNone) {
0618         const QRectF originalRect = mSelectionItem->boundingRect();
0619         QRectF updatedRect = originalRect;
0620         switch (mMoving) {
0621         case ImageCanvas::MoveNone:
0622             break;
0623 
0624         case ImageCanvas::MoveTopLeft:
0625             if (sx < originalRect.right()) {
0626                 updatedRect.setLeft(sx);
0627             }
0628             if (sy < originalRect.bottom()) {
0629                 updatedRect.setTop(sy);
0630             }
0631             break;
0632 
0633         case ImageCanvas::MoveTop:
0634             if (sy < originalRect.bottom()) {
0635                 updatedRect.setTop(sy);
0636             }
0637             break;
0638 
0639         case ImageCanvas::MoveTopRight:
0640             if (sx > originalRect.left()) {
0641                 updatedRect.setRight(sx);
0642             }
0643             if (sy < originalRect.bottom()) {
0644                 updatedRect.setTop(sy);
0645             }
0646             break;
0647 
0648         case ImageCanvas::MoveRight:
0649             if (sx > originalRect.left()) {
0650                 updatedRect.setRight(sx);
0651             }
0652             break;
0653 
0654         case ImageCanvas::MoveBottomRight:
0655             if (sx > originalRect.left()) {
0656                 updatedRect.setRight(sx);
0657             }
0658             if (sy > originalRect.top()) {
0659                 updatedRect.setBottom(sy);
0660             }
0661             break;
0662 
0663         case ImageCanvas::MoveBottom:
0664             if (sy > originalRect.top()) {
0665                 updatedRect.setBottom(sy);
0666             }
0667             break;
0668 
0669         case ImageCanvas::MoveBottomLeft:
0670             if (sx < originalRect.right()) {
0671                 updatedRect.setLeft(sx);
0672             }
0673             if (sy > originalRect.top()) {
0674                 updatedRect.setBottom(sy);
0675             }
0676             break;
0677 
0678         case ImageCanvas::MoveLeft:
0679             if (sx < originalRect.right()) {
0680                 updatedRect.setLeft(sx);
0681             }
0682             break;
0683 
0684         case ImageCanvas::MoveNew:
0685             if (sx > mStartPoint.x()) {     // drag to the right
0686                 updatedRect.setLeft(mStartPoint.x());
0687                 updatedRect.setRight(sx);
0688             } else {                // drag to the left
0689                 updatedRect.setRight(mStartPoint.x());
0690                 updatedRect.setLeft(sx);
0691             }
0692 
0693             if (sy > mStartPoint.y()) {     // drag down
0694                 updatedRect.setTop(mStartPoint.y());
0695                 updatedRect.setBottom(sy);
0696             } else {                // drag up
0697                 updatedRect.setBottom(mStartPoint.y());
0698                 updatedRect.setTop(sy);
0699             }
0700             break;
0701 
0702         case ImageCanvas::MoveWhole:
0703             int dx = sx - mLastPoint.x();
0704             int dy = sy - mLastPoint.y();
0705 
0706             QRectF r = updatedRect.translated(dx, dy);  // prospective new rectangle
0707 
0708             if (r.left() < 0) {
0709                 dx -= r.left();    // limit to edges
0710             }
0711             if (r.right() >= lx) {
0712                 dx -= (r.right() - lx + 1);
0713             }
0714             if (r.top() < 0) {
0715                 dy -= r.top();
0716             }
0717             if (r.bottom() >= ly) {
0718                 dy -= (r.bottom() - ly + 1);
0719             }
0720 
0721             updatedRect.translate(dx, dy);
0722             break;
0723         }
0724 
0725         if (updatedRect != originalRect) {
0726             mSelectionItem->setRect(updatedRect);
0727         }
0728     }
0729 
0730     mLastPoint = scenePos;
0731 }
0732 
0733 void ImageCanvas::mouseDoubleClickEvent(QMouseEvent *ev)
0734 {
0735     if (!hasImage()) {
0736         return;    // no image displayed
0737     }
0738     // convert to image pixels
0739     emit doubleClicked(mapToScene(ev->pos()).toPoint());
0740 }
0741 
0742 void ImageCanvas::resizeEvent(QResizeEvent *ev)
0743 {
0744     QGraphicsView::resizeEvent(ev);
0745     recalculateViewScale();
0746 }
0747 
0748 //  Scaling settings
0749 //  ----------------
0750 
0751 void ImageCanvas::setScaleFactor(int i)
0752 {
0753     //qCDebug(LIBKOOKASCAN_LOG) << "to" << i;
0754     mScaleFactor = i;
0755     if (i == 0) {
0756         setScaleType(ImageCanvas::ScaleDynamic);
0757     }
0758 
0759     recalculateViewScale();
0760 }
0761 
0762 ImageCanvas::ScaleType ImageCanvas::scaleType() const
0763 {
0764     if (mScaleType != ImageCanvas::ScaleUnspecified) {
0765         return (mScaleType);
0766     } else {
0767         return (defaultScaleType());
0768     }
0769 }
0770 
0771 void ImageCanvas::setScaleType(ScaleType type)
0772 {
0773     if (type == mScaleType) {
0774         return;    // no change
0775     }
0776 
0777     //qCDebug(LIBKOOKASCAN_LOG) << "to" << type;
0778     mScaleType = type;
0779     emit scalingChanged(scaleTypeString());
0780 }
0781 
0782 const QString ImageCanvas::scaleTypeString() const
0783 {
0784     switch (scaleType()) {
0785     case ImageCanvas::ScaleDynamic:     return (i18n("Fit Best"));
0786     case ImageCanvas::ScaleOriginal:    return (i18n("Original size"));
0787     case ImageCanvas::ScaleFitWidth:    return (i18n("Fit Width"));
0788     case ImageCanvas::ScaleFitHeight:   return (i18n("Fit Height"));
0789     case ImageCanvas::ScaleZoom:            return (i18n("Zoom %1%", mScaleFactor));
0790     default:                return (i18n("Unknown"));
0791     }
0792 }
0793 
0794 //  Calculate an appropriate scale (i.e. view transform) for displaying the image
0795 //  -----------------------------------------------------------------------------
0796 
0797 void ImageCanvas::recalculateViewScale()
0798 {
0799     if (!hasImage()) {
0800         return;
0801     }
0802 
0803     const int iw = mImage->width();         // original image size
0804     const int ih = mImage->height();
0805     const int aw = width() - 2 * frameWidth();  // available display size
0806     const int ah = height() - 2 * frameWidth();
0807     // width/height of scroll bar
0808     const int sbSize = QApplication::style()->pixelMetric(QStyle::PM_ScrollBarExtent);
0809 
0810     double xscale, yscale;              // calculated scale factors
0811 
0812     switch (scaleType()) {
0813     case ImageCanvas::ScaleDynamic:
0814         xscale = double(aw) / double(iw);
0815         yscale = double(ah) / double(ih);
0816         mScaleFactor = 0;
0817 
0818         if (mMaintainAspect) {
0819             xscale = yscale < xscale ? yscale : xscale;
0820             yscale = xscale;
0821         }
0822         break;
0823 
0824     default:
0825         qCDebug(LIBKOOKASCAN_LOG) << "Unknown scale type" << scaleType();
0826     // fall through
0827     case ImageCanvas::ScaleOriginal:
0828         yscale = xscale = 1.0;
0829         mScaleFactor = 100;
0830         break;
0831 
0832     case ImageCanvas::ScaleFitWidth:
0833         xscale = yscale = double(aw) / double(iw);
0834         if ((yscale * ih) >= ah) {          // will there be a scroll bar?
0835             // account for vertical scroll bar
0836             xscale = yscale = double(aw - sbSize) / double(iw);
0837             //qCDebug(LIBKOOKASCAN_LOG) << "FIT WIDTH scrollbar to subtract:" << sbSize;
0838         }
0839         mScaleFactor = static_cast<int>(100 * xscale);
0840         break;
0841 
0842     case ImageCanvas::ScaleFitHeight:
0843         yscale = xscale = double(ah) / double(ih);
0844         if ((xscale * iw) >= aw) {          // will there be a scroll bar?
0845             // account for horizontal scroll bar
0846             yscale = xscale = double(ah - sbSize) / double(ih);
0847             //qCDebug(LIBKOOKASCAN_LOG) << "FIT HEIGHT scrollbar to subtract:" << sbSize;
0848         }
0849         mScaleFactor = static_cast<int>(100 * yscale);
0850         break;
0851 
0852     case ImageCanvas::ScaleZoom:
0853         xscale = yscale = double(mScaleFactor) / 100.0;
0854         mScaleFactor = static_cast<int>(100 * xscale);
0855         break;
0856     }
0857 
0858     QTransform trans;
0859     trans.scale(xscale, yscale);
0860     //qCDebug(LIBKOOKASCAN_LOG) << "setting transform to" << trans;
0861     setTransform(trans);
0862 }
0863 
0864 //  Miscellaneous settings and information
0865 //  --------------------------------------
0866 
0867 void ImageCanvas::setCursorShape(Qt::CursorShape cs)
0868 {
0869     if (mCurrentCursor != cs) {         // optimise no-change
0870         setCursor(cs);
0871         mCurrentCursor = cs;
0872     }
0873 }
0874 
0875 void ImageCanvas::setReadOnly(bool ro)
0876 {
0877     mReadOnly = ro;
0878     if (mReadOnly) {
0879         stopMarqueeTimer();             // clear selection
0880         setCursorShape(Qt::CrossCursor);        // ensure cursor is reset
0881     }
0882     emit imageReadOnly(ro);
0883 }
0884 
0885 
0886 /* static */ const QString ImageCanvas::imageInfoString(int w, int h, int d)
0887 {
0888     return (i18n("%1x%2 pix, %3 bpp", w, h, d));
0889 }
0890 
0891 
0892 /* static */ const QString ImageCanvas::imageInfoString(const QImage *img)
0893 {
0894     if (img==nullptr) return ("-");
0895     else return (imageInfoString(img->width(), img->height(), img->depth()));
0896 }
0897 
0898 
0899 const QString ImageCanvas::imageInfoString() const
0900 {
0901     return (imageInfoString(mImage.data()));
0902 }
0903 
0904 
0905 bool ImageCanvas::hasImage() const
0906 {
0907     return (!mImage.isNull() && !mImage->isNull());
0908 }
0909 
0910 
0911 //  Highlight areas
0912 //  ---------------
0913 
0914 int ImageCanvas::addHighlight(const QRect &rect, bool ensureVis)
0915 {
0916     HighlightItem *item = new HighlightItem(rect, mHighlightStyle,
0917                                             mHighlightPen, mHighlightBrush);
0918 
0919     int idx = mHighlights.indexOf(nullptr);        // any empty slots?
0920     if (idx != -1) {
0921         mHighlights[idx] = item;    // yes, reuse that
0922     } else {                    // no, append new item
0923         idx = mHighlights.size();
0924         mHighlights.append(item);
0925     }
0926 
0927     mScene->addItem(item);
0928     if (ensureVis) {
0929         scrollTo(rect);
0930     }
0931     return (idx);
0932 }
0933 
0934 void ImageCanvas::removeAllHighlights()
0935 {
0936     for (int idx = 0; idx < mHighlights.size(); ++idx) {
0937         removeHighlight(idx);
0938     }
0939 }
0940 
0941 void ImageCanvas::removeHighlight(int idx)
0942 {
0943     if (idx < 0 || idx > mHighlights.size()) {
0944         return;
0945     }
0946 
0947     QGraphicsItem *item = mHighlights[idx];
0948     if (item == nullptr) {
0949         return;
0950     }
0951 
0952     mScene->removeItem(item);
0953     delete item;
0954     mHighlights[idx] = nullptr;
0955 }
0956 
0957 void ImageCanvas::scrollTo(const QRect &rect)
0958 {
0959     if (rect.isValid()) {
0960         ensureVisible(rect);
0961     }
0962 }
0963 
0964 void ImageCanvas::setHighlightStyle(ImageCanvas::HighlightStyle style,
0965                                     const QPen &pen, const QBrush &brush)
0966 {
0967     mHighlightStyle = style;
0968     mHighlightPen = pen;
0969     mHighlightBrush = brush;
0970 }
0971 
0972 //  Mouse position on the selection rectangle
0973 //  -----------------------------------------
0974 
0975 // This works in view coordinates, in order that the DELTA tolerance
0976 // is consistent regardless of the view scale.
0977 
0978 ImageCanvas::MoveState ImageCanvas::classifyPoint(const QPoint &p) const
0979 {
0980     if (!mSelectionItem->isVisible()) {
0981         return (ImageCanvas::MoveNone);
0982     }
0983 
0984     const QRect r = mapFromScene(mSelectionItem->boundingRect()).boundingRect().normalized();
0985     //qCDebug(LIBKOOKASCAN_LOG) << p << "for rect" << r;
0986     if (r.isEmpty()) {
0987         return (ImageCanvas::MoveNone);
0988     }
0989 
0990     const bool onLeft = (abs(p.x() - r.left()) < DELTA);
0991     const bool onRight = (abs(p.x() - r.right()) < DELTA);
0992 
0993     const bool onTop = (abs(p.y() - r.top()) < DELTA);
0994     const bool onBottom = (abs(p.y() - r.bottom()) < DELTA);
0995 
0996     const bool withinRect = r.contains(p);
0997     const bool overRect = r.adjusted(-DELTA, -DELTA, DELTA, DELTA).contains(p);
0998 
0999     if (onLeft && onTop) {
1000         return (ImageCanvas::MoveTopLeft);
1001     }
1002     if (onLeft && onBottom) {
1003         return (ImageCanvas::MoveBottomLeft);
1004     }
1005 
1006     if (onRight && onTop) {
1007         return (ImageCanvas::MoveTopRight);
1008     }
1009     if (onRight && onBottom) {
1010         return (ImageCanvas::MoveBottomRight);
1011     }
1012 
1013     if (onLeft && overRect) {
1014         return (ImageCanvas::MoveLeft);
1015     }
1016     if (onRight && overRect) {
1017         return (ImageCanvas::MoveRight);
1018     }
1019     if (onTop && overRect) {
1020         return (ImageCanvas::MoveTop);
1021     }
1022     if (onBottom && overRect) {
1023         return (ImageCanvas::MoveBottom);
1024     }
1025 
1026     if (withinRect) {
1027         return (ImageCanvas::MoveWhole);
1028     }
1029 
1030     return (ImageCanvas::MoveNone);
1031 }