File indexing completed on 2024-06-09 04:20:36

0001 /* This file is part of the KDE project
0002  *
0003  * SPDX-FileCopyrightText: 2006 Thorsten Zachmann <zachmann@kde.org>
0004  * SPDX-FileCopyrightText: 2008-2010 Jan Hambrecht <jaham@gmx.net>
0005  *
0006  * SPDX-License-Identifier: LGPL-2.0-or-later
0007  */
0008 
0009 #include "KoCreatePathTool.h"
0010 #include "KoCreatePathTool_p.h"
0011 
0012 #include <KoUnit.h>
0013 #include "KoPointerEvent.h"
0014 #include "KoPathShape.h"
0015 #include "KoSelection.h"
0016 #include "KoDocumentResourceManager.h"
0017 #include "KoShapeStroke.h"
0018 #include "KoCanvasBase.h"
0019 #include "kis_int_parse_spin_box.h"
0020 #include <KoColor.h>
0021 #include "kis_canvas_resource_provider.h"
0022 #include <KisHandlePainterHelper.h>
0023 #include "KoPathPointTypeCommand.h"
0024 #include <KisAngleSelector.h>
0025 
0026 #include <klocalizedstring.h>
0027 
0028 #include <QCheckBox>
0029 #include <QHBoxLayout>
0030 #include <QLabel>
0031 #include <QPainter>
0032 #include <QSpinBox>
0033 #include <QVBoxLayout>
0034 
0035 KoCreatePathTool::KoCreatePathTool(KoCanvasBase *canvas)
0036     : KoToolBase(*(new KoCreatePathToolPrivate(this, canvas)))
0037 {
0038 }
0039 
0040 KoCreatePathTool::~KoCreatePathTool()
0041 {
0042 }
0043 
0044 QRectF KoCreatePathTool::decorationsRect() const
0045 {
0046     Q_D(const KoCreatePathTool);
0047 
0048     QRectF dirtyRect;
0049 
0050     if (pathStarted()) {
0051         dirtyRect |= kisGrowRect(d->shape->boundingRect(), handleDocRadius());
0052     }
0053 
0054     if (d->hoveredPoint) {
0055         dirtyRect |= kisGrowRect(d->hoveredPoint->boundingRect(false), handleDocRadius());
0056     }
0057 
0058     if (d->activePoint) {
0059         dirtyRect |= kisGrowRect(d->activePoint->boundingRect(false), handleDocRadius());
0060 
0061         if (d->pointIsDragged) {
0062             // the path is not closed, therefore the point is not marked as
0063             // active inside the path itself
0064             dirtyRect |= handlePaintRect(
0065                         d->activePoint->parent()->shapeToDocument(
0066                             d->activePoint->controlPoint2()));
0067         }
0068 
0069     }
0070 
0071     if (canvas()->snapGuide()->isSnapping()) {
0072         dirtyRect |= canvas()->snapGuide()->boundingRect();
0073     }
0074 
0075     return dirtyRect;
0076 }
0077 
0078 void KoCreatePathTool::paint(QPainter &painter, const KoViewConverter &converter)
0079 {
0080     Q_D(KoCreatePathTool);
0081 
0082     if (pathStarted()) {
0083 
0084         painter.save();
0085         paintPath(*(d->shape), painter, converter);
0086         painter.restore();
0087 
0088         KisHandlePainterHelper helper =
0089             KoShape::createHandlePainterHelperView(&painter, d->shape, converter, d->handleRadius, d->decorationThickness);
0090 
0091         const bool firstPointActive = d->firstPoint == d->activePoint;
0092 
0093         if (d->pointIsDragged || firstPointActive) {
0094             const bool onlyPaintActivePoints = false;
0095             KoPathPoint::PointTypes paintFlags = KoPathPoint::ControlPoint2;
0096 
0097             if (d->activePoint->activeControlPoint1()) {
0098                 paintFlags |= KoPathPoint::ControlPoint1;
0099             }
0100 
0101             helper.setHandleStyle(KisHandleStyle::highlightedPrimaryHandles());
0102             d->activePoint->paint(helper, paintFlags, onlyPaintActivePoints);
0103         }
0104 
0105         if (!firstPointActive) {
0106             helper.setHandleStyle(d->mouseOverFirstPoint ?
0107                                       KisHandleStyle::highlightedPrimaryHandles() :
0108                                       KisHandleStyle::primarySelection());
0109             d->firstPoint->paint(helper, KoPathPoint::Node);
0110         }
0111     }
0112 
0113     if (d->hoveredPoint) {
0114         KisHandlePainterHelper helper = KoShape::createHandlePainterHelperView(&painter, d->hoveredPoint->parent(), converter, d->handleRadius, d->decorationThickness);
0115         helper.setHandleStyle(KisHandleStyle::highlightedPrimaryHandles());
0116         d->hoveredPoint->paint(helper, KoPathPoint::Node);
0117     }
0118 
0119     painter.save();
0120     painter.setTransform(converter.documentToView(), true);
0121     canvas()->snapGuide()->paint(painter, converter);
0122     painter.restore();
0123 }
0124 
0125 void KoCreatePathTool::paintPath(KoPathShape& pathShape, QPainter &painter, const KoViewConverter &converter)
0126 {
0127     Q_D(KoCreatePathTool);
0128     painter.setTransform(pathShape.absoluteTransformation() *
0129                          converter.documentToView() *
0130                          painter.transform());
0131     painter.save();
0132 
0133     pathShape.paint(painter);
0134     painter.restore();
0135 
0136     if (pathShape.stroke()) {
0137         painter.save();
0138         pathShape.stroke()->paint(d->shape, painter);
0139         painter.restore();
0140     }
0141 }
0142 
0143 void KoCreatePathTool::mousePressEvent(KoPointerEvent *event)
0144 {
0145     Q_D(KoCreatePathTool);
0146 
0147     //Right click removes last point
0148     if (event->button() == Qt::RightButton) {
0149         removeLastPoint();
0150         return;
0151     }
0152 
0153     const bool isOverFirstPoint = d->shape &&
0154             handleGrabRect(d->firstPoint->point()).contains(event->point);
0155 
0156     const bool haveCloseModifier = d->enableClosePathShortcut
0157             && d->shape
0158             && d->shape->pointCount() > 2
0159             && (event->modifiers() & Qt::ShiftModifier);
0160 
0161     if ((event->button() == Qt::LeftButton) && haveCloseModifier && !isOverFirstPoint) {
0162         endPathWithoutLastPoint();
0163         return;
0164     }
0165 
0166     d->finishAfterThisPoint = false;
0167 
0168     if (d->shape && pathStarted()) {
0169         if (isOverFirstPoint) {
0170             d->activePoint->setPoint(d->firstPoint->point());
0171             canvas()->updateCanvas(d->shape->boundingRect());
0172             canvas()->updateCanvas(canvas()->snapGuide()->boundingRect());
0173 
0174             if (haveCloseModifier) {
0175                 d->shape->closeMerge();
0176                 // we are closing the path, so reset the existing start path point
0177                 d->existingStartPoint = 0;
0178                 // finish path
0179                 endPath();
0180             } else {
0181                 // the path shape will get closed when the user releases
0182                 // the mouse button
0183                 d->finishAfterThisPoint = true;
0184                 repaintDecorations();
0185             }
0186         } else {
0187             QPointF point = canvas()->snapGuide()->snap(event->point, event->modifiers());
0188 
0189             // check whether we hit an start/end node of an existing path
0190             d->existingEndPoint = d->endPointAtPosition(point);
0191             if (d->existingEndPoint.isValid() && d->existingEndPoint != d->existingStartPoint) {
0192                 point = d->existingEndPoint.path->shapeToDocument(d->existingEndPoint.point->point());
0193                 d->activePoint->setPoint(point);
0194                 // finish path
0195                 endPath();
0196             } else {
0197                 d->activePoint->setPoint(point);
0198                 repaintDecorations();
0199             }
0200         }
0201     } else {
0202         beginShape();
0203 
0204         KoPathShape *pathShape = new KoPathShape();
0205         d->shape = pathShape;
0206         pathShape->setShapeId(KoPathShapeId);
0207 
0208         KoShapeStrokeSP stroke(new KoShapeStroke());
0209         const qreal size = canvas()->resourceManager()->resource(KoCanvasResource::Size).toReal();
0210 
0211         stroke->setLineWidth(canvas()->unit().fromUserValue(size));
0212         stroke->setColor(canvas()->resourceManager()->foregroundColor().toQColor());
0213 
0214         pathShape->setStroke(stroke);
0215         QPointF point = canvas()->snapGuide()->snap(event->point, event->modifiers());
0216 
0217         // check whether we hit an start/end node of an existing path
0218         d->existingStartPoint = d->endPointAtPosition(point);
0219 
0220         if (d->existingStartPoint.isValid()) {
0221             point = d->existingStartPoint.path->shapeToDocument(d->existingStartPoint.point->point());
0222         }
0223 
0224         d->activePoint = pathShape->moveTo(point);
0225         d->firstPoint = d->activePoint;
0226 
0227         canvas()->snapGuide()->setAdditionalEditedShape(pathShape);
0228 
0229         d->angleSnapStrategy = new AngleSnapStrategy(d->angleSnappingDelta, d->angleSnapStatus);
0230         canvas()->snapGuide()->addCustomSnapStrategy(d->angleSnapStrategy);
0231 
0232         repaintDecorations();
0233     }
0234 
0235     d->dragStartPoint = event->point;
0236 
0237     if (d->angleSnapStrategy)
0238         d->angleSnapStrategy->setStartPoint(d->activePoint->point());
0239 }
0240 
0241 bool KoCreatePathTool::pathStarted() const
0242 {
0243     Q_D(const KoCreatePathTool);
0244     return ((bool) d->shape);
0245 }
0246 
0247 bool KoCreatePathTool::tryMergeInPathShape(KoPathShape *pathShape)
0248 {
0249     return addPathShapeImpl(pathShape, true);
0250 }
0251 
0252 void KoCreatePathTool::setEnableClosePathShortcut(bool value)
0253 {
0254     Q_D(KoCreatePathTool);
0255     d->enableClosePathShortcut = value;
0256 }
0257 
0258 void KoCreatePathTool::mouseDoubleClickEvent(KoPointerEvent *event)
0259 {
0260     //remove handle
0261     canvas()->updateCanvas(handlePaintRect(event->point));
0262 
0263     endPathWithoutLastPoint();
0264 }
0265 
0266 void KoCreatePathTool::mouseMoveEvent(KoPointerEvent *event)
0267 {
0268     Q_D(KoCreatePathTool);
0269 
0270     d->hoveredPoint = d->endPointAtPosition(event->point);
0271 
0272     if (!pathStarted()) {
0273         canvas()->snapGuide()->snap(event->point, event->modifiers());
0274         repaintDecorations();
0275 
0276         d->mouseOverFirstPoint = false;
0277         return;
0278     }
0279 
0280     d->mouseOverFirstPoint = handleGrabRect(d->firstPoint->point()).contains(event->point);
0281 
0282     QPointF snappedPosition = canvas()->snapGuide()->snap(event->point, event->modifiers());
0283 
0284     if (event->buttons() & Qt::LeftButton) {
0285         if (d->pointIsDragged ||
0286             !handleGrabRect(d->dragStartPoint).contains(event->point)) {
0287 
0288             d->pointIsDragged = true;
0289             QPointF offset = snappedPosition - d->activePoint->point();
0290             d->activePoint->setControlPoint2(d->activePoint->point() + offset);
0291             // pressing <alt> stops controls points moving symmetrically
0292             if ((event->modifiers() & Qt::AltModifier) == 0) {
0293                 d->activePoint->setControlPoint1(d->activePoint->point() - offset);
0294             }
0295         }
0296     } else {
0297         d->activePoint->setPoint(snappedPosition);
0298 
0299         if (!d->prevPointWasDragged && d->autoSmoothCurves) {
0300             KoPathPointIndex index = d->shape->pathPointIndex(d->activePoint);
0301             if (index.second > 0) {
0302 
0303                 KoPathPointIndex prevIndex(index.first, index.second - 1);
0304                 KoPathPoint *prevPoint = d->shape->pointByIndex(prevIndex);
0305 
0306                 if (prevPoint) {
0307                     KoPathPoint *prevPrevPoint = 0;
0308 
0309                     if (index.second > 1) {
0310                         KoPathPointIndex prevPrevIndex(index.first, index.second - 2);
0311                         prevPrevPoint = d->shape->pointByIndex(prevPrevIndex);
0312                     }
0313 
0314                     if (prevPrevPoint) {
0315                         const QPointF control1 = prevPoint->point() + 0.3 * (prevPrevPoint->point() - prevPoint->point());
0316                         prevPoint->setControlPoint1(control1);
0317                     }
0318 
0319                     const QPointF control2 = prevPoint->point() + 0.3 * (d->activePoint->point() - prevPoint->point());
0320                     prevPoint->setControlPoint2(control2);
0321 
0322                     const QPointF activeControl = d->activePoint->point() + 0.3 * (prevPoint->point() - d->activePoint->point());
0323                     d->activePoint->setControlPoint1(activeControl);
0324 
0325                     KoPathPointTypeCommand::makeCubicPointSmooth(prevPoint);
0326                 }
0327             }
0328         }
0329 
0330     }
0331 
0332     repaintDecorations();
0333 }
0334 
0335 void KoCreatePathTool::mouseReleaseEvent(KoPointerEvent *event)
0336 {
0337     Q_D(KoCreatePathTool);
0338 
0339     if (! d->shape || (event->buttons() & Qt::RightButton)) return;
0340 
0341     d->prevPointWasDragged  = d->pointIsDragged;
0342     d->pointIsDragged = false;
0343     KoPathPoint *lastActivePoint = d->activePoint;
0344 
0345     if (!d->finishAfterThisPoint) {
0346         d->activePoint = d->shape->lineTo(event->point);
0347         canvas()->snapGuide()->setIgnoredPathPoints((QList<KoPathPoint*>() << d->activePoint));
0348     }
0349 
0350     // apply symmetric point property if applicable
0351     if (lastActivePoint->activeControlPoint1() && lastActivePoint->activeControlPoint2()) {
0352         QPointF diff1 = lastActivePoint->point() - lastActivePoint->controlPoint1();
0353         QPointF diff2 = lastActivePoint->controlPoint2() - lastActivePoint->point();
0354         if (qFuzzyCompare(diff1.x(), diff2.x()) && qFuzzyCompare(diff1.y(), diff2.y()))
0355             lastActivePoint->setProperty(KoPathPoint::IsSymmetric);
0356     }
0357 
0358     if (d->finishAfterThisPoint) {
0359 
0360         d->firstPoint->setControlPoint1(d->activePoint->controlPoint1());
0361         delete d->shape->removePoint(d->shape->pathPointIndex(d->activePoint));
0362         d->activePoint = d->firstPoint;
0363 
0364         if (!d->prevPointWasDragged && d->autoSmoothCurves) {
0365             KoPathPointTypeCommand::makeCubicPointSmooth(d->activePoint);
0366         }
0367 
0368         d->shape->closeMerge();
0369 
0370         // we are closing the path, so reset the existing start path point
0371         d->existingStartPoint = 0;
0372         // finish path
0373         endPath();
0374     }
0375 
0376     if (d->angleSnapStrategy && lastActivePoint->activeControlPoint2()) {
0377         d->angleSnapStrategy->deactivate();
0378     }
0379 
0380     repaintDecorations();
0381 }
0382 
0383 void KoCreatePathTool::endPath()
0384 {
0385     Q_D(KoCreatePathTool);
0386 
0387     if (!d->shape) {
0388         return;
0389     }
0390     d->addPathShape();
0391     repaintDecorations();
0392     endShape();
0393 }
0394 
0395 void KoCreatePathTool::endPathWithoutLastPoint()
0396 {
0397     Q_D(KoCreatePathTool);
0398 
0399     if (!d->shape) {
0400         return;
0401     }
0402     delete d->shape->removePoint(d->shape->pathPointIndex(d->activePoint));
0403     d->addPathShape();
0404 
0405     repaintDecorations();
0406     endShape();
0407 }
0408 
0409 void KoCreatePathTool::cancelPath()
0410 {
0411     Q_D(KoCreatePathTool);
0412 
0413     if (!d->shape) {
0414         return;
0415     }
0416     d->firstPoint = 0;
0417     d->activePoint = 0;
0418     d->cleanUp();
0419     repaintDecorations();
0420     endShape();
0421 }
0422 
0423 void KoCreatePathTool::removeLastPoint()
0424 {
0425     Q_D(KoCreatePathTool);
0426 
0427     if ((d->shape)) {
0428         KoPathPointIndex lastPointIndex = d->shape->pathPointIndex(d->activePoint);
0429 
0430         if (lastPointIndex.second > 1) {
0431             lastPointIndex.second--;
0432             delete d->shape->removePoint(lastPointIndex);
0433 
0434             d->hoveredPoint = 0;
0435 
0436             repaintDecorations();
0437         }
0438     }
0439 }
0440 
0441 void KoCreatePathTool::activate(const QSet<KoShape*> &shapes)
0442 {
0443     KoToolBase::activate(shapes);
0444 
0445     Q_D(KoCreatePathTool);
0446     useCursor(Qt::ArrowCursor);
0447 
0448     // retrieve the actual global handle radius
0449     d->handleRadius = handleRadius();
0450     d->decorationThickness = decorationThickness();
0451     d->loadAutoSmoothValueFromConfig();
0452 
0453     // reset snap guide
0454     canvas()->snapGuide()->reset();
0455     repaintDecorations();
0456 }
0457 
0458 void KoCreatePathTool::deactivate()
0459 {
0460     cancelPath();
0461     KoToolBase::deactivate();
0462 }
0463 
0464 void KoCreatePathTool::canvasResourceChanged(int key, const QVariant & res)
0465 {
0466     Q_D(KoCreatePathTool);
0467 
0468     switch (key) {
0469     case KoCanvasResource::HandleRadius: {
0470         d->handleRadius = res.toUInt();
0471     }
0472     break;
0473     case KoCanvasResource::DecorationThickness: {
0474         d->decorationThickness = res.toUInt();
0475     }
0476     break;
0477     default:
0478         return;
0479     }
0480 }
0481 
0482 bool KoCreatePathTool::addPathShapeImpl(KoPathShape *pathShape, bool tryMergeOnly)
0483 {
0484     Q_D(KoCreatePathTool);
0485 
0486     KoPathShape *startShape = 0;
0487     KoPathShape *endShape = 0;
0488     pathShape->normalize();
0489 
0490     // check if existing start/end points are still valid
0491     d->existingStartPoint.validate(canvas());
0492     d->existingEndPoint.validate(canvas());
0493 
0494     if (d->connectPaths(pathShape, d->existingStartPoint, d->existingEndPoint)) {
0495         if (d->existingStartPoint.isValid()) {
0496             startShape = d->existingStartPoint.path;
0497         }
0498         if (d->existingEndPoint.isValid() && d->existingEndPoint != d->existingStartPoint) {
0499             endShape = d->existingEndPoint.path;
0500         }
0501     }
0502 
0503     if (tryMergeOnly && !startShape && !endShape) {
0504         return false;
0505     }
0506 
0507     KUndo2Command *cmd = canvas()->shapeController()->addShape(pathShape, 0);
0508     KIS_SAFE_ASSERT_RECOVER(cmd) {
0509         canvas()->updateCanvas(pathShape->boundingRect());
0510         delete pathShape;
0511         return true;
0512     }
0513 
0514     KoSelection *selection = canvas()->shapeManager()->selection();
0515     selection->deselectAll();
0516     selection->select(pathShape);
0517 
0518     if (startShape) {
0519         pathShape->setBackground(startShape->background());
0520         pathShape->setStroke(startShape->stroke());
0521     } else if (endShape) {
0522         pathShape->setBackground(endShape->background());
0523         pathShape->setStroke(endShape->stroke());
0524     }
0525 
0526 
0527     if (startShape) {
0528         canvas()->shapeController()->removeShape(startShape, cmd);
0529     }
0530     if (endShape && startShape != endShape) {
0531         canvas()->shapeController()->removeShape(endShape, cmd);
0532     }
0533     canvas()->addCommand(cmd);
0534 
0535     return true;
0536 }
0537 
0538 
0539 void KoCreatePathTool::addPathShape(KoPathShape *pathShape)
0540 {
0541     addPathShapeImpl(pathShape, false);
0542 }
0543 
0544 QList<QPointer<QWidget> > KoCreatePathTool::createOptionWidgets()
0545 {
0546     Q_D(KoCreatePathTool);
0547 
0548     QList<QPointer<QWidget> > list;
0549 
0550     QWidget *widget = new QWidget();
0551     widget->setObjectName("bezier-curve-tool-widget");
0552     widget->setWindowTitle(i18n("Path options"));
0553 
0554     QCheckBox *smoothCurves = new QCheckBox(i18n("Autosmooth curve"), widget);
0555     smoothCurves->setObjectName("smooth-curves-widget");
0556     smoothCurves->setChecked(d->autoSmoothCurves);
0557 
0558     QCheckBox *angleSnap = new QCheckBox(i18n("Activate angle snap"), widget);
0559     angleSnap->setObjectName("angle-snap-widget");
0560     angleSnap->setChecked(false);
0561     angleSnap->setCheckable(true);
0562 
0563     KisAngleSelector *angleEdit = new KisAngleSelector(widget);
0564     angleEdit->setObjectName("angle-edit-widget");
0565     angleEdit->setAngle(d->angleSnappingDelta);
0566     angleEdit->setRange(1, 360);
0567     angleEdit->setDecimals(0);
0568     angleEdit->setFlipOptionsMode(KisAngleSelector::FlipOptionsMode_MenuButton);
0569     angleEdit->setEnabled(angleSnap->isChecked());
0570 
0571     QHBoxLayout *angleEditLayout = new QHBoxLayout;
0572     angleEditLayout->setContentsMargins(10, 0, 0, 0);
0573     angleEditLayout->setSpacing(0);
0574     angleEditLayout->addWidget(angleEdit);
0575 
0576     QVBoxLayout *mainLayout = new QVBoxLayout;
0577     mainLayout->setContentsMargins(0, 0, 0, 0);
0578     mainLayout->setSpacing(5);
0579 
0580     mainLayout->addWidget(smoothCurves);
0581     mainLayout->addWidget(angleSnap);
0582     mainLayout->addLayout(angleEditLayout);
0583 
0584     widget->setLayout(mainLayout);
0585 
0586     list.append(widget);
0587 
0588     connect(smoothCurves,
0589             SIGNAL(toggled(bool)),
0590             this,
0591             SLOT(autoSmoothCurvesChanged(bool)));
0592     connect(this,
0593             SIGNAL(sigUpdateAutoSmoothCurvesGUI(bool)),
0594             smoothCurves,
0595             SLOT(setChecked(bool)));
0596     connect(angleEdit, SIGNAL(angleChanged(qreal)), this, SLOT(angleDeltaChanged(qreal)));
0597     connect(angleSnap, SIGNAL(stateChanged(int)), this, SLOT(angleSnapChanged(int)));
0598     connect(angleSnap,
0599             SIGNAL(toggled(bool)),
0600             angleEdit,
0601             SLOT(setEnabled(bool)));
0602 
0603     return list;
0604 }
0605 
0606 //have to include this because of Q_PRIVATE_SLOT
0607 #include <moc_KoCreatePathTool.cpp>