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>