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