File indexing completed on 2024-05-12 04:19:35

0001 // vim: set tabstop=4 shiftwidth=4 expandtab:
0002 /*
0003 Gwenview: an image viewer
0004 Copyright 2007 Aurélien Gâteau <agateau@kde.org>
0005 
0006 This program is free software; you can redistribute it and/or
0007 modify it under the terms of the GNU General Public License
0008 as published by the Free Software Foundation; either version 2
0009 of the License, or (at your option) any later version.
0010 
0011 This program is distributed in the hope that it will be useful,
0012 but WITHOUT ANY WARRANTY; without even the implied warranty of
0013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0014 GNU General Public License for more details.
0015 
0016 You should have received a copy of the GNU General Public License
0017 along with this program; if not, write to the Free Software
0018 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
0019 
0020 */
0021 // Self
0022 #include "croptool.h"
0023 
0024 // Qt
0025 #include <QDialogButtonBox>
0026 #include <QGraphicsSceneMouseEvent>
0027 #include <QPainter>
0028 #include <QPushButton>
0029 #include <QRect>
0030 
0031 // KF
0032 
0033 // Local
0034 #include "cropimageoperation.h"
0035 #include "cropwidget.h"
0036 #include "gwenview_lib_debug.h"
0037 #include "gwenviewconfig.h"
0038 #include <lib/documentview/rasterimageview.h>
0039 
0040 static const int HANDLE_SIZE = 15;
0041 
0042 namespace Gwenview
0043 {
0044 enum CropHandleFlag {
0045     CH_None,
0046     CH_Top = 1,
0047     CH_Left = 2,
0048     CH_Right = 4,
0049     CH_Bottom = 8,
0050     CH_TopLeft = CH_Top | CH_Left,
0051     CH_BottomLeft = CH_Bottom | CH_Left,
0052     CH_TopRight = CH_Top | CH_Right,
0053     CH_BottomRight = CH_Bottom | CH_Right,
0054     CH_Content = 16,
0055 };
0056 
0057 Q_DECLARE_FLAGS(CropHandle, CropHandleFlag)
0058 
0059 } // namespace
0060 
0061 inline QPoint boundPointX(const QPoint &point, const QRect &rect)
0062 {
0063     return QPoint(qBound(rect.left(), point.x(), rect.right()), point.y());
0064 }
0065 
0066 inline QPoint boundPointXY(const QPoint &point, const QRect &rect)
0067 {
0068     return QPoint(qBound(rect.left(), point.x(), rect.right()), qBound(rect.top(), point.y(), rect.bottom()));
0069 }
0070 
0071 Q_DECLARE_OPERATORS_FOR_FLAGS(Gwenview::CropHandle)
0072 
0073 namespace Gwenview
0074 {
0075 struct CropToolPrivate {
0076     CropTool *q = nullptr;
0077     QRect mRect;
0078     QList<CropHandle> mCropHandleList;
0079     CropHandle mMovingHandle;
0080     QPoint mLastMouseMovePos;
0081     double mCropRatio;
0082     double mLockedCropRatio;
0083     CropWidget *mCropWidget = nullptr;
0084 
0085     QRect viewportCropRect() const
0086     {
0087         return q->imageView()->mapToView(mRect);
0088     }
0089 
0090     QRect handleViewportRect(CropHandle handle)
0091     {
0092         QSize viewportSize = q->imageView()->size().toSize();
0093         QRect rect = viewportCropRect();
0094         int left, top;
0095         if (handle & CH_Top) {
0096             top = rect.top();
0097         } else if (handle & CH_Bottom) {
0098             top = rect.bottom() + 1 - HANDLE_SIZE;
0099         } else {
0100             top = rect.top() + (rect.height() - HANDLE_SIZE) / 2;
0101             top = qBound(0, top, viewportSize.height() - HANDLE_SIZE);
0102             top = qBound(rect.top() + HANDLE_SIZE, top, rect.bottom() - 2 * HANDLE_SIZE);
0103         }
0104 
0105         if (handle & CH_Left) {
0106             left = rect.left();
0107         } else if (handle & CH_Right) {
0108             left = rect.right() + 1 - HANDLE_SIZE;
0109         } else {
0110             left = rect.left() + (rect.width() - HANDLE_SIZE) / 2;
0111             left = qBound(0, left, viewportSize.width() - HANDLE_SIZE);
0112             left = qBound(rect.left() + HANDLE_SIZE, left, rect.right() - 2 * HANDLE_SIZE);
0113         }
0114 
0115         return QRect(left, top, HANDLE_SIZE, HANDLE_SIZE);
0116     }
0117 
0118     CropHandle handleAt(const QPointF &pos)
0119     {
0120         for (const CropHandle &handle : qAsConst(mCropHandleList)) {
0121             QRectF rect = handleViewportRect(handle);
0122             if (rect.contains(pos)) {
0123                 return handle;
0124             }
0125         }
0126         QRectF rect = viewportCropRect();
0127         if (rect.contains(pos)) {
0128             return CH_Content;
0129         }
0130         return CH_None;
0131     }
0132 
0133     void updateCursor(CropHandle handle, bool buttonDown)
0134     {
0135         Qt::CursorShape shape;
0136         switch (handle) {
0137         case CH_TopLeft:
0138         case CH_BottomRight:
0139             shape = Qt::SizeFDiagCursor;
0140             break;
0141 
0142         case CH_TopRight:
0143         case CH_BottomLeft:
0144             shape = Qt::SizeBDiagCursor;
0145             break;
0146 
0147         case CH_Left:
0148         case CH_Right:
0149             shape = Qt::SizeHorCursor;
0150             break;
0151 
0152         case CH_Top:
0153         case CH_Bottom:
0154             shape = Qt::SizeVerCursor;
0155             break;
0156 
0157         case CH_Content:
0158             shape = buttonDown ? Qt::ClosedHandCursor : Qt::OpenHandCursor;
0159             break;
0160 
0161         default:
0162             shape = Qt::ArrowCursor;
0163             break;
0164         }
0165         q->imageView()->setCursor(shape);
0166     }
0167 
0168     void keepRectInsideImage()
0169     {
0170         const QSize imageSize = q->imageView()->documentSize().toSize();
0171         if (mRect.width() > imageSize.width() || mRect.height() > imageSize.height()) {
0172             // This can happen when the crop ratio changes
0173             QSize rectSize = mRect.size();
0174             rectSize.scale(imageSize, Qt::KeepAspectRatio);
0175             mRect.setSize(rectSize);
0176         }
0177 
0178         if (mRect.right() >= imageSize.width()) {
0179             mRect.moveRight(imageSize.width() - 1);
0180         } else if (mRect.left() < 0) {
0181             mRect.moveLeft(0);
0182         }
0183         if (mRect.bottom() >= imageSize.height()) {
0184             mRect.moveBottom(imageSize.height() - 1);
0185         } else if (mRect.top() < 0) {
0186             mRect.moveTop(0);
0187         }
0188     }
0189 
0190     void setupWidget()
0191     {
0192         RasterImageView *view = q->imageView();
0193         mCropWidget = new CropWidget(nullptr, view, q);
0194         QObject::connect(mCropWidget, SIGNAL(cropRequested()), q, SLOT(slotCropRequested()));
0195         QObject::connect(mCropWidget, &CropWidget::done, q, &CropTool::done);
0196         QObject::connect(mCropWidget, &CropWidget::rectReset, q, &CropTool::rectReset);
0197 
0198         // This is needed when crop ratio set to Current Image, and the image is rotated
0199         QObject::connect(view, &RasterImageView::imageRectUpdated, mCropWidget, &CropWidget::updateCropRatio);
0200     }
0201 
0202     QRect computeVisibleImageRect() const
0203     {
0204         RasterImageView *view = q->imageView();
0205         const QRect imageRect = QRect(QPoint(0, 0), view->documentSize().toSize());
0206         const QRect viewportRect = view->mapToImage(view->rect().toRect());
0207         return imageRect & viewportRect;
0208     }
0209 };
0210 
0211 CropTool::CropTool(RasterImageView *view)
0212     : AbstractRasterImageViewTool(view)
0213     , d(new CropToolPrivate)
0214 {
0215     d->q = this;
0216     d->mCropHandleList << CH_Left << CH_Right << CH_Top << CH_Bottom << CH_TopLeft << CH_TopRight << CH_BottomLeft << CH_BottomRight;
0217     d->mMovingHandle = CH_None;
0218     d->mCropRatio = 0.;
0219     d->mLockedCropRatio = 0.;
0220     d->mRect = d->computeVisibleImageRect();
0221     d->setupWidget();
0222 }
0223 
0224 CropTool::~CropTool()
0225 {
0226     // mCropWidget is a child of its container not of us, so it is not deleted automatically
0227     delete d->mCropWidget;
0228     delete d;
0229 }
0230 
0231 void CropTool::setCropRatio(double ratio)
0232 {
0233     d->mCropRatio = ratio;
0234 }
0235 
0236 void CropTool::setRect(const QRect &rect)
0237 {
0238     QRect oldRect = d->mRect;
0239     d->mRect = rect;
0240     d->keepRectInsideImage();
0241     if (d->mRect != oldRect) {
0242         Q_EMIT rectUpdated(d->mRect);
0243     }
0244     imageView()->update();
0245 }
0246 
0247 QRect CropTool::rect() const
0248 {
0249     return d->mRect;
0250 }
0251 
0252 void CropTool::paint(QPainter *painter)
0253 {
0254     QRect rect = d->viewportCropRect();
0255 
0256     QRect imageRect = imageView()->rect().toRect();
0257 
0258     static const QColor outerColor = QColor::fromHsvF(0, 0, 0, 0.5);
0259     // For some reason nothing gets drawn if borderColor is not fully opaque!
0260     // static const QColor borderColor = QColor::fromHsvF(0, 0, 1.0, 0.66);
0261     static const QColor borderColor = QColor::fromHsvF(0, 0, 1.0);
0262     static const QColor fillColor = QColor::fromHsvF(0, 0, 0.75, 0.66);
0263 
0264     const QRegion outerRegion = QRegion(imageRect) - QRegion(rect);
0265     for (const QRect &outerRect : outerRegion) {
0266         painter->fillRect(outerRect, outerColor);
0267     }
0268 
0269     painter->setPen(borderColor);
0270 
0271     painter->drawRect(rect);
0272 
0273     if (d->mMovingHandle == CH_None) {
0274         // Only draw handles when user is not resizing
0275         painter->setBrush(fillColor);
0276         for (const CropHandle &handle : qAsConst(d->mCropHandleList)) {
0277             rect = d->handleViewportRect(handle);
0278             painter->drawRect(rect);
0279         }
0280     }
0281 }
0282 
0283 void CropTool::mousePressEvent(QGraphicsSceneMouseEvent *event)
0284 {
0285     if (event->buttons() != Qt::LeftButton) {
0286         event->ignore();
0287         return;
0288     }
0289     const CropHandle newMovingHandle = d->handleAt(event->pos());
0290     if (event->modifiers() & Qt::ControlModifier && !(newMovingHandle & (CH_Top | CH_Left | CH_Right | CH_Bottom))) {
0291         event->ignore();
0292         return;
0293     }
0294 
0295     event->accept();
0296     d->mMovingHandle = newMovingHandle;
0297     d->updateCursor(d->mMovingHandle, true /* down */);
0298 
0299     if (d->mMovingHandle == CH_Content) {
0300         d->mLastMouseMovePos = imageView()->mapToImage(event->pos().toPoint());
0301     }
0302 
0303     // Update to hide handles
0304     imageView()->update();
0305 }
0306 
0307 void CropTool::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
0308 {
0309     event->accept();
0310     if (event->buttons() != Qt::LeftButton) {
0311         return;
0312     }
0313 
0314     const QSize imageSize = imageView()->document()->size();
0315 
0316     QPoint point = imageView()->mapToImage(event->pos().toPoint());
0317     int posX = qBound(0, point.x(), imageSize.width() - 1);
0318     int posY = qBound(0, point.y(), imageSize.height() - 1);
0319 
0320     if (d->mMovingHandle == CH_None) {
0321         return;
0322     }
0323 
0324     // Adjust edge
0325     if (d->mMovingHandle & CH_Top) {
0326         d->mRect.setTop(posY);
0327     } else if (d->mMovingHandle & CH_Bottom) {
0328         d->mRect.setBottom(posY);
0329     }
0330     if (d->mMovingHandle & CH_Left) {
0331         d->mRect.setLeft(posX);
0332     } else if (d->mMovingHandle & CH_Right) {
0333         d->mRect.setRight(posX);
0334     }
0335 
0336     // Normalize rect and handles (this is useful when user drag the right side
0337     // of the crop rect to the left of the left side)
0338     if (d->mRect.height() < 0) {
0339         d->mMovingHandle = d->mMovingHandle ^ (CH_Top | CH_Bottom);
0340     }
0341     if (d->mRect.width() < 0) {
0342         d->mMovingHandle = d->mMovingHandle ^ (CH_Left | CH_Right);
0343     }
0344     d->mRect = d->mRect.normalized();
0345 
0346     // Enforce ratio:
0347     double ratioToEnforce = d->mCropRatio;
0348     //  - if user is holding down Ctrl/Shift when resizing rect, lock to current rect ratio
0349     if (event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier) && d->mLockedCropRatio > 0) {
0350         ratioToEnforce = d->mLockedCropRatio;
0351     }
0352     //  - if user has restricted the ratio via the GUI
0353     if (ratioToEnforce > 0.) {
0354         if (d->mMovingHandle == CH_Top || d->mMovingHandle == CH_Bottom) {
0355             // Top or bottom
0356             int width = int(d->mRect.height() / ratioToEnforce);
0357             d->mRect.setWidth(width);
0358         } else if (d->mMovingHandle == CH_Left || d->mMovingHandle == CH_Right) {
0359             // Left or right
0360             int height = int(d->mRect.width() * ratioToEnforce);
0361             d->mRect.setHeight(height);
0362         } else if (d->mMovingHandle & CH_Top) {
0363             // Top left or top right
0364             int height = int(d->mRect.width() * ratioToEnforce);
0365             d->mRect.setTop(d->mRect.y() + d->mRect.height() - height);
0366         } else if (d->mMovingHandle & CH_Bottom) {
0367             // Bottom left or bottom right
0368             int height = int(d->mRect.width() * ratioToEnforce);
0369             d->mRect.setHeight(height);
0370         }
0371     }
0372 
0373     if (d->mMovingHandle == CH_Content) {
0374         d->mRect.translate(point - d->mLastMouseMovePos);
0375         d->mLastMouseMovePos = point;
0376     }
0377 
0378     d->keepRectInsideImage();
0379 
0380     imageView()->update();
0381     Q_EMIT rectUpdated(d->mRect);
0382 }
0383 
0384 void CropTool::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
0385 {
0386     event->accept();
0387     d->mMovingHandle = CH_None;
0388     d->updateCursor(d->handleAt(event->lastPos()), false);
0389 
0390     // Update to show handles
0391     imageView()->update();
0392 }
0393 
0394 void CropTool::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *event)
0395 {
0396     if (event->buttons() != Qt::LeftButton || d->handleAt(event->pos()) == CH_None) {
0397         event->ignore();
0398         return;
0399     }
0400     event->accept();
0401     Q_EMIT d->mCropWidget->findChild<QDialogButtonBox *>()->accepted();
0402 }
0403 
0404 void CropTool::hoverMoveEvent(QGraphicsSceneHoverEvent *event)
0405 {
0406     event->accept();
0407     // Make sure cursor is updated when moving over handles
0408     CropHandle handle = d->handleAt(event->lastPos());
0409     d->updateCursor(handle, false /* buttonDown */);
0410 }
0411 
0412 void CropTool::keyPressEvent(QKeyEvent *event)
0413 {
0414     // Lock crop ratio to current rect when user presses Control or Shift
0415     if (event->key() == Qt::Key_Control || event->key() == Qt::Key_Shift) {
0416         d->mLockedCropRatio = 1. * d->mRect.height() / d->mRect.width();
0417     }
0418 
0419     auto buttons = d->mCropWidget->findChild<QDialogButtonBox *>();
0420     switch (event->key()) {
0421     case Qt::Key_Escape:
0422         event->accept();
0423         Q_EMIT buttons->rejected();
0424         break;
0425     case Qt::Key_Return:
0426     case Qt::Key_Enter: {
0427         event->accept();
0428         auto focusButton = static_cast<QPushButton *>(buttons->focusWidget());
0429         if (focusButton && buttons->buttonRole(focusButton) == QDialogButtonBox::RejectRole) {
0430             Q_EMIT buttons->rejected();
0431         } else {
0432             Q_EMIT buttons->accepted();
0433         }
0434         break;
0435     }
0436     default:
0437         break;
0438     }
0439 }
0440 
0441 void CropTool::toolActivated()
0442 {
0443     d->mCropWidget->setAdvancedSettingsEnabled(GwenviewConfig::cropAdvancedSettingsEnabled());
0444     d->mCropWidget->setPreserveAspectRatio(GwenviewConfig::cropPreserveAspectRatio());
0445     const int index = GwenviewConfig::cropRatioIndex();
0446     if (index >= 0) {
0447         // Preset ratio
0448         d->mCropWidget->setCropRatioIndex(index);
0449     } else {
0450         // Must be a custom ratio, or blank
0451         const QSizeF ratio = QSizeF(GwenviewConfig::cropRatioWidth(), GwenviewConfig::cropRatioHeight());
0452         d->mCropWidget->setCropRatio(ratio);
0453     }
0454 }
0455 
0456 void CropTool::toolDeactivated()
0457 {
0458     GwenviewConfig::setCropAdvancedSettingsEnabled(d->mCropWidget->advancedSettingsEnabled());
0459     GwenviewConfig::setCropPreserveAspectRatio(d->mCropWidget->preserveAspectRatio());
0460     GwenviewConfig::setCropRatioIndex(d->mCropWidget->cropRatioIndex());
0461     const QSizeF ratio = d->mCropWidget->cropRatio();
0462     GwenviewConfig::setCropRatioWidth(ratio.width());
0463     GwenviewConfig::setCropRatioHeight(ratio.height());
0464 }
0465 
0466 void CropTool::slotCropRequested()
0467 {
0468     auto op = new CropImageOperation(d->mRect);
0469     Q_EMIT imageOperationRequested(op);
0470     Q_EMIT done();
0471 }
0472 
0473 QWidget *CropTool::widget() const
0474 {
0475     return d->mCropWidget;
0476 }
0477 
0478 } // namespace
0479 
0480 #include "moc_croptool.cpp"