File indexing completed on 2024-12-22 04:13:01

0001 /* This file is part of the KDE project
0002  * SPDX-FileCopyrightText: 2009 Boudewijn Rempt <boud@valdyas.org>
0003  *
0004  * SPDX-License-Identifier: LGPL-2.0-or-later
0005  */
0006 
0007 #include "kis_tool_rectangle_base.h"
0008 
0009 #include <QtCore/qmath.h>
0010 
0011 #include "KisViewManager.h"
0012 #include "kis_canvas2.h"
0013 #include <KisOptionCollectionWidget.h>
0014 #include <KoCanvasBase.h>
0015 #include <KoCanvasController.h>
0016 #include <KoPointerEvent.h>
0017 #include <KoViewConverter.h>
0018 #include <input/kis_extended_modifiers_mapper.h>
0019 #include <kis_icon.h>
0020 
0021 #include "kis_rectangle_constraint_widget.h"
0022 
0023 KisToolRectangleBase::KisToolRectangleBase(KoCanvasBase * canvas, KisToolRectangleBase::ToolType type, const QCursor & cursor)
0024     : KisToolShape(canvas, cursor)
0025     , m_dragStart(0, 0)
0026     , m_dragEnd(0, 0)
0027     , m_type(type)
0028     , m_isRatioForced(false)
0029     , m_isWidthForced(false)
0030     , m_isHeightForced(false)
0031     , m_rotateActive(false)
0032     , m_forcedRatio(1.0)
0033     , m_forcedWidth(0)
0034     , m_forcedHeight(0)
0035     , m_roundCornersX(0)
0036     , m_roundCornersY(0)
0037     , m_referenceAngle(0)
0038     , m_angle(0)
0039     , m_angleBuffer(0)
0040     , m_currentModifiers(Qt::NoModifier)
0041 {
0042 }
0043 
0044 
0045 QList<QPointer<QWidget> > KisToolRectangleBase::createOptionWidgets()
0046 {
0047     QList<QPointer<QWidget>> widgetsList = KisToolShape::createOptionWidgets();
0048 
0049     KisRectangleConstraintWidget *widget =
0050         new KisRectangleConstraintWidget(0, this, showRoundCornersGUI());
0051 
0052     if (widgetsList.size() > 0
0053         && dynamic_cast<KisOptionCollectionWidget *>(
0054             widgetsList.first().data())) {
0055         KisOptionCollectionWidget *baseOptions =
0056             dynamic_cast<KisOptionCollectionWidget *>(
0057                 widgetsList.first().data());
0058         KisOptionCollectionWidgetWithHeader *sectionRectangle =
0059             new KisOptionCollectionWidgetWithHeader(widget->windowTitle());
0060         sectionRectangle->appendWidget("rectangleConstraintWidget", widget);
0061         baseOptions->appendWidget("sectionRectangle", sectionRectangle);
0062     } else {
0063         widget->setContentsMargins(10, 10, 10, 10);
0064         widgetsList.append(widget);
0065     }
0066 
0067     return widgetsList;
0068 }
0069 
0070 void KisToolRectangleBase::constraintsChanged(bool forceRatio, bool forceWidth, bool forceHeight, float ratio, float width, float height)
0071 {
0072     m_isWidthForced = forceWidth;
0073     m_isHeightForced = forceHeight;
0074     m_isRatioForced = forceRatio;
0075 
0076     m_forcedHeight = height;
0077     m_forcedWidth = width;
0078     m_forcedRatio = ratio;
0079 
0080     // Avoid division by zero in size calculations
0081     if (ratio < 0.0001f)
0082         m_isRatioForced = false;
0083 }
0084 
0085 void KisToolRectangleBase::roundCornersChanged(int rx, int ry)
0086 {
0087     m_roundCornersX = rx;
0088     m_roundCornersY = ry;
0089 }
0090 
0091 void KisToolRectangleBase::showSize()
0092 {
0093     KisCanvas2 *kisCanvas =dynamic_cast<KisCanvas2*>(canvas());
0094     KIS_SAFE_ASSERT_RECOVER_RETURN(kisCanvas);
0095     kisCanvas->viewManager()->showFloatingMessage(i18n("Width: %1 px\nHeight: %2 px"
0096                                                        , createRect(m_dragStart, m_dragEnd).width()
0097                                                        , createRect(m_dragStart, m_dragEnd).height()), QIcon(), 1000
0098                                                        , KisFloatingMessage::High,  Qt::AlignLeft | Qt::TextWordWrap | Qt::AlignVCenter);
0099 
0100 }
0101 void KisToolRectangleBase::paint(QPainter& gc, const KoViewConverter &converter)
0102 {
0103     if(mode() == KisTool::PAINT_MODE) {
0104         paintRectangle(gc, createRect(m_dragStart, m_dragEnd));
0105     }
0106 
0107     KisToolPaint::paint(gc, converter);
0108 }
0109 
0110 void KisToolRectangleBase::activate(const QSet<KoShape *> &shapes)
0111 {
0112     KisToolShape::activate(shapes);
0113 
0114     emit sigRequestReloadConfig();
0115 }
0116 
0117 void KisToolRectangleBase::deactivate()
0118 {
0119     cancelStroke();
0120     KisToolShape::deactivate();
0121 }
0122 
0123 void KisToolRectangleBase::keyPressEvent(QKeyEvent *event) {
0124     const Qt::Key key = KisExtendedModifiersMapper::workaroundShiftAltMetaHell(event);
0125 
0126     if (key == Qt::Key_Control) {
0127         m_currentModifiers |= Qt::ControlModifier;
0128     } else if (key == Qt::Key_Shift) {
0129         m_currentModifiers |= Qt::ShiftModifier;
0130     } else if (key == Qt::Key_Alt) {
0131         m_currentModifiers |= Qt::AltModifier;
0132     }
0133 
0134     KisToolShape::keyPressEvent(event);
0135 }
0136 
0137 void KisToolRectangleBase::keyReleaseEvent(QKeyEvent *event) {
0138     const Qt::Key key = KisExtendedModifiersMapper::workaroundShiftAltMetaHell(event);
0139 
0140     if (key == Qt::Key_Control) {
0141         m_currentModifiers &= ~Qt::ControlModifier;
0142     } else if (key == Qt::Key_Shift) {
0143         m_currentModifiers &= ~Qt::ShiftModifier;
0144     } else if (key == Qt::Key_Alt) {
0145         m_currentModifiers &= ~Qt::AltModifier;
0146     }
0147 
0148     KisToolShape::keyReleaseEvent(event);
0149 }
0150 
0151 void KisToolRectangleBase::beginPrimaryAction(KoPointerEvent *event)
0152 {
0153     NodePaintAbility paintability = nodePaintAbility();
0154     if ((m_type == PAINT && (!nodeEditable() || paintability == UNPAINTABLE || paintability  == KisToolPaint::CLONE || paintability == KisToolPaint::MYPAINTBRUSH_UNPAINTABLE)) || (m_type == SELECT && !selectionEditable())) {
0155 
0156         if (paintability == KisToolPaint::CLONE){
0157             KisCanvas2 * kiscanvas = static_cast<KisCanvas2*>(canvas());
0158             QString message = i18n("This tool cannot paint on clone layers.  Please select a paint or vector layer or mask.");
0159             kiscanvas->viewManager()->showFloatingMessage(message, koIcon("object-locked"));
0160         }
0161 
0162         if (paintability == KisToolPaint::MYPAINTBRUSH_UNPAINTABLE) {
0163             KisCanvas2 * kiscanvas = static_cast<KisCanvas2*>(canvas());
0164             QString message = i18n("The MyPaint Brush Engine is not available for this colorspace");
0165             kiscanvas->viewManager()->showFloatingMessage(message, koIcon("object-locked"));
0166         }
0167 
0168         event->ignore();
0169         return;
0170     }
0171     setMode(KisTool::PAINT_MODE);
0172     beginShape();
0173 
0174     m_currentModifiers = Qt::NoModifier;
0175 
0176     QPointF pos = convertToPixelCoordAndSnap(event, QPointF(), false);
0177     m_dragStart = m_dragCenter = pos;
0178     m_angle = m_angleBuffer = 0;
0179     m_rotateActive = false;
0180 
0181     QSizeF area = QSizeF(0,0);
0182 
0183     applyConstraints(area, false);
0184 
0185     m_dragEnd.setX(m_dragStart.x() + area.width());
0186     m_dragEnd.setY(m_dragStart.y() + area.height());
0187 
0188     m_dragCenter = QPointF((m_dragStart.x() + m_dragEnd.x()) / 2,
0189                            (m_dragStart.y() + m_dragEnd.y()) / 2);
0190     showSize();
0191     event->accept();
0192 }
0193 
0194 bool KisToolRectangleBase::isFixedSize() {
0195   if (m_isWidthForced && m_isHeightForced) return true;
0196   if (m_isRatioForced && (m_isWidthForced || m_isHeightForced)) return true;
0197 
0198   return false;
0199 }
0200 
0201 void KisToolRectangleBase::applyConstraints(QSizeF &area, bool overrideRatio) {
0202   if (m_isWidthForced) {
0203     area.setWidth(m_forcedWidth);
0204   }
0205   if (m_isHeightForced) {
0206     area.setHeight(m_forcedHeight);
0207   }
0208 
0209   if (m_isHeightForced && m_isWidthForced) return;
0210 
0211   if (m_isRatioForced || overrideRatio) {
0212     float ratio = m_isRatioForced ? m_forcedRatio : 1.0f;
0213 
0214     if (m_isWidthForced) {
0215       area.setHeight(area.width() / ratio);
0216     } else {
0217       area.setWidth(area.height() * ratio);
0218     }
0219   }
0220 }
0221 
0222 void KisToolRectangleBase::continuePrimaryAction(KoPointerEvent *event)
0223 {
0224     CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE);
0225 
0226     bool constraintToggle = m_currentModifiers & Qt::ShiftModifier;
0227     bool translateMode = m_currentModifiers & Qt::AltModifier;
0228     bool expandFromCenter = m_currentModifiers & Qt::ControlModifier;
0229 
0230     bool rotateMode = expandFromCenter && translateMode;
0231     bool fixedSize = isFixedSize() && !constraintToggle;
0232 
0233     QPointF pos = convertToPixelCoordAndSnap(event, QPointF(), false);
0234 
0235     if (rotateMode) {
0236         QPointF angleVector;
0237         if (!m_rotateActive) {
0238             m_rotateActive = true;
0239             angleVector = (fixedSize)? m_dragEnd: pos;
0240             angleVector -= m_dragStart;
0241             m_referenceAngle = atan2(angleVector.y(), angleVector.x());
0242         }
0243         angleVector = pos - m_dragStart;
0244         qreal a2 = atan2(angleVector.y(), angleVector.x());
0245         m_angleBuffer = a2 - m_referenceAngle;
0246     } else {
0247         m_rotateActive = false;
0248         m_angle += m_angleBuffer;
0249         m_angleBuffer = 0;
0250     }
0251 
0252     if (fixedSize && !rotateMode) {
0253       m_dragStart = pos;
0254     } else if (translateMode && !rotateMode) {
0255       QPointF trans = pos - m_dragEnd;
0256       m_dragStart += trans;
0257       m_dragEnd += trans;
0258 
0259     }
0260 
0261     QPointF diag = pos - m_dragStart;
0262     QTransform t1, t2;
0263     t1.rotateRadians(-getRotationAngle());
0264     QPointF baseDiag = t1.map(diag);
0265     QSizeF area = QSizeF(fabs(baseDiag.x()), fabs(baseDiag.y()));
0266 
0267     bool overrideRatio = constraintToggle && !(m_isHeightForced || m_isWidthForced || m_isRatioForced);
0268     if (!constraintToggle || overrideRatio) {
0269       applyConstraints(area, overrideRatio);
0270     }
0271 
0272     baseDiag = QPointF(
0273       (baseDiag.x() < 0) ? -area.width() : area.width(),
0274       (baseDiag.y() < 0) ? -area.height() : area.height()
0275     );
0276 
0277     t2.rotateRadians(getRotationAngle());
0278     diag = t2.map(baseDiag);
0279 
0280     // resize around center point?
0281     if (expandFromCenter && !fixedSize && !rotateMode) {
0282       m_dragStart = m_dragCenter - diag / 2;
0283       m_dragEnd = m_dragCenter + diag / 2;
0284     } else {
0285       m_dragEnd = m_dragStart + diag;
0286     }
0287 
0288     if(!translateMode) {
0289         showSize();
0290     }
0291     else {
0292         KisCanvas2 *kisCanvas =dynamic_cast<KisCanvas2*>(canvas());
0293         KIS_ASSERT(kisCanvas);
0294         kisCanvas->viewManager()->showFloatingMessage(i18n("X: %1 px\nY: %2 px"
0295                                                            , QString::number(m_dragStart.x(), 'f', 1)
0296                                                            , QString::number(m_dragStart.y(), 'f', 1)), QIcon(), 1000
0297                                                            , KisFloatingMessage::High,  Qt::AlignLeft | Qt::TextWordWrap | Qt::AlignVCenter);
0298     }
0299     updateArea();
0300     m_dragCenter = QPointF((m_dragStart.x() + m_dragEnd.x()) / 2,
0301                            (m_dragStart.y() + m_dragEnd.y()) / 2);
0302 
0303     KisToolPaint::requestUpdateOutline(event->point, event);
0304 }
0305 
0306 void KisToolRectangleBase::endPrimaryAction(KoPointerEvent *event)
0307 {
0308     CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE);
0309     // If the event was not originated by the user releasing the button
0310     // (for example due to the canvas loosing focus), then we just cancel the
0311     // operation. This prevents some issues with shapes being added after
0312     // the image was closed while the shape was being made
0313     if (event->spontaneous()) {
0314         endStroke();
0315     } else {
0316         cancelStroke();
0317     }
0318     event->accept();
0319 }
0320 
0321 void KisToolRectangleBase::requestStrokeEnd()
0322 {
0323     if (mode() != KisTool::PAINT_MODE) {
0324         return;
0325     }
0326     endStroke();
0327 }
0328 
0329 void KisToolRectangleBase::requestStrokeCancellation()
0330 {
0331     if (mode() != KisTool::PAINT_MODE) {
0332         return;
0333     }
0334     cancelStroke();
0335 }
0336 
0337 void KisToolRectangleBase::endStroke()
0338 {
0339     setMode(KisTool::HOVER_MODE);
0340     updateArea();
0341     finishRect(createRect(m_dragStart, m_dragEnd), m_roundCornersX, m_roundCornersY);
0342     endShape();
0343 }
0344 
0345 void KisToolRectangleBase::cancelStroke()
0346 {
0347     setMode(KisTool::HOVER_MODE);
0348     updateArea();
0349     endShape();
0350 }
0351 
0352 QRectF KisToolRectangleBase::createRect(const QPointF &start, const QPointF &end)
0353 {
0354     QTransform t;
0355     t.translate(start.x(), start.y());
0356     t.rotateRadians(-getRotationAngle());
0357     t.translate(-start.x(), -start.y());
0358     const QTransform tInv = t.inverted();
0359 
0360     const QPointF end1 = t.map(end);
0361     const QPointF newStart(qRound(start.x()), qRound(start.y()));
0362     const QPointF newEnd(qRound(end1.x()), qRound(end1.y()));
0363     const QPointF newCenter = (newStart + newEnd) / 2.0;
0364    
0365     QRectF result(newStart, newEnd);
0366     result.moveCenter(tInv.map(newCenter));
0367 
0368     return result.normalized();
0369 }
0370 
0371 bool KisToolRectangleBase::showRoundCornersGUI() const
0372 {
0373     return true;
0374 }
0375 
0376 void KisToolRectangleBase::paintRectangle(QPainter &gc, const QRectF &imageRect)
0377 {
0378     KIS_ASSERT_RECOVER_RETURN(canvas());
0379 
0380     const QRect viewRect = pixelToView(imageRect).toAlignedRect();
0381 
0382     KisCanvas2 *kritaCanvas = dynamic_cast<KisCanvas2*>(canvas());
0383     KIS_SAFE_ASSERT_RECOVER_RETURN(kritaCanvas);
0384 
0385     const KisCoordinatesConverter *converter = kritaCanvas->coordinatesConverter();
0386     const qreal roundCornersX = converter->effectiveZoom() * m_roundCornersX;
0387     const qreal roundCornersY = converter->effectiveZoom() * m_roundCornersY;
0388 
0389     QPainterPath path;
0390     if (m_roundCornersX > 0 || m_roundCornersY > 0) {
0391         path.addRoundedRect(viewRect,
0392                             roundCornersX, roundCornersY);
0393     } else {
0394         path.addRect(viewRect);
0395     }
0396 
0397     getRotatedPath(path, viewRect.center(), getRotationAngle());
0398     path.addPath(drawX(pixelToView(m_dragStart)));
0399     path.addPath(drawX(pixelToView(m_dragCenter)));
0400     paintToolOutline(&gc, path);
0401 }
0402 
0403 void KisToolRectangleBase::updateArea() {
0404     const QRectF bound = createRect(m_dragStart, m_dragEnd);
0405 
0406     canvas()->updateCanvas(convertToPt(bound).adjusted(-100, -100, +200, +200));
0407 
0408     emit rectangleChanged(bound);
0409 }
0410 
0411 qreal KisToolRectangleBase::getRotationAngle() {
0412     return m_angle + m_angleBuffer;
0413 }
0414 
0415 QPainterPath KisToolRectangleBase::drawX(const QPointF &pt) {
0416     QPainterPath path;
0417     path.moveTo(QPointF(pt.x() - 5.0, pt.y() - 5.0)); path.lineTo(QPointF(pt.x() + 5.0, pt.y() + 5.0));
0418     path.moveTo(QPointF(pt.x() - 5.0, pt.y() + 5.0)); path.lineTo(QPointF(pt.x() + 5.0, pt.y() - 5.0));
0419     return path;
0420 }
0421 
0422 void KisToolRectangleBase::getRotatedPath(QPainterPath &path, const QPointF &center, const qreal &angle) {
0423     QTransform t;
0424     t.translate(center.x(), center.y());
0425     t.rotateRadians(angle);
0426     t.translate(-center.x(), -center.y());
0427 
0428     path = t.map(path);
0429 }