Krita Source Code Documentation
Loading...
Searching...
No Matches
KisAsyncColorSamplerHelper.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2022 Dmitry Kazakov <dimula73@gmail.com>
3 * SPDX-FileCopyrightText: 2025 Carsten Hartenfels <carsten.hartenfels@pm.me>
4 *
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
9
10#include <QApplication>
11#include <QPainter>
12#include <QPainterPath>
13#include <QPalette>
14#include <QPixmap>
15#include <QTransform>
16
19#include "KoViewConverter.h"
20#include "KoIcon.h"
21#include "kis_cursor.h"
24#include "kis_canvas2.h"
25#include "KisViewManager.h"
26#include "KisDocument.h"
31
32
33namespace {
34QColor colorWithAlpha(QColor color, int alpha)
35{
36 color.setAlpha(alpha);
37 return color;
38}
39}
40
42{
43 static constexpr qreal PREVIEW_RECT_SIZE = 48.0;
44
46 : canvas(_canvas)
47 {}
48
50
52 bool sampleCurrentLayer {true};
53 bool updateGlobalColor {true};
54
55 bool isActive {false};
56 bool showPreview {false};
57 bool haveSample {false};
58
61 QScopedPointer<SamplingCompressor> samplingCompressor;
62
64
71
73 QColor baseColor;
74
75 QPixmap cache;
76 qreal cacheRotation = 0.0;
77 bool cacheMirror = false;
78
80 return canvas->image().data();
81 }
82
83 const KoViewConverter &converter() const {
84 return *canvas->imageView()->viewConverter();
85 }
86
88 {
89 // Offsetting to the sides is both vertical and horizontal, when
90 // offsetting above it's only vertical, so it needs a bit more space.
91 constexpr qreal OFFSET = 32.0;
92 constexpr qreal OFFSET_ABOVE = OFFSET * 1.5;
93 constexpr qreal SIZE = PREVIEW_RECT_SIZE;
94
95 bool mirrored = canvas->xAxisMirrored();
96 bool flipped = canvas->yAxisMirrored();
97
103 } else {
104 effectiveStyle = style;
105 }
106
107 qreal width = haveSample ? SIZE * 2.0 : SIZE;
108
109 qreal x, y;
110 switch (effectiveStyle) {
112 x = -(OFFSET + width);
113 y = flipped ? -(OFFSET + SIZE) : OFFSET;
114 break;
116 x = OFFSET;
117 y = flipped ? -(OFFSET + SIZE) : OFFSET;
118 break;
119 default:
120 x = width / -2.0;
121 y = flipped ? OFFSET_ABOVE : -(OFFSET_ABOVE + SIZE);
122 break;
123 }
124
125 QRectF rect(x, y, width, SIZE);
126
127 qreal canvasRotationAngle = canvas->rotationAngle();
128 if (!qFuzzyIsNull(canvasRotationAngle)) {
129 QTransform tf;
130 tf.rotate(mirrored ? canvasRotationAngle : -canvasRotationAngle);
131 rect = tf.mapRect(rect);
132 }
133
134 return rect;
135 }
136
141
142 QRectF colorPreviewDocRect(const QPointF &outlineDocPoint)
143 {
144 QRectF colorPreviewViewRect;
145 switch (style) {
147 return QRectF();
151 colorPreviewViewRect = colorPreviewRectForRectangle();
152 break;
153 default:
154 // Showing a preview without sampling a color (by just holding a
155 // modifier) is used to compare the foreground color with the
156 // canvas. The circle doesn't work well for that purpose, so we
157 // use the handedness-independent rectangle above instead.
158 if (haveSample) {
159 colorPreviewViewRect = colorPreviewRectForCircle();
160 } else {
161 colorPreviewViewRect = colorPreviewRectForRectangle();
162 }
163 break;
164 }
165
166 const QRectF colorPreviewDocumentRect = converter().viewToDocument(colorPreviewViewRect);
167 return colorPreviewDocumentRect.translated(outlineDocPoint);
168 }
169};
170
172 : m_d(new Private(canvas))
173{
174 using namespace std::placeholders; // For _1 placeholder
175 std::function<void(QPointF)> callback =
177 m_d->samplingCompressor.reset(
179
180 m_d->activationDelayTimer.setInterval(100);
181 m_d->activationDelayTimer.setSingleShot(true);
182 connect(&m_d->activationDelayTimer, SIGNAL(timeout()), this, SLOT(activateDelayedPreview()));
183}
184
189
191{
192 return m_d->isActive;
193}
194
195void KisAsyncColorSamplerHelper::activate(bool sampleCurrentLayer, bool pickFgColor)
196{
198 m_d->isActive = true;
199
200 m_d->sampleResourceId =
201 pickFgColor ?
204
205 m_d->sampleCurrentLayer = sampleCurrentLayer;
206 m_d->haveSample = false;
207
208
209 KisConfig cfg(true);
210 m_d->style = cfg.colorSamplerPreviewStyle();
211
212 m_d->circlePreviewDiameter = cfg.colorSamplerPreviewCircleDiameter();
213 m_d->circlePreviewThickness = cfg.colorSamplerPreviewCircleThickness()/100.0; // saved in percentages
214 m_d->circlePreviewOutlineEnabled = cfg.colorSamplerPreviewCircleOutlineEnabled();
215 m_d->circlePreviewExtraCircles = cfg.colorSamplerPreviewCircleExtraCirclesEnabled();
216
217 m_d->activationDelayTimer.start();
218}
219
221{
222 // the event may come after we have started or even
223 // finished color picking if the user is quick
224 if (!m_d->isActive || m_d->showPreview) {
225 return;
226 }
227
229
231}
232
234{
235 m_d->activationDelayTimer.stop();
236 m_d->showPreview = true;
237
238 const KoColor currentColor =
239 m_d->canvas->resourceManager()->koColorResource(m_d->sampleResourceId);
240 const QColor previewColor = m_d->canvas->displayColorConverter()->toQColor(currentColor);
241
242 m_d->currentColor = previewColor;
243 m_d->baseColor = previewColor;
244 m_d->cache = QPixmap();
245
246 updateCursor(m_d->sampleCurrentLayer, m_d->sampleResourceId == KoCanvasResource::ForegroundColor);
247}
248
249void KisAsyncColorSamplerHelper::updateCursor(bool sampleCurrentLayer, bool pickFgColor)
250{
251 const int sampleResourceId =
252 pickFgColor ?
255
256 QCursor cursor;
257
258 if (sampleCurrentLayer) {
259 if (sampleResourceId == KoCanvasResource::ForegroundColor) {
261 } else {
263 }
264 } else {
265 if (sampleResourceId == KoCanvasResource::ForegroundColor) {
267 } else {
269 }
270 }
271
272 Q_EMIT sigRequestCursor(cursor);
273}
274
276{
277 m_d->updateGlobalColor = value;
278}
279
281{
282 return m_d->updateGlobalColor;
283}
284
286{
287 KIS_SAFE_ASSERT_RECOVER(!m_d->strokeId) {
288 endAction();
289 }
290
291 m_d->activationDelayTimer.stop();
292
293 m_d->showPreview = false;
294 m_d->haveSample = false;
295
296 m_d->previewDocRect = QRectF();
297 m_d->currentColor = QColor();
298 m_d->baseColor = QColor();
299 m_d->cache = QPixmap();
300
301 m_d->isActive = false;
302
303 Q_EMIT sigRequestCursorReset();
305}
306
307void KisAsyncColorSamplerHelper::startAction(const QPointF &docPoint, int radius, int blend)
308{
314
316 m_d->haveSample = true;
317 m_d->strokeId = m_d->strokesFacade()->startStroke(strategy);
318 m_d->samplingCompressor->start(docPoint);
319}
320
322{
324 m_d->samplingCompressor->start(docPoint);
325}
326
328{
330
331 m_d->strokesFacade()->addJob(m_d->strokeId,
333
334 m_d->strokesFacade()->endStroke(m_d->strokeId);
335 m_d->strokeId.clear();
336}
337
339{
340 if (!m_d->showPreview) return QRectF();
341
342 KisConfig cfg(true);
343 m_d->style = cfg.colorSamplerPreviewStyle();
344 m_d->previewDocRect = m_d->colorPreviewDocRect(docPoint);
345 return m_d->previewDocRect;
346}
347
348void KisAsyncColorSamplerHelper::paint(QPainter &gc, const KoViewConverter &converter)
349{
350 if (!m_d->showPreview) {
351 return;
352 }
353
354 QRectF viewRectF = converter.documentToView(m_d->previewDocRect);
355 QColor currentColor = colorWithAlpha(m_d->currentColor, OPACITY_OPAQUE_U8);
356 QColor baseColor = m_d->haveSample ? colorWithAlpha(m_d->baseColor, OPACITY_OPAQUE_U8) : currentColor;
357
358 switch (m_d->style) {
362 paintRectangle(gc, viewRectF, currentColor, baseColor);
363 break;
364 default:
365 // See comment in colorPreviewDocRect.
366 if (m_d->haveSample) {
367 paintCircle(gc, viewRectF, currentColor, baseColor);
368 } else {
369 paintRectangle(gc, viewRectF, currentColor, baseColor);
370 }
371 break;
372 }
373}
374
376 const QRectF &viewRectF,
377 const QColor &currentColor,
378 const QColor &baseColor)
379{
380 qreal dpr = gc.device()->devicePixelRatioF();
381 QSizeF cacheSizeF = viewRectF.size() * dpr;
382 QSize cacheSize(qCeil(cacheSizeF.width()), qCeil(cacheSizeF.height()));
383 bool needsNewCache = m_d->cache.isNull() || m_d->cache.size() != cacheSize;
384 if (needsNewCache) {
385 m_d->cache = QPixmap(cacheSize);
386 m_d->cache.fill(Qt::transparent);
387 }
388
389 qreal canvasRotationAngle = m_d->canvas->rotationAngle();
390 bool canvasMirror = m_d->canvas->xAxisMirrored();
391 if (needsNewCache || !qFuzzyCompare(canvasRotationAngle, m_d->cacheRotation) || canvasMirror != m_d->cacheMirror) {
392 m_d->cacheRotation = canvasRotationAngle;
393 m_d->cacheMirror = canvasMirror;
394
395 QPainter cachePainter(&m_d->cache);
396 cachePainter.setRenderHint(QPainter::Antialiasing);
397
398 qreal size = Private::PREVIEW_RECT_SIZE * dpr;
399 QRectF rect(0.0, 0.0, m_d->haveSample ? size * 2.0 : size, size);
400 rect.moveTopLeft(-rect.center());
401
402 QTransform tf;
403 QPointF offset = QRectF(m_d->cache.rect()).center();
404 tf.translate(offset.x(), offset.y());
405 tf.rotate(canvasMirror ? canvasRotationAngle : -canvasRotationAngle);
406 cachePainter.setTransform(tf);
407
408 if (m_d->haveSample) {
409 qreal centerX = rect.center().x();
410 QRectF currentRect(rect.topLeft(), QPointF(centerX + 1.0, rect.bottom()));
411 QRectF baseRect(QPointF(centerX, rect.top()), rect.bottomRight());
412 if (m_d->canvas->xAxisMirrored()) {
413 std::swap(currentRect, baseRect);
414 }
415 cachePainter.fillRect(currentRect, currentColor);
416 cachePainter.fillRect(baseRect, baseColor);
417 } else {
418 cachePainter.fillRect(rect, currentColor);
419 }
420 }
421
422 gc.drawPixmap(viewRectF.toRect(), m_d->cache);
423}
424
426 const QRectF &viewRectF,
427 const QColor &currentColor,
428 const QColor &baseColor)
429{
430 if (!m_d->haveSample) {
431 return;
432 }
433
434
435
436 gc.save();
437
438 qreal dpr = gc.device()->devicePixelRatioF();
439 QSizeF cacheSizeF = viewRectF.size() * dpr;
440 QSize cacheSize(qCeil(cacheSizeF.width()), qCeil(cacheSizeF.height()));
441 bool needsNewCache = m_d->cache.isNull() || m_d->cache.size() != cacheSize;
442 if (needsNewCache) {
443 m_d->cache = QPixmap(cacheSize);
444 m_d->cache.fill(Qt::transparent);
445 }
446
447 qreal canvasRotationAngle = m_d->canvas->rotationAngle();
448 if (m_d->canvas->xAxisMirrored()) {
449 canvasRotationAngle = -canvasRotationAngle;
450 }
451
452 bool needsDualColor = currentColor != baseColor;
453 if (needsNewCache || (needsDualColor && !qFuzzyCompare(m_d->cacheRotation, canvasRotationAngle))) {
454 m_d->cacheRotation = canvasRotationAngle;
455
456 QPainter cachePainter(&m_d->cache);
457 cachePainter.setRenderHint(QPainter::Antialiasing);
458
459 QColor backgroundColor = colorWithAlpha(qApp->palette().color(QPalette::Base), OPACITY_OPAQUE_U8 / 2 + 1);
460 qreal penWidth = m_d->circlePreviewDiameter > 100 ? (2.0 * dpr) : (1.0 * dpr);
461 QPen pen = QPen(backgroundColor, penWidth);
462 if (m_d->circlePreviewOutlineEnabled) {
463 cachePainter.setPen(pen);
464 } else {
465 cachePainter.setPen(Qt::NoPen);
466 }
467
468 QRectF cacheRect = m_d->cache.rect();
469 QRectF outerRect = cacheRect.marginsRemoved(QMarginsF(penWidth, penWidth, penWidth, penWidth));
470
471 QTransform tf;
472
473 QPointF cacheCenter = cacheRect.center();
474 tf.translate(cacheCenter.x(), cacheCenter.y());
475 tf.rotate(-canvasRotationAngle);
476 tf.translate(-cacheCenter.x(), -cacheCenter.y());
477
478
479 if (needsDualColor) {
480 // The color sampler preview is an outline and those rotate along
481 // with the canvas. That's undesirable for the sampler preview
482 // though, so we un-rotate its contents here accordingly.
483
484
485 QPainterPath clipPath;
486 clipPath.addPolygon(tf.map(QPolygonF(QRectF(0, 0, cacheRect.width(), cacheRect.height() / 2.0 + 1.0))));
487 cachePainter.setClipPath(clipPath);
488
489 bool flipped = m_d->canvas->yAxisMirrored();
490 cachePainter.setBrush(flipped ? baseColor : currentColor);
491 cachePainter.drawEllipse(outerRect);
492
493 cachePainter.setBrush(baseColor);
494 clipPath.clear();
495 clipPath.addPolygon(
496 tf.map(QRectF(0, cacheRect.height() / 2.0, cacheRect.width(), cacheRect.height() / 2.0)));
497 cachePainter.setClipPath(clipPath);
498
499 cachePainter.setBrush(flipped ? currentColor : baseColor);
500 cachePainter.drawEllipse(outerRect);
501
502 cachePainter.setClipPath(QPainterPath(), Qt::NoClip);
503 } else {
504 cachePainter.setBrush(currentColor);
505 cachePainter.drawEllipse(outerRect);
506 }
507
508 qreal innerX = cacheRect.width() * (1.0 - m_d->circlePreviewThickness);
509 qreal innerY = cacheRect.height() * (1.0 - m_d->circlePreviewThickness);
510 QRectF innerRect = cacheRect.marginsRemoved(QMarginsF(innerX, innerY, innerX, innerY));
511 QPainterPath innerEllipse;
512 innerEllipse.addEllipse(innerRect);
513
514 QPainterPath innerPath;
515 innerPath.addPath(innerEllipse);
516
517
518 if (m_d->circlePreviewThickness < 0.5 && m_d->circlePreviewExtraCircles) {
519 qreal extraMargin = 0.1*m_d->circlePreviewThickness*innerRect.width(); // looks better
520 QPointF leftCenter = QPointF(innerRect.left() - extraMargin, innerRect.top() + innerRect.height()/2.0);
521 QPointF rightCenter = QPointF(innerRect.right() + extraMargin, innerRect.top() + innerRect.height()/2.0);
522
523 innerPath.setFillRule(Qt::OddEvenFill);
524 innerPath.addEllipse(leftCenter, m_d->circlePreviewThickness*cacheRect.width(), m_d->circlePreviewThickness*cacheRect.width());
525 innerPath.addEllipse(rightCenter, m_d->circlePreviewThickness*cacheRect.width(), m_d->circlePreviewThickness*cacheRect.width());
526
527 innerPath = innerPath.intersected(innerEllipse);
528 }
529
530 cachePainter.setPen(Qt::NoPen);
531 cachePainter.setCompositionMode(QPainter::CompositionMode_Clear);
532 cachePainter.drawPath(tf.map(innerPath));
533
534 if (m_d->circlePreviewOutlineEnabled) {
535 cachePainter.setBrush(Qt::transparent);
536 cachePainter.setPen(pen);
537 cachePainter.setCompositionMode(QPainter::CompositionMode_SourceOver);
538 cachePainter.drawPath(tf.map(innerPath));
539 }
540 }
541 gc.drawPixmap(viewRectF.toRect(), m_d->cache);
542
543 gc.restore();
544}
545
547{
552 if (!m_d->strokeId) return;
553
554 KisImageSP image = m_d->canvas->image();
555
556 const QPoint imagePoint = image->documentToImagePixelFloored(docPoint);
557
558 if (!m_d->sampleCurrentLayer) {
559 KisSharedPtr<KisReferenceImagesLayer> referencesLayer = m_d->canvas->imageView()->document()->referenceImagesLayer();
560 if (referencesLayer && m_d->canvas->referenceImagesDecoration()->visible()) {
561 QColor color = referencesLayer->getPixel(imagePoint);
562 if (color.isValid() && color.alpha() != 0) {
564 return;
565 }
566 }
567 }
568
569 KisPaintDeviceSP device = m_d->sampleCurrentLayer ?
570 m_d->canvas->imageView()->currentNode()->colorSampleSourceDevice() :
571 image->projection();
572
573 if (device) {
574 // Used for color sampler blending.
575 const KoColor currentColor =
576 m_d->canvas->resourceManager()->koColorResource(m_d->sampleResourceId);
577
578 m_d->strokesFacade()->addJob(m_d->strokeId,
579 new KisColorSamplerStrokeStrategy::Data(device, imagePoint, currentColor));
580 } else {
581 QString message = i18n("Color sampler does not work on this layer.");
582 m_d->canvas->viewManager()->showFloatingMessage(message, koIcon("object-locked"));
583 }
584}
585
587{
588 KoColor color(rawColor);
589
591
592 if (m_d->updateGlobalColor) {
593 m_d->canvas->resourceManager()->setResource(m_d->sampleResourceId, color);
594 }
595
596 Q_EMIT sigRawColorSelected(rawColor);
597 Q_EMIT sigColorSelected(color);
598
599 if (!m_d->showPreview) return;
600
601 const QColor previewColor = m_d->canvas->displayColorConverter()->toQColor(color);
602
603 if (!m_d->haveSample || m_d->currentColor != previewColor) {
604 m_d->haveSample = true;
605 m_d->currentColor = previewColor;
606 m_d->cache = QPixmap();
607 }
608
610}
float value(const T *src, size_t ch)
const quint8 OPACITY_OPAQUE_U8
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 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 sigRawColorSelected(const KoColor &color)
void paintCircle(QPainter &gc, const QRectF &viewRectF, const QColor &currentColor, const QColor &baseColor)
QRectF colorPreviewDocRect(const QPointF &docPoint)
qreal rotationAngle() const
canvas rotation in degrees
bool xAxisMirrored() const
Bools indicating canvasmirroring.
KisImageWSP image() const
QPointer< KisView > imageView() const
bool yAxisMirrored() const
void sigFinalColorSelected(const KoColor &color)
void sigColorUpdated(const KoColor &color)
bool colorSamplerPreviewCircleOutlineEnabled(bool defaultValue=false) const
int colorSamplerPreviewCircleDiameter(bool defaultValue=false) const
ColorSamplerPreviewStyle
Definition kis_config.h:138
qreal colorSamplerPreviewCircleThickness(bool defaultValue=false) const
bool colorSamplerPreviewCircleExtraCirclesEnabled(bool defaultValue=false) const
ColorSamplerPreviewStyle colorSamplerPreviewStyle(bool defaultValue=false) const
static QCursor samplerImageBackgroundCursor()
static QCursor samplerLayerBackgroundCursor()
static QCursor samplerLayerForegroundCursor()
static QCursor samplerImageForegroundCursor()
const KoColorSpace * colorSpace() const
KisPaintDeviceSP projection() const
QPoint documentToImagePixelFloored(const QPointF &documentCoord) const
void setOpacity(quint8 alpha)
Definition KoColor.cpp:333
void toQColor(QColor *c) const
a convenience method for the above.
Definition KoColor.cpp:198
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)
@ BackgroundColor
The active background color selected for this canvas.
@ ForegroundColor
The active foreground color selected for this canvas.
KisConfig::ColorSamplerPreviewStyle style
QScopedPointer< SamplingCompressor > samplingCompressor
QRectF colorPreviewDocRect(const QPointF &outlineDocPoint)
KisSignalCompressorWithParam< QPointF > SamplingCompressor
KisCanvas2 * canvas