Krita Source Code Documentation
Loading...
Searching...
No Matches
KisAsyncColorSamplerHelper.cpp
Go to the documentation of this file.
1
2/*
3 * SPDX-FileCopyrightText: 2022 Dmitry Kazakov <dimula73@gmail.com>
4 * SPDX-FileCopyrightText: 2025 Carsten Hartenfels <carsten.hartenfels@pm.me>
5 *
6 * SPDX-License-Identifier: GPL-2.0-or-later
7 */
8
10
11#include <QApplication>
12#include <QPainter>
13#include <QPainterPath>
14#include <QPalette>
15#include <QPixmap>
16#include <QTransform>
17
20#include "KoShapeManager.h"
21#include "KoViewConverter.h"
22#include "KoIcon.h"
23#include "kis_cursor.h"
26#include "kis_canvas2.h"
27#include "KisViewManager.h"
28#include "KisDocument.h"
33#include "kis_painting_tweaks.h"
34
35
36namespace {
37QColor colorWithAlpha(QColor color, int alpha)
38{
39 color.setAlpha(alpha);
40 return color;
41}
42}
43
45{
46 static constexpr qreal PREVIEW_RECT_SIZE = 48.0;
47
49 : canvas(_canvas)
50 {}
51
53
55 bool sampleCurrentLayer {true};
56 bool updateGlobalColor {true};
57
58 bool isActive {false};
59 bool showPreview {false};
60 bool haveSample {false};
61
64 QScopedPointer<SamplingCompressor> samplingCompressor;
65
67
74
80
84 QPainterPath cacheCrosshairPath;
88
90 QColor baseColor;
92
93 QPixmap cache;
94 qreal cacheRotation = 0.0;
95 bool cacheMirror = false;
96
98 return canvas->image().data();
99 }
100
101 const KoViewConverter &converter() const {
102 return *canvas->imageView()->viewConverter();
103 }
104
106 {
107 // Offsetting to the sides is both vertical and horizontal, when
108 // offsetting above it's only vertical, so it needs a bit more space.
109 constexpr qreal OFFSET = 32.0;
110 constexpr qreal OFFSET_ABOVE = OFFSET * 1.5;
111 constexpr qreal SIZE = PREVIEW_RECT_SIZE;
112
113 bool mirrored = canvas->xAxisMirrored();
114 bool flipped = canvas->yAxisMirrored();
115
121 } else {
122 effectiveStyle = style;
123 }
124
125 qreal width = haveSample ? SIZE * 2.0 : SIZE;
126
127 qreal x, y;
128 switch (effectiveStyle) {
130 x = -(OFFSET + width);
131 y = flipped ? -(OFFSET + SIZE) : OFFSET;
132 break;
134 x = OFFSET;
135 y = flipped ? -(OFFSET + SIZE) : OFFSET;
136 break;
137 default:
138 x = width / -2.0;
139 y = flipped ? OFFSET_ABOVE : -(OFFSET_ABOVE + SIZE);
140 break;
141 }
142
143 QRectF rect(x, y, width, SIZE);
144
145 qreal canvasRotationAngle = canvas->rotationAngle();
146 if (!qFuzzyIsNull(canvasRotationAngle)) {
147 QTransform tf;
148 tf.rotate(mirrored ? canvasRotationAngle : -canvasRotationAngle);
149 rect = tf.mapRect(rect);
150 }
151
152 return rect;
153 }
154
156 {
158
159 // If zoom preview enabled, it's possible to offset the color sampler preview and still be accurate
160 // This can help when sampling color using finger gesture
163
164 constexpr qreal OFFSET = 10.0;
165 constexpr qreal OFFSET_ABOVE = 20.0;
166 bool mirrored = canvas->xAxisMirrored();
167 bool flipped = canvas->yAxisMirrored();
171 }
174 }
175 else effectivePos = circlePreviewPosition;
176
177 qreal x = 0;
178 qreal y = flipped ? circlePreviewDiameter / 2.0 + OFFSET_ABOVE : -circlePreviewDiameter / 2.0 - OFFSET_ABOVE;
179
180 switch (effectivePos) {
182 x = -circlePreviewDiameter / 2.0 - OFFSET;
183 break;
185 x = circlePreviewDiameter / 2.0 + OFFSET;
186 break;
187 default:
188 x = 0;
189 break;
190 }
191
192 QTransform tf;
193
194 qreal canvasRotationAngle = canvas->rotationAngle();
195 if (!qFuzzyIsNull(canvasRotationAngle)) {
196 tf.rotate(mirrored ? canvasRotationAngle : -canvasRotationAngle);
197 }
198
199 QPointF offset = tf.map(QPointF(x,y));
200
201 circleRect.translate(offset.x(), offset.y());
202 }
203
204 return circleRect;
205 }
206
207 QRectF colorPreviewDocRect(const QPointF &outlineDocPoint)
208 {
209 QRectF colorPreviewViewRect;
210 switch (style) {
212 return QRectF();
216 colorPreviewViewRect = colorPreviewRectForRectangle();
217 break;
218 default:
219 // Showing a preview without sampling a color (by just holding a
220 // modifier) is used to compare the foreground color with the
221 // canvas. The circle doesn't work well for that purpose, so we
222 // use the handedness-independent rectangle above instead.
223 if (haveSample) {
224 colorPreviewViewRect = colorPreviewRectForCircle();
225 } else {
226 colorPreviewViewRect = colorPreviewRectForRectangle();
227 }
228 break;
229 }
230
231 const QRectF colorPreviewDocumentRect = converter().viewToDocument(colorPreviewViewRect);
232 return colorPreviewDocumentRect.translated(outlineDocPoint);
233 }
234
235 QRect standardizeZoomPreviewPixelRect(QRect pixelRect) {
236 // If width and height get rounded to different number, the center will be stuck between pixel border
237 if (pixelRect.width() != pixelRect.height()) {
238 if (pixelRect.width() % 2 == 0) pixelRect.setWidth(pixelRect.height());
239 else pixelRect.setHeight(pixelRect.width());
240 }
241 // If width and height is even, the center will be stuck in pixel corner
242 else if (pixelRect.width() % 2 == 0) {
243 pixelRect.setWidth(pixelRect.width() - 1);
244 pixelRect.setHeight(pixelRect.height() - 1);
245 }
246
247 // Minimum sample size should be 3 to be meaningful
248 if (pixelRect.width() < 3 || pixelRect.height() < 3) pixelRect.setSize(QSize(3, 3));
249
250 return pixelRect;
251 }
252
255
257
258 int gapRadius = 3;
259 int crosshairRadius = circlePreviewDiameter / 2 * 0.2;
260 cacheCrosshairPath = QPainterPath(QPointF(-crosshairRadius, 0));
261
262 cacheCrosshairPath.lineTo(-gapRadius, 0);
263 cacheCrosshairPath.moveTo(gapRadius, 0);
264 cacheCrosshairPath.lineTo(crosshairRadius, 0);
265
266 QTransform tf;
267 tf.translate(0, 0);
268 tf.rotate(90);
269
270 cacheCrosshairPath.moveTo(0,0);
271
272 cacheCrosshairPath.addPath(tf.map(cacheCrosshairPath));
273
274 return cacheCrosshairPath;
275 }
276};
277
279 : m_d(new Private(canvas))
280{
281 using namespace std::placeholders; // For _1 placeholder
282 std::function<void(QPointF)> callback =
284 m_d->samplingCompressor.reset(
286
287 m_d->activationDelayTimer.setInterval(100);
288 m_d->activationDelayTimer.setSingleShot(true);
289 connect(&m_d->activationDelayTimer, SIGNAL(timeout()), this, SLOT(activateDelayedPreview()));
290 connect(m_d->canvas->displayColorConverter(), SIGNAL(displayConfigurationChanged()), this, SLOT(slotUpdateBgColor()));
292}
293
298
300{
301 return m_d->isActive;
302}
303
304void KisAsyncColorSamplerHelper::activate(bool sampleCurrentLayer, bool pickFgColor)
305{
306
308 m_d->isActive = true;
309
310 m_d->sampleResourceId =
311 pickFgColor ?
314
315 m_d->sampleCurrentLayer = sampleCurrentLayer;
316 m_d->haveSample = false;
317
318
319 KisConfig cfg(true);
320 m_d->style = cfg.colorSamplerPreviewStyle();
321
322 m_d->circlePreviewDiameter = cfg.colorSamplerPreviewCircleDiameter();
323 m_d->circlePreviewThickness = cfg.colorSamplerPreviewCircleThickness()/100.0; // saved in percentages
324 m_d->circlePreviewOutlineEnabled = cfg.colorSamplerPreviewCircleOutlineEnabled();
325 m_d->circlePreviewExtraCircles = cfg.colorSamplerPreviewCircleExtraCirclesEnabled();
326 m_d->circleZoomPreviewEnabled = cfg.colorSamplerZoomPreviewEnabled();
327 m_d->circleZoomPreviewScale = cfg.colorSamplerZoomPreviewScale();
328 m_d->circlePreviewPosition = cfg.colorSamplerPreviewCirclePosition();
329
330 m_d->activationDelayTimer.start();
331}
332
334{
335 // the event may come after we have started or even
336 // finished color picking if the user is quick
337 if (!m_d->isActive || m_d->showPreview) {
338 return;
339 }
340
342
344}
345
347{
348 m_d->activationDelayTimer.stop();
349 m_d->showPreview = true;
350
351 const KoColor currentColor =
352 m_d->canvas->resourceManager()->koColorResource(m_d->sampleResourceId);
353 const QColor previewColor = m_d->canvas->displayColorConverter()->convertColorToDisplayColorSpace(currentColor, true);
354
355 m_d->currentColor = previewColor;
356 m_d->baseColor = previewColor;
357 m_d->cache = QPixmap();
358
359 updateCursor(m_d->sampleCurrentLayer, m_d->sampleResourceId == KoCanvasResource::ForegroundColor);
360}
361
362void KisAsyncColorSamplerHelper::updateCursor(bool sampleCurrentLayer, bool pickFgColor)
363{
364 const int sampleResourceId =
365 pickFgColor ?
368
369 QCursor cursor;
370
371 if (sampleCurrentLayer) {
372 if (sampleResourceId == KoCanvasResource::ForegroundColor) {
374 } else {
376 }
377 } else {
378 if (sampleResourceId == KoCanvasResource::ForegroundColor) {
380 } else {
382 }
383 }
384
385 Q_EMIT sigRequestCursor(cursor);
386}
387
389{
390 m_d->updateGlobalColor = value;
391}
392
394{
395 return m_d->updateGlobalColor;
396}
397
399{
400 KIS_SAFE_ASSERT_RECOVER(!m_d->strokeId) {
401 endAction();
402 }
403
404 m_d->activationDelayTimer.stop();
405
406 m_d->showPreview = false;
407 m_d->haveSample = false;
408
409 m_d->previewDocRect = QRectF();
410 m_d->currentColor = QColor();
411 m_d->baseColor = QColor();
412 m_d->cache = QPixmap();
413
414 // Reset the cached zoom preview image and rect
415 m_d->cacheCanvasPreviewImage = QImage();
416 m_d->cacheCanvasPreviewRect = QRect();
417 m_d->zoomPreviewHasPainted = false;
418 m_d->zoomDocRectF = QRectF();
419
420 m_d->isActive = false;
421
422 Q_EMIT sigRequestCursorReset();
424}
425
426void KisAsyncColorSamplerHelper::startAction(const QPointF &docPoint, int radius, int blend)
427{
435
437 m_d->haveSample = true;
438 m_d->strokeId = m_d->strokesFacade()->startStroke(strategy);
439 m_d->samplingCompressor->start(docPoint);
440}
441
443{
445 m_d->samplingCompressor->start(docPoint);
446}
447
449{
451
452 m_d->strokesFacade()->addJob(m_d->strokeId,
454
455 m_d->strokesFacade()->endStroke(m_d->strokeId);
456 m_d->strokeId.clear();
457}
458
460{
461 if (!m_d->showPreview) return QRectF();
462
463 m_d->sampleDocPoint = docPoint;
464
465 KisConfig cfg(true);
466 m_d->style = cfg.colorSamplerPreviewStyle();
467 QRectF previewDocRect = m_d->colorPreviewDocRect(docPoint);
468
469 if (previewDocRect != m_d->previewDocRect) prepareZoomPreview(previewDocRect);
470
471 m_d->previewDocRect = previewDocRect;
472
473 return m_d->previewDocRect;
474}
475
477 // This can be called before startAction(), so there may not be a strokeId
478 if (!m_d->circleZoomPreviewEnabled || !m_d->strokeId) return;
479
480 KisImageSP image = m_d->canvas->image();
481 if (!image) return;
482
483 // Prepare zoomed doc rect
484 QRectF zoomDocRectF = docRect;
485
486 zoomDocRectF.setSize(zoomDocRectF.size() / m_d->circleZoomPreviewScale);
487 zoomDocRectF.moveCenter(m_d->sampleDocPoint);
488
489 m_d->zoomDocRectF = zoomDocRectF;
490
491 // Prepare canvas preview because it takes time
492 QRectF canvasPixelRectF = image->documentToPixel(zoomDocRectF);
493 // Floor manually to avoid width != height when using toRect()
494 QRect canvasPixelRect = QRect(canvasPixelRectF.topLeft().toPoint(), QSize(qFloor(canvasPixelRectF.width()), qFloor(canvasPixelRectF.height())));
495
496 canvasPixelRect = m_d->standardizeZoomPreviewPixelRect(canvasPixelRect);
497 // Make sure the center is the pixel currently sampled because standardizing may change the shape
498 canvasPixelRect.moveCenter(image->documentToImagePixelFloored(zoomDocRectF.center()));
499
500 if (!image->bounds().intersects(canvasPixelRect) && !image->wrapAroundModeActive()) {
501 m_d->cacheCanvasPreviewRect = QRect();
502 m_d->cacheCanvasPreviewImage = QImage();
503 return;
504 }
505
506 // If not already have a job fetching canvas image, then do it
507 if (!m_d->canvasPreviewFetchCookie && m_d->cacheCanvasPreviewRect != canvasPixelRect) {
509 new KisColorSamplerStrokeStrategy::GenerateCanvasZoomPreviewData(image->projection(), canvasPixelRect, m_d->canvas->displayColorConverter());
510
511 m_d->canvasPreviewFetchCookie = data->cookie();
512
513 m_d->strokesFacade()->addJob(m_d->strokeId, data);
514 }
515}
516
517void KisAsyncColorSamplerHelper::paint(QPainter &gc, const KoViewConverter &converter)
518{
519 if (!m_d->showPreview) {
520 return;
521 }
522
523 QRectF viewRectF = converter.documentToView(m_d->previewDocRect);
524 QColor currentColor = colorWithAlpha(m_d->currentColor, OPACITY_OPAQUE_U8);
525 QColor baseColor = m_d->haveSample ? colorWithAlpha(m_d->baseColor, OPACITY_OPAQUE_U8) : currentColor;
526
527 switch (m_d->style) {
531 paintRectangle(gc, viewRectF, currentColor, baseColor);
532 break;
533 default:
534 // See comment in colorPreviewDocRect.
535 if (m_d->haveSample) {
536 paintCircle(gc, viewRectF, currentColor, baseColor);
537 } else {
538 paintRectangle(gc, viewRectF, currentColor, baseColor);
539 }
540 break;
541 }
542}
543
545 const QRectF &viewRectF,
546 const QColor &currentColor,
547 const QColor &baseColor)
548{
549 qreal dpr = gc.device()->devicePixelRatioF();
550 QSizeF cacheSizeF = viewRectF.size() * dpr;
551 QSize cacheSize(qCeil(cacheSizeF.width()), qCeil(cacheSizeF.height()));
552 bool needsNewCache = m_d->cache.isNull() || m_d->cache.size() != cacheSize;
553 if (needsNewCache) {
554 m_d->cache = QPixmap(cacheSize);
555 m_d->cache.fill(Qt::transparent);
556 }
557
558 qreal canvasRotationAngle = m_d->canvas->rotationAngle();
559 bool canvasMirror = m_d->canvas->xAxisMirrored();
560 if (needsNewCache || !qFuzzyCompare(canvasRotationAngle, m_d->cacheRotation) || canvasMirror != m_d->cacheMirror) {
561 m_d->cacheRotation = canvasRotationAngle;
562 m_d->cacheMirror = canvasMirror;
563
564 QPainter cachePainter(&m_d->cache);
565 cachePainter.setRenderHint(QPainter::Antialiasing);
566
567 qreal size = Private::PREVIEW_RECT_SIZE * dpr;
568 QRectF rect(0.0, 0.0, m_d->haveSample ? size * 2.0 : size, size);
569 rect.moveTopLeft(-rect.center());
570
571 QTransform tf;
572 QPointF offset = QRectF(m_d->cache.rect()).center();
573 tf.translate(offset.x(), offset.y());
574 tf.rotate(canvasMirror ? canvasRotationAngle : -canvasRotationAngle);
575 cachePainter.setTransform(tf);
576
577 if (m_d->haveSample) {
578 qreal centerX = rect.center().x();
579 QRectF currentRect(rect.topLeft(), QPointF(centerX + 1.0, rect.bottom()));
580 QRectF baseRect(QPointF(centerX, rect.top()), rect.bottomRight());
581 if (m_d->canvas->xAxisMirrored()) {
582 std::swap(currentRect, baseRect);
583 }
584 cachePainter.fillRect(currentRect, currentColor);
585 cachePainter.fillRect(baseRect, baseColor);
586 } else {
587 cachePainter.fillRect(rect, currentColor);
588 }
589 }
590
591 gc.drawPixmap(viewRectF.toRect(), m_d->cache);
592}
593
595 const QRectF &viewRectF,
596 const QColor &currentColor,
597 const QColor &baseColor)
598{
599 if (!m_d->haveSample) {
600 return;
601 }
602
603 gc.save();
604
605 qreal dpr = gc.device()->devicePixelRatioF();
606 QSizeF cacheSizeF = viewRectF.size() * dpr;
607 QSize cacheSize(qCeil(cacheSizeF.width()), qCeil(cacheSizeF.height()));
608 bool needsNewCache = m_d->cache.isNull() || m_d->cache.size() != cacheSize;
609 if (needsNewCache) {
610 m_d->cache = QPixmap(cacheSize);
611 m_d->cache.fill(Qt::transparent);
612 }
613
614 qreal canvasRotationAngle = m_d->canvas->rotationAngle();
615 if (m_d->canvas->xAxisMirrored()) {
616 canvasRotationAngle = -canvasRotationAngle;
617 }
618
619 QPainter cachePainter(&m_d->cache);
620 cachePainter.setRenderHint(QPainter::Antialiasing);
621
622 QRectF cacheRect = m_d->cache.rect();
623
624 // The color sampler preview is an outline and those rotate along
625 // with the canvas. That's undesirable for the sampler preview
626 // though, so we calculate the transformation to counter the rotation here
627 QPointF cacheCenter = cacheRect.center();
628 QTransform tf;
629 tf.translate(cacheCenter.x(), cacheCenter.y());
630 tf.rotate(-canvasRotationAngle);
631 tf.translate(-cacheCenter.x(), -cacheCenter.y());
632
633 bool needsDualColor = currentColor != baseColor;
634 if (needsNewCache || (needsDualColor && !qFuzzyCompare(m_d->cacheRotation, canvasRotationAngle))) {
635 m_d->cacheRotation = canvasRotationAngle;
636
637 QColor backgroundColor = colorWithAlpha(m_d->backgroundColor, OPACITY_OPAQUE_U8 / 2 + 1);
638 qreal penWidth = m_d->circlePreviewDiameter > 100 ? (2.0 * dpr) : (1.0 * dpr);
639 QPen pen = QPen(backgroundColor, penWidth);
640 if (m_d->circlePreviewOutlineEnabled) {
641 cachePainter.setPen(pen);
642 } else {
643 cachePainter.setPen(Qt::NoPen);
644 }
645
646 QRectF outerRect = cacheRect.marginsRemoved(QMarginsF(penWidth, penWidth, penWidth, penWidth));
647
648 if (needsDualColor) {
649 // The color sampler preview is an outline and those rotate along
650 // with the canvas. That's undesirable for the sampler preview
651 // though, so we un-rotate its contents here accordingly.
652
653 QPainterPath clipPath;
654 clipPath.addPolygon(tf.map(QPolygonF(QRectF(0, 0, cacheRect.width(), cacheRect.height() / 2.0 + 1.0))));
655 cachePainter.setClipPath(clipPath);
656
657 bool flipped = m_d->canvas->yAxisMirrored();
658 cachePainter.setBrush(flipped ? baseColor : currentColor);
659 cachePainter.drawEllipse(outerRect);
660
661 cachePainter.setBrush(baseColor);
662 clipPath.clear();
663 clipPath.addPolygon(
664 tf.map(QRectF(0, cacheRect.height() / 2.0, cacheRect.width(), cacheRect.height() / 2.0)));
665 cachePainter.setClipPath(clipPath);
666
667 cachePainter.setBrush(flipped ? currentColor : baseColor);
668 cachePainter.drawEllipse(outerRect);
669
670 cachePainter.setClipPath(QPainterPath(), Qt::NoClip);
671 } else {
672 cachePainter.setBrush(currentColor);
673 cachePainter.drawEllipse(outerRect);
674 }
675
676 qreal innerX = cacheRect.width() * (1.0 - m_d->circlePreviewThickness);
677 qreal innerY = cacheRect.height() * (1.0 - m_d->circlePreviewThickness);
678 QRectF innerRect = cacheRect.marginsRemoved(QMarginsF(innerX, innerY, innerX, innerY));
679 QPainterPath innerEllipse;
680 innerEllipse.addEllipse(innerRect);
681
682 QPainterPath innerPath;
683 innerPath.addPath(innerEllipse);
684
685 if (m_d->circlePreviewThickness < 0.5 && m_d->circlePreviewExtraCircles) {
686 qreal extraMargin = 0.1*m_d->circlePreviewThickness*innerRect.width(); // looks better
687 QPointF leftCenter = QPointF(innerRect.left() - extraMargin, innerRect.top() + innerRect.height()/2.0);
688 QPointF rightCenter = QPointF(innerRect.right() + extraMargin, innerRect.top() + innerRect.height()/2.0);
689
690 innerPath.setFillRule(Qt::OddEvenFill);
691 innerPath.addEllipse(leftCenter, m_d->circlePreviewThickness*cacheRect.width(), m_d->circlePreviewThickness*cacheRect.width());
692 innerPath.addEllipse(rightCenter, m_d->circlePreviewThickness*cacheRect.width(), m_d->circlePreviewThickness*cacheRect.width());
693
694 innerPath = innerPath.intersected(innerEllipse);
695 }
696
697 m_d->cacheCircleInnerClip = innerPath;
698
699 // Draw inner circle outline if enabled
700 if (m_d->circlePreviewOutlineEnabled) {
701 cachePainter.setBrush(Qt::transparent);
702 cachePainter.setPen(pen);
703 cachePainter.setCompositionMode(QPainter::CompositionMode_SourceOver);
704 cachePainter.drawPath(tf.map(m_d->cacheCircleInnerClip));
705 }
706 }
707
708 cachePainter.setPen(Qt::NoPen);
709 cachePainter.setBrush(m_d->backgroundColor);
710
711 // Clear the center
712 cachePainter.setCompositionMode(QPainter::CompositionMode_Clear);
713 cachePainter.drawPath(tf.map(m_d->cacheCircleInnerClip));
714
715 // If canvas preview image is null and it's being fetched (which means canvas preview is needed),
716 // don't paint preview until it's done to avoid flickering
717 bool shouldPaintPreview = !(m_d->cacheCanvasPreviewImage.isNull() && m_d->canvasPreviewFetchCookie);
718
719 // But if zoom preview has already been painted, keeps painting to avoid flickering
720 if (m_d->circleZoomPreviewEnabled && (shouldPaintPreview || m_d->zoomPreviewHasPainted)) {
721 paintCircleCanvasPreview(cachePainter, cacheRect, tf.map(m_d->cacheCircleInnerClip));
722 paintCircleReferenceImagePreview(cachePainter, cacheRect, tf.map(m_d->cacheCircleInnerClip));
723
724 // Draw crosshair if preview is offseted
725 if (m_d->circlePreviewPosition != KisConfig::ColorSamplerPreviewCirclePosition::Center) {
726 paintCircleCrosshair(cachePainter, cacheRect, currentColor);
727 }
728
729 // Fill empty spaces to hide the underlying canvas
730 cachePainter.setCompositionMode(QPainter::CompositionMode_DestinationOver);
731 cachePainter.drawPath(tf.map(m_d->cacheCircleInnerClip));
732
733 m_d->zoomPreviewHasPainted = true;
734 }
735
736 gc.drawPixmap(viewRectF.toRect(), m_d->cache);
737
738 gc.restore();
739}
740
741void KisAsyncColorSamplerHelper::paintCircleCrosshair(QPainter &gc, const QRectF &viewRectF, const QColor &currentColor)
742{
743 QColor crosshairColor = Qt::black;
744 qreal luminance = KisPaintingTweaks::luminosityCoarse(currentColor);
745 if (luminance < 0.5) crosshairColor = Qt::white;
746
747 gc.save();
748
749 gc.setRenderHints(QPainter::Antialiasing | QPainter::SmoothPixmapTransform);
750 gc.setCompositionMode(QPainter::CompositionMode_SourceOver);
751 gc.setPen(crosshairColor);
752
753 QTransform tf;
754 tf.translate(viewRectF.center().x(), viewRectF.center().y());
755 tf.rotate(-m_d->cacheRotation);
756 gc.drawPath(tf.map(m_d->crosshairForOffsettedCircle()));
757
758 gc.restore();
759}
760
761void KisAsyncColorSamplerHelper::paintCircleCanvasPreview(QPainter &gc, const QRectF &viewRectF, const QPainterPath &clip) {
762 QRect canvasPixelRect = m_d->cacheCanvasPreviewRect;
763 QImage cachedImage = m_d->cacheCanvasPreviewImage;
764
765 if (cachedImage.isNull()) return;
766
767 gc.save();
768 gc.setCompositionMode(QPainter::CompositionMode_SourceOver);
769
770 gc.setClipPath(clip);
771
772 // QtDoc: The image is scaled to fit the rectangle, if both the image and rectangle size disagree.
773 // Since the piece of canvas is (zoomPreviewScale) times smaller than cacheRect
774 // drawImage will scale it up that many times, thus achieving the zoom effect
775 gc.drawImage(viewRectF, cachedImage, canvasPixelRect);
776
777 gc.restore();
778}
779
780// Preview won't respect opacity because the color sampled doesn't respect opacity either
781// But if opacity = 0, preview for that image won't show at all
782void KisAsyncColorSamplerHelper::paintCircleReferenceImagePreview(QPainter &gc, const QRectF &viewRectF, const QPainterPath &clip) {
783 KisDocument *document = m_d->canvas->viewManager()->document();
784 if (!document || !document->referenceImagesLayer()) return;
785
786 gc.save();
787
788 gc.setCompositionMode(QPainter::CompositionMode_SourceOver);
789 gc.setClipPath(clip);
790
791 QRectF zoomDocRectF = m_d->zoomDocRectF;
792
793 QList<KoShape*> shapeList = document->referenceImagesLayer()->shapeManager()->shapesAt(zoomDocRectF);
794 if (shapeList.size() > 1) std::sort(shapeList.begin(), shapeList.end(), KoShape::compareShapeZIndex);
795
796 Q_FOREACH(KoShape *shape, shapeList) {
797 KisReferenceImage *refImage = dynamic_cast<KisReferenceImage*>(shape);
798 if (!refImage) continue;
799
800 if (refImage->transparency() == 1.0) continue;
801
802 gc.save();
803
804 QImage image = refImage->getCachedImage();
805
806 QPoint refPixelPoint = refImage->documentToPixelFloored(zoomDocRectF.center());
807
808 // Avoid using KisReferenceImage::documentToPixel because it use absoluteTransformation.invert()
809 // Which will take rotation into account and cause some quirks when ref image is rotated
810 qreal xScale = refImage->size().width() / image.width();
811 qreal yScale = refImage->size().height() / image.height();
812
813 // This only happens when refImage size is 0, so painting it probably won't make senses
814 if (xScale == 0 || yScale == 0) continue;
815
816 QRect refPixelRect = QRect(refPixelPoint, QSize(zoomDocRectF.width() / xScale, zoomDocRectF.height() / yScale));
817
818 refPixelRect = m_d->standardizeZoomPreviewPixelRect(refPixelRect);
819
820 refPixelRect.moveCenter(refPixelPoint);
821
822 // Rotate the painter, draw, rotate back is seemingly easier than
823 // trying to rotate the image and the shenanighens that follow
824 gc.translate(viewRectF.center());
825 gc.rotate(refImage->rotation());
826 gc.translate(-viewRectF.center());
827
828 gc.drawImage(viewRectF, image, refPixelRect);
829
830 gc.restore();
831 }
832
833 gc.restore();
834}
835
836void KisAsyncColorSamplerHelper::slotCanvasZoomPreviewUpdated(const QImage &canvasImage, QRect canvasRect) {
837 m_d->cacheCanvasPreviewRect = canvasRect;
838 m_d->cacheCanvasPreviewImage = canvasImage;
839
841}
842
844{
849 if (!m_d->strokeId) return;
850
851 KisImageSP image = m_d->canvas->image();
852 const QPoint imagePoint = image->documentToImagePixelFloored(docPoint);
853
854 if (!m_d->sampleCurrentLayer) {
855 KisSharedPtr<KisReferenceImagesLayer> referencesLayer = m_d->canvas->imageView()->document()->referenceImagesLayer();
856 if (referencesLayer && m_d->canvas->referenceImagesDecoration()->visible()) {
857 KoColor color = referencesLayer->getPixelForDocPoint(docPoint);
858 if (color.opacityU8() > 0) {
860 return;
861 }
862 }
863 }
864
865 KisPaintDeviceSP device = m_d->sampleCurrentLayer ?
866 m_d->canvas->imageView()->currentNode()->colorSampleSourceDevice() :
867 image->projection();
868
869 if (device) {
870 // Used for color sampler blending.
871 const KoColor currentColor =
872 m_d->canvas->resourceManager()->koColorResource(m_d->sampleResourceId);
873
874 m_d->strokesFacade()->addJob(m_d->strokeId,
875 new KisColorSamplerStrokeStrategy::Data(device, imagePoint, currentColor));
876 } else {
877 QString message = i18n("Color sampler does not work on this layer.");
878 m_d->canvas->viewManager()->showFloatingMessage(message, koIcon("object-locked"));
879 }
880}
881
883{
884 KoColor color(rawColor);
885
887
888 if (m_d->updateGlobalColor) {
889 m_d->canvas->resourceManager()->setResource(m_d->sampleResourceId, color);
890 }
891
892 Q_EMIT sigRawColorSelected(rawColor);
893 Q_EMIT sigColorSelected(color);
894
895 if (!m_d->showPreview) return;
896
897 const QColor previewColor = m_d->canvas->displayColorConverter()->convertColorToDisplayColorSpace(color, true);
898
899 if (!m_d->haveSample || m_d->currentColor != previewColor) {
900 m_d->haveSample = true;
901 m_d->currentColor = previewColor;
902 m_d->cache = QPixmap();
903 }
904
906}
907
909{
910 KoColor bgColor;
911 bgColor.fromQColor(qApp->palette().color(QPalette::Base));
912 m_d->backgroundColor = m_d->canvas->displayColorConverter()->convertColorToDisplayColorSpace(bgColor);
913}
float value(const T *src, size_t ch)
const quint8 OPACITY_OPAQUE_U8
void paintCircleCrosshair(QPainter &gc, const QRectF &viewRectF, const QColor &currentColor)
void slotColorSamplingFinished(const KoColor &rawColor)
void activate(bool sampleCurrentLayer, bool pickFgColor)
void sigFinalColorSelected(const KoColor &color)
void slotAddSamplingJob(const QPointF &docPoint)
void sigRequestCursor(const QCursor &cursor)
void paintRectangle(QPainter &gc, const QRectF &viewRectF, const QColor &currentColor, const QColor &baseColor)
void paint(QPainter &gc, const KoViewConverter &converter)
void slotCanvasZoomPreviewUpdated(const QImage &canvasImage, QRect canvasRect)
void prepareZoomPreview(const QRectF &docRect)
void updateCursor(bool sampleCurrentLayer, bool pickFgColor)
void continueAction(const QPointF &docPoint)
void startAction(const QPointF &docPoint, int radius, int blend)
void sigColorSelected(const KoColor &color)
void paintCircleReferenceImagePreview(QPainter &gc, const QRectF &viewRectF, const QPainterPath &clip)
void sigRawColorSelected(const KoColor &color)
void paintCircle(QPainter &gc, const QRectF &viewRectF, const QColor &currentColor, const QColor &baseColor)
void paintCircleCanvasPreview(QPainter &gc, const QRectF &viewRectF, const QPainterPath &clip)
QRectF colorPreviewDocRect(const QPointF &docPoint)
qreal rotationAngle() const
canvas rotation in degrees
bool xAxisMirrored() const
Bools indicating canvasmirroring.
KisImageWSP image() const
KisViewManager * viewManager() const
QPointer< KisView > imageView() const
bool yAxisMirrored() const
void sigCanvasZoomPreviewUpdated(const QImage &canvasImage, const QRect &canvasRect)
void sigFinalColorSelected(const KoColor &color)
void sigColorUpdated(const KoColor &color)
ColorSamplerPreviewCirclePosition colorSamplerPreviewCirclePosition(bool defaultValue=false) const
bool colorSamplerPreviewCircleOutlineEnabled(bool defaultValue=false) const
bool colorSamplerZoomPreviewEnabled(bool defaultValue=false) const
int colorSamplerPreviewCircleDiameter(bool defaultValue=false) const
ColorSamplerPreviewStyle
Definition kis_config.h:138
qreal colorSamplerPreviewCircleThickness(bool defaultValue=false) const
ColorSamplerPreviewCirclePosition
Definition kis_config.h:168
bool colorSamplerPreviewCircleExtraCirclesEnabled(bool defaultValue=false) const
ColorSamplerPreviewStyle colorSamplerPreviewStyle(bool defaultValue=false) const
qreal colorSamplerZoomPreviewScale(bool defaultValue=false) const
static QCursor samplerImageBackgroundCursor()
static QCursor samplerLayerBackgroundCursor()
static QCursor samplerLayerForegroundCursor()
static QCursor samplerImageForegroundCursor()
bool wrapAroundModeActive() const
QPointF documentToPixel(const QPointF &documentCoord) const
KisPaintDeviceSP projection() const
QPoint documentToImagePixelFloored(const QPointF &documentCoord) const
QRect bounds() const override
The KisReferenceImage class represents a single reference image.
QPoint documentToPixelFloored(const QPointF &docPoint)
KisDocument * document() const
void setOpacity(quint8 alpha)
Definition KoColor.cpp:333
void fromQColor(const QColor &c)
Convenient function for converting from a QColor.
Definition KoColor.cpp:213
quint8 opacityU8() const
Definition KoColor.cpp:341
virtual QSizeF size() const
Get the size of the shape in pt.
Definition KoShape.cpp:735
qreal rotation() const
Definition KoShape.cpp:715
static bool compareShapeZIndex(KoShape *s1, KoShape *s2)
Definition KoShape.cpp:388
qreal transparency(bool recursive=false) const
Definition KoShape.cpp:645
virtual QPointF viewToDocument(const QPointF &viewPoint) const
virtual QPointF documentToView(const QPointF &documentPoint) const
static bool qFuzzyCompare(half p1, half p2)
static bool qFuzzyIsNull(half h)
#define KIS_SAFE_ASSERT_RECOVER(cond)
Definition kis_assert.h:126
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define KIS_SAFE_ASSERT_RECOVER_NOOP(cond)
Definition kis_assert.h:130
#define koIcon(name)
Use these macros for icons without any issues.
Definition kis_icon.h:25
typedef void(QOPENGLF_APIENTRYP PFNGLINVALIDATEBUFFERDATAPROC)(GLuint buffer)
qreal luminosityCoarse(const QColor &c, bool sRGBtrc)
luminosityCoarse This calculates the luminosity of the given QColor. It uses a very coarse (10 step) ...
@ BackgroundColor
The active background color selected for this canvas.
@ ForegroundColor
The active foreground color selected for this canvas.
KisConfig::ColorSamplerPreviewCirclePosition circlePreviewPosition
KisConfig::ColorSamplerPreviewStyle style
QScopedPointer< SamplingCompressor > samplingCompressor
QRectF colorPreviewDocRect(const QPointF &outlineDocPoint)
KisSignalCompressorWithParam< QPointF > SamplingCompressor
QWeakPointer< boost::none_t > canvasPreviewFetchCookie
KisCanvas2 * canvas