Krita Source Code Documentation
Loading...
Searching...
No Matches
kis_painting_assistants_decoration.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2009 Cyrille Berger <cberger@cberger.net>
3 * SPDX-FileCopyrightText: 2017 Scott Petrovic <scottpetrovic@gmail.com>
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
9
10#include <cstdint>
11#include <limits>
12
13#include <QList>
14#include <QPointF>
15#include <klocalizedstring.h>
16#include <kactioncollection.h>
17#include <ktoggleaction.h>
18#include <kis_algebra_2d.h>
19#include "kis_debug.h"
20#include "KisDocument.h"
21#include "kis_canvas2.h"
23#include "kis_icon_utils.h"
24#include "KisViewManager.h"
26#include "kis_tool_proxy.h"
27
28#include <QPainter>
29#include <QPainterPath>
30#include <QApplication>
31
55
56
57
59 KisCanvasDecoration("paintingAssistantsDecoration", parent),
60 d(new Private)
61{
64 setPriority(95);
65 d->snapOnlyOneAssistant = true; //turn on by default.
66 d->snapEraser = false;
67
68 slotConfigChanged(); // load the initial config
69}
70
75
77{
78 const bool shouldBeVisible = !assistants().isEmpty();
79
80 if (visible() != shouldBeVisible) {
81 setVisible(shouldBeVisible);
82 }
83}
84
94
96{
97 QList<KisPaintingAssistantSP> assistants = view()->document()->assistants();
98 if (assistants.contains(assistant)) return;
99
100 assistants.append(assistant);
101 assistant->setAssistantGlobalColorCache(view()->document()->assistantsGlobalColor());
102
103 view()->document()->setAssistants(assistants);
104 setVisible(!assistants.isEmpty());
105 Q_EMIT assistantChanged();
106}
107
108// bring assistant to front of assistants list, effectively causing assistant to be rendered above the others
110{
111 QList<KisPaintingAssistantSP> assistants = view()->document()->assistants();
112 assistants.removeOne(assistant);
113
114 if (!assistant) return;
115 assistants.append(assistant);
116
117 view()->document()->setAssistants(assistants);
118 setVisible(!assistants.isEmpty());
119 Q_EMIT assistantChanged();
120
121}
122
124{
125 QList<KisPaintingAssistantSP> assistants = view()->document()->assistants();
126 KIS_ASSERT_RECOVER_NOOP(assistants.contains(assistant));
127
128 if (assistants.removeAll(assistant)) {
129 view()->document()->setAssistants(assistants);
130 setVisible(!assistants.isEmpty());
131 Q_EMIT assistantChanged();
132 }
133}
134
136{
137 QList<KisPaintingAssistantSP> assistants = view()->document()->assistants();
138 assistants.clear();
139 view()->document()->setAssistants(assistants);
140 setVisible(!assistants.isEmpty());
141
142 Q_EMIT assistantChanged();
143}
144
146{
147 Q_FOREACH (KisPaintingAssistantSP assistant, assistants) {
148 assistant->setAssistantGlobalColorCache(view()->document()->assistantsGlobalColor());
149 }
150 view()->document()->setAssistants(assistants);
151 setVisible(!assistants.isEmpty());
152
153 Q_EMIT assistantChanged();
154}
155
157{
158 if (!assistants().empty()) {
159 Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
160 assistant->setAdjustedBrushPosition(position);
161 }
162 }
163}
164
165
166QPointF KisPaintingAssistantsDecoration::adjustPosition(const QPointF& point, const QPointF& strokeBegin)
167{
168
169 if (assistants().empty()) {
170 // No assistants, so no adjustment
171 return point;
172 }
173
174 if (!d->snapEraser
176 // No snapping if eraser snapping is disabled and brush is an eraser
177 return point;
178 }
179
180 KisImageSP image = d->m_canvas->image();
182
184 const qreal moveThresholdPt = 4.0 / (converter->effectiveZoom() * qMax(image->xRes(), image->yRes()));
185
186 QPointF best = point;
187 qreal minSquareDistance = std::numeric_limits<qreal>::max();
188 qreal secondSquareDistance = std::numeric_limits<qreal>::max();
189
191 // In these cases the best assistant needs to be determined.
192 int numSuitableAssistants = 0;
193 KisPaintingAssistantSP bestAssistant;
194
195 Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
196 if (assistant->isSnappingActive() == true){ // the toggle button with eye icon to disable assistants
197 QPointF newpoint = assistant->adjustPosition(point, strokeBegin, true, moveThresholdPt);
198 // Assistants that can't or don't want to snap return NaN values (aside from possible numeric issues)
199 // NOTE: It would be safer to also reject points too far outside canvas (or widget?) area,
200 // because for example squashed concentric ellipses can currently shoot points far off.
201 if (qIsNaN(newpoint.x()) || qIsNaN(newpoint.y())) {
202 continue;
203 }
204 ++numSuitableAssistants;
205 qreal dist = kisSquareDistance(newpoint, point);
206 if (dist < minSquareDistance) {
207 best = newpoint;
208 secondSquareDistance = minSquareDistance;
209 minSquareDistance = dist;
210 bestAssistant = assistant;
211 } else if (dist < secondSquareDistance) {
212 secondSquareDistance = dist;
213 }
214 assistant->setFollowBrushPosition(true);
215 }
216 }
217
218 // When there are multiple choices within moveThresholdPt, delay the decision until movement leaves
219 // threshold to determine which curve follows cursor movement the closest. Assistants with multiple
220 // snapping curves also need this movement to decide the best choice.
221 // NOTE: It is currently not possible to tell painting tools that a decision is pending, or that
222 // the active snapping curve changed, so certain artifact lines from snapping changes are unavoidable.
223
224 if (numSuitableAssistants > 1 && KisAlgebra2D::norm(point - strokeBegin) <= moveThresholdPt
225 && (sqrt(secondSquareDistance) < moveThresholdPt)) {
226 return strokeBegin;
227 } else if (numSuitableAssistants > 0 && d->snapOnlyOneAssistant) {
228 // if only snapping to one assistant, register it
229 d->firstAssistant = bestAssistant;
230 }
231 } else {
232 // Make sure BUG:448187 doesn't crop up again
233 KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(d->firstAssistant->isSnappingActive(), point);
234 // there already is an assistant locked in, check if it can be used
235 QPointF newpoint = d->firstAssistant->adjustPosition(point, strokeBegin, false, moveThresholdPt);
236 // BUGFIX: 402535
237 // assistants might return (NaN,NaN), must always check for that
238 if (!(qIsNaN(newpoint.x()) || qIsNaN(newpoint.y()))) {
239 best = newpoint;
240 }
241 }
242
243 return best;
244}
245
246void KisPaintingAssistantsDecoration::adjustLine(QPointF &point, QPointF &strokeBegin)
247{
248 if (assistants().empty()) {
249 // No assistants, so no adjustment
250 return;
251 }
252
253 // TODO: figure it out
254 if (!d->snapEraser
256 // No snapping if eraser snapping is disabled and brush is an eraser
257 return;
258 }
259
260 QPointF originalPoint = point;
261 QPointF originalStrokeBegin = strokeBegin;
262
263 qreal minDistance = 10000.0;
264 bool minDistValid = false;
265 QPointF finalPoint = originalPoint;
266 QPointF finalStrokeBegin = originalStrokeBegin;
267 int id = 0;
268 KisPaintingAssistantSP bestAssistant;
269 Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
270 if(assistant->isSnappingActive() == true){//this checks if the assistant in question has it's snapping boolean turned on//
271 //QPointF pt = assistant->adjustPosition(point, strokeBegin, true);
272 QPointF p1 = originalPoint;
273 QPointF p2 = originalStrokeBegin;
274 assistant->adjustLine(p1, p2);
275 if (p1.isNull() || p2.isNull()) {
276 // possibly lines cannot snap to this assistant, or this line cannot, at least
277 continue;
278 }
279 qreal distance = kisSquareDistance(p1, originalPoint) + kisSquareDistance(p2, originalStrokeBegin);
280 if (distance < minDistance || !minDistValid) {
281 finalPoint = p1;
282 finalStrokeBegin = p2;
283 minDistValid = true;
284 bestAssistant = assistant;
285 }
286 }
287 id ++;
288 }
289 if (bestAssistant) {
290 bestAssistant->setFollowBrushPosition(true);
291 }
292 point = finalPoint;
293 strokeBegin = finalStrokeBegin;
294}
295
297{
298 d->firstAssistant.clear();
299
300 Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
301 assistant->endStroke();
302 }
303}
304
305void KisPaintingAssistantsDecoration::drawDecoration(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter *converter, KisCanvas2* canvas)
306{
307 if(assistants().isEmpty()) {
308 return; // no assistants to worry about, ok to exit
309 }
310
311 if (!canvas) {
312 dbgFile<<"canvas does not exist in painting assistant decoration, you may have passed arguments incorrectly:"<<canvas;
313 } else {
314 d->m_canvas = canvas;
315 }
316
317 // the preview functionality for assistants. do not show while editing
318
319 KoToolProxy *proxy = view()->canvasBase()->toolProxy();
321 KisToolProxy *kritaProxy = dynamic_cast<KisToolProxy*>(proxy);
323
324 const bool outlineVisible =
327 kritaProxy->supportsPaintingAssistants();
328
329 Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
330 assistant->drawAssistant(gc, updateRect, converter, d->useCache, canvas, assistantVisibility(), outlineVisible);
331
332 if (isEditingAssistants()) {
333 drawHandles(assistant, gc, converter);
334 }
335 }
336
337 // draw editor controls on top of all assistant lines (why this code is last)
338 if (isEditingAssistants()) {
339 Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
340 drawEditorWidget(assistant, gc, converter);
341 }
342 }
343}
344
346{
347 QTransform initialTransform = converter->documentToWidgetTransform();
348
349 QColor colorToPaint = assistant->effectiveAssistantColor();
350
351 Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->handles()) {
352
353
354 QPointF transformedHandle = initialTransform.map(*handle);
355 QRectF ellipse(transformedHandle - QPointF(handleSize() * 0.5, handleSize() * 0.5), QSizeF(handleSize(), handleSize()));
356
357 QPainterPath path;
358 path.addEllipse(ellipse);
359
360 gc.save();
361 gc.setPen(Qt::NoPen);
362 gc.setBrush(colorToPaint);
363 gc.drawPath(path);
364 gc.restore();
365 }
366
367 // some assistants have side handles like the vanishing point assistant
368 Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->sideHandles()) {
369 QPointF transformedHandle = initialTransform.map(*handle);
370 QRectF ellipse(transformedHandle - QPointF(handleSize() * 0.5, handleSize() * 0.5), QSizeF(handleSize(), handleSize()));
371
372 QPainterPath path;
373 path.addEllipse(ellipse);
374
375 gc.save();
376 gc.setPen(Qt::NoPen);
377 gc.setBrush(colorToPaint);
378 gc.drawPath(path);
379 gc.restore();
380 }
381}
382
387
392
394{
396 Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
397 Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->handles()) {
398 if (!hs.contains(handle)) {
399 hs.push_back(handle);
400 }
401 }
402 Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->sideHandles()) {
403 if (!hs.contains(handle)) {
404 hs.push_back(handle);
405 }
406 }
407 }
408 return hs;
409}
410
412{
414 if (view()) {
415 if (view()->document()) {
416 assistants = view()->document()->assistants();
417 }
418 }
419 return assistants;
420}
421
423{
424 return !assistants().isEmpty();
425}
426
431
437
442
443
448
453
458
460{
461 d->snapEraser = assistant;
462}
463
473{
474 Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
475 assistant->uncache();
476 }
477}
483
485{
486 // "outline" means assistant preview (line that depends on the mouse cursor)
488}
489
491{
492 return view()->document()->assistantsGlobalColor();
493}
494
496{
497 // view()->document() is referenced multiple times in this class
498 // it is used to later store things in the KRA file when saving.
499 view()->document()->setAssistantsGlobalColor(color);
500
501 Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
502 assistant->setAssistantGlobalColorCache(color);
503 }
504
505 uncache();
506}
507
509{
510 setVisible(true); // this turns on the decorations in general. we leave it on at this point
511 d->m_isEditingAssistants = true;
512 uncache(); // updates visuals when editing
513}
514
516{
517 if (!d->m_canvas) {
518 return;
519 }
520
521 d->m_isEditingAssistants = false; // some elements are hidden when we aren't editing
522 uncache(); // updates visuals when not editing
523}
524
529
530QPointF KisPaintingAssistantsDecoration::snapToGuide(KoPointerEvent *e, const QPointF &offset, bool useModifiers)
531{
532 if (!d->m_canvas || !d->m_canvas->currentImage()) {
533 return e->point;
534 }
535
536
537 KoSnapGuide *snapGuide = d->m_canvas->snapGuide();
538 QPointF pos = snapGuide->snap(e->point, offset, useModifiers ? e->modifiers() : Qt::NoModifier);
539
540 return pos;
541}
542
543QPointF KisPaintingAssistantsDecoration::snapToGuide(const QPointF& pt, const QPointF &offset)
544{
545 if (!d->m_canvas) {
546 return pt;
547 }
548
549
550 KoSnapGuide *snapGuide = d->m_canvas->snapGuide();
551 QPointF pos = snapGuide->snap(pt, offset, Qt::NoModifier);
552
553 return pos;
554}
555
556/*
557 * functions only used internally in this class
558 * we potentially could make some of these inline to speed up performance
559*/
560
562{
563 if (!assistant->isAssistantComplete() || !globalEditorWidgetData.widgetActivated) {
564 return;
565 }
566
567 QTransform initialTransform = converter->documentToWidgetTransform();
568 QPointF actionsPosition = initialTransform.map(assistant->viewportConstrainedEditorPosition(converter, globalEditorWidgetData.boundingSize));
569
570 //draw editor widget background
571 QBrush backgroundColor = d->m_canvas->viewManager()->mainWindowAsQWidget()->palette().window();
572 QPointF actionsBGRectangle(actionsPosition + QPointF(globalEditorWidgetData.widgetOffset,globalEditorWidgetData.widgetOffset));
573 QPen stroke(QColor(60, 60, 60, 80), 2);
574
575 gc.setRenderHint(QPainter::Antialiasing);
576
577
578 QPainterPath bgPath;
579 bgPath.addRoundedRect(QRectF(actionsBGRectangle.x(), actionsBGRectangle.y(), globalEditorWidgetData.boundingSize.width(), globalEditorWidgetData.boundingSize.height()), 6, 6);
580
581
582 // if the assistant is selected, make outline stroke fatter and use theme's highlight color
583 // for better visual feedback
584 if (selectedAssistant()) { // there might not be a selected assistant, so do not seg fault
585 if (assistant->getEditorPosition() == selectedAssistant()->getEditorPosition()) {
586 stroke.setWidth(6);
587 stroke.setColor(qApp->palette().color(QPalette::Highlight));
588
589 }
590 }
591
592 gc.setPen(stroke);
593 gc.drawPath(bgPath);
594 gc.fillPath(bgPath, backgroundColor);
595
596
597 //draw drag handle
598 QColor dragDecorationColor(150,150,150,255);
599
600 QPainterPath dragRect;
602 int height = actionsPosition.y()+globalEditorWidgetData.boundingSize.height()+globalEditorWidgetData.widgetOffset;
604
605 gc.fillPath(bgPath.intersected(dragRect),dragDecorationColor);
606
607 //draw dot decoration on handle
608 QPainterPath dragRectDots;
609 QColor dragDecorationDotsColor(50,50,50,255);
610 int dotSize = 2;
611 dragRectDots.addEllipse(3,2.5,dotSize,dotSize);
612 dragRectDots.addEllipse(3,7.5,dotSize,dotSize);
613 dragRectDots.addEllipse(3,-2.5,dotSize,dotSize);
614 dragRectDots.addEllipse(3,-7.5,dotSize,dotSize);
615 dragRectDots.addEllipse(-3,2.5,dotSize,dotSize);
616 dragRectDots.addEllipse(-3,7.5,dotSize,dotSize);
617 dragRectDots.addEllipse(-3,-2.5,dotSize,dotSize);
618 dragRectDots.addEllipse(-3,-7.5,dotSize,dotSize);
619 dragRectDots.translate((globalEditorWidgetData.dragDecorationWidth/2)+width,(globalEditorWidgetData.boundingSize.height()/2)+actionsPosition.y()+globalEditorWidgetData.widgetOffset);
620 gc.fillPath(dragRectDots,dragDecorationDotsColor);
621
622
623 //loop over all visible buttons and render them
625 QPointF iconMovePosition(actionsPosition + globalEditorWidgetData.moveIconPosition);
626 gc.drawPixmap(iconMovePosition, globalEditorWidgetData.m_iconMove);
627 }
629 QPointF iconSnapPosition(actionsPosition + globalEditorWidgetData.snapIconPosition);
630 if (assistant->isSnappingActive() == true) {
631 gc.drawPixmap(iconSnapPosition, globalEditorWidgetData.m_iconSnapOn);
632 }else {
633 gc.drawPixmap(iconSnapPosition, globalEditorWidgetData.m_iconSnapOff);
634 }
635 }
637 QPointF iconLockedPosition(actionsPosition + globalEditorWidgetData.lockedIconPosition);
638 if (assistant->isLocked()) {
639 gc.drawPixmap(iconLockedPosition, globalEditorWidgetData.m_iconLockOn);
640 } else {
641 qreal oldOpacity = gc.opacity();
642 gc.setOpacity(0.35);
643 gc.drawPixmap(iconLockedPosition, globalEditorWidgetData.m_iconLockOff);
644 gc.setOpacity(oldOpacity);
645 }
646 }
648 QPointF iconDuplicatePosition(actionsPosition + globalEditorWidgetData.duplicateIconPosition);
649 if(assistant->isDuplicating()) {
650 //draw button depressed
651 qreal oldOpacity = gc.opacity();
652 gc.setOpacity(0.35);
653 gc.drawPixmap(iconDuplicatePosition,globalEditorWidgetData.m_iconDuplicate);
654 gc.setOpacity(oldOpacity);
655 }else {
656 gc.drawPixmap(iconDuplicatePosition,globalEditorWidgetData.m_iconDuplicate);
657 }
658 }
660 QPointF iconDeletePosition(actionsPosition + globalEditorWidgetData.deleteIconPosition);
661 gc.drawPixmap(iconDeletePosition, globalEditorWidgetData.m_iconDelete);
662 }
663
664
665
666
667}
QPointF p2
QPointF p1
const QString COMPOSITE_ERASE
qreal distance(const QPointF &p1, const QPointF &p2)
KisImageWSP currentImage() const
KisCoordinatesConverter * coordinatesConverter
KisImageWSP image() const
KisViewManager * viewManager() const
QPointer< KisView > view() const
virtual void setVisible(bool v)
@ ASSISTANTS_DRAW_MODE_PIXMAP_CACHE
Definition kis_config.h:769
@ ASSISTANTS_DRAW_MODE_LARGE_PIXMAP_CACHE
Definition kis_config.h:770
AssistantsDrawMode assistantsDrawMode(bool defaultValue=false) const
double xRes() const
double yRes() const
QList< KisPaintingAssistantSP > assistants() const
void addAssistant(KisPaintingAssistantSP assistant)
QPointF snapToGuide(KoPointerEvent *e, const QPointF &offset, bool useModifiers)
void setAssistantVisible(bool set)
sets whether the main assistant is visible
void toggleOutlineVisible()
toggles whether there will be a preview of the assistant result when painting
void setSelectedAssistant(KisPaintingAssistantSP assistant)
void toggleAssistantVisible()
toggles whether the assistant is active or not
void setOnlyOneAssistantSnap(bool assistant)
sets whether we snap to only one assistant
KisPaintingAssistantsDecoration(QPointer< KisView > parent)
void removeAssistant(KisPaintingAssistantSP assistant)
void raiseAssistant(KisPaintingAssistantSP assistant)
void drawEditorWidget(KisPaintingAssistantSP assistant, QPainter &gc, const KisCoordinatesConverter *converter)
bool assistantVisibility()
returns assistant visibility
void setAssistants(const QList< KisPaintingAssistantSP > &assistants)
void drawDecoration(QPainter &gc, const QRectF &updateRect, const KisCoordinatesConverter *converter, KisCanvas2 *canvas) override
void setEraserSnap(bool assistant)
sets whether eraser brushes snap
void adjustLine(QPointF &point, QPointF &strokeBegin)
void setOutlineVisible(bool set)
sets whether the preview is visible
QList< KisPaintingAssistantHandleSP > handles()
QPointF adjustPosition(const QPointF &point, const QPointF &strokeBegin)
void drawHandles(KisPaintingAssistantSP assistant, QPainter &gc, const KisCoordinatesConverter *converter)
bool supportsPaintingAssistants() const
QWidget * mainWindowAsQWidget() const
KoSnapGuide * snapGuide
QPointer< KoCanvasResourceProvider > resourceManager
Qt::KeyboardModifiers modifiers() const
QPointF point
The point in document coordinates.
QPointF snap(const QPointF &mousePosition, Qt::KeyboardModifiers modifiers)
snaps the mouse position, returns if mouse was snapped
#define KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cond, val)
Definition kis_assert.h:129
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define KIS_ASSERT_RECOVER_NOOP(cond)
Definition kis_assert.h:97
#define dbgFile
Definition kis_debug.h:53
qreal kisSquareDistance(const QPointF &pt1, const QPointF &pt2)
Definition kis_global.h:194
qreal norm(const T &a)