Krita Source Code Documentation
Loading...
Searching...
No Matches
KoCreatePathTool.cpp
Go to the documentation of this file.
1/* This file is part of the KDE project
2 *
3 * SPDX-FileCopyrightText: 2006 Thorsten Zachmann <zachmann@kde.org>
4 * SPDX-FileCopyrightText: 2008-2010 Jan Hambrecht <jaham@gmx.net>
5 *
6 * SPDX-License-Identifier: LGPL-2.0-or-later
7 */
8
9#include "KoCreatePathTool.h"
10#include "KoCreatePathTool_p.h"
11
12#include <KoUnit.h>
13#include "KoPointerEvent.h"
14#include "KoPathShape.h"
15#include "KoSelection.h"
17#include "KoShapeStroke.h"
18#include "KoCanvasBase.h"
20#include <KoColor.h>
24#include <KisAngleSelector.h>
25
26#include <klocalizedstring.h>
27
28#include <QCheckBox>
29#include <QHBoxLayout>
30#include <QPainter>
31#include <QSpinBox>
32#include <QVBoxLayout>
33
38
42
44{
45 Q_D(const KoCreatePathTool);
46
47 QRectF dirtyRect;
48
49 if (pathStarted()) {
50 dirtyRect |= kisGrowRect(d->shape->boundingRect(), handleDocRadius());
51 }
52
53 if (d->hoveredPoint) {
54 dirtyRect |= kisGrowRect(d->hoveredPoint->boundingRect(false), handleDocRadius());
55 }
56
57 if (d->activePoint) {
58 dirtyRect |= kisGrowRect(d->activePoint->boundingRect(false), handleDocRadius());
59
60 if (d->pointIsDragged) {
61 // the path is not closed, therefore the point is not marked as
62 // active inside the path itself
63 dirtyRect |= handlePaintRect(
64 d->activePoint->parent()->shapeToDocument(
65 d->activePoint->controlPoint2()));
66 }
67
68 }
69
70 if (canvas()->snapGuide()->isSnapping()) {
71 dirtyRect |= canvas()->snapGuide()->boundingRect();
72 }
73
74 return dirtyRect;
75}
76
77void KoCreatePathTool::paint(QPainter &painter, const KoViewConverter &converter)
78{
80
81 if (pathStarted()) {
82
83 painter.save();
84 paintPath(*(d->shape), painter, converter);
85 painter.restore();
86
88 KoShape::createHandlePainterHelperView(&painter, d->shape, converter, d->handleRadius, d->decorationThickness);
89
90 const bool firstPointActive = d->firstPoint == d->activePoint;
91
92 if (d->pointIsDragged || firstPointActive) {
93 const bool onlyPaintActivePoints = false;
94 KoPathPoint::PointTypes paintFlags = KoPathPoint::ControlPoint2;
96 if (d->activePoint->activeControlPoint1()) {
98 }
99
101 d->activePoint->paint(helper, paintFlags, onlyPaintActivePoints);
102 }
104 if (!firstPointActive) {
105 helper.setHandleStyle(d->mouseOverFirstPoint ?
108 d->firstPoint->paint(helper, KoPathPoint::Node);
109 }
110 }
111
112 if (d->hoveredPoint) {
113 KisHandlePainterHelper helper = KoShape::createHandlePainterHelperView(&painter, d->hoveredPoint->parent(), converter, d->handleRadius, d->decorationThickness);
115 d->hoveredPoint->paint(helper, KoPathPoint::Node);
116 }
117
118 painter.save();
119 painter.setTransform(converter.documentToView(), true);
120 canvas()->snapGuide()->paint(painter, converter);
121 painter.restore();
122}
123
124void KoCreatePathTool::paintPath(KoPathShape& pathShape, QPainter &painter, const KoViewConverter &converter)
125{
126 Q_D(KoCreatePathTool);
127 painter.setTransform(pathShape.absoluteTransformation() *
128 converter.documentToView() *
129 painter.transform());
130 painter.save();
131
132 pathShape.paint(painter);
133 painter.restore();
134
135 if (pathShape.stroke()) {
136 painter.save();
137 pathShape.stroke()->paint(d->shape, painter);
138 painter.restore();
139 }
140}
141
143{
144 Q_D(KoCreatePathTool);
145
146 // When using touch drawing, we only ever receive move events after the
147 // finger has pressed down. We have to issue an artificial move here so that
148 // the tool's state is updated properly to handle the press.
149 if (event->isTouchEvent()) {
150 handleMouseMove(event, false);
151 }
152
153 //Right click removes last point
154 if (event->button() == Qt::RightButton) {
156 return;
157 }
158
159 const bool isOverFirstPoint = d->shape &&
160 handleGrabRect(d->firstPoint->point()).contains(event->point);
161
162 const bool haveCloseModifier = d->enableClosePathShortcut
163 && d->shape
164 && d->shape->pointCount() > 2
165 && (event->modifiers() & Qt::ShiftModifier);
166
167 if ((event->button() == Qt::LeftButton) && haveCloseModifier && !isOverFirstPoint) {
169 return;
170 }
171
172 d->finishAfterThisPoint = false;
173
174 if (d->shape && pathStarted()) {
175 if (isOverFirstPoint) {
176 d->activePoint->setPoint(d->firstPoint->point());
177 canvas()->updateCanvas(d->shape->boundingRect());
178 canvas()->updateCanvas(canvas()->snapGuide()->boundingRect());
179
180 if (haveCloseModifier) {
181 d->shape->closeMerge();
182 // we are closing the path, so reset the existing start path point
183 d->existingStartPoint = 0;
184 // finish path
185 endPath();
186 } else {
187 // the path shape will get closed when the user releases
188 // the mouse button
189 d->finishAfterThisPoint = true;
191 }
192 } else {
193 QPointF point = canvas()->snapGuide()->snap(event->point, event->modifiers());
194
195 // check whether we hit an start/end node of an existing path
196 d->existingEndPoint = d->endPointAtPosition(point);
197 if (d->existingEndPoint.isValid() && d->existingEndPoint != d->existingStartPoint) {
198 point = d->existingEndPoint.path->shapeToDocument(d->existingEndPoint.point->point());
199 d->activePoint->setPoint(point);
200 // finish path
201 endPath();
202 } else {
203 d->activePoint->setPoint(point);
205 }
206 }
207 } else {
208 beginShape();
209
210 KoPathShape *pathShape = new KoPathShape();
211 d->shape = pathShape;
212 pathShape->setShapeId(KoPathShapeId);
213
214 KoShapeStrokeSP stroke(new KoShapeStroke());
215 const qreal size = canvas()->resourceManager()->resource(KoCanvasResource::Size).toReal();
216
217 stroke->setLineWidth(canvas()->unit().fromUserValue(size));
218 stroke->setColor(canvas()->resourceManager()->foregroundColor().toQColor());
219
220 pathShape->setStroke(stroke);
221 QPointF point = canvas()->snapGuide()->snap(event->point, event->modifiers());
222
223 // check whether we hit an start/end node of an existing path
224 d->existingStartPoint = d->endPointAtPosition(point);
225
226 if (d->existingStartPoint.isValid()) {
227 point = d->existingStartPoint.path->shapeToDocument(d->existingStartPoint.point->point());
228 }
229
230 d->activePoint = pathShape->moveTo(point);
231 d->firstPoint = d->activePoint;
232
234
235 d->angleSnapStrategy = new AngleSnapStrategy(d->angleSnappingDelta, d->angleSnapStatus);
236 canvas()->snapGuide()->addCustomSnapStrategy(d->angleSnapStrategy);
237
239 }
240
241 d->dragStartPoint = event->point;
242
243 if (d->angleSnapStrategy)
244 d->angleSnapStrategy->setStartPoint(d->activePoint->point());
245}
246
248{
249 Q_D(const KoCreatePathTool);
250 return ((bool) d->shape);
251}
252
254{
255 return addPathShapeImpl(pathShape, true);
256}
257
259{
260 Q_D(KoCreatePathTool);
261 d->enableClosePathShortcut = value;
262}
263
271
273{
274 handleMouseMove(event, true);
275}
276
277void KoCreatePathTool::handleMouseMove(const KoPointerEvent *event, bool considerDrag)
278{
279 Q_D(KoCreatePathTool);
280
281 d->hoveredPoint = d->endPointAtPosition(event->point);
282
283 if (!pathStarted()) {
284 canvas()->snapGuide()->snap(event->point, event->modifiers());
286
287 d->mouseOverFirstPoint = false;
288 return;
289 }
290
291 d->mouseOverFirstPoint = handleGrabRect(d->firstPoint->point()).contains(event->point);
292
293 QPointF snappedPosition = canvas()->snapGuide()->snap(event->point, event->modifiers());
294
295 if (considerDrag && (event->buttons() & Qt::LeftButton)) {
296 if (d->pointIsDragged ||
297 !handleGrabRect(d->dragStartPoint).contains(event->point)) {
298
299 d->pointIsDragged = true;
300 QPointF offset = snappedPosition - d->activePoint->point();
301 d->activePoint->setControlPoint2(d->activePoint->point() + offset);
302 // pressing <alt> stops controls points moving symmetrically
303 if ((event->modifiers() & Qt::AltModifier) == 0) {
304 d->activePoint->setControlPoint1(d->activePoint->point() - offset);
305 }
306 }
307 } else {
308 d->activePoint->setPoint(snappedPosition);
309
310 if (!d->prevPointWasDragged && d->autoSmoothCurves) {
311 KoPathPointIndex index = d->shape->pathPointIndex(d->activePoint);
312 if (index.second > 0) {
313
314 KoPathPointIndex prevIndex(index.first, index.second - 1);
315 KoPathPoint *prevPoint = d->shape->pointByIndex(prevIndex);
316
317 if (prevPoint) {
318 KoPathPoint *prevPrevPoint = 0;
319
320 if (index.second > 1) {
321 KoPathPointIndex prevPrevIndex(index.first, index.second - 2);
322 prevPrevPoint = d->shape->pointByIndex(prevPrevIndex);
323 }
324
325 if (prevPrevPoint) {
326 const QPointF control1 = prevPoint->point() + 0.3 * (prevPrevPoint->point() - prevPoint->point());
327 prevPoint->setControlPoint1(control1);
328 }
329
330 const QPointF control2 = prevPoint->point() + 0.3 * (d->activePoint->point() - prevPoint->point());
331 prevPoint->setControlPoint2(control2);
332
333 const QPointF activeControl = d->activePoint->point() + 0.3 * (prevPoint->point() - d->activePoint->point());
334 d->activePoint->setControlPoint1(activeControl);
335
337 }
338 }
339 }
340
341 }
342
344}
345
347{
348 Q_D(KoCreatePathTool);
349
350 if (! d->shape || (event->buttons() & Qt::RightButton)) return;
351
352 d->prevPointWasDragged = d->pointIsDragged;
353 d->pointIsDragged = false;
354 KoPathPoint *lastActivePoint = d->activePoint;
355
356 if (!d->finishAfterThisPoint) {
357 d->activePoint = d->shape->lineTo(event->point);
358 canvas()->snapGuide()->setIgnoredPathPoints((QList<KoPathPoint*>() << d->activePoint));
359 }
360
361 // apply symmetric point property if applicable
362 if (lastActivePoint->activeControlPoint1() && lastActivePoint->activeControlPoint2()) {
363 QPointF diff1 = lastActivePoint->point() - lastActivePoint->controlPoint1();
364 QPointF diff2 = lastActivePoint->controlPoint2() - lastActivePoint->point();
365 if (qFuzzyCompare(diff1.x(), diff2.x()) && qFuzzyCompare(diff1.y(), diff2.y()))
366 lastActivePoint->setProperty(KoPathPoint::IsSymmetric);
367 }
368
369 if (d->finishAfterThisPoint) {
370
371 d->firstPoint->setControlPoint1(d->activePoint->controlPoint1());
372 delete d->shape->removePoint(d->shape->pathPointIndex(d->activePoint));
373 d->activePoint = d->firstPoint;
374
375 if (!d->prevPointWasDragged && d->autoSmoothCurves) {
377 }
378
379 d->shape->closeMerge();
380
381 // we are closing the path, so reset the existing start path point
382 d->existingStartPoint = 0;
383 // finish path
384 endPath();
385 }
386
387 if (d->angleSnapStrategy && lastActivePoint->activeControlPoint2()) {
388 d->angleSnapStrategy->deactivate();
389 }
390
392}
393
395{
396 Q_D(KoCreatePathTool);
397
398 if (!d->shape) {
399 return;
400 }
401 d->addPathShape();
403 endShape();
404}
405
407{
408 Q_D(KoCreatePathTool);
409
410 if (!d->shape) {
411 return;
412 }
413 delete d->shape->removePoint(d->shape->pathPointIndex(d->activePoint));
414 d->addPathShape();
415
417 endShape();
418}
419
421{
422 Q_D(KoCreatePathTool);
423
424 if (!d->shape) {
425 return;
426 }
427 d->firstPoint = 0;
428 d->activePoint = 0;
429 d->cleanUp();
431 endShape();
432}
433
435{
436 Q_D(KoCreatePathTool);
437
438 if ((d->shape)) {
439 KoPathPointIndex lastPointIndex = d->shape->pathPointIndex(d->activePoint);
440
441 if (lastPointIndex.second > 1) {
442 lastPointIndex.second--;
443 delete d->shape->removePoint(lastPointIndex);
444
445 d->hoveredPoint = 0;
446
448 }
449 }
450}
451
452void KoCreatePathTool::activate(const QSet<KoShape*> &shapes)
453{
454 KoToolBase::activate(shapes);
455
456 Q_D(KoCreatePathTool);
457 useCursor(Qt::ArrowCursor);
458
459 // retrieve the actual global handle radius
460 d->handleRadius = handleRadius();
461 d->decorationThickness = decorationThickness();
462 d->loadAutoSmoothValueFromConfig();
463
464 // reset snap guide
465 canvas()->snapGuide()->reset();
467}
468
474
475void KoCreatePathTool::canvasResourceChanged(int key, const QVariant & res)
476{
477 Q_D(KoCreatePathTool);
478
479 switch (key) {
481 d->handleRadius = res.toUInt();
482 }
483 break;
485 d->decorationThickness = res.toUInt();
486 }
487 break;
488 default:
489 return;
490 }
491}
492
493bool KoCreatePathTool::addPathShapeImpl(KoPathShape *pathShape, bool tryMergeOnly)
494{
495 Q_D(KoCreatePathTool);
496
497 KoPathShape *startShape = 0;
499 pathShape->normalize();
500
501 // check if existing start/end points are still valid
502 d->existingStartPoint.validate(canvas());
503 d->existingEndPoint.validate(canvas());
504
505 if (d->connectPaths(pathShape, d->existingStartPoint, d->existingEndPoint)) {
506 if (d->existingStartPoint.isValid()) {
507 startShape = d->existingStartPoint.path;
508 }
509 if (d->existingEndPoint.isValid() && d->existingEndPoint != d->existingStartPoint) {
510 endShape = d->existingEndPoint.path;
511 }
512 }
513
514 if (tryMergeOnly && !startShape && !endShape) {
515 return false;
516 }
517
518 KUndo2Command *cmd = canvas()->shapeController()->addShape(pathShape, 0);
520 canvas()->updateCanvas(pathShape->boundingRect());
521 delete pathShape;
522 return true;
523 }
524
526 selection->deselectAll();
527 selection->select(pathShape);
528
529 if (startShape) {
530 pathShape->setBackground(startShape->background());
531 pathShape->setStroke(startShape->stroke());
532 } else if (endShape) {
533 pathShape->setBackground(endShape->background());
534 pathShape->setStroke(endShape->stroke());
535 }
536
537
538 if (startShape) {
539 canvas()->shapeController()->removeShape(startShape, cmd);
540 }
541 if (endShape && startShape != endShape) {
542 canvas()->shapeController()->removeShape(endShape, cmd);
543 }
544 canvas()->addCommand(cmd);
545
546 return true;
547}
548
549
551{
552 addPathShapeImpl(pathShape, false);
553}
554
556{
557 Q_D(KoCreatePathTool);
558
560
561 QWidget *widget = new QWidget();
562 widget->setObjectName("bezier-curve-tool-widget");
563 widget->setWindowTitle(i18n("Path options"));
564
565 QCheckBox *smoothCurves = new QCheckBox(i18n("Autosmooth curve"), widget);
566 smoothCurves->setObjectName("smooth-curves-widget");
567 smoothCurves->setChecked(d->autoSmoothCurves);
568
569 QCheckBox *angleSnap = new QCheckBox(i18n("Activate angle snap"), widget);
570 angleSnap->setObjectName("angle-snap-widget");
571 angleSnap->setChecked(false);
572 angleSnap->setCheckable(true);
573
574 KisAngleSelector *angleEdit = new KisAngleSelector(widget);
575 angleEdit->setObjectName("angle-edit-widget");
576 angleEdit->setAngle(d->angleSnappingDelta);
577 angleEdit->setRange(1, 360);
578 angleEdit->setDecimals(0);
580 angleEdit->setEnabled(angleSnap->isChecked());
581
582 QHBoxLayout *angleEditLayout = new QHBoxLayout;
583 angleEditLayout->setContentsMargins(10, 0, 0, 0);
584 angleEditLayout->setSpacing(0);
585 angleEditLayout->addWidget(angleEdit);
586
587 QVBoxLayout *mainLayout = new QVBoxLayout;
588 mainLayout->setContentsMargins(0, 0, 0, 0);
589 mainLayout->setSpacing(5);
590
591 mainLayout->addWidget(smoothCurves);
592 mainLayout->addWidget(angleSnap);
593 mainLayout->addLayout(angleEditLayout);
594
595 widget->setLayout(mainLayout);
596
597 list.append(widget);
598
599 connect(smoothCurves,
600 SIGNAL(toggled(bool)),
601 this,
602 SLOT(autoSmoothCurvesChanged(bool)));
603 connect(this,
604 SIGNAL(sigUpdateAutoSmoothCurvesGUI(bool)),
605 smoothCurves,
606 SLOT(setChecked(bool)));
607 connect(angleEdit, SIGNAL(angleChanged(qreal)), this, SLOT(angleDeltaChanged(qreal)));
608 connect(angleSnap, SIGNAL(stateChanged(int)), this, SLOT(angleSnapChanged(int)));
609 connect(angleSnap,
610 SIGNAL(toggled(bool)),
611 angleEdit,
612 SLOT(setEnabled(bool)));
613
614 return list;
615}
616
617//have to include this because of Q_PRIVATE_SLOT
618#include <moc_KoCreatePathTool.cpp>
float value(const T *src, size_t ch)
#define KoPathShapeId
Definition KoPathShape.h:20
QPair< int, int > KoPathPointIndex
Definition KoPathShape.h:28
connect(this, SIGNAL(optionsChanged()), this, SLOT(saveOptions()))
A widget with several options to select an angle.
@ FlipOptionsMode_MenuButton
The flip options are shown as a menu accessible via a options button.
void setFlipOptionsMode(FlipOptionsMode newMode)
Sets the mode in which the flip options should be shown.
void setAngle(qreal newAngle)
Sets the current angle.
void setRange(qreal newMinimum, qreal newMaximum)
Sets the minimum and maximum values for the angle.
void setDecimals(int newNumberOfDecimals)
Sets the number of decimals (precision) used by the angle.
The KisHandlePainterHelper class is a special helper for painting handles around objects....
void setHandleStyle(const KisHandleStyle &style)
static KisHandleStyle & highlightedPrimaryHandles()
static KisHandleStyle & primarySelection()
KoSnapGuide * snapGuide
QPointer< KoShapeController > shapeController
virtual KoShapeManager * shapeManager() const =0
virtual void updateCanvas(const QRectF &rc)=0
virtual void addCommand(KUndo2Command *command)=0
QPointer< KoCanvasResourceProvider > resourceManager
void deactivate() override
reimplemented
void canvasResourceChanged(int key, const QVariant &res) override
reimplemented
KoCreatePathTool(KoCanvasBase *canvas)
void mousePressEvent(KoPointerEvent *event) override
reimplemented
void mouseDoubleClickEvent(KoPointerEvent *event) override
reimplemented
void mouseReleaseEvent(KoPointerEvent *event) override
reimplemented
void mouseMoveEvent(KoPointerEvent *event) override
reimplemented
virtual void paintPath(KoPathShape &pathShape, QPainter &painter, const KoViewConverter &converter)
void setEnableClosePathShortcut(bool value)
virtual void beginShape()
QList< QPointer< QWidget > > createOptionWidgets() override
reimplemented
bool tryMergeInPathShape(KoPathShape *pathShape)
void paint(QPainter &painter, const KoViewConverter &converter) override
reimplemented
void handleMouseMove(const KoPointerEvent *event, bool considerDrag)
QRectF decorationsRect() const override
virtual void addPathShape(KoPathShape *pathShape)
~KoCreatePathTool() override
bool addPathShapeImpl(KoPathShape *pathShape, bool tryMergeOnly)
void sigUpdateAutoSmoothCurvesGUI(bool value)
void angleSnapChanged(int)) Q_PRIVATE_SLOT(d_func()
virtual void endShape()
void activate(const QSet< KoShape * > &shapes) override
reimplemented
static void makeCubicPointSmooth(KoPathPoint *point)
A KoPathPoint represents a point in a path.
void setControlPoint1(const QPointF &point)
Set the control point 1.
QPointF point
void setProperty(PointProperty property)
Sets a single property of a point.
void setControlPoint2(const QPointF &point)
Set the control point 2.
QPointF controlPoint1
@ IsSymmetric
it is symmetric, like smooth but control points have same distance to point
Definition KoPathPoint.h:42
@ ControlPoint2
the second control point
Definition KoPathPoint.h:51
@ ControlPoint1
the first control point
Definition KoPathPoint.h:50
@ Node
the node point
Definition KoPathPoint.h:49
bool activeControlPoint1
bool activeControlPoint2
QPointF controlPoint2
The position of a path point within a path shape.
Definition KoPathShape.h:63
virtual QPointF normalize()
Normalizes the path data.
KoPathPoint * moveTo(const QPointF &p)
Starts a new Subpath.
void paint(QPainter &painter) const override
reimplemented
QRectF boundingRect() const override
reimplemented
Qt::MouseButton button() const
return button pressed (see QMouseEvent::button());
Qt::MouseButtons buttons() const
return buttons pressed (see QMouseEvent::buttons());
bool isTouchEvent() const
Qt::KeyboardModifiers modifiers() const
QPointF point
The point in document coordinates.
KoSelection * selection
virtual KoShapeStrokeModelSP stroke() const
Definition KoShape.cpp:1067
virtual void setStroke(KoShapeStrokeModelSP stroke)
Definition KoShape.cpp:1081
QTransform absoluteTransformation() const
Definition KoShape.cpp:382
virtual void setBackground(QSharedPointer< KoShapeBackground > background)
Definition KoShape.cpp:918
static KisHandlePainterHelper createHandlePainterHelperView(QPainter *painter, KoShape *shape, const KoViewConverter &converter, qreal handleRadius=0.0, int decorationThickness=1)
Definition KoShape.cpp:1177
virtual QSharedPointer< KoShapeBackground > background() const
Definition KoShape.cpp:926
void setShapeId(const QString &id)
Definition KoShape.cpp:1062
bool addCustomSnapStrategy(KoSnapStrategy *customStrategy)
void reset()
Resets the snap guide.
void setIgnoredPathPoints(const QList< KoPathPoint * > &ignoredPoints)
Sets a list of path points to ignore.
void paint(QPainter &painter, const KoViewConverter &converter)
paints the guide
QPointF snap(const QPointF &mousePosition, Qt::KeyboardModifiers modifiers)
snaps the mouse position, returns if mouse was snapped
void setAdditionalEditedShape(KoShape *shape)
Adds an additional shape to snap to (useful when creating a path)
QRectF boundingRect()
returns the bounding rect of the guide
qreal handleDocRadius() const
KoCanvasBase * canvas() const
Returns the canvas the tool is working on.
QRectF handlePaintRect(const QPointF &position) const
virtual KoToolSelection * selection()
virtual void repaintDecorations()
int handleRadius() const
Convenience function to get the current handle radius.
void useCursor(const QCursor &cursor)
virtual void activate(const QSet< KoShape * > &shapes)
QRectF handleGrabRect(const QPointF &position) const
virtual void deactivate()
int decorationThickness() const
decorationThickness The minimum thickness for tool decoration lines, this is derived from the screen ...
virtual QPointF documentToView(const QPointF &documentPoint) const
static bool qFuzzyCompare(half p1, half p2)
#define KIS_SAFE_ASSERT_RECOVER(cond)
Definition kis_assert.h:126
T kisGrowRect(const T &rect, U offset)
Definition kis_global.h:186
@ DecorationThickness
Integer, the thickness of single px decorations, will be adjusted by HiDPI settings....
@ HandleRadius
The handle radius used for drawing handles of any kind.