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 "KoViewConverter.h"
21#include "KoIcon.h"
22#include "kis_cursor.h"
25#include "kis_canvas2.h"
26#include "KisViewManager.h"
27#include "KisDocument.h"
32
33
34namespace {
35QColor colorWithAlpha(QColor color, int alpha)
36{
37 color.setAlpha(alpha);
38 return color;
39}
40}
41
43{
44 static constexpr qreal PREVIEW_RECT_SIZE = 48.0;
45
47 : canvas(_canvas)
48 {}
49
51
53 bool sampleCurrentLayer {true};
54 bool updateGlobalColor {true};
55
56 bool isActive {false};
57 bool showPreview {false};
58 bool haveSample {false};
59
62 QScopedPointer<SamplingCompressor> samplingCompressor;
63
65
72
74 QColor baseColor;
76
77 QPixmap cache;
78 qreal cacheRotation = 0.0;
79 bool cacheMirror = false;
80
82 return canvas->image().data();
83 }
84
85 const KoViewConverter &converter() const {
86 return *canvas->imageView()->viewConverter();
87 }
88
90 {
91 // Offsetting to the sides is both vertical and horizontal, when
92 // offsetting above it's only vertical, so it needs a bit more space.
93 constexpr qreal OFFSET = 32.0;
94 constexpr qreal OFFSET_ABOVE = OFFSET * 1.5;
95 constexpr qreal SIZE = PREVIEW_RECT_SIZE;
96
97 bool mirrored = canvas->xAxisMirrored();
98 bool flipped = canvas->yAxisMirrored();
99
105 } else {
106 effectiveStyle = style;
107 }
108
109 qreal width = haveSample ? SIZE * 2.0 : SIZE;
110
111 qreal x, y;
112 switch (effectiveStyle) {
114 x = -(OFFSET + width);
115 y = flipped ? -(OFFSET + SIZE) : OFFSET;
116 break;
118 x = OFFSET;
119 y = flipped ? -(OFFSET + SIZE) : OFFSET;
120 break;
121 default:
122 x = width / -2.0;
123 y = flipped ? OFFSET_ABOVE : -(OFFSET_ABOVE + SIZE);
124 break;
125 }
126
127 QRectF rect(x, y, width, SIZE);
128
129 qreal canvasRotationAngle = canvas->rotationAngle();
130 if (!qFuzzyIsNull(canvasRotationAngle)) {
131 QTransform tf;
132 tf.rotate(mirrored ? canvasRotationAngle : -canvasRotationAngle);
133 rect = tf.mapRect(rect);
134 }
135
136 return rect;
137 }
138
143
144 QRectF colorPreviewDocRect(const QPointF &outlineDocPoint)
145 {
146 QRectF colorPreviewViewRect;
147 switch (style) {
149 return QRectF();
153 colorPreviewViewRect = colorPreviewRectForRectangle();
154 break;
155 default:
156 // Showing a preview without sampling a color (by just holding a
157 // modifier) is used to compare the foreground color with the
158 // canvas. The circle doesn't work well for that purpose, so we
159 // use the handedness-independent rectangle above instead.
160 if (haveSample) {
161 colorPreviewViewRect = colorPreviewRectForCircle();
162 } else {
163 colorPreviewViewRect = colorPreviewRectForRectangle();
164 }
165 break;
166 }
167
168 const QRectF colorPreviewDocumentRect = converter().viewToDocument(colorPreviewViewRect);
169 return colorPreviewDocumentRect.translated(outlineDocPoint);
170 }
171};
172
174 : m_d(new Private(canvas))
175{
176 using namespace std::placeholders; // For _1 placeholder
177 std::function<void(QPointF)> callback =
179 m_d->samplingCompressor.reset(
181
182 m_d->activationDelayTimer.setInterval(100);
183 m_d->activationDelayTimer.setSingleShot(true);
184 connect(&m_d->activationDelayTimer, SIGNAL(timeout()), this, SLOT(activateDelayedPreview()));
185 connect(m_d->canvas->displayColorConverter(), SIGNAL(displayConfigurationChanged()), this, SLOT(slotUpdateBgColor()));
187}
188
193
195{
196 return m_d->isActive;
197}
198
199void KisAsyncColorSamplerHelper::activate(bool sampleCurrentLayer, bool pickFgColor)
200{
201
203 m_d->isActive = true;
204
205 m_d->sampleResourceId =
206 pickFgColor ?
209
210 m_d->sampleCurrentLayer = sampleCurrentLayer;
211 m_d->haveSample = false;
212
213
214 KisConfig cfg(true);
215 m_d->style = cfg.colorSamplerPreviewStyle();
216
217 m_d->circlePreviewDiameter = cfg.colorSamplerPreviewCircleDiameter();
218 m_d->circlePreviewThickness = cfg.colorSamplerPreviewCircleThickness()/100.0; // saved in percentages
219 m_d->circlePreviewOutlineEnabled = cfg.colorSamplerPreviewCircleOutlineEnabled();
220 m_d->circlePreviewExtraCircles = cfg.colorSamplerPreviewCircleExtraCirclesEnabled();
221
222 m_d->activationDelayTimer.start();
223}
224
226{
227 // the event may come after we have started or even
228 // finished color picking if the user is quick
229 if (!m_d->isActive || m_d->showPreview) {
230 return;
231 }
232
234
236}
237
239{
240 m_d->activationDelayTimer.stop();
241 m_d->showPreview = true;
242
243 const KoColor currentColor =
244 m_d->canvas->resourceManager()->koColorResource(m_d->sampleResourceId);
245 const QColor previewColor = m_d->canvas->displayColorConverter()->convertColorToDisplayColorSpace(currentColor, true);
246
247 m_d->currentColor = previewColor;
248 m_d->baseColor = previewColor;
249 m_d->cache = QPixmap();
250
251 updateCursor(m_d->sampleCurrentLayer, m_d->sampleResourceId == KoCanvasResource::ForegroundColor);
252}
253
254void KisAsyncColorSamplerHelper::updateCursor(bool sampleCurrentLayer, bool pickFgColor)
255{
256 const int sampleResourceId =
257 pickFgColor ?
260
261 QCursor cursor;
262
263 if (sampleCurrentLayer) {
264 if (sampleResourceId == KoCanvasResource::ForegroundColor) {
266 } else {
268 }
269 } else {
270 if (sampleResourceId == KoCanvasResource::ForegroundColor) {
272 } else {
274 }
275 }
276
277 Q_EMIT sigRequestCursor(cursor);
278}
279
281{
282 m_d->updateGlobalColor = value;
283}
284
286{
287 return m_d->updateGlobalColor;
288}
289
291{
292 KIS_SAFE_ASSERT_RECOVER(!m_d->strokeId) {
293 endAction();
294 }
295
296 m_d->activationDelayTimer.stop();
297
298 m_d->showPreview = false;
299 m_d->haveSample = false;
300
301 m_d->previewDocRect = QRectF();
302 m_d->currentColor = QColor();
303 m_d->baseColor = QColor();
304 m_d->cache = QPixmap();
305
306 m_d->isActive = false;
307
308 Q_EMIT sigRequestCursorReset();
310}
311
312void KisAsyncColorSamplerHelper::startAction(const QPointF &docPoint, int radius, int blend)
313{
319
321 m_d->haveSample = true;
322 m_d->strokeId = m_d->strokesFacade()->startStroke(strategy);
323 m_d->samplingCompressor->start(docPoint);
324}
325
327{
329 m_d->samplingCompressor->start(docPoint);
330}
331
333{
335
336 m_d->strokesFacade()->addJob(m_d->strokeId,
338
339 m_d->strokesFacade()->endStroke(m_d->strokeId);
340 m_d->strokeId.clear();
341}
342
344{
345 if (!m_d->showPreview) return QRectF();
346
347 KisConfig cfg(true);
348 m_d->style = cfg.colorSamplerPreviewStyle();
349 m_d->previewDocRect = m_d->colorPreviewDocRect(docPoint);
350 return m_d->previewDocRect;
351}
352
353void KisAsyncColorSamplerHelper::paint(QPainter &gc, const KoViewConverter &converter)
354{
355 if (!m_d->showPreview) {
356 return;
357 }
358
359 QRectF viewRectF = converter.documentToView(m_d->previewDocRect);
360 QColor currentColor = colorWithAlpha(m_d->currentColor, OPACITY_OPAQUE_U8);
361 QColor baseColor = m_d->haveSample ? colorWithAlpha(m_d->baseColor, OPACITY_OPAQUE_U8) : currentColor;
362
363 switch (m_d->style) {
367 paintRectangle(gc, viewRectF, currentColor, baseColor);
368 break;
369 default:
370 // See comment in colorPreviewDocRect.
371 if (m_d->haveSample) {
372 paintCircle(gc, viewRectF, currentColor, baseColor);
373 } else {
374 paintRectangle(gc, viewRectF, currentColor, baseColor);
375 }
376 break;
377 }
378}
379
381 const QRectF &viewRectF,
382 const QColor &currentColor,
383 const QColor &baseColor)
384{
385 qreal dpr = gc.device()->devicePixelRatioF();
386 QSizeF cacheSizeF = viewRectF.size() * dpr;
387 QSize cacheSize(qCeil(cacheSizeF.width()), qCeil(cacheSizeF.height()));
388 bool needsNewCache = m_d->cache.isNull() || m_d->cache.size() != cacheSize;
389 if (needsNewCache) {
390 m_d->cache = QPixmap(cacheSize);
391 m_d->cache.fill(Qt::transparent);
392 }
393
394 qreal canvasRotationAngle = m_d->canvas->rotationAngle();
395 bool canvasMirror = m_d->canvas->xAxisMirrored();
396 if (needsNewCache || !qFuzzyCompare(canvasRotationAngle, m_d->cacheRotation) || canvasMirror != m_d->cacheMirror) {
397 m_d->cacheRotation = canvasRotationAngle;
398 m_d->cacheMirror = canvasMirror;
399
400 QPainter cachePainter(&m_d->cache);
401 cachePainter.setRenderHint(QPainter::Antialiasing);
402
403 qreal size = Private::PREVIEW_RECT_SIZE * dpr;
404 QRectF rect(0.0, 0.0, m_d->haveSample ? size * 2.0 : size, size);
405 rect.moveTopLeft(-rect.center());
406
407 QTransform tf;
408 QPointF offset = QRectF(m_d->cache.rect()).center();
409 tf.translate(offset.x(), offset.y());
410 tf.rotate(canvasMirror ? canvasRotationAngle : -canvasRotationAngle);
411 cachePainter.setTransform(tf);
412
413 if (m_d->haveSample) {
414 qreal centerX = rect.center().x();
415 QRectF currentRect(rect.topLeft(), QPointF(centerX + 1.0, rect.bottom()));
416 QRectF baseRect(QPointF(centerX, rect.top()), rect.bottomRight());
417 if (m_d->canvas->xAxisMirrored()) {
418 std::swap(currentRect, baseRect);
419 }
420 cachePainter.fillRect(currentRect, currentColor);
421 cachePainter.fillRect(baseRect, baseColor);
422 } else {
423 cachePainter.fillRect(rect, currentColor);
424 }
425 }
426
427 gc.drawPixmap(viewRectF.toRect(), m_d->cache);
428}
429
431 const QRectF &viewRectF,
432 const QColor &currentColor,
433 const QColor &baseColor)
434{
435 if (!m_d->haveSample) {
436 return;
437 }
438
439
440
441 gc.save();
442
443 qreal dpr = gc.device()->devicePixelRatioF();
444 QSizeF cacheSizeF = viewRectF.size() * dpr;
445 QSize cacheSize(qCeil(cacheSizeF.width()), qCeil(cacheSizeF.height()));
446 bool needsNewCache = m_d->cache.isNull() || m_d->cache.size() != cacheSize;
447 if (needsNewCache) {
448 m_d->cache = QPixmap(cacheSize);
449 m_d->cache.fill(Qt::transparent);
450 }
451
452 qreal canvasRotationAngle = m_d->canvas->rotationAngle();
453 if (m_d->canvas->xAxisMirrored()) {
454 canvasRotationAngle = -canvasRotationAngle;
455 }
456
457 bool needsDualColor = currentColor != baseColor;
458 if (needsNewCache || (needsDualColor && !qFuzzyCompare(m_d->cacheRotation, canvasRotationAngle))) {
459 m_d->cacheRotation = canvasRotationAngle;
460
461 QPainter cachePainter(&m_d->cache);
462 cachePainter.setRenderHint(QPainter::Antialiasing);
463
464 QColor backgroundColor = colorWithAlpha(m_d->backgroundColor, OPACITY_OPAQUE_U8 / 2 + 1);
465 qreal penWidth = m_d->circlePreviewDiameter > 100 ? (2.0 * dpr) : (1.0 * dpr);
466 QPen pen = QPen(backgroundColor, penWidth);
467 if (m_d->circlePreviewOutlineEnabled) {
468 cachePainter.setPen(pen);
469 } else {
470 cachePainter.setPen(Qt::NoPen);
471 }
472
473 QRectF cacheRect = m_d->cache.rect();
474 QRectF outerRect = cacheRect.marginsRemoved(QMarginsF(penWidth, penWidth, penWidth, penWidth));
475
476 QTransform tf;
477
478 QPointF cacheCenter = cacheRect.center();
479 tf.translate(cacheCenter.x(), cacheCenter.y());
480 tf.rotate(-canvasRotationAngle);
481 tf.translate(-cacheCenter.x(), -cacheCenter.y());
482
483
484 if (needsDualColor) {
485 // The color sampler preview is an outline and those rotate along
486 // with the canvas. That's undesirable for the sampler preview
487 // though, so we un-rotate its contents here accordingly.
488
489
490 QPainterPath clipPath;
491 clipPath.addPolygon(tf.map(QPolygonF(QRectF(0, 0, cacheRect.width(), cacheRect.height() / 2.0 + 1.0))));
492 cachePainter.setClipPath(clipPath);
493
494 bool flipped = m_d->canvas->yAxisMirrored();
495 cachePainter.setBrush(flipped ? baseColor : currentColor);
496 cachePainter.drawEllipse(outerRect);
497
498 cachePainter.setBrush(baseColor);
499 clipPath.clear();
500 clipPath.addPolygon(
501 tf.map(QRectF(0, cacheRect.height() / 2.0, cacheRect.width(), cacheRect.height() / 2.0)));
502 cachePainter.setClipPath(clipPath);
503
504 cachePainter.setBrush(flipped ? currentColor : baseColor);
505 cachePainter.drawEllipse(outerRect);
506
507 cachePainter.setClipPath(QPainterPath(), Qt::NoClip);
508 } else {
509 cachePainter.setBrush(currentColor);
510 cachePainter.drawEllipse(outerRect);
511 }
512
513 qreal innerX = cacheRect.width() * (1.0 - m_d->circlePreviewThickness);
514 qreal innerY = cacheRect.height() * (1.0 - m_d->circlePreviewThickness);
515 QRectF innerRect = cacheRect.marginsRemoved(QMarginsF(innerX, innerY, innerX, innerY));
516 QPainterPath innerEllipse;
517 innerEllipse.addEllipse(innerRect);
518
519 QPainterPath innerPath;
520 innerPath.addPath(innerEllipse);
521
522
523 if (m_d->circlePreviewThickness < 0.5 && m_d->circlePreviewExtraCircles) {
524 qreal extraMargin = 0.1*m_d->circlePreviewThickness*innerRect.width(); // looks better
525 QPointF leftCenter = QPointF(innerRect.left() - extraMargin, innerRect.top() + innerRect.height()/2.0);
526 QPointF rightCenter = QPointF(innerRect.right() + extraMargin, innerRect.top() + innerRect.height()/2.0);
527
528 innerPath.setFillRule(Qt::OddEvenFill);
529 innerPath.addEllipse(leftCenter, m_d->circlePreviewThickness*cacheRect.width(), m_d->circlePreviewThickness*cacheRect.width());
530 innerPath.addEllipse(rightCenter, m_d->circlePreviewThickness*cacheRect.width(), m_d->circlePreviewThickness*cacheRect.width());
531
532 innerPath = innerPath.intersected(innerEllipse);
533 }
534
535 cachePainter.setPen(Qt::NoPen);
536 cachePainter.setCompositionMode(QPainter::CompositionMode_Clear);
537 cachePainter.drawPath(tf.map(innerPath));
538
539 if (m_d->circlePreviewOutlineEnabled) {
540 cachePainter.setBrush(Qt::transparent);
541 cachePainter.setPen(pen);
542 cachePainter.setCompositionMode(QPainter::CompositionMode_SourceOver);
543 cachePainter.drawPath(tf.map(innerPath));
544 }
545 }
546 gc.drawPixmap(viewRectF.toRect(), m_d->cache);
547
548 gc.restore();
549}
550
552{
557 if (!m_d->strokeId) return;
558
559 KisImageSP image = m_d->canvas->image();
560
561 const QPoint imagePoint = image->documentToImagePixelFloored(docPoint);
562
563 if (!m_d->sampleCurrentLayer) {
564 KisSharedPtr<KisReferenceImagesLayer> referencesLayer = m_d->canvas->imageView()->document()->referenceImagesLayer();
565 if (referencesLayer && m_d->canvas->referenceImagesDecoration()->visible()) {
566 KoColor color = referencesLayer->getPixel(imagePoint);
567 if (color.opacityU8() > 0) {
569 return;
570 }
571 }
572 }
573
574 KisPaintDeviceSP device = m_d->sampleCurrentLayer ?
575 m_d->canvas->imageView()->currentNode()->colorSampleSourceDevice() :
576 image->projection();
577
578 if (device) {
579 // Used for color sampler blending.
580 const KoColor currentColor =
581 m_d->canvas->resourceManager()->koColorResource(m_d->sampleResourceId);
582
583 m_d->strokesFacade()->addJob(m_d->strokeId,
584 new KisColorSamplerStrokeStrategy::Data(device, imagePoint, currentColor));
585 } else {
586 QString message = i18n("Color sampler does not work on this layer.");
587 m_d->canvas->viewManager()->showFloatingMessage(message, koIcon("object-locked"));
588 }
589}
590
592{
593 KoColor color(rawColor);
594
596
597 if (m_d->updateGlobalColor) {
598 m_d->canvas->resourceManager()->setResource(m_d->sampleResourceId, color);
599 }
600
601 Q_EMIT sigRawColorSelected(rawColor);
602 Q_EMIT sigColorSelected(color);
603
604 if (!m_d->showPreview) return;
605
606 const QColor previewColor = m_d->canvas->displayColorConverter()->convertColorToDisplayColorSpace(color, true);
607
608 if (!m_d->haveSample || m_d->currentColor != previewColor) {
609 m_d->haveSample = true;
610 m_d->currentColor = previewColor;
611 m_d->cache = QPixmap();
612 }
613
615}
616
618{
619 KoColor bgColor;
620 bgColor.fromQColor(qApp->palette().color(QPalette::Base));
621 m_d->backgroundColor = m_d->canvas->displayColorConverter()->convertColorToDisplayColorSpace(bgColor);
622}
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()
KisPaintDeviceSP projection() const
QPoint documentToImagePixelFloored(const QPointF &documentCoord) 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 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