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"