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