1 /*
2  *  Copyright (c) 2009 Cyrille Berger <cberger@cberger.net>
3  *  Copyright (c) 2017 Scott Petrovic <scottpetrovic@gmail.com>
4  *
5  *  This program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2 of the License, or
8  *  (at your option) any later version.
9  *
10  *  This program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with this program; if not, write to the Free Software
17  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  */
19 
20 #include "kis_painting_assistants_decoration.h"
21 
22 #include <cfloat>
23 
24 #include <QList>
25 #include <QPointF>
26 #include <klocalizedstring.h>
27 #include <kactioncollection.h>
28 #include <ktoggleaction.h>
29 #include "kis_debug.h"
30 #include "KisDocument.h"
31 #include "kis_canvas2.h"
32 #include "kis_icon_utils.h"
33 #include "KisViewManager.h"
34 
35 #include <QPainter>
36 #include <QPainterPath>
37 #include <QApplication>
38 
39 struct KisPaintingAssistantsDecoration::Private {
PrivateKisPaintingAssistantsDecoration::Private40     Private()
41         : assistantVisible(false)
42         , outlineVisible(false)
43         , snapOnlyOneAssistant(true)
44         , firstAssistant(0)
45         , aFirstStroke(false)
46         , m_handleSize(14)
47     {}
48 
49     bool assistantVisible;
50     bool outlineVisible;
51     bool snapOnlyOneAssistant;
52     KisPaintingAssistantSP firstAssistant;
53     KisPaintingAssistantSP selectedAssistant;
54     bool aFirstStroke;
55     bool m_isEditingAssistants = false;
56     bool m_outlineVisible = false;
57     int m_handleSize; // size of editor handles on assistants
58 
59     // move, visibility, delete icons for each assistant. These only display while the assistant tool is active
60     // these icons will be covered by the kis_paintint_assistant_decoration with things like the perspective assistant
61 
62     AssistantEditorData toolData;
63 
64     QPixmap m_iconDelete = KisIconUtils::loadIcon("dialog-cancel").pixmap(toolData.deleteIconSize, toolData.deleteIconSize);
65     QPixmap m_iconSnapOn = KisIconUtils::loadIcon("visible").pixmap(toolData.snapIconSize, toolData.snapIconSize);
66     QPixmap m_iconSnapOff = KisIconUtils::loadIcon("novisible").pixmap(toolData.snapIconSize, toolData.snapIconSize);
67     QPixmap m_iconMove = KisIconUtils::loadIcon("transform-move").pixmap(toolData.moveIconSize, toolData.moveIconSize);
68 
69     KisCanvas2 * m_canvas = 0;
70 };
71 
72 
73 
KisPaintingAssistantsDecoration(QPointer<KisView> parent)74 KisPaintingAssistantsDecoration::KisPaintingAssistantsDecoration(QPointer<KisView> parent) :
75     KisCanvasDecoration("paintingAssistantsDecoration", parent),
76     d(new Private)
77 {
78     setAssistantVisible(true);
79     setOutlineVisible(true);
80     setPriority(95);
81     d->snapOnlyOneAssistant = true; //turn on by default.
82 }
83 
~KisPaintingAssistantsDecoration()84 KisPaintingAssistantsDecoration::~KisPaintingAssistantsDecoration()
85 {
86     delete d;
87 }
88 
slotUpdateDecorationVisibility()89 void KisPaintingAssistantsDecoration::slotUpdateDecorationVisibility()
90 {
91     const bool shouldBeVisible = !assistants().isEmpty();
92 
93     if (visible() != shouldBeVisible) {
94         setVisible(shouldBeVisible);
95     }
96 }
97 
addAssistant(KisPaintingAssistantSP assistant)98 void KisPaintingAssistantsDecoration::addAssistant(KisPaintingAssistantSP assistant)
99 {
100     QList<KisPaintingAssistantSP> assistants = view()->document()->assistants();
101     if (assistants.contains(assistant)) return;
102 
103     assistants.append(assistant);
104     assistant->setAssistantGlobalColorCache(view()->document()->assistantsGlobalColor());
105 
106     view()->document()->setAssistants(assistants);
107     setVisible(!assistants.isEmpty());
108     emit assistantChanged();
109 }
110 
removeAssistant(KisPaintingAssistantSP assistant)111 void KisPaintingAssistantsDecoration::removeAssistant(KisPaintingAssistantSP assistant)
112 {
113     QList<KisPaintingAssistantSP> assistants = view()->document()->assistants();
114     KIS_ASSERT_RECOVER_NOOP(assistants.contains(assistant));
115 
116     if (assistants.removeAll(assistant)) {
117         view()->document()->setAssistants(assistants);
118         setVisible(!assistants.isEmpty());
119         emit assistantChanged();
120     }
121 }
122 
removeAll()123 void KisPaintingAssistantsDecoration::removeAll()
124 {
125     QList<KisPaintingAssistantSP> assistants = view()->document()->assistants();
126     assistants.clear();
127     view()->document()->setAssistants(assistants);
128     setVisible(!assistants.isEmpty());
129 
130     emit assistantChanged();
131 }
132 
setAssistants(const QList<KisPaintingAssistantSP> & assistants)133 void KisPaintingAssistantsDecoration::setAssistants(const QList<KisPaintingAssistantSP> &assistants)
134 {
135     Q_FOREACH (KisPaintingAssistantSP assistant, assistants) {
136         assistant->setAssistantGlobalColorCache(view()->document()->assistantsGlobalColor());
137     }
138     view()->document()->setAssistants(assistants);
139     setVisible(!assistants.isEmpty());
140 
141     emit assistantChanged();
142 }
143 
setAdjustedBrushPosition(const QPointF position)144 void KisPaintingAssistantsDecoration::setAdjustedBrushPosition(const QPointF position)
145 {
146     if (!assistants().empty()) {
147         Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
148             assistant->setAdjustedBrushPosition(position);
149         }
150     }
151 }
152 
153 
adjustPosition(const QPointF & point,const QPointF & strokeBegin)154 QPointF KisPaintingAssistantsDecoration::adjustPosition(const QPointF& point, const QPointF& strokeBegin)
155 {
156 
157     if (assistants().empty()) {
158         // No assisants, so no adjustment
159         return point;
160     }
161 
162     // There is at least 1 assistant
163     if (assistants().count() == 1) {
164         // Things are easy when there is only one assistant
165         if(assistants().first()->isSnappingActive() == true){
166             QPointF newpoint = assistants().first()->adjustPosition(point, strokeBegin);
167             // check for NaN
168             if (newpoint.x() != newpoint.x()) return point;
169             // Tell the assistant that its guidelines should
170             // follow the adjusted bush position.
171             assistants().first()->setFollowBrushPosition(true);
172             return newpoint;
173         } else {
174             // One assisant, but it is not active, so no adjustment
175             return point;
176         }
177     }
178 
179     // There is more than one assistant.
180     // We have to chose one of these.
181     // This is done by checking which assistant gives the an adjusted position
182     // that is closest to the current position.
183 
184     QPointF best = point;
185     double distance = DBL_MAX;
186     if (!d->snapOnlyOneAssistant) {
187         // In this mode the best assistant is constantly computed anew.
188         Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
189             if (assistant->isSnappingActive() == true){//this checks if the assistant in question has it's snapping boolean turned on//
190                 QPointF pt = assistant->adjustPosition(point, strokeBegin);
191                 // check for NaN
192                 if (pt.x() != pt.x()) continue;
193                 double dist = qAbs(pt.x() - point.x()) + qAbs(pt.y() - point.y());
194                 if (dist < distance) {
195                     best = pt;
196                     distance = dist;
197                 }
198                 assistant->setFollowBrushPosition(true);
199             }
200         }
201     } else if (d->aFirstStroke==false) {
202         // In this mode we compute the best assistant during the initial
203         // movement and then keep using that assistant. (Called the first
204         // assistant).
205         Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
206             if(assistant->isSnappingActive() == true){//this checks if the assistant in question has it's snapping boolean turned on//
207                 QPointF pt = assistant->adjustPosition(point, strokeBegin);
208                 if (pt.x() != pt.x()) continue;
209                 double dist = qAbs(pt.x() - point.x()) + qAbs(pt.y() - point.y());
210                 if (dist < distance) {
211                     best = pt;
212                     distance = dist;
213                     d->firstAssistant = assistant;
214                 }
215                 assistant->setFollowBrushPosition(true);
216             }
217         }
218     } else if (d->firstAssistant) {
219         //make sure there's a first assistant to begin with.//
220         QPointF newpoint = d->firstAssistant->adjustPosition(point, strokeBegin);
221         // BUGFIX: 402535
222         // assistants might return (NaN,NaN), must always check for that
223         if (newpoint.x() == newpoint.x()) {
224             // not a NaN
225             best = newpoint;
226         }
227     } else {
228         d->aFirstStroke=false;
229     }
230 
231     //this is here to be compatible with the movement in the perspective tool.
232     qreal dx = point.x() - strokeBegin.x();
233     qreal dy = point.y() - strokeBegin.y();
234     if (dx * dx + dy * dy >= 4.0) {
235         // allow some movement before snapping
236         d->aFirstStroke=true;
237     }
238 
239     return best;
240 }
241 
endStroke()242 void KisPaintingAssistantsDecoration::endStroke()
243 {
244     d->aFirstStroke = false;
245 
246     Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
247         assistant->endStroke();
248     }
249 }
250 
drawDecoration(QPainter & gc,const QRectF & updateRect,const KisCoordinatesConverter * converter,KisCanvas2 * canvas)251 void KisPaintingAssistantsDecoration::drawDecoration(QPainter& gc, const QRectF& updateRect, const KisCoordinatesConverter *converter,KisCanvas2* canvas)
252 {
253     if(assistants().isEmpty()) {
254         return; // no assistants to worry about, ok to exit
255     }
256 
257     if (!canvas) {
258         dbgFile<<"canvas does not exist in painting assistant decoration, you may have passed arguments incorrectly:"<<canvas;
259     } else {
260         d->m_canvas = canvas;
261     }
262 
263     // the preview functionality for assistants. do not show while editing
264     if (d->m_isEditingAssistants) {
265         d->m_outlineVisible = false;
266     }
267     else {
268         d->m_outlineVisible = outlineVisibility();
269     }
270 
271     Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
272         assistant->drawAssistant(gc, updateRect, converter, true, canvas, assistantVisibility(), d->m_outlineVisible);
273 
274         if (isEditingAssistants()) {
275             drawHandles(assistant, gc, converter);
276         }
277     }
278 
279     // draw editor controls on top of all assistant lines (why this code is last)
280     if (isEditingAssistants()) {
281         Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
282             drawEditorWidget(assistant, gc, converter);
283         }
284      }
285 }
286 
drawHandles(KisPaintingAssistantSP assistant,QPainter & gc,const KisCoordinatesConverter * converter)287 void KisPaintingAssistantsDecoration::drawHandles(KisPaintingAssistantSP assistant, QPainter& gc, const KisCoordinatesConverter *converter)
288 {
289         QTransform initialTransform = converter->documentToWidgetTransform();
290 
291         QColor colorToPaint = assistant->effectiveAssistantColor();
292 
293         Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->handles()) {
294 
295 
296             QPointF transformedHandle = initialTransform.map(*handle);
297             QRectF ellipse(transformedHandle -  QPointF(handleSize() * 0.5, handleSize() * 0.5), QSizeF(handleSize(), handleSize()));
298 
299             QPainterPath path;
300             path.addEllipse(ellipse);
301 
302             gc.save();
303             gc.setPen(Qt::NoPen);
304             gc.setBrush(colorToPaint);
305             gc.drawPath(path);
306             gc.restore();
307         }
308 
309          // some assistants have side handles like the vanishing point assistant
310          Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->sideHandles()) {
311              QPointF transformedHandle = initialTransform.map(*handle);
312              QRectF ellipse(transformedHandle -  QPointF(handleSize() * 0.5, handleSize() * 0.5), QSizeF(handleSize(), handleSize()));
313 
314              QPainterPath path;
315              path.addEllipse(ellipse);
316 
317              gc.save();
318              gc.setPen(Qt::NoPen);
319              gc.setBrush(colorToPaint);
320              gc.drawPath(path);
321              gc.restore();
322          }
323 }
324 
handleSize()325 int KisPaintingAssistantsDecoration::handleSize()
326 {
327     return  d->m_handleSize;
328 }
329 
setHandleSize(int handleSize)330 void KisPaintingAssistantsDecoration::setHandleSize(int handleSize)
331 {
332     d->m_handleSize = handleSize;
333 }
334 
handles()335 QList<KisPaintingAssistantHandleSP> KisPaintingAssistantsDecoration::handles()
336 {
337     QList<KisPaintingAssistantHandleSP> hs;
338     Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
339         Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->handles()) {
340             if (!hs.contains(handle)) {
341                 hs.push_back(handle);
342             }
343         }
344         Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->sideHandles()) {
345             if (!hs.contains(handle)) {
346                 hs.push_back(handle);
347             }
348         }
349     }
350     return hs;
351 }
352 
assistants() const353 QList<KisPaintingAssistantSP> KisPaintingAssistantsDecoration::assistants() const
354 {
355     QList<KisPaintingAssistantSP> assistants;
356     if (view()) {
357         if (view()->document()) {
358             assistants = view()->document()->assistants();
359         }
360     }
361     return assistants;
362 }
363 
hasPaintableAssistants() const364 bool KisPaintingAssistantsDecoration::hasPaintableAssistants() const
365 {
366     return !assistants().isEmpty();
367 }
368 
selectedAssistant()369 KisPaintingAssistantSP KisPaintingAssistantsDecoration::selectedAssistant()
370 {
371     return d->selectedAssistant;
372 }
373 
setSelectedAssistant(KisPaintingAssistantSP assistant)374 void KisPaintingAssistantsDecoration::setSelectedAssistant(KisPaintingAssistantSP assistant)
375 {
376     d->selectedAssistant = assistant;
377     emit selectedAssistantChanged();
378 }
379 
deselectAssistant()380 void KisPaintingAssistantsDecoration::deselectAssistant()
381 {
382     d->selectedAssistant.clear();
383 }
384 
385 
setAssistantVisible(bool set)386 void KisPaintingAssistantsDecoration::setAssistantVisible(bool set)
387 {
388     d->assistantVisible=set;
389 }
390 
setOutlineVisible(bool set)391 void KisPaintingAssistantsDecoration::setOutlineVisible(bool set)
392 {
393     d->outlineVisible=set;
394 }
395 
setOnlyOneAssistantSnap(bool assistant)396 void KisPaintingAssistantsDecoration::setOnlyOneAssistantSnap(bool assistant)
397 {
398     d->snapOnlyOneAssistant = assistant;
399 }
400 
assistantVisibility()401 bool KisPaintingAssistantsDecoration::assistantVisibility()
402 {
403     return d->assistantVisible;
404 }
outlineVisibility()405 bool KisPaintingAssistantsDecoration::outlineVisibility()
406 {
407     return d->outlineVisible;
408 }
uncache()409 void KisPaintingAssistantsDecoration::uncache()
410 {
411     Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
412         assistant->uncache();
413     }
414 }
toggleAssistantVisible()415 void KisPaintingAssistantsDecoration::toggleAssistantVisible()
416 {
417     setAssistantVisible(!assistantVisibility());
418     uncache();
419 }
420 
toggleOutlineVisible()421 void KisPaintingAssistantsDecoration::toggleOutlineVisible()
422 {
423     setOutlineVisible(!outlineVisibility());
424 }
425 
globalAssistantsColor()426 QColor KisPaintingAssistantsDecoration::globalAssistantsColor()
427 {
428     return view()->document()->assistantsGlobalColor();
429 }
430 
setGlobalAssistantsColor(QColor color)431 void KisPaintingAssistantsDecoration::setGlobalAssistantsColor(QColor color)
432 {
433     // view()->document() is referenced multiple times in this class
434     // it is used to later store things in the KRA file when saving.
435     view()->document()->setAssistantsGlobalColor(color);
436 
437     Q_FOREACH (KisPaintingAssistantSP assistant, assistants()) {
438         assistant->setAssistantGlobalColorCache(color);
439     }
440 
441     uncache();
442 }
443 
activateAssistantsEditor()444 void KisPaintingAssistantsDecoration::activateAssistantsEditor()
445 {
446     setVisible(true); // this turns on the decorations in general. we leave it on at this point
447     d->m_isEditingAssistants = true;
448     uncache(); // updates visuals when editing
449 }
450 
deactivateAssistantsEditor()451 void KisPaintingAssistantsDecoration::deactivateAssistantsEditor()
452 {
453     if (!d->m_canvas) {
454         return;
455     }
456 
457     d->m_isEditingAssistants = false; // some elements are hidden when we aren't editing
458     uncache(); // updates visuals when not editing
459 }
460 
isEditingAssistants()461 bool KisPaintingAssistantsDecoration::isEditingAssistants()
462 {
463     return d->m_isEditingAssistants;
464 }
465 
snapToGuide(KoPointerEvent * e,const QPointF & offset,bool useModifiers)466 QPointF KisPaintingAssistantsDecoration::snapToGuide(KoPointerEvent *e, const QPointF &offset, bool useModifiers)
467 {
468     if (!d->m_canvas || !d->m_canvas->currentImage()) {
469         return e->point;
470     }
471 
472 
473     KoSnapGuide *snapGuide = d->m_canvas->snapGuide();
474     QPointF pos = snapGuide->snap(e->point, offset, useModifiers ? e->modifiers() : Qt::NoModifier);
475 
476     return pos;
477 }
478 
snapToGuide(const QPointF & pt,const QPointF & offset)479 QPointF KisPaintingAssistantsDecoration::snapToGuide(const QPointF& pt, const QPointF &offset)
480 {
481     if (!d->m_canvas) {
482          return pt;
483     }
484 
485 
486     KoSnapGuide *snapGuide = d->m_canvas->snapGuide();
487     QPointF pos = snapGuide->snap(pt, offset, Qt::NoModifier);
488 
489     return pos;
490 }
491 
492 /*
493  * functions only used internally in this class
494  * we potentially could make some of these inline to speed up performance
495 */
496 
drawEditorWidget(KisPaintingAssistantSP assistant,QPainter & gc,const KisCoordinatesConverter * converter)497 void KisPaintingAssistantsDecoration::drawEditorWidget(KisPaintingAssistantSP assistant, QPainter& gc, const KisCoordinatesConverter *converter)
498 {
499     if (!assistant->isAssistantComplete()) {
500         return;
501     }
502 
503     AssistantEditorData toolData; // shared const data for positioning and sizing
504 
505     QTransform initialTransform = converter->documentToWidgetTransform();
506 
507     QPointF actionsPosition = initialTransform.map(assistant->viewportConstrainedEditorPosition(converter, toolData.boundingSize));
508 
509     QPointF iconMovePosition(actionsPosition + toolData.moveIconPosition);
510     QPointF iconSnapPosition(actionsPosition + toolData.snapIconPosition);
511     QPointF iconDeletePosition(actionsPosition + toolData.deleteIconPosition);
512 
513     // Background container for helpers
514     QBrush backgroundColor = d->m_canvas->viewManager()->mainWindow()->palette().window();
515     QPointF actionsBGRectangle(actionsPosition + QPointF(10, 10));
516 
517     gc.setRenderHint(QPainter::Antialiasing);
518 
519     QPainterPath bgPath;
520     bgPath.addRoundedRect(QRectF(actionsBGRectangle.x(), actionsBGRectangle.y(), toolData.boundingSize.width(), toolData.boundingSize.height()), 6, 6);
521     QPen stroke(QColor(60, 60, 60, 80), 2);
522 
523     // if the assistant is selected, make outline stroke fatter and use theme's highlight color
524     // for better visual feedback
525     if (selectedAssistant()) { // there might not be a selected assistant, so do not seg fault
526         if (assistant->getEditorPosition() == selectedAssistant()->getEditorPosition()) {
527             stroke.setWidth(4);
528             stroke.setColor(qApp->palette().color(QPalette::Highlight));
529         }
530     }
531 
532     // draw the final result
533     gc.setPen(stroke);
534     gc.fillPath(bgPath, backgroundColor);
535     gc.drawPath(bgPath);
536 
537 
538     // Move Assistant Tool helper
539     gc.drawPixmap(iconMovePosition, d->m_iconMove);
540 
541     // active toggle
542     if (assistant->isSnappingActive() == true) {
543         gc.drawPixmap(iconSnapPosition, d->m_iconSnapOn);
544     }
545     else {
546         gc.drawPixmap(iconSnapPosition, d->m_iconSnapOff);
547     }
548 
549     gc.drawPixmap(iconDeletePosition, d->m_iconDelete);
550 
551 
552 }
553