Pencil2D Animation
Download Community News Docs Contribute
  • Overview
  • Articles
  • Code
  •  
  • Class List
  • Class Index
  • Class Hierarchy
  • Class Members
  • File List
Loading...
Searching...
No Matches
  • core_lib
  • src
  • interface
scribblearea.cpp
1/*
2
3Pencil2D - Traditional Animation Software
4Copyright (C) 2005-2007 Patrick Corrieri & Pascal Naidon
5Copyright (C) 2012-2020 Matthew Chiawen Chang
6
7This program is free software; you can redistribute it and/or
8modify it under the terms of the GNU General Public License
9as published by the Free Software Foundation; version 2 of the License.
10
11This program is distributed in the hope that it will be useful,
12but WITHOUT ANY WARRANTY; without even the implied warranty of
13MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14GNU General Public License for more details.
15
16*/
17
18#include "scribblearea.h"
19
20#include <cmath>
21#include <QGuiApplication>
22#include <QMessageBox>
23#include <QPixmapCache>
24#include <QTimer>
25
26#include "basetool.h"
27#include "transformtool.h"
28#include "pointerevent.h"
29#include "beziercurve.h"
30#include "object.h"
31#include "editor.h"
32#include "layerbitmap.h"
33#include "layervector.h"
34#include "layercamera.h"
35#include "bitmapimage.h"
36#include "vectorimage.h"
37#include "blitrect.h"
38#include "tile.h"
39
40#include "onionskinpainteroptions.h"
41
42#include "colormanager.h"
43#include "toolmanager.h"
44#include "layermanager.h"
45#include "playbackmanager.h"
46#include "viewmanager.h"
47#include "selectionmanager.h"
48#include "overlaymanager.h"
49
50ScribbleArea::ScribbleArea(QWidget* parent) : QWidget(parent),
51 mCanvasPainter(mCanvas),
52 mCameraPainter(mCanvas)
53{
54 setObjectName("ScribbleArea");
55
56 // Qt::WA_StaticContents ensure that the widget contents are rooted to the top-left corner
57 // and don't change when the widget is resized.
58 setAttribute(Qt::WA_StaticContents);
59}
60
61ScribbleArea::~ScribbleArea()
62{
63}
64
65bool ScribbleArea::init()
66{
67 mPrefs = mEditor->preference();
68 mDoubleClickTimer = new QTimer(this);
69 mMouseFilterTimer = new QTimer(this);
70
71 connect(mPrefs, &PreferenceManager::optionChanged, this, &ScribbleArea::settingUpdated);
72 connect(mEditor->tools(), &ToolManager::toolChanged, this, &ScribbleArea::onToolChanged);
73
74 connect(mDoubleClickTimer, &QTimer::timeout, this, &ScribbleArea::handleDoubleClick);
75 connect(mMouseFilterTimer, &QTimer::timeout, this, &ScribbleArea::tabletReleaseEventFired);
76
77 connect(mEditor->select(), &SelectionManager::selectionChanged, this, &ScribbleArea::onSelectionChanged);
78 connect(mEditor->select(), &SelectionManager::needDeleteSelection, this, &ScribbleArea::deleteSelection);
79
80 connect(&mTiledBuffer, &TiledBuffer::tileUpdated, this, &ScribbleArea::onTileUpdated);
81 connect(&mTiledBuffer, &TiledBuffer::tileCreated, this, &ScribbleArea::onTileCreated);
82
83 mDoubleClickTimer->setInterval(50);
84 mMouseFilterTimer->setInterval(50);
85
86 const int curveSmoothingLevel = mPrefs->getInt(SETTING::CURVE_SMOOTHING);
87 mCurveSmoothingLevel = curveSmoothingLevel / 20.0; // default value is 1.0
88
89 mMakeInvisible = false;
90
91 mLayerVisibility = static_cast<LayerVisibility>(mPrefs->getInt(SETTING::LAYER_VISIBILITY));
92
93 mDeltaFactor = mEditor->preference()->isOn(SETTING::INVERT_SCROLL_ZOOM_DIRECTION) ? -1 : 1;
94
95 setMouseTracking(true); // reacts to mouse move events, even if the button is not pressed
96#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
97 setTabletTracking(true); // tablet tracking first added in 5.9
98#endif
99
100 setSizePolicy(QSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding));
101
102 QPixmapCache::setCacheLimit(100 * 1024); // unit is kb, so it's 100MB cache
103 mPixmapCacheKeys.clear();
104
105 return true;
106}
107
108void ScribbleArea::settingUpdated(SETTING setting)
109{
110 switch (setting)
111 {
112 case SETTING::CURVE_SMOOTHING:
113 setCurveSmoothing(mPrefs->getInt(SETTING::CURVE_SMOOTHING));
114 break;
115 case SETTING::TOOL_CURSOR:
116 updateToolCursor();
117 break;
118 case SETTING::ONION_PREV_FRAMES_NUM:
119 case SETTING::ONION_NEXT_FRAMES_NUM:
120 case SETTING::ONION_MIN_OPACITY:
121 case SETTING::ONION_MAX_OPACITY:
122 invalidateAllCache();
123 break;
124 case SETTING::ANTIALIAS:
125 case SETTING::GRID:
126 case SETTING::GRID_SIZE_W:
127 case SETTING::GRID_SIZE_H:
128 case SETTING::OVERLAY_CENTER:
129 case SETTING::OVERLAY_THIRDS:
130 case SETTING::OVERLAY_GOLDEN:
131 case SETTING::OVERLAY_SAFE:
132 case SETTING::OVERLAY_PERSPECTIVE1:
133 case SETTING::OVERLAY_PERSPECTIVE2:
134 case SETTING::OVERLAY_PERSPECTIVE3:
135 case SETTING::ACTION_SAFE_ON:
136 case SETTING::ACTION_SAFE:
137 case SETTING::TITLE_SAFE_ON:
138 case SETTING::TITLE_SAFE:
139 case SETTING::OVERLAY_SAFE_HELPER_TEXT_ON:
140 case SETTING::PREV_ONION:
141 case SETTING::NEXT_ONION:
142 case SETTING::ONION_BLUE:
143 case SETTING::ONION_RED:
144 case SETTING::INVISIBLE_LINES:
145 case SETTING::OUTLINES:
146 case SETTING::ONION_TYPE:
147 case SETTING::ONION_WHILE_PLAYBACK:
148 invalidateAllCache();
149 break;
150 case SETTING::ONION_MUTLIPLE_LAYERS:
151 invalidateAllCache();
152 break;
153 case SETTING::LAYER_VISIBILITY_THRESHOLD:
154 case SETTING::LAYER_VISIBILITY:
155 setLayerVisibility(static_cast<LayerVisibility>(mPrefs->getInt(SETTING::LAYER_VISIBILITY)));
156 break;
157 case SETTING::INVERT_SCROLL_ZOOM_DIRECTION:
158 mDeltaFactor = mEditor->preference()->isOn(SETTING::INVERT_SCROLL_ZOOM_DIRECTION) ? -1 : 1;
159 break;
160 default:
161 break;
162 }
163
164}
165
166void ScribbleArea::updateToolCursor()
167{
168 setCursor(currentTool()->cursor());
169}
170
171void ScribbleArea::setCurveSmoothing(int newSmoothingLevel)
172{
173 mCurveSmoothingLevel = newSmoothingLevel / 20.0;
174 invalidatePainterCaches();
175}
176
177void ScribbleArea::setEffect(SETTING e, bool isOn)
178{
179 mPrefs->set(e, isOn);
180 invalidatePainterCaches();
181}
182
183/************************************************************************************/
184// update methods
185
186void ScribbleArea::onTileUpdated(TiledBuffer* tiledBuffer, Tile* tile)
187{
188 Q_UNUSED(tiledBuffer);
189 const QRectF& mappedRect = mEditor->view()->getView().mapRect(QRectF(tile->bounds()));
190 update(mappedRect.toAlignedRect());
191}
192
193void ScribbleArea::onTileCreated(TiledBuffer* tiledBuffer, Tile* tile)
194{
195 Q_UNUSED(tiledBuffer)
196 const QRectF& mappedRect = mEditor->view()->getView().mapRect(QRectF(tile->bounds()));
197 update(mappedRect.toAlignedRect());
198}
199
200void ScribbleArea::updateFrame()
201{
202 if (currentTool()->isActive() && currentTool()->isDrawingTool()) {
203 return;
204 }
205
206 update();
207}
208
209void ScribbleArea::invalidateCacheForDirtyFrames()
210{
211 Layer* currentLayer = mEditor->layers()->currentLayer();
212 for (int pos : currentLayer->dirtyFrames()) {
213
214 invalidateCacheForFrame(pos);
215 invalidateOnionSkinsCacheAround(pos);
216 }
217 currentLayer->clearDirtyFrames();
218}
219
220void ScribbleArea::invalidateOnionSkinsCacheAround(int frameNumber)
221{
222 if (frameNumber < 0) { return; }
223
224 bool isOnionAbsolute = mPrefs->getString(SETTING::ONION_TYPE) == "absolute";
225 Layer *layer = mEditor->layers()->currentLayer(0);
226
227 // The current layer can be null if updateFrame is triggered when creating a new project
228 if (!layer) return;
229
230 if (mPrefs->isOn(SETTING::PREV_ONION))
231 {
232 int onionFrameNumber = frameNumber;
233 if (isOnionAbsolute)
234 {
235 onionFrameNumber = layer->getPreviousFrameNumber(onionFrameNumber + 1, true);
236 }
237
238 for(int i = 1; i <= mPrefs->getInt(SETTING::ONION_PREV_FRAMES_NUM); i++)
239 {
240 onionFrameNumber = layer->getPreviousFrameNumber(onionFrameNumber, isOnionAbsolute);
241 if (onionFrameNumber < 0) break;
242
243 invalidateCacheForFrame(onionFrameNumber);
244 }
245 }
246
247 if (mPrefs->isOn(SETTING::NEXT_ONION))
248 {
249 int onionFrameNumber = frameNumber;
250
251 for(int i = 1; i <= mPrefs->getInt(SETTING::ONION_NEXT_FRAMES_NUM); i++)
252 {
253 onionFrameNumber = layer->getNextFrameNumber(onionFrameNumber, isOnionAbsolute);
254 if (onionFrameNumber < 0) break;
255
256 invalidateCacheForFrame(onionFrameNumber);
257 }
258 }
259}
260
261void ScribbleArea::invalidateAllCache()
262{
263 if (currentTool()->isDrawingTool() && currentTool()->isActive()) { return; }
264
265 QPixmapCache::clear();
266 mPixmapCacheKeys.clear();
267 invalidatePainterCaches();
268 mEditor->layers()->currentLayer()->clearDirtyFrames();
269
270 updateFrame();
271}
272
273void ScribbleArea::invalidateCacheForFrame(int frameNumber)
274{
275 auto cacheKeyIter = mPixmapCacheKeys.find(static_cast<unsigned int>(frameNumber));
276 if (cacheKeyIter != mPixmapCacheKeys.end())
277 {
278 QPixmapCache::remove(cacheKeyIter.value());
279 unsigned int key = cacheKeyIter.key();
280 mPixmapCacheKeys.remove(key);
281 }
282}
283
284void ScribbleArea::invalidatePainterCaches()
285{
286 mCameraPainter.resetCache();
287 mCanvasPainter.resetLayerCache();
288 updateFrame();
289}
290
291void ScribbleArea::onToolChanged(ToolType)
292{
293 int frame = mEditor->currentFrame();
294 prepOverlays(frame);
295 prepCameraPainter(frame);
296 invalidateCacheForFrame(frame);
297 updateFrame();
298}
299
300
301void ScribbleArea::onPlayStateChanged()
302{
303 int currentFrame = mEditor->currentFrame();
304 if (mPrefs->isOn(SETTING::PREV_ONION) ||
305 mPrefs->isOn(SETTING::NEXT_ONION)) {
306 invalidatePainterCaches();
307 }
308
309 prepOverlays(currentFrame);
310 prepCameraPainter(currentFrame);
311 invalidateCacheForFrame(currentFrame);
312 updateFrame();
313}
314
315void ScribbleArea::onScrubbed(int frameNumber)
316{
317 Q_UNUSED(frameNumber)
318 invalidatePainterCaches();
319 updateFrame();
320}
321
322void ScribbleArea::onFramesModified()
323{
324 invalidateCacheForDirtyFrames();
325 if (mPrefs->isOn(SETTING::PREV_ONION) || mPrefs->isOn(SETTING::NEXT_ONION)) {
326 invalidatePainterCaches();
327 }
328 updateFrame();
329}
330
331void ScribbleArea::onFrameModified(int frameNumber)
332{
333 if (mPrefs->isOn(SETTING::PREV_ONION) || mPrefs->isOn(SETTING::NEXT_ONION)) {
334 invalidateOnionSkinsCacheAround(frameNumber);
335 invalidatePainterCaches();
336 }
337 invalidateCacheForFrame(frameNumber);
338 updateFrame();
339}
340
341void ScribbleArea::onViewChanged()
342{
343 invalidateAllCache();
344}
345
346void ScribbleArea::onLayerChanged()
347{
348 invalidateAllCache();
349}
350
351void ScribbleArea::onSelectionChanged()
352{
353 int currentFrame = mEditor->currentFrame();
354 invalidateCacheForFrame(currentFrame);
355 updateFrame();
356}
357
358void ScribbleArea::onOnionSkinTypeChanged()
359{
360 invalidateAllCache();
361}
362
363void ScribbleArea::onObjectLoaded()
364{
365 invalidateAllCache();
366}
367
368bool ScribbleArea::event(QEvent *event)
369{
370 bool processed = false;
371 if (event->type() == QEvent::WindowDeactivate)
372 {
373 editor()->tools()->clearTemporaryTool();
374 processed = true;
375 } else if (event->type() == QEvent::Enter)
376 {
377 processed = currentTool()->enterEvent(static_cast<QEnterEvent*>(event)) || processed;
378 } else if (event->type() == QEvent::Leave)
379 {
380 processed = currentTool()->leaveEvent(event) || processed;
381 }
382
383 return QWidget::event(event) || processed;
384}
385
386/************************************************************************/
387/* key event handlers */
388/************************************************************************/
389
390void ScribbleArea::keyPressEvent(QKeyEvent *event)
391{
392 // Don't handle this event on auto repeat
393 if (event->isAutoRepeat()) { return; }
394
395 if (isPointerInUse()) { return; } // prevents shortcuts calls while drawing
396
397 if (currentTool()->keyPressEvent(event))
398 {
399 return; // has been handled by tool
400 }
401
402 // --- fixed control key shortcuts ---
403 if (event->modifiers() == (Qt::ControlModifier | Qt::ShiftModifier) &&
404 editor()->tools()->setTemporaryTool(ERASER, {}, event->modifiers()))
405 {
406 return;
407 }
408
409 // ---- fixed normal keys ----
410
411 auto selectMan = mEditor->select();
412 bool isSomethingSelected = selectMan->somethingSelected();
413 if (isSomethingSelected)
414 {
415 keyEventForSelection(event);
416 }
417 else
418 {
419 keyEvent(event);
420 }
421}
422
423void ScribbleArea::keyEventForSelection(QKeyEvent* event)
424{
425 auto selectMan = mEditor->select();
426 switch (event->key())
427 {
428 case Qt::Key_Right:
429 selectMan->translate(QPointF(1, 0));
430 selectMan->calculateSelectionTransformation();
431 emit mEditor->frameModified(mEditor->currentFrame());
432 return;
433 case Qt::Key_Left:
434 selectMan->translate(QPointF(-1, 0));
435 selectMan->calculateSelectionTransformation();
436 emit mEditor->frameModified(mEditor->currentFrame());
437 return;
438 case Qt::Key_Up:
439 selectMan->translate(QPointF(0, -1));
440 selectMan->calculateSelectionTransformation();
441 emit mEditor->frameModified(mEditor->currentFrame());
442 return;
443 case Qt::Key_Down:
444 selectMan->translate(QPointF(0, 1));
445 selectMan->calculateSelectionTransformation();
446 emit mEditor->frameModified(mEditor->currentFrame());
447 return;
448 case Qt::Key_Return:
449 applyTransformedSelection();
450 mEditor->deselectAll();
451 return;
452 case Qt::Key_Escape:
453 cancelTransformedSelection();
454 mEditor->deselectAll();
455 return;
456 case Qt::Key_Backspace:
457 deleteSelection();
458 mEditor->deselectAll();
459 return;
460 case Qt::Key_Space:
461 if (editor()->tools()->setTemporaryTool(HAND, Qt::Key_Space, Qt::NoModifier))
462 {
463 return;
464 }
465 break;
466 default:
467 break;
468 }
469 event->ignore();
470}
471
472void ScribbleArea::keyEvent(QKeyEvent* event)
473{
474 switch (event->key())
475 {
476 case Qt::Key_Right:
477 mEditor->scrubForward();
478 break;
479 case Qt::Key_Left:
480 mEditor->scrubBackward();
481 break;
482 case Qt::Key_Up:
483 mEditor->layers()->gotoNextLayer();
484 break;
485 case Qt::Key_Down:
486 mEditor->layers()->gotoPreviouslayer();
487 break;
488 case Qt::Key_Space:
489 if(editor()->tools()->setTemporaryTool(HAND, Qt::Key_Space, Qt::NoModifier))
490 {
491 return;
492 }
493 break;
494 default:
495 break;
496 }
497 event->ignore();
498}
499
500void ScribbleArea::keyReleaseEvent(QKeyEvent *event)
501{
502 // Don't handle this event on auto repeat
503 //
504 if (event->isAutoRepeat()) {
505 return;
506 }
507
508 if (event->key() == 0)
509 {
510 editor()->tools()->tryClearTemporaryTool(Qt::Key_unknown);
511 }
512 else
513 {
514 editor()->tools()->tryClearTemporaryTool(static_cast<Qt::Key>(event->key()));
515 }
516
517 if (isPointerInUse()) { return; }
518
519 if (currentTool()->keyReleaseEvent(event))
520 {
521 // has been handled by tool
522 return;
523 }
524}
525
526/************************************************************************************/
527// mouse and tablet event handlers
528void ScribbleArea::wheelEvent(QWheelEvent* event)
529{
530 // Don't change view if the tool is in use
531 if (isPointerInUse()) return;
532
533 static const bool isX11 = QGuiApplication::platformName() == "xcb";
534 const QPoint pixels = event->pixelDelta();
535 const QPoint angle = event->angleDelta();
536#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
537 const QPointF offset = mEditor->view()->mapScreenToCanvas(event->position());
538#else
539 const QPointF offset = mEditor->view()->mapScreenToCanvas(event->posF());
540#endif
541
542 const qreal currentScale = mEditor->view()->scaling();
543
544 // From the pixelDelta documentation: On X11 this value is driver-specific and unreliable, use angleDelta() instead
545 int delta = 0;
546 if (!isX11 && !pixels.isNull())
547 {
548 delta = pixels.y();
549 }
550 else if (!angle.isNull()) // Wheel based delta
551 {
552 delta = angle.y();
553 }
554
555 if (delta != 0) {
556 const qreal newScale = currentScale * std::pow(100, (delta * mDeltaFactor) / (12.0 * 120));
557 mEditor->view()->scaleAtOffset(newScale, offset);
558 }
559 event->accept();
560}
561
562void ScribbleArea::tabletEvent(QTabletEvent *e)
563{
564 PointerEvent event(e, mEditor->view()->mapScreenToCanvas(e->posF()));
565
566#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
567 if (event.pointerType() == QPointingDevice::PointerType::Eraser)
568#else
569 if (event.pointerType() == QTabletEvent::Eraser)
570#endif
571 {
572 editor()->tools()->tabletSwitchToEraser();
573 }
574 else
575 {
576 editor()->tools()->tabletRestorePrevTool();
577 }
578
579 if (event.eventType() == PointerEvent::Press)
580 {
581 event.accept();
582 if (mIsFirstClick)
583 {
584 mIsFirstClick = false;
585 mDoubleClickTimer->start();
586 pointerPressEvent(&event);
587 }
588 else
589 {
590 qreal distance = QLineF(e->posF(), mTabletPressPos).length();
591 mTabletPressPos = e->posF();
592
593 if (mDoubleClickMillis <= DOUBLE_CLICK_THRESHOLD && distance < 5.0) {
594 currentTool()->pointerDoubleClickEvent(&event);
595 }
596 else
597 {
598 // in case we handled the event as double click but really should have handled it as single click.
599 pointerPressEvent(&event);
600 }
601 }
602 mTabletInUse = event.isAccepted();
603 }
604 else if (event.eventType() == PointerEvent::Move)
605 {
606 if (!(event.buttons() & (Qt::LeftButton | Qt::RightButton)) || mTabletInUse)
607 {
608 pointerMoveEvent(&event);
609 }
610 }
611 else if (event.eventType() == PointerEvent::Release)
612 {
613 mTabletReleaseMillisAgo = 0;
614 mMouseFilterTimer->start();
615 if (mTabletInUse)
616 {
617 pointerReleaseEvent(&event);
618 mTabletInUse = false;
619 }
620 }
621
622#if defined Q_OS_LINUX
623 // Generate mouse events on linux to work around bug where the
624 // widget will not receive mouseEnter/mouseLeave
625 // events and the cursor will not update correctly.
626 // See https://codereview.qt-project.org/c/qt/qtbase/+/255384
627 // Scribblearea should not do anything with the mouse event when mTabletInUse is true.
628 event.ignore();
629#else
630 // Always accept so that mouse events are not generated (theoretically)
631 // Unfortunately Windows sometimes generates the events anyway
632 // As long as mTabletInUse is true, mouse events *should* be ignored even when
633 // the are generated
634 event.accept();
635#endif
636}
637
638void ScribbleArea::pointerPressEvent(PointerEvent* event)
639{
640 bool isCameraLayer = mEditor->layers()->currentLayer()->type() == Layer::CAMERA;
641 if ((currentTool()->type() != HAND || isCameraLayer) && (event->button() != Qt::RightButton) && (event->button() != Qt::MiddleButton || isCameraLayer))
642 {
643 Layer* layer = mEditor->layers()->currentLayer();
644 if (!layer->visible())
645 {
646 event->ignore();
647 // This needs to be async so that mTabletInUse is set to false before
648 // further events are created (modal dialogs do not currently block tablet events)
649 QTimer::singleShot(0, this, &ScribbleArea::showLayerNotVisibleWarning);
650 return;
651 }
652 }
653
654 if (event->buttons() & (Qt::MiddleButton | Qt::RightButton) &&
655 editor()->tools()->setTemporaryTool(HAND, event->buttons()))
656 {
657 currentTool()->pointerPressEvent(event);
658 } else if (event->button() == Qt::LeftButton)
659 {
660 currentTool()->pointerPressEvent(event);
661 }
662}
663
664void ScribbleArea::pointerMoveEvent(PointerEvent* event)
665{
666 currentTool()->pointerMoveEvent(event);
667}
668
669void ScribbleArea::pointerReleaseEvent(PointerEvent* event)
670{
671 currentTool()->pointerReleaseEvent(event);
672
673 editor()->tools()->tryClearTemporaryTool(event->button());
674}
675
676void ScribbleArea::handleDoubleClick()
677{
678 mDoubleClickMillis += 100;
679
680 if (mDoubleClickMillis >= DOUBLE_CLICK_THRESHOLD)
681 {
682 mDoubleClickMillis = 0;
683 mIsFirstClick = true;
684 mDoubleClickTimer->stop();
685 }
686}
687
688void ScribbleArea::tabletReleaseEventFired()
689{
690 // Under certain circumstances, a mouse press event will fire after a tablet release event.
691 // This causes unexpected behaviors for some tools, e.g., the bucket tool.
692 // The problem only seems to occur on Windows and only when tapping.
693 // Prior to this fix, the event queue would look like this:
694 // e.g.: TabletPress -> TabletRelease -> MousePress
695 // The following will filter mouse events created after a tablet release event.
696 mTabletReleaseMillisAgo += 50;
697
698 if (mTabletReleaseMillisAgo >= MOUSE_FILTER_THRESHOLD) {
699 mTabletReleaseMillisAgo = 0;
700 mMouseFilterTimer->stop();
701 }
702}
703
704void ScribbleArea::mousePressEvent(QMouseEvent* e)
705{
706 if (mTabletInUse || (mMouseFilterTimer->isActive() && mTabletReleaseMillisAgo < MOUSE_FILTER_THRESHOLD))
707 {
708 e->ignore();
709 return;
710 }
711
712 PointerEvent event(e, mEditor->view()->mapScreenToCanvas(e->localPos()));
713 pointerPressEvent(&event);
714 mMouseInUse = event.isAccepted();
715}
716
717void ScribbleArea::mouseMoveEvent(QMouseEvent* e)
718{
719 if (mTabletInUse || (mMouseFilterTimer->isActive() && mTabletReleaseMillisAgo < MOUSE_FILTER_THRESHOLD)) { e->ignore(); return; }
720
721 PointerEvent event(e, mEditor->view()->mapScreenToCanvas(e->localPos()));
722 pointerMoveEvent(&event);
723}
724
725void ScribbleArea::mouseReleaseEvent(QMouseEvent* e)
726{
727 if (mTabletInUse || (mMouseFilterTimer->isActive() && mTabletReleaseMillisAgo < MOUSE_FILTER_THRESHOLD)) { e->ignore(); return; }
728
729 PointerEvent event(e, mEditor->view()->mapScreenToCanvas(e->localPos()));
730 pointerReleaseEvent(&event);
731 mMouseInUse = (e->buttons() & Qt::RightButton) || (e->buttons() & Qt::LeftButton);
732}
733
734void ScribbleArea::mouseDoubleClickEvent(QMouseEvent* e)
735{
736 if (mTabletInUse) { e->ignore(); return; }
737
738 PointerEvent event(e, mEditor->view()->mapScreenToCanvas(e->localPos()));
739 currentTool()->pointerDoubleClickEvent(&event);
740}
741
742void ScribbleArea::resizeEvent(QResizeEvent* event)
743{
744 QWidget::resizeEvent(event);
745 mDevicePixelRatio = devicePixelRatioF();
746 mCanvas = QPixmap(QSizeF(size() * mDevicePixelRatio).toSize());
747 mCanvas.setDevicePixelRatio(mDevicePixelRatio);
748 mEditor->view()->setCanvasSize(size());
749
750 invalidateCacheForFrame(mEditor->currentFrame());
751 invalidatePainterCaches();
752 mCanvasPainter.reset();
753 mCameraPainter.reset();
754}
755
756void ScribbleArea::showLayerNotVisibleWarning()
757{
758 QMessageBox::warning(this, tr("Warning"),
759 tr("You are trying to modify a hidden layer! Please select another layer (or make the current layer visible)."),
760 QMessageBox::Ok,
761 QMessageBox::Ok);
762}
763
764void ScribbleArea::paintBitmapBuffer()
765{
766 LayerBitmap* layer = static_cast<LayerBitmap*>(mEditor->layers()->currentLayer());
767 Q_ASSERT(layer);
768 Q_ASSERT(layer->type() == Layer::BITMAP);
769
770 int frameNumber = mEditor->currentFrame();
771
772 // If there is no keyframe at or before the current position,
773 // just return (since we have nothing to paint on).
774 if (layer->getLastKeyFrameAtPosition(frameNumber) == nullptr)
775 {
776 updateFrame();
777 return;
778 }
779
780 BitmapImage* targetImage = currentBitmapImage(layer);
781 if (targetImage != nullptr)
782 {
783 QPainter::CompositionMode cm = QPainter::CompositionMode_SourceOver;
784 switch (currentTool()->type())
785 {
786 case ERASER:
787 cm = QPainter::CompositionMode_DestinationOut;
788 break;
789 case BRUSH:
790 case PEN:
791 case PENCIL:
792 break;
793 default: //nothing
794 break;
795 }
796 targetImage->paste(&mTiledBuffer, cm);
797 }
798
799 QRect rect = mEditor->view()->mapCanvasToScreen(mTiledBuffer.bounds()).toRect();
800
801 update(rect);
802
803 layer->setModified(frameNumber, true);
804 mTiledBuffer.clear();
805}
806
807void ScribbleArea::clearDrawingBuffer()
808{
809 mTiledBuffer.clear();
810}
811
812void ScribbleArea::handleDrawingOnEmptyFrame()
813{
814 auto layer = mEditor->layers()->currentLayer();
815
816 if (!layer || !layer->isPaintable())
817 {
818 return;
819 }
820
821 if (currentTool()->type() == ERASER) {
822 return;
823 }
824
825 int frameNumber = mEditor->currentFrame();
826 if (layer->getKeyFrameAt(frameNumber)) { return; }
827
828 // Drawing on an empty frame; take action based on preference.
829 int action = mPrefs->getInt(SETTING::DRAW_ON_EMPTY_FRAME_ACTION);
830 auto previousKeyFrame = layer->getLastKeyFrameAtPosition(frameNumber);
831 switch (action)
832 {
833 case KEEP_DRAWING_ON_PREVIOUS_KEY:
834 {
835 if (previousKeyFrame == nullptr) {
836 mEditor->addNewKey();
837 } else {
838 onFrameModified(previousKeyFrame->pos());
839 }
840 break;
841 }
842 case DUPLICATE_PREVIOUS_KEY:
843 {
844 if (previousKeyFrame != nullptr) {
845 KeyFrame* dupKey = previousKeyFrame->clone();
846 layer->addKeyFrame(frameNumber, dupKey);
847 mEditor->scrubTo(frameNumber);
848 break;
849 }
850 }
851 // if the previous keyframe doesn't exist,
852 // an empty keyframe needs to be created, so
853 // fallthrough
854 case CREATE_NEW_KEY:
855 mEditor->addNewKey();
856
857 // Refresh canvas
858 drawCanvas(frameNumber, mCanvas.rect());
859 update();
860 break;
861 default:
862 break;
863 }
864}
865
866void ScribbleArea::paintEvent(QPaintEvent* event)
867{
868 int currentFrame = mEditor->currentFrame();
869 if (!currentTool()->isActive())
870 {
871 // --- we retrieve the canvas from the cache; we create it if it doesn't exist
872 const int frameNumber = mEditor->layers()->lastFrameAtFrame(currentFrame);
873
874 if (frameNumber < 0)
875 {
876 drawCanvas(currentFrame, event->rect());
877 }
878 else
879 {
880 auto cacheKeyIter = mPixmapCacheKeys.find(static_cast<unsigned>(frameNumber));
881
882 if (cacheKeyIter == mPixmapCacheKeys.end() || !QPixmapCache::find(cacheKeyIter.value(), &mCanvas))
883 {
884 drawCanvas(currentFrame, event->rect());
885 mPixmapCacheKeys[static_cast<unsigned>(currentFrame)] = QPixmapCache::insert(mCanvas);
886 //qDebug() << "Repaint canvas!";
887 }
888 else
889 {
890 // Simply use the cached canvas from PixmapCache
891 }
892 }
893 }
894 else
895 {
896 prepCanvas(currentFrame);
897 prepCameraPainter(currentFrame);
898 prepOverlays(currentFrame);
899
900 mCanvasPainter.paintCached(event->rect());
901 mCameraPainter.paintCached(event->rect());
902 }
903
904 if (currentTool()->type() == MOVE)
905 {
906 Layer* layer = mEditor->layers()->currentLayer();
907 Q_CHECK_PTR(layer);
908 if (layer->type() == Layer::VECTOR)
909 {
910 VectorImage* vectorImage = currentVectorImage(layer);
911 if (vectorImage != nullptr)
912 {
913 vectorImage->setModified(true);
914 }
915 }
916 }
917
918 QPainter painter(this);
919
920 // paints the canvas
921 painter.setWorldMatrixEnabled(false);
922
923 // In other places we use the blitRect to paint the buffer pixmap, however
924 // the main pixmap which needs to be scaled accordingly to DPI, which is not accounted for when using the event rect
925 // instead we can set a clipRect to avoid the area being updated needlessly
926 painter.setClipRect(event->rect());
927 painter.drawPixmap(QPointF(), mCanvas);
928
929 currentTool()->paint(painter, event->rect());
930
931 if (!editor()->playback()->isPlaying()) // we don't need to display the following when the animation is playing
932 {
933 Layer* layer = mEditor->layers()->currentLayer();
934 if (layer->type() == Layer::VECTOR)
935 {
936 VectorImage* vectorImage = currentVectorImage(layer);
937 if (vectorImage != nullptr)
938 {
939 switch (currentTool()->type())
940 {
941 case SMUDGE:
942 case HAND:
943 {
944 auto selectMan = mEditor->select();
945 painter.save();
946 painter.setWorldMatrixEnabled(false);
947 painter.setRenderHint(QPainter::Antialiasing, false);
948 // ----- paints the edited elements
949 QPen pen2(Qt::black, 0.5, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
950 painter.setPen(pen2);
951 QColor color;
952 // ------------ vertices of the edited curves
953 color = QColor(200, 200, 200);
954 painter.setBrush(color);
955 VectorSelection vectorSelection = selectMan->vectorSelection;
956 for (int k = 0; k < vectorSelection.curve.size(); k++)
957 {
958 int curveNumber = vectorSelection.curve.at(k);
959
960 for (int vertexNumber = -1; vertexNumber < vectorImage->getCurveSize(curveNumber); vertexNumber++)
961 {
962 QPointF vertexPoint = vectorImage->getVertex(curveNumber, vertexNumber);
963 QRectF rectangle(mEditor->view()->mapCanvasToScreen(vertexPoint) - QPointF(3.0, 3.0), QSizeF(7, 7));
964 if (rect().contains(mEditor->view()->mapCanvasToScreen(vertexPoint).toPoint()))
965 {
966 painter.drawRect(rectangle);
967 }
968 }
969 }
970 // ------------ selected vertices of the edited curves
971 color = QColor(100, 100, 255);
972 painter.setBrush(color);
973 for (int k = 0; k < vectorSelection.vertex.size(); k++)
974 {
975 VertexRef vertexRef = vectorSelection.vertex.at(k);
976 QPointF vertexPoint = vectorImage->getVertex(vertexRef);
977 QRectF rectangle0 = QRectF(mEditor->view()->mapCanvasToScreen(vertexPoint) - QPointF(3.0, 3.0), QSizeF(7, 7));
978 painter.drawRect(rectangle0);
979 }
980 // ----- paints the closest vertices
981 color = QColor(255, 0, 0);
982 painter.setBrush(color);
983 QList<VertexRef> closestVertices = selectMan->closestVertices();
984 if (vectorSelection.curve.size() > 0)
985 {
986 for (int k = 0; k < closestVertices.size(); k++)
987 {
988 VertexRef vertexRef = closestVertices.at(k);
989 QPointF vertexPoint = vectorImage->getVertex(vertexRef);
990
991 QRectF rectangle = QRectF(mEditor->view()->mapCanvasToScreen(vertexPoint) - QPointF(3.0, 3.0), QSizeF(7, 7));
992 painter.drawRect(rectangle);
993 }
994 }
995 painter.restore();
996 break;
997 }
998 default:
999 {
1000 break;
1001 }
1002 } // end switch
1003 }
1004 }
1005
1006 mOverlayPainter.paint(painter, rect());
1007
1008 // paints the selection outline
1009 if (mEditor->select()->somethingSelected())
1010 {
1011 paintSelectionVisuals(painter);
1012 }
1013 }
1014
1015 // outlines the frame of the viewport
1016#ifdef _DEBUG
1017 painter.setWorldMatrixEnabled(false);
1018 painter.setPen(QPen(Qt::gray, 2));
1019 painter.setBrush(Qt::NoBrush);
1020 painter.drawRect(QRect(0, 0, width(), height()));
1021#endif
1022
1023 event->accept();
1024}
1025
1026void ScribbleArea::paintSelectionVisuals(QPainter &painter)
1027{
1028 Object* object = mEditor->object();
1029
1030 auto selectMan = mEditor->select();
1031
1032 QRectF currentSelectionRect = selectMan->mySelectionRect();
1033
1034 TransformParameters params = { currentSelectionRect, editor()->view()->getView(), selectMan->selectionTransform() };
1035
1036 mSelectionPainter.paint(painter,
1037 object,
1038 mEditor->currentLayerIndex(),
1039 static_cast<const TransformTool*>(editor()->tools()->getTool(SELECT))->transformSettings(),
1040 params);
1041 emit selectionUpdated();
1042}
1043
1044BitmapImage* ScribbleArea::currentBitmapImage(Layer* layer) const
1045{
1046 Q_ASSERT(layer->type() == Layer::BITMAP);
1047 auto bitmapLayer = static_cast<LayerBitmap*>(layer);
1048 return bitmapLayer->getLastBitmapImageAtFrame(mEditor->currentFrame());
1049}
1050
1051VectorImage* ScribbleArea::currentVectorImage(Layer* layer) const
1052{
1053 Q_ASSERT(layer->type() == Layer::VECTOR);
1054 auto vectorLayer = static_cast<LayerVector*>(layer);
1055 return vectorLayer->getLastVectorImageAtFrame(mEditor->currentFrame(), 0);
1056}
1057
1058void ScribbleArea::prepCameraPainter(int frame)
1059{
1060 Object* object = mEditor->object();
1061
1062 mCameraPainter.preparePainter(object,
1063 mEditor->currentLayerIndex(),
1064 frame,
1065 mEditor->view()->getView(),
1066 mEditor->playback()->isPlaying(),
1067 mLayerVisibility,
1068 mPrefs->getFloat(SETTING::LAYER_VISIBILITY_THRESHOLD),
1069 mEditor->view()->getScaleInversed());
1070
1071 OnionSkinPainterOptions onionSkinOptions;
1072 onionSkinOptions.enabledWhilePlaying = mPrefs->getInt(SETTING::ONION_WHILE_PLAYBACK);
1073 onionSkinOptions.isPlaying = mEditor->playback()->isPlaying();
1074 onionSkinOptions.isAbsolute = (mPrefs->getString(SETTING::ONION_TYPE) == "absolute");
1075 onionSkinOptions.skinPrevFrames = mPrefs->isOn(SETTING::PREV_ONION);
1076 onionSkinOptions.skinNextFrames = mPrefs->isOn(SETTING::NEXT_ONION);
1077 onionSkinOptions.colorizePrevFrames = mPrefs->isOn(SETTING::ONION_RED);
1078 onionSkinOptions.colorizeNextFrames = mPrefs->isOn(SETTING::ONION_BLUE);
1079 onionSkinOptions.framesToSkinPrev = mPrefs->getInt(SETTING::ONION_PREV_FRAMES_NUM);
1080 onionSkinOptions.framesToSkinNext = mPrefs->getInt(SETTING::ONION_NEXT_FRAMES_NUM);
1081 onionSkinOptions.maxOpacity = mPrefs->getInt(SETTING::ONION_MAX_OPACITY);
1082 onionSkinOptions.minOpacity = mPrefs->getInt(SETTING::ONION_MIN_OPACITY);
1083
1084 mCameraPainter.setOnionSkinPainterOptions(onionSkinOptions);
1085}
1086
1087void ScribbleArea::prepCanvas(int frame)
1088{
1089 Object* object = mEditor->object();
1090
1091 CanvasPainterOptions o;
1092 o.bOnionSkinMultiLayer = mPrefs->isOn(SETTING::ONION_MUTLIPLE_LAYERS);
1093 o.bAntiAlias = mPrefs->isOn(SETTING::ANTIALIAS);
1094 o.bThinLines = mPrefs->isOn(SETTING::INVISIBLE_LINES);
1095 o.bOutlines = mPrefs->isOn(SETTING::OUTLINES);
1096 o.eLayerVisibility = mLayerVisibility;
1097 o.fLayerVisibilityThreshold = mPrefs->getFloat(SETTING::LAYER_VISIBILITY_THRESHOLD);
1098 o.scaling = mEditor->view()->scaling();
1099 o.cmBufferBlendMode = mEditor->tools()->currentTool()->type() == ToolType::ERASER ? QPainter::CompositionMode_DestinationOut : QPainter::CompositionMode_SourceOver;
1100
1101 OnionSkinPainterOptions onionSkinOptions;
1102 onionSkinOptions.enabledWhilePlaying = mPrefs->getInt(SETTING::ONION_WHILE_PLAYBACK);
1103 onionSkinOptions.isPlaying = mEditor->playback()->isPlaying();
1104 onionSkinOptions.isAbsolute = (mPrefs->getString(SETTING::ONION_TYPE) == "absolute");
1105 onionSkinOptions.skinPrevFrames = mPrefs->isOn(SETTING::PREV_ONION);
1106 onionSkinOptions.skinNextFrames = mPrefs->isOn(SETTING::NEXT_ONION);
1107 onionSkinOptions.colorizePrevFrames = mPrefs->isOn(SETTING::ONION_RED);
1108 onionSkinOptions.colorizeNextFrames = mPrefs->isOn(SETTING::ONION_BLUE);
1109 onionSkinOptions.framesToSkinPrev = mPrefs->getInt(SETTING::ONION_PREV_FRAMES_NUM);
1110 onionSkinOptions.framesToSkinNext = mPrefs->getInt(SETTING::ONION_NEXT_FRAMES_NUM);
1111 onionSkinOptions.maxOpacity = mPrefs->getInt(SETTING::ONION_MAX_OPACITY);
1112 onionSkinOptions.minOpacity = mPrefs->getInt(SETTING::ONION_MIN_OPACITY);
1113
1114 mCanvasPainter.setOnionSkinOptions(onionSkinOptions);
1115 mCanvasPainter.setOptions(o);
1116
1117 ViewManager* vm = mEditor->view();
1118 SelectionManager* sm = mEditor->select();
1119 mCanvasPainter.setViewTransform(vm->getView(), vm->getViewInverse());
1120 mCanvasPainter.setTransformedSelection(sm->mySelectionRect().toRect(), sm->selectionTransform());
1121
1122 mCanvasPainter.setPaintSettings(object, mEditor->layers()->currentLayerIndex(), frame, &mTiledBuffer);
1123}
1124
1125void ScribbleArea::drawCanvas(int frame, QRect rect)
1126{
1127 prepCanvas(frame);
1128 prepCameraPainter(frame);
1129 prepOverlays(frame);
1130 mCanvasPainter.paint(rect);
1131 mCameraPainter.paint(rect);
1132}
1133
1134void ScribbleArea::setGaussianGradient(QGradient &gradient, QColor color, qreal opacity, qreal offset)
1135{
1136 if (offset < 0) { offset = 0; }
1137 if (offset > 100) { offset = 100; }
1138
1139 int r = color.red();
1140 int g = color.green();
1141 int b = color.blue();
1142 qreal a = color.alphaF();
1143
1144 int mainColorAlpha = qRound(a * 255 * opacity);
1145
1146 // the more feather (offset), the more softness (opacity)
1147 int alphaAdded = qRound((mainColorAlpha * offset) / 100);
1148
1149 gradient.setColorAt(0.0, QColor(r, g, b, mainColorAlpha - alphaAdded));
1150 gradient.setColorAt(1.0, QColor(r, g, b, 0));
1151 gradient.setColorAt(1.0 - (offset / 100.0), QColor(r, g, b, mainColorAlpha - alphaAdded));
1152}
1153
1154void ScribbleArea::drawPath(QPainterPath path, QPen pen, QBrush brush, QPainter::CompositionMode cm)
1155{
1156 mTiledBuffer.drawPath(mEditor->view()->mapScreenToCanvas(path), pen, brush, cm, mPrefs->isOn(SETTING::ANTIALIAS));
1157}
1158
1159void ScribbleArea::drawPen(QPointF thePoint, qreal brushWidth, QColor fillColor, bool useAA)
1160{
1161 // We use Source as opposed to SourceOver here to avoid the dabs being added on top of each other
1162 mTiledBuffer.drawBrush(thePoint, brushWidth, Qt::NoPen, QBrush(fillColor, Qt::SolidPattern), QPainter::CompositionMode_Source, useAA);
1163}
1164
1165void ScribbleArea::drawPencil(QPointF thePoint, qreal brushWidth, qreal fixedBrushFeather, QColor fillColor, qreal opacity)
1166{
1167 drawBrush(thePoint, brushWidth, fixedBrushFeather, fillColor, QPainter::CompositionMode_SourceOver, opacity, true);
1168}
1169
1170void ScribbleArea::drawBrush(QPointF thePoint, qreal brushWidth, qreal mOffset, QColor fillColor, QPainter::CompositionMode compMode, qreal opacity, bool usingFeather, bool useAA)
1171{
1172 QBrush brush;
1173 if (usingFeather)
1174 {
1175 QRadialGradient radialGrad(thePoint, 0.5 * brushWidth);
1176 setGaussianGradient(radialGrad, fillColor, opacity, mOffset);
1177 brush = radialGrad;
1178 }
1179 else
1180 {
1181 brush = QBrush(fillColor, Qt::SolidPattern);
1182 }
1183 mTiledBuffer.drawBrush(thePoint, brushWidth, Qt::NoPen, brush, compMode, useAA);
1184}
1185
1186void ScribbleArea::drawPolyline(QPainterPath path, QPen pen, bool useAA)
1187{
1188 BlitRect blitRect;
1189
1190 // In order to clear what was previously dirty, we need to include the previous buffer bound
1191 // this ensures that we won't see stroke artifacts
1192 blitRect.extend(mEditor->view()->mapCanvasToScreen(mTiledBuffer.bounds()).toRect());
1193
1194 QRect updateRect = mEditor->view()->mapCanvasToScreen(path.boundingRect()).toRect();
1195 // Now extend with the new path bounds mapped to the local coordinate
1196 blitRect.extend(updateRect);
1197
1198 mTiledBuffer.clear();
1199 mTiledBuffer.drawPath(path, pen, Qt::NoBrush, QPainter::CompositionMode_SourceOver, useAA);
1200
1201 // And update only the affected area
1202 update(blitRect.adjusted(-1, -1, 1, 1));
1203}
1204
1205void ScribbleArea::endStroke()
1206{
1207 if (mEditor->layers()->currentLayer()->type() == Layer::BITMAP) {
1208 paintBitmapBuffer();
1209 }
1210
1211 onFrameModified(mEditor->currentFrame());
1212}
1213
1214void ScribbleArea::flipSelection(bool flipVertical)
1215{
1216 mEditor->select()->flipSelection(flipVertical);
1217}
1218
1219void ScribbleArea::prepOverlays(int frame)
1220{
1221 OverlayPainterOptions o;
1222
1223 o.bGrid = mPrefs->isOn(SETTING::GRID);
1224 o.nGridSizeW = mPrefs->getInt(SETTING::GRID_SIZE_W);
1225 o.nGridSizeH = mPrefs->getInt(SETTING::GRID_SIZE_H);
1226 o.bCenter = mPrefs->isOn(SETTING::OVERLAY_CENTER);
1227 o.bThirds = mPrefs->isOn(SETTING::OVERLAY_THIRDS);
1228 o.bGoldenRatio = mPrefs->isOn(SETTING::OVERLAY_GOLDEN);
1229 o.bSafeArea = mPrefs->isOn(SETTING::OVERLAY_SAFE);
1230 o.bPerspective1 = mPrefs->isOn(SETTING::OVERLAY_PERSPECTIVE1);
1231 o.bPerspective2 = mPrefs->isOn(SETTING::OVERLAY_PERSPECTIVE2);
1232 o.bPerspective3 = mPrefs->isOn(SETTING::OVERLAY_PERSPECTIVE3);
1233 o.nOverlayAngle = mPrefs->getInt(SETTING::OVERLAY_ANGLE);
1234 o.bActionSafe = mPrefs->isOn(SETTING::ACTION_SAFE_ON);
1235 o.nActionSafe = mPrefs->getInt(SETTING::ACTION_SAFE);
1236 o.bShowSafeAreaHelperText = mPrefs->isOn(SETTING::OVERLAY_SAFE_HELPER_TEXT_ON);
1237 o.bTitleSafe = mPrefs->isOn(SETTING::TITLE_SAFE_ON);
1238 o.nTitleSafe = mPrefs->getInt(SETTING::TITLE_SAFE);
1239 o.nOverlayAngle = mPrefs->getInt(SETTING::OVERLAY_ANGLE);
1240 o.bShowHandle = mEditor->tools()->currentTool()->type() == MOVE && mEditor->layers()->currentLayer()->type() != Layer::CAMERA;
1241
1242 o.mSinglePerspPoint = mEditor->overlays()->getSinglePerspectivePoint();
1243 o.mLeftPerspPoint = mEditor->overlays()->getLeftPerspectivePoint();
1244 o.mRightPerspPoint = mEditor->overlays()->getRightPerspectivePoint();
1245 o.mMiddlePerspPoint = mEditor->overlays()->getMiddlePerspectivePoint();
1246
1247 o.nFrameIndex = frame;
1248
1249 mOverlayPainter.setOptions(o);
1250 mOverlayPainter.preparePainter(mEditor->layers()->getCameraLayerBelow(mEditor->currentLayerIndex()), palette());
1251
1252 ViewManager* vm = mEditor->view();
1253 mOverlayPainter.setViewTransform(vm->getView());
1254}
1255
1256void ScribbleArea::blurBrush(BitmapImage *bmiSource_, QPointF srcPoint_, QPointF thePoint_, qreal brushWidth_, qreal mOffset_, qreal opacity_)
1257{
1258 QRadialGradient radialGrad(thePoint_, 0.5 * brushWidth_);
1259 setGaussianGradient(radialGrad, QColor(255, 255, 255, 127), opacity_, mOffset_);
1260
1261 QRectF srcRect(srcPoint_.x() - 0.5 * brushWidth_, srcPoint_.y() - 0.5 * brushWidth_, brushWidth_, brushWidth_);
1262 QRectF trgRect(thePoint_.x() - 0.5 * brushWidth_, thePoint_.y() - 0.5 * brushWidth_, brushWidth_, brushWidth_);
1263
1264 BitmapImage bmiSrcClip = bmiSource_->copy(srcRect.toAlignedRect());
1265 BitmapImage bmiTmpClip = bmiSrcClip; // TODO: find a shorter way
1266
1267 bmiTmpClip.drawRect(srcRect, Qt::NoPen, radialGrad, QPainter::CompositionMode_Source, true);
1268 bmiSrcClip.bounds().moveTo(trgRect.topLeft().toPoint());
1269 bmiTmpClip.paste(&bmiSrcClip, QPainter::CompositionMode_SourceIn);
1270 mTiledBuffer.drawImage(*bmiTmpClip.image(), bmiTmpClip.bounds(), QPainter::CompositionMode_SourceOver, mPrefs->isOn(SETTING::ANTIALIAS));
1271}
1272
1273void ScribbleArea::liquifyBrush(BitmapImage *bmiSource_, QPointF srcPoint_, QPointF thePoint_, qreal brushWidth_, qreal mOffset_, qreal opacity_)
1274{
1275 QPointF delta = (thePoint_ - srcPoint_); // increment vector
1276 QRectF trgRect(thePoint_.x() - 0.5 * brushWidth_, thePoint_.y() - 0.5 * brushWidth_, brushWidth_, brushWidth_);
1277
1278 QRadialGradient radialGrad(thePoint_, 0.5 * brushWidth_);
1279 setGaussianGradient(radialGrad, QColor(255, 255, 255, 255), opacity_, mOffset_);
1280
1281 // Create gradient brush
1282 BitmapImage bmiTmpClip;
1283 bmiTmpClip.drawRect(trgRect, Qt::NoPen, radialGrad, QPainter::CompositionMode_Source, mPrefs->isOn(SETTING::ANTIALIAS));
1284
1285 // Slide texture/pixels of the source image
1286 qreal factor, factorGrad;
1287
1288 for (int yb = bmiTmpClip.top(); yb < bmiTmpClip.bottom(); yb++)
1289 {
1290 for (int xb = bmiTmpClip.left(); xb < bmiTmpClip.right(); xb++)
1291 {
1292 QColor color;
1293 color.setRgba(bmiTmpClip.pixel(xb, yb));
1294 factorGrad = color.alphaF(); // any from r g b a is ok
1295
1296 int xa = xb - factorGrad * delta.x();
1297 int ya = yb - factorGrad * delta.y();
1298
1299 color.setRgba(bmiSource_->pixel(xa, ya));
1300 factor = color.alphaF();
1301
1302 if (factor > 0.0)
1303 {
1304 color.setRed(color.red() / factor);
1305 color.setGreen(color.green() / factor);
1306 color.setBlue(color.blue() / factor);
1307 color.setAlpha(255); // Premultiplied color
1308
1309 color.setRed(color.red()*factorGrad);
1310 color.setGreen(color.green()*factorGrad);
1311 color.setBlue(color.blue()*factorGrad);
1312 color.setAlpha(255 * factorGrad); // Premultiplied color
1313
1314 bmiTmpClip.setPixel(xb, yb, color.rgba());
1315 }
1316 else
1317 {
1318 bmiTmpClip.setPixel(xb, yb, qRgba(255, 255, 255, 0));
1319 }
1320 }
1321 }
1322 mTiledBuffer.drawImage(*bmiTmpClip.image(), bmiTmpClip.bounds(), QPainter::CompositionMode_SourceOver, mPrefs->isOn(SETTING::ANTIALIAS));
1323}
1324
1325/************************************************************************************/
1326// view handling
1327
1328QPointF ScribbleArea::getCentralPoint()
1329{
1330 return mEditor->view()->mapScreenToCanvas(QPointF(width() / 2, height() / 2));
1331}
1332
1333void ScribbleArea::applyTransformedSelection()
1334{
1335 mCanvasPainter.ignoreTransformedSelection();
1336
1337 Layer* layer = mEditor->layers()->currentLayer();
1338
1339 bool useAA = mEditor->tools()->getTool(ToolType::MOVE)->toolProperties().getInfo(TransformToolProperties::ANTI_ALIASING_ENABLED).boolValue();
1340
1341 if (layer == nullptr) { return; }
1342
1343 auto selectMan = mEditor->select();
1344 if (selectMan->somethingSelected())
1345 {
1346 if (selectMan->mySelectionRect().isEmpty() || selectMan->selectionTransform().isIdentity()) { return; }
1347
1348 if (layer->type() == Layer::BITMAP)
1349 {
1350 handleDrawingOnEmptyFrame();
1351 BitmapImage* bitmapImage = currentBitmapImage(layer);
1352 if (bitmapImage == nullptr) { return; }
1353 BitmapImage transformedImage = bitmapImage->transformed(selectMan->mySelectionRect().toRect(), selectMan->selectionTransform(), useAA);
1354
1355
1356 bitmapImage->clear(selectMan->mySelectionRect());
1357 bitmapImage->paste(&transformedImage, QPainter::CompositionMode_SourceOver);
1358 }
1359 else if (layer->type() == Layer::VECTOR)
1360 {
1361 // Unfortunately this doesn't work right currently so vector transforms
1362 // will always be applied on the previous keyframe when on an empty frame
1363 //handleDrawingOnEmptyFrame();
1364 VectorImage* vectorImage = currentVectorImage(layer);
1365 if (vectorImage == nullptr) { return; }
1366
1367 vectorImage->applySelectionTransformation();
1368 }
1369
1370 mEditor->setModified(mEditor->layers()->currentLayerIndex(), mEditor->currentFrame());
1371 }
1372
1373 updateFrame();
1374}
1375
1376void ScribbleArea::cancelTransformedSelection()
1377{
1378 mCanvasPainter.ignoreTransformedSelection();
1379
1380 auto selectMan = mEditor->select();
1381 if (selectMan->somethingSelected())
1382 {
1383 Layer* layer = mEditor->layers()->currentLayer();
1384 if (layer == nullptr) { return; }
1385
1386 if (layer->type() == Layer::VECTOR)
1387 {
1388 VectorImage* vectorImage = currentVectorImage(layer);
1389 if (vectorImage != nullptr)
1390 {
1391 vectorImage->setSelectionTransformation(QTransform());
1392 }
1393 }
1394
1395 mEditor->select()->setSelection(selectMan->mySelectionRect(), false);
1396
1397 selectMan->resetSelectionProperties();
1398 mOriginalPolygonF = QPolygonF();
1399
1400 mEditor->setModified(mEditor->layers()->currentLayerIndex(), mEditor->currentFrame());
1401 updateFrame();
1402 }
1403}
1404
1405void ScribbleArea::toggleThinLines()
1406{
1407 bool previousValue = mPrefs->isOn(SETTING::INVISIBLE_LINES);
1408 setEffect(SETTING::INVISIBLE_LINES, !previousValue);
1409}
1410
1411void ScribbleArea::setLayerVisibility(LayerVisibility visibility)
1412{
1413 mLayerVisibility = visibility;
1414 mPrefs->set(SETTING::LAYER_VISIBILITY, static_cast<int>(mLayerVisibility));
1415
1416 invalidateAllCache();
1417}
1418
1419void ScribbleArea::increaseLayerVisibilityIndex()
1420{
1421 ++mLayerVisibility;
1422 mPrefs->set(SETTING::LAYER_VISIBILITY, static_cast<int>(mLayerVisibility));
1423
1424 invalidateAllCache();
1425}
1426
1427void ScribbleArea::decreaseLayerVisibilityIndex()
1428{
1429 --mLayerVisibility;
1430 mPrefs->set(SETTING::LAYER_VISIBILITY, static_cast<int>(mLayerVisibility));
1431
1432 invalidateAllCache();
1433}
1434
1435/************************************************************************************/
1436// tool handling
1437
1438BaseTool* ScribbleArea::currentTool() const
1439{
1440 return editor()->tools()->currentTool();
1441}
1442
1443void ScribbleArea::deleteSelection()
1444{
1445 auto selectMan = mEditor->select();
1446 if (selectMan->somethingSelected()) // there is something selected
1447 {
1448 Layer* layer = mEditor->layers()->currentLayer();
1449 if (layer == nullptr) { return; }
1450
1451 handleDrawingOnEmptyFrame();
1452
1453 mEditor->backup(tr("Delete Selection", "Undo Step: clear the selection area."));
1454
1455 selectMan->clearCurves();
1456 if (layer->type() == Layer::VECTOR)
1457 {
1458 VectorImage* vectorImage = currentVectorImage(layer);
1459 Q_CHECK_PTR(vectorImage);
1460 vectorImage->deleteSelection();
1461 }
1462 else if (layer->type() == Layer::BITMAP)
1463 {
1464 BitmapImage* bitmapImage = currentBitmapImage(layer);
1465 Q_CHECK_PTR(bitmapImage);
1466 bitmapImage->clear(selectMan->mySelectionRect());
1467 }
1468 mEditor->setModified(mEditor->currentLayerIndex(), mEditor->currentFrame());
1469 }
1470}
1471
1472void ScribbleArea::clearImage()
1473{
1474 Layer* layer = mEditor->layers()->currentLayer();
1475 if (layer == nullptr) { return; }
1476
1477 if (layer->type() == Layer::VECTOR)
1478 {
1479 mEditor->backup(tr("Clear Image", "Undo step text"));
1480
1481 VectorImage* vectorImage = currentVectorImage(layer);
1482 if (vectorImage != nullptr)
1483 {
1484 vectorImage->clear();
1485 }
1486 mEditor->select()->clearCurves();
1487 mEditor->select()->clearVertices();
1488 }
1489 else if (layer->type() == Layer::BITMAP)
1490 {
1491 mEditor->backup(tr("Clear Image", "Undo step text"));
1492
1493 BitmapImage* bitmapImage = currentBitmapImage(layer);
1494 if (bitmapImage == nullptr) return;
1495 bitmapImage->clear();
1496 }
1497 else
1498 {
1499 return; // skip updates when nothing changes
1500 }
1501 mEditor->setModified(mEditor->layers()->currentLayerIndex(), mEditor->currentFrame());
1502}
1503
1504void ScribbleArea::paletteColorChanged(QColor color)
1505{
1506 Q_UNUSED(color)
1507
1508 for (int i = 0; i < mEditor->layers()->count(); i++)
1509 {
1510 Layer* layer = mEditor->layers()->getLayer(i);
1511 if (layer->type() == Layer::VECTOR)
1512 {
1513 VectorImage* vectorImage = static_cast<LayerVector*>(layer)->getVectorImageAtFrame(mEditor->currentFrame());
1514 if (vectorImage != nullptr)
1515 {
1516 vectorImage->modification();
1517 }
1518 }
1519 }
1520
1521 invalidateAllCache();
1522}
BaseTool
Definition: basetool.h:47
BitmapImage
Definition: bitmapimage.h:28
BlitRect
Definition: blitrect.h:25
Editor::addNewKey
KeyFrame * addNewKey()
Attempts to create a new keyframe at the current frame and layer.
Definition: editor.cpp:905
Editor::frameModified
void frameModified(int frameNumber)
This should be emitted after modifying the frame content.
KeyFrame
Definition: keyframe.h:30
LayerBitmap
Definition: layerbitmap.h:26
Layer
Definition: layer.h:33
Layer::dirtyFrames
QList< int > dirtyFrames() const
Returns a list of dirty frame positions.
Definition: layer.h:166
Layer::addKeyFrame
virtual bool addKeyFrame(int position, KeyFrame *pKeyFrame)
Adds a keyframe at the given position, unless one already exists.
Definition: layer.cpp:191
Layer::clearDirtyFrames
void clearDirtyFrames()
Clear the list of dirty keyframes.
Definition: layer.h:173
LayerVector
Definition: layervector.h:26
Object
Definition: object.h:42
PointerEvent
Definition: pointerevent.h:8
PointerEvent::button
Qt::MouseButton button() const
Returns Qt::MouseButton()
Definition: pointerevent.cpp:38
PointerEvent::buttons
Qt::MouseButtons buttons() const
Returns Qt::MouseButtons()
Definition: pointerevent.cpp:54
ScribbleArea::onScrubbed
void onScrubbed(int frameNumber)
Frame scrubbed, invalidate relevant cache.
Definition: scribblearea.cpp:315
ScribbleArea::onViewChanged
void onViewChanged()
View updated, invalidate relevant cache.
Definition: scribblearea.cpp:341
ScribbleArea::onObjectLoaded
void onObjectLoaded()
Object updated, invalidate all cache.
Definition: scribblearea.cpp:363
ScribbleArea::invalidateCacheForDirtyFrames
void invalidateCacheForDirtyFrames()
invalidate cache for dirty keyframes.
Definition: scribblearea.cpp:209
ScribbleArea::updateFrame
void updateFrame()
Update frame.
Definition: scribblearea.cpp:200
ScribbleArea::handleDrawingOnEmptyFrame
void handleDrawingOnEmptyFrame()
Call this when starting to use a paint tool.
Definition: scribblearea.cpp:812
ScribbleArea::invalidatePainterCaches
void invalidatePainterCaches()
Invalidate the layer pixmap and camera painter caches.
Definition: scribblearea.cpp:284
ScribbleArea::invalidateOnionSkinsCacheAround
void invalidateOnionSkinsCacheAround(int frame)
invalidate onion skin cache around frame
Definition: scribblearea.cpp:220
ScribbleArea::onSelectionChanged
void onSelectionChanged()
Selection was changed, keep cache.
Definition: scribblearea.cpp:351
ScribbleArea::onOnionSkinTypeChanged
void onOnionSkinTypeChanged()
Onion skin type changed, all frames will be affected.
Definition: scribblearea.cpp:358
ScribbleArea::onFramesModified
void onFramesModified()
Multiple frames modified, invalidate cache for affected frames.
Definition: scribblearea.cpp:322
ScribbleArea::onToolChanged
void onToolChanged(ToolType)
Tool changed, invalidate cache and frame if needed.
Definition: scribblearea.cpp:291
ScribbleArea::onPlayStateChanged
void onPlayStateChanged()
Playstate changed, invalidate relevant cache.
Definition: scribblearea.cpp:301
ScribbleArea::invalidateAllCache
void invalidateAllCache()
Invalidate all cache.
Definition: scribblearea.cpp:261
ScribbleArea::invalidateCacheForFrame
void invalidateCacheForFrame(int frameNumber)
Invalidate cache for the given frame.
Definition: scribblearea.cpp:273
ScribbleArea::onLayerChanged
void onLayerChanged()
Layer changed, invalidate relevant cache.
Definition: scribblearea.cpp:346
ScribbleArea::onFrameModified
void onFrameModified(int frameNumber)
Frame modified, invalidate cache for frame if any.
Definition: scribblearea.cpp:331
SelectionManager
The SelectionManager class acts as the "Brain" of the selection system.
Definition: selectionmanager.h:51
SelectionManager::flipSelection
void flipSelection(bool flipVertical)
ScribbleArea::flipSelection flip selection along the X or Y axis.
Definition: selectionmanager.cpp:328
SelectionManager::setSelection
void setSelection(QRectF rect, bool roundPixels=false)
Defines the selection area.
Definition: selectionmanager.cpp:273
Tile
Definition: tile.h:24
TiledBuffer
Definition: tiledbuffer.h:50
TiledBuffer::drawPath
void drawPath(QPainterPath path, QPen pen, QBrush brush, QPainter::CompositionMode cm, bool antialiasing)
Draws a path with the specified parameters to the tiled buffer.
Definition: tiledbuffer.cpp:128
TiledBuffer::clear
void clear()
Clears the content of the tiled buffer.
Definition: tiledbuffer.cpp:161
TiledBuffer::drawImage
void drawImage(const QImage &image, const QRect &imageBounds, QPainter::CompositionMode cm, bool antialiasing)
Draws a image with the specified parameters to the tiled buffer.
Definition: tiledbuffer.cpp:99
TiledBuffer::drawBrush
void drawBrush(QPointF point, qreal brushWidth, QPen pen, QBrush brush, QPainter::CompositionMode cm, bool antialiasing)
Draws a brush with the specified parameters to the tiled buffer.
Definition: tiledbuffer.cpp:51
TransformTool
Definition: transformtool.h:23
VectorImage
Definition: vectorimage.h:32
VectorImage::getVertex
QPointF getVertex(int curveNumber, int vertexNumber)
VectorImage::getVertex.
Definition: vectorimage.cpp:1566
VectorImage::clear
void clear()
VectorImage::clear.
Definition: vectorimage.cpp:1249
VectorImage::applySelectionTransformation
void applySelectionTransformation()
VectorImage::applySelectionTransformation.
Definition: vectorimage.cpp:1274
VectorImage::setSelectionTransformation
void setSelectionTransformation(QTransform transform)
VectorImage::setSelectionTransformation.
Definition: vectorimage.cpp:897
VectorImage::getCurveSize
int getCurveSize(int curveNumber)
VectorImage::getCurveSize.
Definition: vectorimage.cpp:1704
VectorImage::deleteSelection
void deleteSelection()
VectorImage::deleteSelection.
Definition: vectorimage.cpp:906
VectorSelection
Definition: vectorselection.h:26
VertexRef
Definition: vertexref.h:22
ViewManager
Definition: viewmanager.h:26
ViewManager::getScaleInversed
qreal getScaleInversed() const
Definition: viewmanager.cpp:111
QBrush
QColor
QColor::alphaF
qreal alphaF() const const
QColor::blue
int blue() const const
QColor::green
int green() const const
QColor::red
int red() const const
QColor::rgba
QRgb rgba() const const
QColor::setAlpha
void setAlpha(int alpha)
QColor::setBlue
void setBlue(int blue)
QColor::setGreen
void setGreen(int green)
QColor::setRed
void setRed(int red)
QColor::setRgba
void setRgba(QRgb rgba)
QEnterEvent
QEvent
QEvent::WindowDeactivate
WindowDeactivate
QEvent::ignore
void ignore()
QEvent::type
QEvent::Type type() const const
QGradient
QGradient::setColorAt
void setColorAt(qreal position, const QColor &color)
QGuiApplication::platformName
platformName
QKeyEvent
QKeyEvent::isAutoRepeat
bool isAutoRepeat() const const
QKeyEvent::key
int key() const const
QKeyEvent::modifiers
Qt::KeyboardModifiers modifiers() const const
QLineF
QLineF::length
qreal length() const const
QList
QList::at
const T & at(int i) const const
QList::size
int size() const const
QMap::clear
void clear()
QMap::end
QMap::iterator end()
QMap::find
QMap::iterator find(const Key &key)
QMap::remove
int remove(const Key &key)
QMessageBox::Ok
Ok
QMessageBox::warning
QMessageBox::StandardButton warning(QWidget *parent, const QString &title, const QString &text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton)
QMouseEvent
QMouseEvent::buttons
Qt::MouseButtons buttons() const const
QMouseEvent::localPos
const QPointF & localPos() const const
QObject::connect
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QObject::tr
QString tr(const char *sourceText, const char *disambiguation, int n)
QPaintDevice::devicePixelRatioF
qreal devicePixelRatioF() const const
QPainter
QPainter::CompositionMode
CompositionMode
QPainter::Antialiasing
Antialiasing
QPainterPath
QPainterPath::boundingRect
QRectF boundingRect() const const
QPaintEvent
QPaintEvent::rect
const QRect & rect() const const
QPen
QPixmap
QPixmap::rect
QRect rect() const const
QPixmap::setDevicePixelRatio
void setDevicePixelRatio(qreal scaleFactor)
QPixmapCache::find
QPixmap * find(const QString &key)
QPixmapCache::clear
void clear()
QPixmapCache::insert
bool insert(const QString &key, const QPixmap &pixmap)
QPixmapCache::remove
void remove(const QString &key)
QPixmapCache::setCacheLimit
void setCacheLimit(int n)
QPoint
QPoint::isNull
bool isNull() const const
QPoint::y
int y() const const
QPointF
QPointF::toPoint
QPoint toPoint() const const
QPointF::x
qreal x() const const
QPointF::y
qreal y() const const
QPolygonF
QRadialGradient
QRect
QRect::adjusted
QRect adjusted(int dx1, int dy1, int dx2, int dy2) const const
QRect::moveTo
void moveTo(int x, int y)
QRectF
QRectF::toAlignedRect
QRect toAlignedRect() const const
QRectF::toRect
QRect toRect() const const
QResizeEvent
QSizeF
QSizePolicy
QSizePolicy::MinimumExpanding
MinimumExpanding
Qt::NoBrush
NoBrush
Qt::black
black
Qt::Key_Right
Key_Right
Qt::ControlModifier
ControlModifier
Qt::LeftButton
LeftButton
Qt::RoundCap
RoundCap
Qt::RoundJoin
RoundJoin
Qt::SolidLine
SolidLine
Qt::WA_StaticContents
WA_StaticContents
QTabletEvent
QTabletEvent::Eraser
Eraser
QTabletEvent::posF
const QPointF & posF() const const
QTimer
QTimer::setInterval
void setInterval(int msec)
QTimer::isActive
bool isActive() const const
QTimer::singleShot
singleShot
QTimer::start
void start(int msec)
QTimer::stop
void stop()
QTimer::timeout
void timeout()
QTransform
QTransform::mapRect
QRect mapRect(const QRect &rectangle) const const
QWheelEvent::posF
const QPointF & posF() const const
QWheelEvent
QWheelEvent::buttons
Qt::MouseButtons buttons() const const
QWheelEvent::position
QPointF position() const const
QWidget
QWidget::setCursor
void setCursor(const QCursor &)
QWidget::event
virtual bool event(QEvent *event) override
QWidget::height
height
QWidget::setMouseTracking
void setMouseTracking(bool enable)
QWidget::palette
palette
QWidget::pos
pos
QWidget::rect
rect
QWidget::resizeEvent
virtual void resizeEvent(QResizeEvent *event)
QWidget::size
size
QWidget::setSizePolicy
void setSizePolicy(QSizePolicy)
QWidget::setTabletTracking
void setTabletTracking(bool enable)
QWidget::update
void update()
QWidget::width
width
CanvasPainterOptions
Definition: canvaspainter.h:40
OnionSkinPainterOptions
Definition: onionskinpainteroptions.h:20
OverlayPainterOptions
Definition: overlaypainter.h:12
TransformParameters
Definition: selectionpainter.h:30
Generated on Tue Jan 6 2026 11:38:51 for Pencil2D by doxygen 1.9.6 based on revision f91a96748ec6712509b9b0ff47979db9c34d556f