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 }