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

0001 /*
0002  *  SPDX-FileCopyrightText: 2003-2009 Boudewijn Rempt <boud@valdyas.org>
0003  *  SPDX-FileCopyrightText: 2015 Moritz Molch <kde@moritzmolch.de>
0004  *
0005  *  SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 
0008 #include "kis_tool_paint.h"
0009 
0010 #include <algorithm>
0011 
0012 #include <QWidget>
0013 #include <QRect>
0014 #include <QLayout>
0015 #include <QLabel>
0016 #include <QPushButton>
0017 #include <QWhatsThis>
0018 #include <QCheckBox>
0019 #include <QVBoxLayout>
0020 #include <QHBoxLayout>
0021 #include <QGridLayout>
0022 #include <QEvent>
0023 #include <QVariant>
0024 #include <QAction>
0025 #include <kis_debug.h>
0026 #include <QPoint>
0027 
0028 #include <klocalizedstring.h>
0029 #include <kactioncollection.h>
0030 
0031 #include <kis_algebra_2d.h>
0032 #include <kis_icon.h>
0033 #include <KoShape.h>
0034 #include <KoCanvasResourceProvider.h>
0035 #include <KoColorSpace.h>
0036 #include <KoPointerEvent.h>
0037 #include <KoColor.h>
0038 #include <KoCanvasBase.h>
0039 #include <KoCanvasController.h>
0040 
0041 #include <kis_types.h>
0042 #include <kis_global.h>
0043 #include <kis_image.h>
0044 #include <kis_paint_device.h>
0045 #include <kis_layer.h>
0046 #include <KisViewManager.h>
0047 #include <kis_canvas2.h>
0048 #include <kis_cubic_curve.h>
0049 #include "kis_display_color_converter.h"
0050 #include <KisDocument.h>
0051 #include <KisReferenceImagesLayer.h>
0052 
0053 #include "kis_config.h"
0054 #include "kis_config_notifier.h"
0055 #include "kis_cursor.h"
0056 #include "kis_image_config.h"
0057 #include "widgets/kis_cmb_composite.h"
0058 #include "kis_slider_spin_box.h"
0059 #include "kis_canvas_resource_provider.h"
0060 #include "kis_tool_utils.h"
0061 #include <brushengine/kis_paintop.h>
0062 #include <brushengine/kis_paintop_preset.h>
0063 #include <brushengine/KisOptimizedBrushOutline.h>
0064 #include <kis_action_manager.h>
0065 #include <kis_action.h>
0066 #include "strokes/kis_color_sampler_stroke_strategy.h"
0067 #include "kis_popup_palette.h"
0068 #include "kis_paintop_utils.h"
0069 
0070 
0071 struct KisToolPaint::Private
0072 {
0073     // Keeps track of past cursor positions. This is used to determine the drawing angle when
0074     // drawing the brush outline or starting a stroke.
0075     KisPaintOpUtils::PositionHistory lastCursorPos;
0076 };
0077 
0078 
0079 KisToolPaint::KisToolPaint(KoCanvasBase *canvas, const QCursor &cursor)
0080     : KisTool(canvas, cursor),
0081       m_isOutlineEnabled(true),
0082       m_isOutlineVisible(true),
0083       m_colorSamplerHelper(dynamic_cast<KisCanvas2*>(canvas)),
0084       m_d(new Private())
0085 {
0086 
0087     {
0088         const int maxSize = KisImageConfig(true).maxBrushSize();
0089 
0090         int brushSize = 1;
0091         do {
0092             m_standardBrushSizes.push_back(brushSize);
0093             int increment = qMax(1, int(std::ceil(qreal(brushSize) / 15)));
0094             brushSize += increment;
0095         } while (brushSize < maxSize);
0096 
0097         m_standardBrushSizes.push_back(maxSize);
0098     }
0099 
0100     KisCanvas2 *kiscanvas = dynamic_cast<KisCanvas2*>(canvas);
0101     KIS_ASSERT(kiscanvas);
0102     connect(this, SIGNAL(sigPaintingFinished()), kiscanvas->viewManager()->canvasResourceProvider(), SLOT(slotPainting()));
0103 
0104     connect(&m_colorSamplerHelper, SIGNAL(sigRequestCursor(QCursor)), this, SLOT(slotColorPickerRequestedCursor(QCursor)));
0105     connect(&m_colorSamplerHelper, SIGNAL(sigRequestCursorReset()), this, SLOT(slotColorPickerRequestedCursorReset()));
0106     connect(&m_colorSamplerHelper, SIGNAL(sigRequestUpdateOutline()), this, SLOT(slotColorPickerRequestedOutlineUpdate()));
0107 }
0108 
0109 
0110 KisToolPaint::~KisToolPaint()
0111 {
0112 }
0113 
0114 int KisToolPaint::flags() const
0115 {
0116     return KisTool::FLAG_USES_CUSTOM_COMPOSITEOP;
0117 }
0118 
0119 void KisToolPaint::canvasResourceChanged(int key, const QVariant& v)
0120 {
0121     KisTool::canvasResourceChanged(key, v);
0122 
0123     switch(key) {
0124     case(KoCanvasResource::Opacity):
0125         setOpacity(v.toDouble());
0126         break;
0127     case(KoCanvasResource::CurrentPaintOpPreset): {
0128         if (isActive()) {
0129             requestUpdateOutline(m_outlineDocPoint, 0);
0130         }
0131         break;
0132     }
0133     case KoCanvasResource::CurrentPaintOpPresetName: {
0134         if (isActive()) {
0135             const QString formattedBrushName = v.toString().replace("_", " ");
0136             emit statusTextChanged(formattedBrushName);
0137         }
0138         break;
0139     }
0140     default: //nothing
0141         break;
0142     }
0143 
0144     connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(resetCursorStyle()), Qt::UniqueConnection);
0145 
0146 }
0147 
0148 void KisToolPaint::tryRestoreOpacitySnapshot()
0149 {
0150     /**
0151      * Here is a weird heuristics on when to restore
0152      * brush opacity and when not. Basically, we should
0153      * restore opacity to its saved if the brush preset
0154      * hasn't changed too much, that is, its version is
0155      * the same and it hasn't been reset into a clean
0156      * state since then. The latter condition is checked
0157      * in a fuzzy manner by just mangling the isDirty
0158      * state before and after.
0159      */
0160 
0161     KisCanvasResourceProvider *provider = qobject_cast<KisCanvas2*>(canvas())->viewManager()->canvasResourceProvider();
0162 
0163     boost::optional<qreal> opacityToRestore;
0164 
0165     KisPaintOpPresetSP newPreset = provider->currentPreset();
0166 
0167     if (newPreset) {
0168         if (newPreset == m_oldPreset && newPreset->version() == m_oldPresetVersion
0169             && (newPreset->isDirty() || !m_oldPresetIsDirty)) {
0170 
0171             opacityToRestore = m_oldOpacity;
0172         }
0173 
0174         m_oldPreset = newPreset;
0175         m_oldPresetIsDirty = newPreset->isDirty();
0176         m_oldPresetVersion = newPreset->version();
0177         m_oldOpacity = provider->opacity();
0178     }
0179 
0180     if (opacityToRestore) {
0181         provider->setOpacity(*opacityToRestore);
0182     }
0183 }
0184 
0185 
0186 void KisToolPaint::activate(const QSet<KoShape*> &shapes)
0187 {
0188     if (currentPaintOpPreset()) {
0189         const QString formattedBrushName = currentPaintOpPreset() ? currentPaintOpPreset()->name().replace("_", " ") : QString();
0190         emit statusTextChanged(formattedBrushName);
0191     }
0192 
0193     KisTool::activate(shapes);
0194     if (flags() & KisTool::FLAG_USES_CUSTOM_SIZE) {
0195         connect(action("increase_brush_size"), SIGNAL(triggered()), SLOT(increaseBrushSize()), Qt::UniqueConnection);
0196         connect(action("decrease_brush_size"), SIGNAL(triggered()), SLOT(decreaseBrushSize()), Qt::UniqueConnection);
0197         connect(action("increase_brush_size"), SIGNAL(triggered()), this, SLOT(showBrushSize()));
0198         connect(action("decrease_brush_size"), SIGNAL(triggered()), this, SLOT(showBrushSize()));
0199 
0200     }
0201 
0202     connect(action("rotate_brush_tip_clockwise"), SIGNAL(triggered()), SLOT(rotateBrushTipClockwise()), Qt::UniqueConnection);
0203     connect(action("rotate_brush_tip_clockwise_precise"), SIGNAL(triggered()), SLOT(rotateBrushTipClockwisePrecise()), Qt::UniqueConnection);
0204     connect(action("rotate_brush_tip_counter_clockwise"), SIGNAL(triggered()), SLOT(rotateBrushTipCounterClockwise()), Qt::UniqueConnection);
0205     connect(action("rotate_brush_tip_counter_clockwise_precise"), SIGNAL(triggered()), SLOT(rotateBrushTipCounterClockwisePrecise()), Qt::UniqueConnection);
0206 
0207     tryRestoreOpacitySnapshot();
0208 }
0209 
0210 void KisToolPaint::deactivate()
0211 {
0212     if (flags() & KisTool::FLAG_USES_CUSTOM_SIZE) {
0213         disconnect(action("increase_brush_size"), 0, this, 0);
0214         disconnect(action("decrease_brush_size"), 0, this, 0);
0215     }
0216 
0217     disconnect(action("rotate_brush_tip_clockwise"), 0, this, 0);
0218     disconnect(action("rotate_brush_tip_clockwise_precise"), 0, this, 0);
0219     disconnect(action("rotate_brush_tip_counter_clockwise"), 0, this, 0);
0220     disconnect(action("rotate_brush_tip_counter_clockwise_precise"), 0, this, 0);
0221 
0222     tryRestoreOpacitySnapshot();
0223     emit statusTextChanged(QString());
0224 
0225     KisTool::deactivate();
0226 }
0227 
0228 void KisToolPaint::slotColorPickerRequestedCursor(const QCursor &cursor)
0229 {
0230     useCursor(cursor);
0231 }
0232 
0233 void KisToolPaint::slotColorPickerRequestedCursorReset()
0234 {
0235     resetCursorStyle();
0236 }
0237 
0238 void KisToolPaint::slotColorPickerRequestedOutlineUpdate()
0239 {
0240     requestUpdateOutline(m_outlineDocPoint, 0);
0241 }
0242 
0243 KisOptimizedBrushOutline KisToolPaint::tryFixBrushOutline(const KisOptimizedBrushOutline &originalOutline)
0244 {
0245     KisConfig cfg(true);
0246 
0247     bool useSeparateEraserCursor = cfg.separateEraserCursor() && isEraser();
0248 
0249     const OutlineStyle currentOutlineStyle = !useSeparateEraserCursor ? cfg.newOutlineStyle() : cfg.eraserOutlineStyle();
0250     if (currentOutlineStyle == OUTLINE_NONE) return originalOutline;
0251 
0252     const qreal minThresholdSize = cfg.outlineSizeMinimum();
0253 
0254     /**
0255      * If the brush outline is bigger than the canvas itself (which
0256      * would make it invisible for a user in most of the cases) just
0257      * add a cross in the center of it
0258      */
0259 
0260     QSize widgetSize = canvas()->canvasWidget()->size();
0261     const int maxThresholdSum = widgetSize.width() + widgetSize.height();
0262 
0263     KisOptimizedBrushOutline outline = originalOutline;
0264     QRectF boundingRect = outline.boundingRect();
0265     const qreal sum = boundingRect.width() + boundingRect.height();
0266 
0267     QPointF center = boundingRect.center();
0268 
0269     if (sum > maxThresholdSum) {
0270         const int hairOffset = 7;
0271 
0272         QPainterPath crossIcon;
0273 
0274         crossIcon.moveTo(center.x(), center.y() - hairOffset);
0275         crossIcon.lineTo(center.x(), center.y() + hairOffset);
0276 
0277         crossIcon.moveTo(center.x() - hairOffset, center.y());
0278         crossIcon.lineTo(center.x() + hairOffset, center.y());
0279 
0280         outline.addPath(crossIcon);
0281 
0282     } else if (sum < minThresholdSize && !outline.isEmpty()) {
0283         outline = QPainterPath();
0284         outline.addEllipse(center, 0.5 * minThresholdSize, 0.5 * minThresholdSize);
0285     }
0286 
0287     return outline;
0288 }
0289 
0290 void KisToolPaint::paint(QPainter &gc, const KoViewConverter &converter)
0291 {
0292     Q_UNUSED(converter);
0293 
0294     KisOptimizedBrushOutline path = tryFixBrushOutline(pixelToView(m_currentOutline));
0295     paintToolOutline(&gc, path);
0296 
0297     m_colorSamplerHelper.paint(gc, converter);
0298 }
0299 
0300 void KisToolPaint::setMode(ToolMode mode)
0301 {
0302     if(this->mode() == KisTool::PAINT_MODE &&
0303             mode != KisTool::PAINT_MODE) {
0304 
0305         // Let's add history information about recently used colors
0306         emit sigPaintingFinished();
0307     }
0308 
0309     KisTool::setMode(mode);
0310 }
0311 
0312 void KisToolPaint::activateAlternateAction(AlternateAction action)
0313 {
0314     if (!isSamplingAction(action)) {
0315         KisTool::activateAlternateAction(action);
0316         return;
0317     }
0318 
0319     const bool sampleCurrentLayer = action == SampleFgNode || action == SampleBgNode;
0320     const bool sampleFgColor = action == SampleFgNode || action == SampleFgImage;
0321     m_colorSamplerHelper.activate(sampleCurrentLayer, sampleFgColor);
0322 }
0323 
0324 void KisToolPaint::deactivateAlternateAction(AlternateAction action)
0325 {
0326     if (!isSamplingAction(action)) {
0327         KisTool::deactivateAlternateAction(action);
0328         return;
0329     }
0330 
0331     m_colorSamplerHelper.deactivate();
0332 }
0333 
0334 bool KisToolPaint::isSamplingAction(AlternateAction action) {
0335     return action == SampleFgNode ||
0336         action == SampleBgNode ||
0337         action == SampleFgImage ||
0338         action == SampleBgImage;
0339 }
0340 
0341 void KisToolPaint::beginAlternateAction(KoPointerEvent *event, AlternateAction action)
0342 {
0343     if (isSamplingAction(action)) {
0344         setMode(SECONDARY_PAINT_MODE);
0345 
0346         KisToolUtils::ColorSamplerConfig config;
0347         config.load();
0348 
0349         m_colorSamplerHelper.startAction(event->point, config.radius, config.blend);
0350         requestUpdateOutline(event->point, event);
0351     } else {
0352         KisTool::beginAlternateAction(event, action);
0353     }
0354 }
0355 
0356 void KisToolPaint::continueAlternateAction(KoPointerEvent *event, AlternateAction action)
0357 {
0358     if (isSamplingAction(action)) {
0359         m_colorSamplerHelper.continueAction(event->point);
0360         requestUpdateOutline(event->point, event);
0361     } else {
0362         KisTool::continueAlternateAction(event, action);
0363     }
0364 }
0365 
0366 void KisToolPaint::endAlternateAction(KoPointerEvent *event, AlternateAction action)
0367 {
0368     if (isSamplingAction(action)) {
0369         m_colorSamplerHelper.endAction();
0370         requestUpdateOutline(event->point, event);
0371         setMode(HOVER_MODE);
0372     } else {
0373         KisTool::endAlternateAction(event, action);
0374     }
0375 }
0376 
0377 void KisToolPaint::mousePressEvent(KoPointerEvent *event)
0378 {
0379     KisTool::mousePressEvent(event);
0380     if (mode() == KisTool::HOVER_MODE) {
0381         requestUpdateOutline(event->point, event);
0382     }
0383 }
0384 
0385 void KisToolPaint::mouseMoveEvent(KoPointerEvent *event)
0386 {
0387     KisTool::mouseMoveEvent(event);
0388     if (mode() == KisTool::HOVER_MODE) {
0389         requestUpdateOutline(event->point, event);
0390     }
0391 }
0392 
0393 KisPopupWidgetInterface *KisToolPaint::popupWidget()
0394 {
0395     KisCanvas2 *kisCanvas = dynamic_cast<KisCanvas2*>(canvas());
0396 
0397     if (!kisCanvas) {
0398         return nullptr;
0399     }
0400 
0401     KisPopupWidgetInterface* popupWidget = kisCanvas->popupPalette();
0402     return popupWidget;
0403 }
0404 
0405 void KisToolPaint::mouseReleaseEvent(KoPointerEvent *event)
0406 {
0407     KisTool::mouseReleaseEvent(event);
0408     if (mode() == KisTool::HOVER_MODE) {
0409         requestUpdateOutline(event->point, event);
0410     }
0411 }
0412 
0413 QWidget *KisToolPaint::createOptionWidget()
0414 {
0415     QWidget *optionWidget = new QWidget();
0416     optionWidget->setObjectName(toolId());
0417 
0418     QVBoxLayout *verticalLayout = new QVBoxLayout(optionWidget);
0419     verticalLayout->setObjectName("KisToolPaint::OptionWidget::VerticalLayout");
0420     verticalLayout->setContentsMargins(0,0,0,0);
0421     verticalLayout->setSpacing(5);
0422 
0423     // See https://bugs.kde.org/show_bug.cgi?id=316896
0424     QWidget *specialSpacer = new QWidget(optionWidget);
0425     specialSpacer->setObjectName("SpecialSpacer");
0426     specialSpacer->setFixedSize(0, 0);
0427     verticalLayout->addWidget(specialSpacer);
0428     verticalLayout->addWidget(specialSpacer);
0429 
0430     m_optionsWidgetLayout = new QGridLayout();
0431     m_optionsWidgetLayout->setColumnStretch(1, 1);
0432     verticalLayout->addLayout(m_optionsWidgetLayout);
0433     m_optionsWidgetLayout->setContentsMargins(0,0,0,0);
0434     m_optionsWidgetLayout->setSpacing(5);
0435 
0436     if (!quickHelp().isEmpty()) {
0437         QPushButton *push = new QPushButton(KisIconUtils::loadIcon("help-contents"), QString(), optionWidget);
0438         connect(push, SIGNAL(clicked()), this, SLOT(slotPopupQuickHelp()));
0439         QHBoxLayout *hLayout = new QHBoxLayout();
0440         hLayout->addWidget(push);
0441         hLayout->addItem(new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Fixed));
0442         verticalLayout->addLayout(hLayout);
0443     }
0444 
0445     return optionWidget;
0446 }
0447 
0448 QWidget* findLabelWidget(QGridLayout *layout, QWidget *control)
0449 {
0450     QWidget *result = 0;
0451 
0452     int index = layout->indexOf(control);
0453 
0454     int row, col, rowSpan, colSpan;
0455     layout->getItemPosition(index, &row, &col, &rowSpan, &colSpan);
0456 
0457     if (col > 0) {
0458         QLayoutItem *item = layout->itemAtPosition(row, col - 1);
0459 
0460         if (item) {
0461             result = item->widget();
0462         }
0463     } else {
0464         QLayoutItem *item = layout->itemAtPosition(row, col + 1);
0465         if (item) {
0466             result = item->widget();
0467         }
0468     }
0469 
0470     return result;
0471 }
0472 
0473 void KisToolPaint::showControl(QWidget *control, bool value)
0474 {
0475     control->setVisible(value);
0476     QWidget *label = findLabelWidget(m_optionsWidgetLayout, control);
0477     if (label) {
0478         label->setVisible(value);
0479     }
0480 }
0481 
0482 void KisToolPaint::enableControl(QWidget *control, bool value)
0483 {
0484     control->setEnabled(value);
0485     QWidget *label = findLabelWidget(m_optionsWidgetLayout, control);
0486     if (label) {
0487         label->setEnabled(value);
0488     }
0489 }
0490 
0491 void KisToolPaint::addOptionWidgetLayout(QLayout *layout)
0492 {
0493     Q_ASSERT(m_optionsWidgetLayout != 0);
0494     int rowCount = m_optionsWidgetLayout->rowCount();
0495     m_optionsWidgetLayout->addLayout(layout, rowCount, 0, 1, 2);
0496 }
0497 
0498 
0499 void KisToolPaint::addOptionWidgetOption(QWidget *control, QWidget *label)
0500 {
0501     Q_ASSERT(m_optionsWidgetLayout != 0);
0502     if (label) {
0503         m_optionsWidgetLayout->addWidget(label, m_optionsWidgetLayout->rowCount(), 0);
0504         m_optionsWidgetLayout->addWidget(control, m_optionsWidgetLayout->rowCount() - 1, 1);
0505     }
0506     else {
0507         m_optionsWidgetLayout->addWidget(control, m_optionsWidgetLayout->rowCount(), 0, 1, 2);
0508     }
0509 }
0510 
0511 
0512 void KisToolPaint::setOpacity(qreal opacity)
0513 {
0514     m_opacity = quint8(opacity * OPACITY_OPAQUE_U8);
0515 }
0516 
0517 void KisToolPaint::slotPopupQuickHelp()
0518 {
0519     QWhatsThis::showText(QCursor::pos(), quickHelp());
0520 }
0521 
0522 void KisToolPaint::activatePrimaryAction()
0523 {
0524     setOutlineVisible(true);
0525     KisTool::activatePrimaryAction();
0526 }
0527 
0528 void KisToolPaint::deactivatePrimaryAction()
0529 {
0530     setOutlineVisible(false);
0531     KisTool::deactivatePrimaryAction();
0532 }
0533 
0534 bool KisToolPaint::isOutlineEnabled() const
0535 {
0536     return m_isOutlineEnabled;
0537 }
0538 
0539 void KisToolPaint::setOutlineEnabled(bool enabled)
0540 {
0541     m_isOutlineEnabled = enabled;
0542     requestUpdateOutline(m_outlineDocPoint, lastDeliveredPointerEvent());
0543 }
0544 
0545 bool KisToolPaint::isOutlineVisible() const
0546 {
0547     return m_isOutlineVisible;
0548 }
0549 
0550 void KisToolPaint::setOutlineVisible(bool visible)
0551 {
0552     m_isOutlineVisible = visible;
0553     requestUpdateOutline(m_outlineDocPoint, lastDeliveredPointerEvent());
0554 }
0555 
0556 void KisToolPaint::increaseBrushSize()
0557 {
0558     qreal paintopSize = currentPaintOpPreset()->settings()->paintOpSize();
0559 
0560     std::vector<int>::iterator result =
0561         std::upper_bound(m_standardBrushSizes.begin(),
0562                          m_standardBrushSizes.end(),
0563                          qRound(paintopSize));
0564 
0565     int newValue = result != m_standardBrushSizes.end() ? *result : m_standardBrushSizes.back();
0566 
0567     currentPaintOpPreset()->settings()->setPaintOpSize(newValue);
0568     requestUpdateOutline(m_outlineDocPoint, 0);
0569 }
0570 
0571 void KisToolPaint::decreaseBrushSize()
0572 {
0573     qreal paintopSize = currentPaintOpPreset()->settings()->paintOpSize();
0574 
0575     std::vector<int>::reverse_iterator result =
0576         std::upper_bound(m_standardBrushSizes.rbegin(),
0577                          m_standardBrushSizes.rend(),
0578                          qRound(paintopSize),
0579                          std::greater<int>());
0580 
0581     int newValue = result != m_standardBrushSizes.rend() ? *result : m_standardBrushSizes.front();
0582 
0583     currentPaintOpPreset()->settings()->setPaintOpSize(newValue);
0584     requestUpdateOutline(m_outlineDocPoint, 0);
0585 }
0586 
0587 void KisToolPaint::showBrushSize()
0588 {
0589      KisCanvas2 *kisCanvas =dynamic_cast<KisCanvas2*>(canvas());
0590      KIS_SAFE_ASSERT_RECOVER_RETURN(kisCanvas);
0591      kisCanvas->viewManager()->showFloatingMessage(i18n("Brush Size: %1 px", currentPaintOpPreset()->settings()->paintOpSize())
0592                                                    , QIcon(), 1000, KisFloatingMessage::High,  Qt::AlignLeft | Qt::TextWordWrap | Qt::AlignVCenter);
0593 }
0594 
0595 void KisToolPaint::rotateBrushTipClockwise()
0596 {
0597     const qreal angle = currentPaintOpPreset()->settings()->paintOpAngle();
0598     currentPaintOpPreset()->settings()->setPaintOpAngle(angle - 15);
0599     requestUpdateOutline(m_outlineDocPoint, 0);
0600 }
0601 
0602 void KisToolPaint::rotateBrushTipClockwisePrecise()
0603 {
0604     const qreal angle = currentPaintOpPreset()->settings()->paintOpAngle();
0605     currentPaintOpPreset()->settings()->setPaintOpAngle(angle - 1);
0606     requestUpdateOutline(m_outlineDocPoint, 0);
0607 }
0608 
0609 void KisToolPaint::rotateBrushTipCounterClockwise()
0610 {
0611     const qreal angle = currentPaintOpPreset()->settings()->paintOpAngle();
0612     currentPaintOpPreset()->settings()->setPaintOpAngle(angle + 15);
0613     requestUpdateOutline(m_outlineDocPoint, 0);
0614 }
0615 
0616 void KisToolPaint::rotateBrushTipCounterClockwisePrecise()
0617 {
0618     const qreal angle = currentPaintOpPreset()->settings()->paintOpAngle();
0619     currentPaintOpPreset()->settings()->setPaintOpAngle(angle + 1);
0620     requestUpdateOutline(m_outlineDocPoint, 0);
0621 }
0622 
0623 void KisToolPaint::requestUpdateOutline(const QPointF &outlineDocPoint, const KoPointerEvent *event)
0624 {
0625     QRectF outlinePixelRect;
0626     QRectF outlineDocRect;
0627 
0628     QRectF colorPreviewDocUpdateRect;
0629 
0630     QPointF outlineMoveVector;
0631 
0632     if (m_supportOutline) {
0633         KisConfig cfg(true);
0634         KisPaintOpSettings::OutlineMode outlineMode;
0635 
0636         bool useSeparateEraserCursor = cfg.separateEraserCursor() && isEraser();
0637 
0638         const OutlineStyle currentOutlineStyle = !useSeparateEraserCursor ? cfg.newOutlineStyle() : cfg.eraserOutlineStyle();
0639         const auto outlineStyleIsVisible = [&]() {
0640             return currentOutlineStyle == OUTLINE_FULL ||
0641                    currentOutlineStyle == OUTLINE_CIRCLE ||
0642                    currentOutlineStyle == OUTLINE_TILT;
0643         };
0644         const auto shouldShowOutlineWhilePainting = [&]() {
0645             return !useSeparateEraserCursor ? cfg.showOutlineWhilePainting() : cfg.showEraserOutlineWhilePainting();
0646         };
0647         if (isOutlineEnabled() && isOutlineVisible() &&
0648                 (mode() == KisTool::GESTURE_MODE ||
0649                     (outlineStyleIsVisible() &&
0650                         (mode() == HOVER_MODE ||
0651                          (mode() == PAINT_MODE && shouldShowOutlineWhilePainting()))))) { // lisp forever!
0652 
0653             outlineMode.isVisible = true;
0654 
0655             switch (!useSeparateEraserCursor ? cfg.newOutlineStyle() : cfg.eraserOutlineStyle()) {
0656             case OUTLINE_CIRCLE:
0657                 outlineMode.forceCircle = true;
0658                 break;
0659             case OUTLINE_TILT:
0660                 outlineMode.forceCircle = true;
0661                 outlineMode.showTiltDecoration = true;
0662                 break;
0663             default:
0664                 break;
0665             }
0666         }
0667 
0668         outlineMode.forceFullSize = !useSeparateEraserCursor ? cfg.forceAlwaysFullSizedOutline() : cfg.forceAlwaysFullSizedEraserOutline();
0669 
0670         outlineMoveVector = outlineDocPoint - m_outlineDocPoint;
0671 
0672         m_outlineDocPoint = outlineDocPoint;
0673         m_currentOutline = getOutlinePath(m_outlineDocPoint, event, outlineMode);
0674 
0675         outlinePixelRect = tryFixBrushOutline(m_currentOutline).boundingRect();
0676         outlineDocRect = currentImage()->pixelToDocument(outlinePixelRect);
0677 
0678         // This adjusted call is needed as we paint with a 3 pixel wide brush and the pen is outside the bounds of the path
0679         // Pen uses view coordinates so we have to zoom the document value to match 2 pixel in view coordinates
0680         // See BUG 275829
0681         qreal zoomX;
0682         qreal zoomY;
0683         canvas()->viewConverter()->zoom(&zoomX, &zoomY);
0684         qreal xoffset = 2.0/zoomX;
0685         qreal yoffset = 2.0/zoomY;
0686 
0687         if (!outlineDocRect.isEmpty()) {
0688             outlineDocRect.adjust(-xoffset,-yoffset,xoffset,yoffset);
0689         }
0690 
0691         colorPreviewDocUpdateRect = m_colorSamplerHelper.colorPreviewDocRect(m_outlineDocPoint);
0692 
0693         if (!colorPreviewDocUpdateRect.isEmpty()) {
0694             colorPreviewDocUpdateRect = colorPreviewDocUpdateRect.adjusted(-xoffset,-yoffset,xoffset,yoffset);
0695         }
0696 
0697     }
0698 
0699     // DIRTY HACK ALERT: we should fetch the assistant's dirty rect when requesting
0700     //                   the update, instead of just dumbly update the entire canvas!
0701 
0702     // WARNING: assistants code is also duplicated in KisDelegatedSelectPathWrapper::mouseMoveEvent
0703 
0704     KisCanvas2 *kiscanvas = qobject_cast<KisCanvas2*>(canvas());
0705     KisPaintingAssistantsDecorationSP decoration = kiscanvas->paintingAssistantsDecoration();
0706     if (decoration && decoration->visible() && decoration->hasPaintableAssistants()) {
0707         kiscanvas->updateCanvasDecorations();
0708     }
0709 
0710     if (!m_oldColorPreviewUpdateRect.isEmpty()) {
0711         kiscanvas->updateCanvasToolOutlineDoc(m_oldColorPreviewUpdateRect);
0712     }
0713 
0714     if (!m_oldOutlineRect.isEmpty()) {
0715         kiscanvas->updateCanvasToolOutlineDoc(m_oldOutlineRect);
0716     }
0717 
0718     if (!outlineDocRect.isEmpty()) {
0719         /**
0720          * A simple "update-ahead" implementation that issues an update a little
0721          * bigger to accomodata the possible following outline.
0722          *
0723          * The point is that canvas rendering comes through two stages of
0724          * compression and the canvas may request outline update when the
0725          * outline itself has already been changed. It causes visual tearing
0726          * on the screen (see https://bugs.kde.org/show_bug.cgi?id=476300).
0727          *
0728          * We can solve that in  two ways:
0729          *
0730          * 1) Pass the actual outline with the update rect itself, which is
0731          *    a bit complicated and may result in the outline being a bit
0732          *    delayed visually. We don't implement this method (yet).
0733          *
0734          * 2) Just pass the update rect a bit bigger than the actual outline
0735          *    to accomodate a possible change in the outline. We calculate
0736          *    this bigger rect by offsetting the rect by the previous cursor
0737          *    offset.
0738          */
0739 
0740         /// Don't try to update-ahead if the offset is bigger than 50%
0741         /// of the brush outline
0742         const qreal maxUpdateAheadOutlinePortion = 0.5;
0743 
0744         /// 10% of extra move is added to offset
0745         const qreal offsetFuzzyExtension = 0.1;
0746 
0747         const qreal moveDistance = KisAlgebra2D::norm(outlineMoveVector);
0748 
0749         QRectF offsetRect;
0750 
0751         if (moveDistance < maxUpdateAheadOutlinePortion * KisAlgebra2D::maxDimension(outlineDocRect)) {
0752             offsetRect = outlineDocRect.translated((1.0 + offsetFuzzyExtension) * outlineMoveVector);
0753         }
0754 
0755         kiscanvas->updateCanvasToolOutlineDoc(outlineDocRect | offsetRect);
0756     }
0757 
0758     if (!colorPreviewDocUpdateRect.isEmpty()) {
0759         kiscanvas->updateCanvasToolOutlineDoc(colorPreviewDocUpdateRect);
0760     }
0761 
0762     m_oldOutlineRect = outlineDocRect;
0763     m_oldColorPreviewUpdateRect = colorPreviewDocUpdateRect;
0764 }
0765 
0766 bool KisToolPaint::isEraser() const {
0767     return canvas()->resourceManager()->resource(KoCanvasResource::CurrentEffectiveCompositeOp).toString() == COMPOSITE_ERASE;
0768 }
0769 
0770 KisOptimizedBrushOutline KisToolPaint::getOutlinePath(const QPointF &documentPos,
0771                                                       const KoPointerEvent *event,
0772                                                       KisPaintOpSettings::OutlineMode outlineMode)
0773 {
0774     Q_UNUSED(event);
0775 
0776     KisCanvas2 *canvas2 = dynamic_cast<KisCanvas2 *>(canvas());
0777     KIS_ASSERT(canvas2);
0778     const KisCoordinatesConverter *converter = canvas2->coordinatesConverter();
0779 
0780     const QPointF pixelPos = convertToPixelCoord(documentPos);
0781     KisPaintInformation info(pixelPos);
0782     info.setCanvasMirroredH(canvas2->coordinatesConverter()->xAxisMirrored());
0783     info.setCanvasMirroredV(canvas2->coordinatesConverter()->yAxisMirrored());
0784     info.setCanvasRotation(canvas2->coordinatesConverter()->rotationAngle());
0785     info.setRandomSource(new KisRandomSource());
0786     info.setPerStrokeRandomSource(new KisPerStrokeRandomSource());
0787 
0788     const qreal currentZoom = canvas2->resourceManager() ? canvas2->resourceManager()->resource(KoCanvasResource::EffectiveZoom).toReal() : 1.0;
0789 
0790     QPointF prevPoint = m_d->lastCursorPos.pushThroughHistory(pixelPos, currentZoom);
0791     qreal startAngle = KisAlgebra2D::directionBetweenPoints(prevPoint, pixelPos, 0);
0792     KisDistanceInformation distanceInfo(prevPoint, startAngle);
0793 
0794     KisPaintInformation::DistanceInformationRegistrar registrar =
0795         info.registerDistanceInformation(&distanceInfo);
0796 
0797     KisOptimizedBrushOutline path = currentPaintOpPreset()->settings()->
0798         brushOutline(info,
0799                      outlineMode, converter->effectivePhysicalZoom());
0800 
0801     return path;
0802 }
0803