File indexing completed on 2024-12-22 04:13:00

0001 /*
0002  *  kis_tool_freehand.cc - part of Krita
0003  *
0004  *  SPDX-FileCopyrightText: 2003-2007 Boudewijn Rempt <boud@valdyas.org>
0005  *  SPDX-FileCopyrightText: 2004 Bart Coppens <kde@bartcoppens.be>
0006  *  SPDX-FileCopyrightText: 2007, 2008, 2010 Cyrille Berger <cberger@cberger.net>
0007  *  SPDX-FileCopyrightText: 2009 Lukáš Tvrdý <lukast.dev@gmail.com>
0008  *
0009  *  SPDX-License-Identifier: GPL-2.0-or-later
0010  */
0011 
0012 #include "kis_tool_freehand.h"
0013 #include <QPainter>
0014 #include <QRect>
0015 #include <QThreadPool>
0016 #include <QApplication>
0017 #include <QDesktopWidget>
0018 #include <QScreen>
0019 
0020 #include <Eigen/Core>
0021 
0022 #include <kis_icon.h>
0023 #include <KoPointerEvent.h>
0024 #include <KoViewConverter.h>
0025 #include <KoCanvasController.h>
0026 
0027 //pop up palette
0028 #include <kis_canvas_resource_provider.h>
0029 
0030 // Krita/image
0031 #include <kis_layer.h>
0032 #include <kis_paint_layer.h>
0033 #include <kis_painter.h>
0034 #include <brushengine/kis_paintop.h>
0035 #include <kis_selection.h>
0036 #include <brushengine/kis_paintop_preset.h>
0037 #include <brushengine/KisOptimizedBrushOutline.h>
0038 
0039 
0040 // Krita/ui
0041 #include "kis_abstract_perspective_grid.h"
0042 #include "kis_config.h"
0043 #include "kis_image_config.h"
0044 #include "canvas/kis_canvas2.h"
0045 #include "kis_cursor.h"
0046 #include <KisViewManager.h>
0047 #include <kis_painting_assistants_decoration.h>
0048 #include "kis_painting_information_builder.h"
0049 #include "kis_tool_freehand_helper.h"
0050 #include "strokes/freehand_stroke.h"
0051 #include "kis_tool_utils.h"
0052 
0053 using namespace std::placeholders; // For _1 placeholder
0054 
0055 
0056 KisToolFreehand::KisToolFreehand(KoCanvasBase * canvas, const QCursor & cursor, const KUndo2MagicString &transactionText)
0057     : KisToolPaint(canvas, cursor),
0058       m_brushResizeCompressor(200, std::bind(&KisToolFreehand::slotDoResizeBrush, this, _1))
0059 {
0060 
0061     setSupportOutline(true);
0062     setMaskSyntheticEvents(KisConfig(true).disableTouchOnCanvas()); // Disallow mouse events from finger presses unless enabled
0063 
0064     m_infoBuilder = new KisToolFreehandPaintingInformationBuilder(this);
0065     m_helper = new KisToolFreehandHelper(m_infoBuilder, canvas->resourceManager(), transactionText);
0066 
0067     connect(m_helper, SIGNAL(requestExplicitUpdateOutline()), SLOT(explicitUpdateOutline()));
0068 
0069     connect(qobject_cast<KisCanvas2*>(canvas)->viewManager(), SIGNAL(brushOutlineToggled()), SLOT(explicitUpdateOutline()));
0070 
0071     KisCanvasResourceProvider *provider = qobject_cast<KisCanvas2*>(canvas)->viewManager()->canvasResourceProvider();
0072 
0073     connect(provider, SIGNAL(sigEffectiveCompositeOpChanged()), SLOT(explicitUpdateOutline()));
0074     connect(provider, SIGNAL(sigEffectiveCompositeOpChanged()), SLOT(resetCursorStyle()));
0075     connect(provider, SIGNAL(sigPaintOpPresetChanged(KisPaintOpPresetSP)), SLOT(explicitUpdateOutline()));
0076     connect(provider, SIGNAL(sigPaintOpPresetChanged(KisPaintOpPresetSP)), SLOT(resetCursorStyle()));
0077 }
0078 
0079 KisToolFreehand::~KisToolFreehand()
0080 {
0081     delete m_helper;
0082     delete m_infoBuilder;
0083 }
0084 
0085 void KisToolFreehand::mouseMoveEvent(KoPointerEvent *event)
0086 {
0087     KisToolPaint::mouseMoveEvent(event);
0088     m_helper->cursorMoved(convertToPixelCoord(event));
0089 }
0090 
0091 KisSmoothingOptionsSP KisToolFreehand::smoothingOptions() const
0092 {
0093     return m_helper->smoothingOptions();
0094 }
0095 
0096 void KisToolFreehand::resetCursorStyle()
0097 {
0098     KisConfig cfg(true);
0099 
0100     bool useSeparateEraserCursor = cfg.separateEraserCursor() && isEraser();
0101 
0102     switch (useSeparateEraserCursor ? cfg.eraserCursorStyle() : cfg.newCursorStyle()) {
0103     case CURSOR_STYLE_NO_CURSOR:
0104         useCursor(KisCursor::blankCursor());
0105         break;
0106     case CURSOR_STYLE_POINTER:
0107         useCursor(KisCursor::arrowCursor());
0108         break;
0109     case CURSOR_STYLE_SMALL_ROUND:
0110         useCursor(KisCursor::roundCursor());
0111         break;
0112     case CURSOR_STYLE_CROSSHAIR:
0113         useCursor(KisCursor::crossCursor());
0114         break;
0115     case CURSOR_STYLE_TRIANGLE_RIGHTHANDED:
0116         useCursor(KisCursor::triangleRightHandedCursor());
0117         break;
0118     case CURSOR_STYLE_TRIANGLE_LEFTHANDED:
0119         useCursor(KisCursor::triangleLeftHandedCursor());
0120         break;
0121     case CURSOR_STYLE_BLACK_PIXEL:
0122         useCursor(KisCursor::pixelBlackCursor());
0123         break;
0124     case CURSOR_STYLE_WHITE_PIXEL:
0125         useCursor(KisCursor::pixelWhiteCursor());
0126         break;
0127     case CURSOR_STYLE_ERASER:
0128         useCursor(KisCursor::eraserCursor());
0129         break;
0130     case CURSOR_STYLE_TOOLICON:
0131     default:
0132         KisToolPaint::resetCursorStyle();
0133         break;
0134     }
0135 }
0136 
0137 KisPaintingInformationBuilder* KisToolFreehand::paintingInformationBuilder() const
0138 {
0139     return m_infoBuilder;
0140 }
0141 
0142 void KisToolFreehand::resetHelper(KisToolFreehandHelper *helper)
0143 {
0144     delete m_helper;
0145     m_helper = helper;
0146 }
0147 
0148 bool KisToolFreehand::supportsPaintingAssistants() const
0149 {
0150     return true;
0151 }
0152 
0153 int KisToolFreehand::flags() const
0154 {
0155     return KisTool::FLAG_USES_CUSTOM_COMPOSITEOP|KisTool::FLAG_USES_CUSTOM_PRESET
0156            |KisTool::FLAG_USES_CUSTOM_SIZE;
0157 }
0158 
0159 void KisToolFreehand::activate(const QSet<KoShape*> &shapes)
0160 {
0161     KisToolPaint::activate(shapes);
0162 }
0163 
0164 void KisToolFreehand::deactivate()
0165 {
0166     if (mode() == PAINT_MODE) {
0167         endStroke();
0168         setMode(KisTool::HOVER_MODE);
0169     }
0170     KisToolPaint::deactivate();
0171 }
0172 
0173 void KisToolFreehand::initStroke(KoPointerEvent *event)
0174 {
0175     m_helper->initPaint(event,
0176                         convertToPixelCoord(event),
0177                         image(),
0178                         currentNode(),
0179                         image().data());
0180 }
0181 
0182 void KisToolFreehand::doStroke(KoPointerEvent *event)
0183 {
0184     m_helper->paintEvent(event);
0185 }
0186 
0187 void KisToolFreehand::endStroke()
0188 {
0189     m_helper->endPaint();
0190     bool paintOpIgnoredEvent = currentPaintOpPreset()->settings()->mouseReleaseEvent();
0191     Q_UNUSED(paintOpIgnoredEvent);
0192 }
0193 
0194 bool KisToolFreehand::primaryActionSupportsHiResEvents() const
0195 {
0196     return true;
0197 }
0198 
0199 void KisToolFreehand::beginPrimaryAction(KoPointerEvent *event)
0200 {
0201     // FIXME: workaround for the Duplicate Op
0202     trySampleByPaintOp(event, SampleFgImage);
0203 
0204     requestUpdateOutline(event->point, event);
0205 
0206     NodePaintAbility paintability = nodePaintAbility();
0207     // XXX: move this to KisTool and make it work properly for clone layers: for clone layers, the shape paint tools don't work either
0208     if (!nodeEditable() || paintability != PAINT) {
0209         if (paintability == KisToolPaint::VECTOR || paintability == KisToolPaint::CLONE){
0210             KisCanvas2 * kiscanvas = static_cast<KisCanvas2*>(canvas());
0211             QString message = i18n("The brush tool cannot paint on this layer.  Please select a paint layer or mask.");
0212             kiscanvas->viewManager()->showFloatingMessage(message, koIcon("object-locked"));
0213         }
0214         else if (paintability == MYPAINTBRUSH_UNPAINTABLE) {
0215             KisCanvas2 * kiscanvas = static_cast<KisCanvas2*>(canvas());
0216             QString message = i18n("The MyPaint Brush Engine is not available for this colorspace");
0217             kiscanvas->viewManager()->showFloatingMessage(message, koIcon("object-locked"));
0218         }
0219         event->ignore();
0220 
0221         return;
0222     }
0223 
0224     KIS_SAFE_ASSERT_RECOVER_RETURN(!m_helper->isRunning());
0225 
0226     setMode(KisTool::PAINT_MODE);
0227 
0228     KisCanvas2 *canvas2 = dynamic_cast<KisCanvas2 *>(canvas());
0229     if (canvas2) {
0230         canvas2->viewManager()->disableControls();
0231     }
0232 
0233     initStroke(event);
0234 }
0235 
0236 void KisToolFreehand::continuePrimaryAction(KoPointerEvent *event)
0237 {
0238     CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE);
0239 
0240     requestUpdateOutline(event->point, event);
0241 
0242     /**
0243      * Actual painting
0244      */
0245     doStroke(event);
0246 }
0247 
0248 void KisToolFreehand::endPrimaryAction(KoPointerEvent *event)
0249 {
0250     Q_UNUSED(event);
0251     CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE);
0252 
0253     endStroke();
0254 
0255     if (m_assistant && static_cast<KisCanvas2*>(canvas())->paintingAssistantsDecoration()) {
0256         static_cast<KisCanvas2*>(canvas())->paintingAssistantsDecoration()->endStroke();
0257     }
0258 
0259     KisCanvas2 *canvas2 = dynamic_cast<KisCanvas2 *>(canvas());
0260     if (canvas2) {
0261         canvas2->viewManager()->enableControls();
0262     }
0263 
0264     setMode(KisTool::HOVER_MODE);
0265 }
0266 
0267 bool KisToolFreehand::trySampleByPaintOp(KoPointerEvent *event, AlternateAction action)
0268 {
0269     if (action != SampleFgNode && action != SampleFgImage) return false;
0270 
0271     /**
0272      * FIXME: we need some better way to implement modifiers
0273      * for a paintop level. This method is used in DuplicateOp only!
0274      */
0275     QPointF pos = adjustPosition(event->point, event->point);
0276     qreal perspective = calculatePerspective(pos);
0277     if (!currentPaintOpPreset()) {
0278         return false;
0279     }
0280     KisPaintInformation info(convertToPixelCoord(event->point),
0281                              m_infoBuilder->pressureToCurve(event->pressure()),
0282                              event->xTilt(), event->yTilt(),
0283                              event->rotation(),
0284                              event->tangentialPressure(),
0285                              perspective, 0, 0);
0286     info.setRandomSource(new KisRandomSource());
0287     info.setPerStrokeRandomSource(new KisPerStrokeRandomSource());
0288 
0289     bool paintOpIgnoredEvent = currentPaintOpPreset()->settings()->mousePressEvent(info,
0290                                                                                    event->modifiers(),
0291                                                                                    currentNode());
0292     // DuplicateOP during the sampling of new source point (origin)
0293     // is the only paintop that returns "false" here
0294     return !paintOpIgnoredEvent;
0295 }
0296 
0297 void KisToolFreehand::activateAlternateAction(AlternateAction action)
0298 {
0299     if (action != ChangeSize && action != ChangeSizeSnap) {
0300         KisToolPaint::activateAlternateAction(action);
0301         return;
0302     }
0303 
0304     useCursor(KisCursor::blankCursor());
0305     setOutlineVisible(true);
0306 }
0307 
0308 void KisToolFreehand::deactivateAlternateAction(AlternateAction action)
0309 {
0310     if (action != ChangeSize && action != ChangeSizeSnap) {
0311         KisToolPaint::deactivateAlternateAction(action);
0312         return;
0313     }
0314 
0315     resetCursorStyle();
0316     setOutlineVisible(false);
0317 }
0318 
0319 void KisToolFreehand::beginAlternateAction(KoPointerEvent *event, AlternateAction action)
0320 {
0321     if (trySampleByPaintOp(event, action)) {
0322         m_paintopBasedSamplingInAction = true;
0323         return;
0324     }
0325 
0326     if (action != ChangeSize && action != ChangeSizeSnap) {
0327         KisToolPaint::beginAlternateAction(event, action);
0328         return;
0329     }
0330 
0331     setMode(GESTURE_MODE);
0332     m_initialGestureDocPoint = event->point;
0333     m_initialGestureGlobalPoint = QCursor::pos();
0334 
0335     m_lastDocumentPoint = event->point;
0336     m_lastPaintOpSize = currentPaintOpPreset()->settings()->paintOpSize();
0337 }
0338 
0339 void KisToolFreehand::continueAlternateAction(KoPointerEvent *event, AlternateAction action)
0340 {
0341     if (trySampleByPaintOp(event, action) || m_paintopBasedSamplingInAction) return;
0342 
0343     if (action != ChangeSize && action != ChangeSizeSnap) {
0344         KisToolPaint::continueAlternateAction(event, action);
0345         return;
0346     }
0347 
0348     QPointF lastWidgetPosition = convertDocumentToWidget(m_lastDocumentPoint);
0349     QPointF actualWidgetPosition = convertDocumentToWidget(event->point);
0350 
0351     QPointF offset = actualWidgetPosition - lastWidgetPosition;
0352 
0353     KisCanvas2 *canvas2 = dynamic_cast<KisCanvas2 *>(canvas());
0354     KIS_SAFE_ASSERT_RECOVER_RETURN(canvas2);
0355     QRect screenRect = QGuiApplication::primaryScreen()->availableVirtualGeometry();
0356 
0357     qreal scaleX = 0;
0358     qreal scaleY = 0;
0359     canvas2->coordinatesConverter()->imageScale(&scaleX, &scaleY);
0360 
0361     const qreal maxBrushSize = KisImageConfig(true).maxBrushSize();
0362     const qreal effectiveMaxDragSize = 0.5 * screenRect.width();
0363     const qreal effectiveMaxBrushSize = qMin(maxBrushSize, effectiveMaxDragSize / scaleX);
0364 
0365     const qreal scaleCoeff = effectiveMaxBrushSize / effectiveMaxDragSize;
0366     const qreal sizeDiff = scaleCoeff * offset.x() ;
0367 
0368     if (qAbs(sizeDiff) > 0.01) {
0369         KisPaintOpSettingsSP settings = currentPaintOpPreset()->settings();
0370 
0371         qreal newSize = m_lastPaintOpSize + sizeDiff;
0372 
0373         if (action == ChangeSizeSnap) {
0374             newSize = qMax(qRound(newSize), 1);
0375         }
0376 
0377         newSize = qBound(0.01, newSize, maxBrushSize);
0378 
0379         settings->setPaintOpSize(newSize);
0380 
0381         requestUpdateOutline(m_initialGestureDocPoint, 0);
0382         //m_brushResizeCompressor.start(newSize);
0383 
0384         m_lastDocumentPoint = event->point;
0385         m_lastPaintOpSize = newSize;
0386     }
0387 }
0388 
0389 void KisToolFreehand::endAlternateAction(KoPointerEvent *event, AlternateAction action)
0390 {
0391     if (trySampleByPaintOp(event, action) || m_paintopBasedSamplingInAction) {
0392         m_paintopBasedSamplingInAction = false;
0393         return;
0394     }
0395 
0396     if (action != ChangeSize && action != ChangeSizeSnap) {
0397         KisToolPaint::endAlternateAction(event, action);
0398         return;
0399     }
0400 
0401     KisToolUtils::setCursorPos(m_initialGestureGlobalPoint);
0402     requestUpdateOutline(m_initialGestureDocPoint, 0);
0403 
0404     setMode(HOVER_MODE);
0405 }
0406 
0407 bool KisToolFreehand::wantsAutoScroll() const
0408 {
0409     return false;
0410 }
0411 
0412 void KisToolFreehand::setAssistant(bool assistant)
0413 {
0414     m_assistant = assistant;
0415 }
0416 
0417 void KisToolFreehand::setOnlyOneAssistantSnap(bool assistant)
0418 {
0419     m_only_one_assistant = assistant;
0420 }
0421 
0422 void KisToolFreehand::setSnapEraser(bool assistant)
0423 {
0424     m_eraser_snapping = assistant;
0425 }
0426 
0427 void KisToolFreehand::slotDoResizeBrush(qreal newSize)
0428 {
0429     KisPaintOpSettingsSP settings = currentPaintOpPreset()->settings();
0430 
0431     settings->setPaintOpSize(newSize);
0432     requestUpdateOutline(m_initialGestureDocPoint, 0);
0433 
0434 }
0435 
0436 QPointF KisToolFreehand::adjustPosition(const QPointF& point, const QPointF& strokeBegin)
0437 {
0438     if (m_assistant && static_cast<KisCanvas2*>(canvas())->paintingAssistantsDecoration()) {
0439         KisCanvas2* c = static_cast<KisCanvas2*>(canvas());
0440         c->paintingAssistantsDecoration()->setOnlyOneAssistantSnap(m_only_one_assistant);
0441         c->paintingAssistantsDecoration()->setEraserSnap(m_eraser_snapping);
0442         QPointF ap = c->paintingAssistantsDecoration()->adjustPosition(point, strokeBegin);
0443         QPointF fp = (1.0 - m_magnetism) * point + m_magnetism * ap;
0444         // Report the final position back to the assistant so the guides
0445         // can follow the brush
0446         c->paintingAssistantsDecoration()->setAdjustedBrushPosition(fp);
0447         return fp;
0448     }
0449     return point;
0450 }
0451 
0452 qreal KisToolFreehand::calculatePerspective(const QPointF &documentPoint)
0453 {
0454     qreal perspective = 1.0;
0455     Q_FOREACH (const KisPaintingAssistantSP assistant, static_cast<KisCanvas2*>(canvas())->paintingAssistantsDecoration()->assistants()) {
0456         QPointer<KisAbstractPerspectiveGrid> grid = dynamic_cast<KisAbstractPerspectiveGrid*>(assistant.data());
0457         if (grid && grid->isActive() && grid->contains(documentPoint)) {
0458             perspective = grid->distance(documentPoint);
0459             break;
0460         }
0461     }
0462     return perspective;
0463 }
0464 
0465 void KisToolFreehand::explicitUpdateOutline()
0466 {
0467     requestUpdateOutline(m_outlineDocPoint, 0);
0468 }
0469 
0470 KisOptimizedBrushOutline KisToolFreehand::getOutlinePath(const QPointF &documentPos,
0471                                              const KoPointerEvent *event,
0472                                              KisPaintOpSettings::OutlineMode outlineMode)
0473 {
0474     if (currentPaintOpPreset())
0475         return m_helper->paintOpOutline(convertToPixelCoord(documentPos),
0476                                         event,
0477                                         currentPaintOpPreset()->settings(),
0478                                         outlineMode);
0479     else
0480         return KisOptimizedBrushOutline();
0481 }
0482 
0483