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 KisPaintingAssistantSP bestAssistant;
268 Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
269 if(assistant->isSnappingActive() == true){//this checks if the assistant in question has it's snapping boolean turned on//
270 //QPointF pt = assistant->adjustPosition(point, strokeBegin, true);
271 QPointF p1 = originalPoint;
272 QPointF p2 = originalStrokeBegin;
273 assistant->adjustLine(p1, p2);
274 if (p1.isNull() || p2.isNull()) {
275 // possibly lines cannot snap to this assistant, or this line cannot, at least
276 continue;
277 }
278 qreal distance = kisSquareDistance(p1, originalPoint) + kisSquareDistance(p2, originalStrokeBegin);
279 if (distance < minDistance || !minDistValid) {
280 finalPoint = p1;
281 finalStrokeBegin = p2;
282 minDistValid = true;
283 bestAssistant = assistant;
284 }
285 }
286 }
287 if (bestAssistant) {
288 bestAssistant->setFollowBrushPosition(true);
289 }
290 point = finalPoint;
291 strokeBegin = finalStrokeBegin;
292}
293
295{
296 d->firstAssistant.clear();
297
298 Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
299 assistant->endStroke();
300 }
301}
302
303void KisPaintingAssistantsDecoration::drawDecoration(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter *converter, KisCanvas2* canvas)
304{
305 if(assistants().isEmpty()) {
306 return; // no assistants to worry about, ok to exit
307 }
308
309 if (!canvas) {
310 dbgFile<<"canvas does not exist in painting assistant decoration, you may have passed arguments incorrectly:"<<canvas;
311 } else {
312 d->m_canvas = canvas;
313 }
314
315 // the preview functionality for assistants. do not show while editing
316
317 KoToolProxy *proxy = view()->canvasBase()->toolProxy();
319 KisToolProxy *kritaProxy = dynamic_cast<KisToolProxy*>(proxy);
321
322 const bool outlineVisible =
325 kritaProxy->supportsPaintingAssistants();
326
327 Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
328 assistant->drawAssistant(gc, updateRect, converter, d->useCache, canvas, assistantVisibility(), outlineVisible);
329
330 if (isEditingAssistants()) {
331 drawHandles(assistant, gc, converter);
332 }
333 }
334
335 // draw editor controls on top of all assistant lines (why this code is last)
336 if (isEditingAssistants()) {
337 Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
338 drawEditorWidget(assistant, gc, converter);
339 }
340 }
341}
342
344{
345 QTransform initialTransform = converter->documentToWidgetTransform();
346
347 QColor colorToPaint = assistant->effectiveAssistantColor();
348
349 Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->handles()) {
350
351
352 QPointF transformedHandle = initialTransform.map(*handle);
353 QRectF ellipse(transformedHandle - QPointF(handleSize() * 0.5, handleSize() * 0.5), QSizeF(handleSize(), handleSize()));
354
355 QPainterPath path;
356 path.addEllipse(ellipse);
357
358 gc.save();
359 gc.setPen(Qt::NoPen);
360 gc.setBrush(colorToPaint);
361 gc.drawPath(path);
362 gc.restore();
363 }
364
365 // some assistants have side handles like the vanishing point assistant
366 Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->sideHandles()) {
367 QPointF transformedHandle = initialTransform.map(*handle);
368 QRectF ellipse(transformedHandle - QPointF(handleSize() * 0.5, handleSize() * 0.5), QSizeF(handleSize(), handleSize()));
369
370 QPainterPath path;
371 path.addEllipse(ellipse);
372
373 gc.save();
374 gc.setPen(Qt::NoPen);
375 gc.setBrush(colorToPaint);
376 gc.drawPath(path);
377 gc.restore();
378 }
379}
380
385
390
392{
394 Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
395 Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->handles()) {
396 if (!hs.contains(handle)) {
397 hs.push_back(handle);
398 }
399 }
400 Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->sideHandles()) {
401 if (!hs.contains(handle)) {
402 hs.push_back(handle);
403 }
404 }
405 }
406 return hs;
407}
408
410{
412 if (view()) {
413 if (view()->document()) {
414 assistants = view()->document()->assistants();
415 }
416 }
417 return assistants;
418}
419
421{
422 return !assistants().isEmpty();
423}
424
429
435
440
441
446
451
456
458{
459 d->snapEraser = assistant;
460}
461
471{
472 Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
473 assistant->uncache();
474 }
475}
481
483{
484 // "outline" means assistant preview (line that depends on the mouse cursor)
486}
487
489{
490 return view()->document()->assistantsGlobalColor();
491}
492
494{
495 // view()->document() is referenced multiple times in this class
496 // it is used to later store things in the KRA file when saving.
497 view()->document()->setAssistantsGlobalColor(color);
498
499 Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
500 assistant->setAssistantGlobalColorCache(color);
501 }
502
503 uncache();
504}
505
507{
508 setVisible(true); // this turns on the decorations in general. we leave it on at this point
509 d->m_isEditingAssistants = true;
510 uncache(); // updates visuals when editing
511}
512
514{
515 if (!d->m_canvas) {
516 return;
517 }
518
519 d->m_isEditingAssistants = false; // some elements are hidden when we aren't editing
520 uncache(); // updates visuals when not editing
521}
522
527
528QPointF KisPaintingAssistantsDecoration::snapToGuide(KoPointerEvent *e, const QPointF &offset, bool useModifiers)
529{
530 if (!d->m_canvas || !d->m_canvas->currentImage()) {
531 return e->point;
532 }
533
534
535 KoSnapGuide *snapGuide = d->m_canvas->snapGuide();
536 QPointF pos = snapGuide->snap(e->point, offset, useModifiers ? e->modifiers() : Qt::NoModifier);
537
538 return pos;
539}
540
541QPointF KisPaintingAssistantsDecoration::snapToGuide(const QPointF& pt, const QPointF &offset)
542{
543 if (!d->m_canvas) {
544 return pt;
545 }
546
547
548 KoSnapGuide *snapGuide = d->m_canvas->snapGuide();
549 QPointF pos = snapGuide->snap(pt, offset, Qt::NoModifier);
550
551 return pos;
552}
553
554/*
555 * functions only used internally in this class
556 * we potentially could make some of these inline to speed up performance
557*/
558
560{
561 if (!assistant->isAssistantComplete() || !globalEditorWidgetData.widgetActivated) {
562 return;
563 }
564
565 QTransform initialTransform = converter->documentToWidgetTransform();
566 QPointF actionsPosition = initialTransform.map(assistant->viewportConstrainedEditorPosition(converter, globalEditorWidgetData.boundingSize));
567
568 //draw editor widget background
569 QBrush backgroundColor = d->m_canvas->viewManager()->mainWindowAsQWidget()->palette().window();
570 QPointF actionsBGRectangle(actionsPosition + QPointF(globalEditorWidgetData.widgetOffset,globalEditorWidgetData.widgetOffset));
571 QPen stroke(QColor(60, 60, 60, 80), 2);
572
573 gc.setRenderHint(QPainter::Antialiasing);
574
575
576 QPainterPath bgPath;
577 bgPath.addRoundedRect(QRectF(actionsBGRectangle.x(), actionsBGRectangle.y(), globalEditorWidgetData.boundingSize.width(), globalEditorWidgetData.boundingSize.height()), 6, 6);
578
579
580 // if the assistant is selected, make outline stroke fatter and use theme's highlight color
581 // for better visual feedback
582 if (selectedAssistant()) { // there might not be a selected assistant, so do not seg fault
583 if (assistant->getEditorPosition() == selectedAssistant()->getEditorPosition()) {
584 stroke.setWidth(6);
585 stroke.setColor(qApp->palette().color(QPalette::Highlight));
586
587 }
588 }
589
590 gc.setPen(stroke);
591 gc.drawPath(bgPath);
592 gc.fillPath(bgPath, backgroundColor);
593
594
595 //draw drag handle
596 QColor dragDecorationColor(150,150,150,255);
597
598 QPainterPath dragRect;
601
602 gc.fillPath(bgPath.intersected(dragRect),dragDecorationColor);
603
604 //draw dot decoration on handle
605 QPainterPath dragRectDots;
606 QColor dragDecorationDotsColor(50,50,50,255);
607 int dotSize = 2;
608 dragRectDots.addEllipse(3,2.5,dotSize,dotSize);
609 dragRectDots.addEllipse(3,7.5,dotSize,dotSize);
610 dragRectDots.addEllipse(3,-2.5,dotSize,dotSize);
611 dragRectDots.addEllipse(3,-7.5,dotSize,dotSize);
612 dragRectDots.addEllipse(-3,2.5,dotSize,dotSize);
613 dragRectDots.addEllipse(-3,7.5,dotSize,dotSize);
614 dragRectDots.addEllipse(-3,-2.5,dotSize,dotSize);
615 dragRectDots.addEllipse(-3,-7.5,dotSize,dotSize);
616 dragRectDots.translate((globalEditorWidgetData.dragDecorationWidth/2)+width,(globalEditorWidgetData.boundingSize.height()/2)+actionsPosition.y()+globalEditorWidgetData.widgetOffset);
617 gc.fillPath(dragRectDots,dragDecorationDotsColor);
618
619
620 //loop over all visible buttons and render them
622 QPointF iconMovePosition(actionsPosition + globalEditorWidgetData.moveIconPosition);
623 gc.drawPixmap(iconMovePosition, globalEditorWidgetData.m_iconMove);
624 }
626 QPointF iconSnapPosition(actionsPosition + globalEditorWidgetData.snapIconPosition);
627 if (assistant->isSnappingActive() == true) {
628 gc.drawPixmap(iconSnapPosition, globalEditorWidgetData.m_iconSnapOn);
629 }else {
630 gc.drawPixmap(iconSnapPosition, globalEditorWidgetData.m_iconSnapOff);
631 }
632 }
634 QPointF iconLockedPosition(actionsPosition + globalEditorWidgetData.lockedIconPosition);
635 if (assistant->isLocked()) {
636 gc.drawPixmap(iconLockedPosition, globalEditorWidgetData.m_iconLockOn);
637 } else {
638 qreal oldOpacity = gc.opacity();
639 gc.setOpacity(0.35);
640 gc.drawPixmap(iconLockedPosition, globalEditorWidgetData.m_iconLockOff);
641 gc.setOpacity(oldOpacity);
642 }
643 }
645 QPointF iconDuplicatePosition(actionsPosition + globalEditorWidgetData.duplicateIconPosition);
646 if(assistant->isDuplicating()) {
647 //draw button depressed
648 qreal oldOpacity = gc.opacity();
649 gc.setOpacity(0.35);
650 gc.drawPixmap(iconDuplicatePosition,globalEditorWidgetData.m_iconDuplicate);
651 gc.setOpacity(oldOpacity);
652 }else {
653 gc.drawPixmap(iconDuplicatePosition,globalEditorWidgetData.m_iconDuplicate);
654 }
655 }
657 QPointF iconDeletePosition(actionsPosition + globalEditorWidgetData.deleteIconPosition);
658 gc.drawPixmap(iconDeletePosition, globalEditorWidgetData.m_iconDelete);
659 }
660
661
662
663
664}
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:806
@ ASSISTANTS_DRAW_MODE_LARGE_PIXMAP_CACHE
Definition kis_config.h:807
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)