File indexing completed on 2024-06-16 04:17:43

0001 /*
0002  *  kis_tool_line.cc - part of Krayon
0003  *
0004  *  SPDX-FileCopyrightText: 2000 John Califf <jwcaliff@compuzone.net>
0005  *  SPDX-FileCopyrightText: 2002 Patrick Julien <freak@codepimps.org>
0006  *  SPDX-FileCopyrightText: 2003 Boudewijn Rempt <boud@valdyas.org>
0007  *  SPDX-FileCopyrightText: 2009 Lukáš Tvrdý <lukast.dev@gmail.com>
0008  *  SPDX-FileCopyrightText: 2007, 2010 Cyrille Berger <cberger@cberger.net>
0009  *
0010  *  SPDX-License-Identifier: GPL-2.0-or-later
0011  */
0012 
0013 #include "kis_tool_line.h"
0014 
0015 
0016 #include <QPushButton>
0017 
0018 #include <ksharedconfig.h>
0019 
0020 #include <KoCanvasBase.h>
0021 #include <KoPointerEvent.h>
0022 #include <KoPathShape.h>
0023 #include <KoShapeController.h>
0024 #include <KoShapeStroke.h>
0025 
0026 #include <kis_debug.h>
0027 #include <kis_cursor.h>
0028 #include <brushengine/kis_paintop_registry.h>
0029 #include <kis_figure_painting_tool_helper.h>
0030 #include <kis_canvas2.h>
0031 #include <kis_canvas_resource_provider.h>
0032 #include <KisViewManager.h>
0033 #include <kis_action_registry.h>
0034 #include <kis_painting_information_builder.h>
0035 
0036 #include "kis_tool_line_helper.h"
0037 
0038 
0039 const KisCoordinatesConverter* getCoordinatesConverter(KoCanvasBase * canvas)
0040 {
0041     KisCanvas2 *kritaCanvas = dynamic_cast<KisCanvas2*>(canvas);
0042     KIS_ASSERT(kritaCanvas);
0043     return kritaCanvas->coordinatesConverter();
0044 }
0045 
0046 
0047 KisToolLine::KisToolLine(KoCanvasBase * canvas)
0048     : KisToolShape(canvas, KisCursor::load("tool_line_cursor.png", 6, 6)),
0049       m_showGuideline(true),
0050       m_strokeIsRunning(false),
0051       m_infoBuilder(new KisConverterPaintingInformationBuilder(getCoordinatesConverter(canvas))),
0052       m_helper(new KisToolLineHelper(m_infoBuilder.data(),
0053                                      canvas->resourceManager(),
0054                                      kundo2_i18n("Draw Line"))),
0055       m_strokeUpdateCompressor(200, KisSignalCompressor::POSTPONE),
0056       m_longStrokeUpdateCompressor(750, KisSignalCompressor::FIRST_INACTIVE)
0057 {
0058     setObjectName("tool_line");
0059 
0060     setSupportOutline(true);
0061 
0062     connect(&m_strokeUpdateCompressor, SIGNAL(timeout()), SLOT(updateStroke()));
0063     connect(&m_longStrokeUpdateCompressor, SIGNAL(timeout()), SLOT(updateStroke()));
0064 
0065     KisCanvas2 *kritaCanvas = dynamic_cast<KisCanvas2*>(canvas);
0066 
0067     connect(kritaCanvas->viewManager()->canvasResourceProvider(), SIGNAL(sigEffectiveCompositeOpChanged()), SLOT(resetCursorStyle()));
0068 }
0069 
0070 KisToolLine::~KisToolLine()
0071 {
0072 }
0073 
0074 void KisToolLine::resetCursorStyle()
0075 {
0076     if (isEraser() && (nodePaintAbility() == PAINT)) {
0077         useCursor(KisCursor::load("tool_line_eraser_cursor.png", 6, 6));
0078     } else {
0079         KisToolPaint::resetCursorStyle();
0080     }
0081 
0082     overrideCursorIfNotEditable();
0083 }
0084 
0085 void KisToolLine::activate(const QSet<KoShape*> &shapes)
0086 {
0087    KisToolPaint::activate(shapes);
0088    configGroup =  KSharedConfig::openConfig()->group(toolId());
0089 }
0090 
0091 void KisToolLine::deactivate()
0092 {
0093     KisToolPaint::deactivate();
0094     cancelStroke();
0095 }
0096 
0097 QWidget* KisToolLine::createOptionWidget()
0098 {
0099     QWidget* widget = KisToolPaint::createOptionWidget();
0100 
0101     m_chkUseSensors = new QCheckBox(i18n("Use sensors"));
0102     addOptionWidgetOption(m_chkUseSensors);
0103 
0104     m_chkShowPreview = new QCheckBox(i18n("Show Preview"));
0105     addOptionWidgetOption(m_chkShowPreview);
0106 
0107     m_chkShowGuideline = new QCheckBox(i18n("Show Guideline"));
0108     addOptionWidgetOption(m_chkShowGuideline);
0109 
0110     m_chkSnapToAssistants = new QCheckBox(i18n("Snap to Assistants"));
0111     addOptionWidgetOption(m_chkSnapToAssistants);
0112 
0113     m_chkSnapEraser = new QCheckBox(i18n("Snap Eraser"));
0114     addOptionWidgetOption(m_chkSnapEraser);
0115 
0116 
0117 
0118 
0119     // hook up connections for value changing
0120     connect(m_chkUseSensors, SIGNAL(clicked(bool)), this, SLOT(setUseSensors(bool)) );
0121     connect(m_chkShowPreview, SIGNAL(clicked(bool)), this, SLOT(setShowPreview(bool)) );
0122     connect(m_chkShowGuideline, SIGNAL(clicked(bool)), this, SLOT(setShowGuideline(bool)) );
0123     connect(m_chkSnapToAssistants, SIGNAL(clicked(bool)), this, SLOT(setSnapToAssistants(bool)) );
0124 
0125 
0126     // read values in from configuration
0127     m_chkUseSensors->setChecked(configGroup.readEntry("useSensors", true));
0128     m_chkShowPreview->setChecked(configGroup.readEntry("showPreview", true));
0129     m_chkShowGuideline->setChecked(configGroup.readEntry("showGuideline", true));
0130     m_chkSnapToAssistants->setChecked(configGroup.readEntry("snapToAssistants", false));
0131     m_chkSnapEraser->setChecked(configGroup.readEntry("snapEraser", false));
0132     if (!m_chkSnapToAssistants->isChecked()) {
0133         m_chkSnapEraser->setEnabled(false);
0134     }
0135 
0136     return widget;
0137 }
0138 
0139 void KisToolLine::setUseSensors(bool value)
0140 {
0141     configGroup.writeEntry("useSensors", value);
0142 }
0143 
0144 void KisToolLine::setShowGuideline(bool value)
0145 {
0146     m_showGuideline = value;
0147     configGroup.writeEntry("showGuideline", value);
0148 }
0149 
0150 void KisToolLine::setShowPreview(bool value)
0151 {
0152     configGroup.writeEntry("showPreview", value);
0153 }
0154 
0155 void KisToolLine::setSnapToAssistants(bool value)
0156 {
0157     configGroup.writeEntry("snapToAssistants", value);
0158     m_chkSnapEraser->setEnabled(value);
0159 }
0160 
0161 void KisToolLine::setSnapEraser(bool value)
0162 {
0163     configGroup.writeEntry("snapEraser", value);
0164 }
0165 
0166 void KisToolLine::requestStrokeCancellation()
0167 {
0168     cancelStroke();
0169 }
0170 
0171 void KisToolLine::requestStrokeEnd()
0172 {
0173     // Terminate any in-progress strokes
0174     if (nodePaintAbility() == PAINT && m_helper->isRunning()) {
0175         endStroke();
0176     }
0177 }
0178 
0179 void KisToolLine::updatePreviewTimer(bool showGuideline)
0180 {
0181     // If the user disables the guideline, we will want to try to draw some
0182     // preview lines even if they're slow, so set the timer to FIRST_ACTIVE.
0183     if (showGuideline) {
0184         m_strokeUpdateCompressor.setMode(KisSignalCompressor::POSTPONE);
0185     } else {
0186         m_strokeUpdateCompressor.setMode(KisSignalCompressor::FIRST_ACTIVE);
0187     }
0188 }
0189 
0190 
0191 void KisToolLine::paint(QPainter& gc, const KoViewConverter &converter)
0192 {
0193     Q_UNUSED(converter);
0194 
0195     if(mode() == KisTool::PAINT_MODE) {
0196         paintLine(gc,QRect());
0197     }
0198     KisToolPaint::paint(gc,converter);
0199 }
0200 
0201 void KisToolLine::beginPrimaryAction(KoPointerEvent *event)
0202 {
0203     NodePaintAbility nodeAbility = nodePaintAbility();
0204     if (nodeAbility == UNPAINTABLE || !nodeEditable()) {
0205         event->ignore();
0206         return;
0207     }
0208 
0209     if (nodeAbility == MYPAINTBRUSH_UNPAINTABLE) {
0210         KisCanvas2 * kiscanvas = static_cast<KisCanvas2*>(canvas());
0211         QString message = i18n("The MyPaint Brush Engine is not available for this colorspace");
0212         kiscanvas->viewManager()->showFloatingMessage(message, koIcon("object-locked"));
0213         event->ignore();
0214         return;
0215     }
0216 
0217     setMode(KisTool::PAINT_MODE);
0218 
0219     const KisToolShape::ShapeAddInfo info =
0220         shouldAddShape(currentNode());
0221 
0222     // Always show guideline on vector layers
0223     m_showGuideline = m_chkShowGuideline->isChecked() || nodeAbility != PAINT;
0224     updatePreviewTimer(m_showGuideline);
0225     m_helper->setEnabled((nodeAbility == PAINT && !info.shouldAddShape) || info.shouldAddSelectionShape);
0226     m_helper->setUseSensors(m_chkUseSensors->isChecked());
0227     m_helper->start(event, canvas()->resourceManager());
0228 
0229     m_startPoint = convertToPixelCoordAndSnap(event);
0230     m_endPoint = m_startPoint;
0231     m_lastUpdatedPoint = m_startPoint;
0232     m_originalStartPoint = m_startPoint;
0233 
0234     m_strokeIsRunning = true;
0235 
0236     showSize();
0237 }
0238 
0239 void KisToolLine::updateStroke()
0240 {
0241     if (!m_strokeIsRunning) return;
0242 
0243     m_helper->repaintLine(image(),
0244                           currentNode(),
0245                           image().data());
0246 }
0247 
0248 void KisToolLine::continuePrimaryAction(KoPointerEvent *event)
0249 {
0250     CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE);
0251     if (!m_strokeIsRunning) return;
0252 
0253     // First ensure the old guideline is deleted
0254     updateGuideline();
0255 
0256     QPointF pos = convertToPixelCoordAndSnap(event);
0257 
0258     if (event->modifiers() == Qt::AltModifier) {
0259         QPointF trans = pos - m_endPoint;
0260         m_helper->translatePoints(trans);
0261         m_startPoint += trans;
0262         m_endPoint += trans;
0263         m_originalStartPoint += trans; // original start point is only original in terms of snapping to assistants
0264     } else if (event->modifiers() == Qt::ShiftModifier) {
0265         pos = straightLine(pos);
0266         m_helper->addPoint(event, pos);
0267     } else {
0268         pos = snapToAssistants(pos);
0269         m_helper->addPoint(event, pos);
0270         m_helper->movePointsTo(m_startPoint, pos);
0271     }
0272     m_endPoint = pos;
0273 
0274     // Draw preview if requested
0275     if (m_chkShowPreview->isChecked()) {
0276         // If the cursor has moved a significant amount, immediately clear the
0277         // current preview and redraw. Otherwise, do slow redraws periodically.
0278         auto updateDistance = (pixelToView(m_lastUpdatedPoint) - pixelToView(pos)).manhattanLength();
0279         if (updateDistance > 10) {
0280             m_helper->clearPaint();
0281             m_longStrokeUpdateCompressor.stop();
0282             m_strokeUpdateCompressor.start();
0283             m_lastUpdatedPoint = pos;
0284         } else if (updateDistance > 1 &&  !m_strokeUpdateCompressor.isActive() && !m_longStrokeUpdateCompressor.isActive()) {
0285             m_longStrokeUpdateCompressor.start();
0286             m_lastUpdatedPoint = pos;
0287         }
0288     }
0289 
0290     if(event->modifiers() == Qt::AltModifier) {
0291         KisCanvas2 *kisCanvas =dynamic_cast<KisCanvas2*>(canvas());
0292         KIS_ASSERT(kisCanvas);
0293         kisCanvas->viewManager()->showFloatingMessage(i18n("X: %1 px\nY: %2 px", QString::number(m_startPoint.x(), 'f',1)
0294                                                            , QString::number(m_startPoint.y(), 'f',1))
0295                                                            , QIcon(), 1000, KisFloatingMessage::High,  Qt::AlignLeft | Qt::TextWordWrap | Qt::AlignVCenter);
0296     }
0297     else {
0298         showSize();
0299     }
0300 
0301     updateGuideline();
0302     KisToolPaint::requestUpdateOutline(event->point, event);
0303 }
0304 
0305 void KisToolLine::endPrimaryAction(KoPointerEvent *event)
0306 {
0307     Q_UNUSED(event);
0308     CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE);
0309     setMode(KisTool::HOVER_MODE);
0310 
0311     updateGuideline();
0312     endStroke();
0313 
0314     if (static_cast<KisCanvas2*>(canvas())->paintingAssistantsDecoration()) {
0315         static_cast<KisCanvas2*>(canvas())->paintingAssistantsDecoration()->endStroke();
0316     }
0317 }
0318 
0319 bool KisToolLine::primaryActionSupportsHiResEvents() const
0320 {
0321     return true;
0322 }
0323 
0324 
0325 void KisToolLine::endStroke()
0326 {
0327     NodePaintAbility nodeAbility = nodePaintAbility();
0328 
0329     if (!m_strokeIsRunning || m_startPoint == m_endPoint || nodeAbility == UNPAINTABLE) {
0330         m_helper->clearPoints();
0331         return;
0332     }
0333 
0334     const KisToolShape::ShapeAddInfo info =
0335         shouldAddShape(currentNode());
0336 
0337     if ((nodeAbility == PAINT && !info.shouldAddShape) || info.shouldAddSelectionShape) {
0338         updateStroke();
0339         m_helper->end();
0340     }
0341     else {
0342         KoPathShape* path = new KoPathShape();
0343         path->setShapeId(KoPathShapeId);
0344 
0345         QTransform resolutionMatrix;
0346         resolutionMatrix.scale(1 / currentImage()->xRes(), 1 / currentImage()->yRes());
0347         path->moveTo(resolutionMatrix.map(m_startPoint));
0348         path->lineTo(resolutionMatrix.map(m_endPoint));
0349         path->normalize();
0350 
0351         KoShapeStrokeSP border(new KoShapeStroke(currentStrokeWidth(), currentFgColor().toQColor()));
0352         path->setStroke(border);
0353 
0354         KUndo2Command * cmd = canvas()->shapeController()->addShape(path, nullptr);
0355         canvas()->addCommand(cmd);
0356     }
0357 
0358     m_strokeIsRunning = false;
0359     m_endPoint = m_startPoint;
0360 }
0361 
0362 void KisToolLine::cancelStroke()
0363 {
0364     if (!m_strokeIsRunning) return;
0365     if (m_startPoint == m_endPoint) return;
0366 
0367     /**
0368      * The actual stroke is run by the timer so it is a legal
0369      * situation when m_strokeIsRunning is true, but the actual redraw
0370      * stroke is not running.
0371      */
0372     if (m_helper->isRunning()) {
0373         m_helper->cancel();
0374     }
0375 
0376     m_strokeIsRunning = false;
0377     m_endPoint = m_startPoint;
0378 }
0379 
0380 QPointF KisToolLine::straightLine(QPointF point)
0381 {
0382     const QPointF lineVector = point - m_startPoint;
0383     qreal lineAngle = std::atan2(lineVector.y(), lineVector.x());
0384 
0385     if (lineAngle < 0) {
0386         lineAngle += 2 * M_PI;
0387     }
0388 
0389     const qreal ANGLE_BETWEEN_CONSTRAINED_LINES = (2 * M_PI) / 24;
0390 
0391     const quint32 constrainedLineIndex = static_cast<quint32>((lineAngle / ANGLE_BETWEEN_CONSTRAINED_LINES) + 0.5);
0392     const qreal constrainedLineAngle = constrainedLineIndex * ANGLE_BETWEEN_CONSTRAINED_LINES;
0393 
0394     const qreal lineLength = std::sqrt((lineVector.x() * lineVector.x()) + (lineVector.y() * lineVector.y()));
0395 
0396     const QPointF constrainedLineVector(lineLength * std::cos(constrainedLineAngle), lineLength * std::sin(constrainedLineAngle));
0397 
0398     const QPointF result = m_startPoint + constrainedLineVector;
0399 
0400     return result;
0401 }
0402 
0403 QPointF KisToolLine::snapToAssistants(QPointF point)
0404 {
0405     if (m_chkSnapToAssistants->isChecked() && static_cast<KisCanvas2*>(canvas())->paintingAssistantsDecoration()) {
0406         KisCanvas2* c = static_cast<KisCanvas2*>(canvas());
0407         c->paintingAssistantsDecoration()->setOnlyOneAssistantSnap(true);
0408         c->paintingAssistantsDecoration()->setEraserSnap(m_chkSnapEraser->isChecked());
0409         QPointF startPoint = m_originalStartPoint;
0410 
0411         // startPoint etc. are in image coordinates system (pixels)
0412         // but assistants work in document coordinates system ("points")
0413         QPointF startPointInDoc = getCoordinatesConverter(canvas())->imageToDocument(startPoint);
0414         QPointF pointInDoc = getCoordinatesConverter(canvas())->imageToDocument(point);
0415 
0416         c->paintingAssistantsDecoration()->adjustLine(pointInDoc, startPointInDoc);
0417         c->paintingAssistantsDecoration()->setAdjustedBrushPosition(pointInDoc);
0418 
0419         startPoint = getCoordinatesConverter(canvas())->documentToImage(startPointInDoc);
0420         point = getCoordinatesConverter(canvas())->documentToImage(pointInDoc);
0421 
0422         m_startPoint = startPoint;
0423         return point;
0424     }
0425     return point;
0426 }
0427 
0428 
0429 void KisToolLine::updateGuideline()
0430 {
0431     if (canvas()) {
0432         QRectF bound(m_startPoint, m_endPoint);
0433         canvas()->updateCanvas(convertToPt(bound.normalized().adjusted(-3, -3, 3, 3)));
0434     }
0435 }
0436 
0437 
0438 void KisToolLine::showSize()
0439 {
0440     KisCanvas2 *kisCanvas =dynamic_cast<KisCanvas2*>(canvas());
0441     KIS_ASSERT(kisCanvas);
0442     kisCanvas->viewManager()->showFloatingMessage(i18n("Length: %1 px", QString::number(QLineF(m_startPoint,m_endPoint).length(), 'f',1))
0443                                                         , QIcon(), 1000, KisFloatingMessage::High,  Qt::AlignLeft | Qt::TextWordWrap | Qt::AlignVCenter);
0444 }
0445 void KisToolLine::paintLine(QPainter& gc, const QRect&)
0446 {
0447     QPointF viewStartPos = pixelToView(m_startPoint);
0448     QPointF viewStartEnd = pixelToView(m_endPoint);
0449 
0450     if (m_showGuideline && canvas()) {
0451         QPainterPath path;
0452         path.moveTo(viewStartPos);
0453         path.lineTo(viewStartEnd);
0454         paintToolOutline(&gc, path);
0455     }
0456 }
0457 
0458 QString KisToolLine::quickHelp() const
0459 {
0460     return i18n("Alt+Drag will move the origin of the currently displayed line around, Shift+Drag will force you to draw straight lines");
0461 }
0462 
0463 bool KisToolLine::supportsPaintingAssistants() const
0464 {
0465     return true;
0466 }