[graphics/krita] libs/ui/tool: WIP thread safe zoom preview
Emmet O'Neill <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 7f068b0ac622948c1c4c88d10c6885f216d22d6b by Emmet O'Neill, on behalf of Dat Le.
Committed on 13/08/2026 at 23:16.
Pushed by emmetoneill into branch 'master'.
WIP thread safe zoom preview
M +62 -32 libs/ui/tool/KisAsyncColorSamplerHelper.cpp
M +3 -1 libs/ui/tool/KisAsyncColorSamplerHelper.h
M +6 -0 libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.cpp
M +17 -0 libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.h
https://invent.kde.org/graphics/krita/-/commit/7f068b0ac622948c1c4c88d10c6885f216d22d6b
diff --git a/libs/ui/tool/KisAsyncColorSamplerHelper.cpp b/libs/ui/tool/KisAsyncColorSamplerHelper.cpp
index 3f533e30a29..7a250fd107f 100644
--- a/libs/ui/tool/KisAsyncColorSamplerHelper.cpp
+++ b/libs/ui/tool/KisAsyncColorSamplerHelper.cpp
@@ -82,6 +82,7 @@ struct KisAsyncColorSamplerHelper::Private
QImage cacheCanvasPreviewImage;
QPainterPath cacheCrosshairPath;
int cacheCirclePreviewDiameter;
+ bool canvasPreviewFetchingStarted {false};
QColor currentColor;
QColor baseColor;
@@ -425,6 +426,8 @@ void KisAsyncColorSamplerHelper::startAction(const QPointF &docPoint, int radius
this, &KisAsyncColorSamplerHelper::slotColorSamplingFinished);
connect(strategy, &KisColorSamplerStrokeStrategy::sigFinalColorSelected,
this, &KisAsyncColorSamplerHelper::sigFinalColorSelected);
+ connect(strategy, &KisColorSamplerStrokeStrategy::sigCanvasZoomPreviewUpdated,
+ this, &KisAsyncColorSamplerHelper::slotCanvasZoomPreviewUpdated);
activatePreview();
m_d->haveSample = true;
@@ -657,12 +660,8 @@ void KisAsyncColorSamplerHelper::paintCircle(QPainter &gc,
cachePainter.setPen(Qt::NoPen);
// If the cache update don't run, no brush is set. So set it
cachePainter.setBrush(m_d->backgroundColor);
- if (m_d->circleZoomPreviewEnabled){
- cachePainter.setCompositionMode(QPainter::CompositionMode_SourceOver);
- }
- else {
- cachePainter.setCompositionMode(QPainter::CompositionMode_Clear);
- }
+
+ cachePainter.setCompositionMode(QPainter::CompositionMode_Clear);
cachePainter.drawPath(tf.map(m_d->cacheCircleInnerClip));
// Draw zoom preview
@@ -672,27 +671,19 @@ void KisAsyncColorSamplerHelper::paintCircle(QPainter &gc,
zoomDocRectF.setSize(zoomDocRectF.size() / m_d->circleZoomPreviewScale);
zoomDocRectF.moveCenter(m_d->sampleDocPoint);
- paintCircleCanvasPreview(cachePainter, cacheRect, zoomDocRectF, tf.map(m_d->cacheCircleInnerClip));
-
- paintCircleReferenceImagePreview(cachePainter, cacheRect, zoomDocRectF, tf.map(m_d->cacheCircleInnerClip));
+ bool paintLater = paintCircleCanvasPreview(cachePainter, cacheRect, zoomDocRectF, tf.map(m_d->cacheCircleInnerClip));
+ if (!paintLater) {
+ paintCircleReferenceImagePreview(cachePainter, cacheRect, zoomDocRectF, tf.map(m_d->cacheCircleInnerClip));
- // Draw crosshair if preview is offseted
- if (m_d->circlePreviewPosition != KisConfig::ColorSamplerPreviewCirclePosition::Center) {
- QColor crosshairColor = Qt::black;
- // TODO: Check back with Wolthera about the sRGB param
- qreal luminance = KisPaintingTweaks::luminosityCoarse(currentColor, true);
- if (luminance < 0.5) crosshairColor = Qt::white;
-
- cachePainter.save();
-
- cachePainter.setPen(crosshairColor);
- QTransform tf;
- tf.translate(cacheCenter.x(), cacheCenter.y());
- tf.rotate(-canvasRotationAngle);
- cachePainter.drawPath(tf.map(m_d->crosshairForOffsettedCircle()));
+ // Draw crosshair if preview is offseted
+ if (m_d->circlePreviewPosition != KisConfig::ColorSamplerPreviewCirclePosition::Center) {
+ paintCircleCrosshair(cachePainter, cacheRect, currentColor);
+ }
- cachePainter.restore();
+ // Fill empty spaces to hide the underlying canvas
+ cachePainter.setCompositionMode(QPainter::CompositionMode_DestinationOver);
+ cachePainter.drawPath(tf.map(m_d->cacheCircleInnerClip));
}
}
@@ -701,10 +692,31 @@ void KisAsyncColorSamplerHelper::paintCircle(QPainter &gc,
gc.restore();
}
+void KisAsyncColorSamplerHelper::paintCircleCrosshair(QPainter &gc, const QRectF &viewRectF, const QColor ¤tColor)
+{
+ QColor crosshairColor = Qt::black;
+ // TODO: Check back with Wolthera about the sRGB param
+ qreal luminance = KisPaintingTweaks::luminosityCoarse(currentColor, true);
+ if (luminance < 0.5) crosshairColor = Qt::white;
+
+ gc.save();
+
+ gc.setCompositionMode(QPainter::CompositionMode_SourceOver);
+ gc.setPen(crosshairColor);
+
+ QTransform tf;
+ tf.translate(viewRectF.center().x(), viewRectF.center().y());
+ tf.rotate(-m_d->cacheRotation);
+ gc.drawPath(tf.map(m_d->crosshairForOffsettedCircle()));
+
+ gc.restore();
+}
+
QImage KisAsyncColorSamplerHelper::cacheCanvasImage(QRect &canvasPixelRect) {
KisImageWSP canvasImage = m_d->canvas->image();
if (!canvasImage->bounds().intersects(canvasPixelRect)) {
+ canvasPixelRect = QRect(); // Make this a null rect to imply no need to draw canvas
return QImage();
}
@@ -716,9 +728,9 @@ QImage KisAsyncColorSamplerHelper::cacheCanvasImage(QRect &canvasPixelRect) {
if (m_d->cacheCanvasPreviewRect.isEmpty() || !m_d->cacheCanvasPreviewRect.contains(canvasPixelRect)) {
// Cache an area larger than the needed preview area to avoid rapid small dynamic allocations
// Attempt to reduce cache size when sample size is big to reduce delay
- qreal cacheScale = 1;
- if (canvasPixelRect.width() < 250) cacheScale = 4;
- else if (canvasPixelRect.width() < 500) cacheScale = 2;
+ qreal cacheScale = 1000;
+ // if (canvasPixelRect.width() < 250) cacheScale = 4;
+ // else if (canvasPixelRect.width() < 500) cacheScale = 2;
QRect cacheCanvasRect = canvasPixelRect;
cacheCanvasRect.setSize(canvasPixelRect.size() * cacheScale);
@@ -729,17 +741,21 @@ QImage KisAsyncColorSamplerHelper::cacheCanvasImage(QRect &canvasPixelRect) {
cacheCanvasRect = canvasImage->bounds();
}
- m_d->cacheCanvasPreviewImage = canvasImage->convertToQImage(cacheCanvasRect, canvasImage->profile());
+ // If already fetching, then do nothing
+ if (m_d->canvasPreviewFetchingStarted) return QImage();
- m_d->cacheCanvasPreviewRect = cacheCanvasRect;
- }
+ m_d->canvasPreviewFetchingStarted = true;
+ m_d->strokesFacade()->addJob(m_d->strokeId,
+ new KisColorSamplerStrokeStrategy::GenerateCanvasZoomPreviewData(canvasImage, cacheCanvasRect, canvasImage->profile()));
+ return QImage();
+ }
canvasPixelRect.translate(-m_d->cacheCanvasPreviewRect.topLeft());
return m_d->cacheCanvasPreviewImage;
}
-void KisAsyncColorSamplerHelper::paintCircleCanvasPreview(QPainter &gc, const QRectF &viewRectF, const QRectF &zoomDocRectF, const QPainterPath &clip) {
+bool KisAsyncColorSamplerHelper::paintCircleCanvasPreview(QPainter &gc, const QRectF &viewRectF, const QRectF &zoomDocRectF, const QPainterPath &clip) {
KisImageWSP image = m_d->canvas->image();
QRectF canvasPixelRectF = image->documentToPixel(zoomDocRectF);
@@ -753,7 +769,10 @@ void KisAsyncColorSamplerHelper::paintCircleCanvasPreview(QPainter &gc, const QR
canvasPixelRect.moveCenter(image->documentToImagePixelFloored(zoomDocRectF.center()));
QImage cachedImage = cacheCanvasImage(canvasPixelRect);
- if (cachedImage.isNull()) return;
+ // A null QImage and a null canvasPixelRect => No need to paint canvas
+ // A null QImage and a not null canvasPixelRect => Cache miss, skip painting and paint later when canvas image is ready
+ bool paintLater = cachedImage.isNull() && !canvasPixelRect.isNull();
+ if (cachedImage.isNull()) return paintLater;
gc.save();
gc.setCompositionMode(QPainter::CompositionMode_SourceOver);
@@ -766,6 +785,8 @@ void KisAsyncColorSamplerHelper::paintCircleCanvasPreview(QPainter &gc, const QR
gc.drawImage(viewRectF, cachedImage, canvasPixelRect);
gc.restore();
+
+ return false;
}
// Preview won't respect opacity because the color sampled doesn't respect opacity either
@@ -819,6 +840,15 @@ void KisAsyncColorSamplerHelper::paintCircleReferenceImagePreview(QPainter &gc,
gc.restore();
}
+void KisAsyncColorSamplerHelper::slotCanvasZoomPreviewUpdated(const QImage &canvasImage, QRect canvasRect) {
+ m_d->cacheCanvasPreviewImage = canvasImage;
+ m_d->cacheCanvasPreviewRect = canvasRect;
+
+ m_d->canvasPreviewFetchingStarted = false;
+
+ Q_EMIT sigRequestUpdateOutline();
+}
+
void KisAsyncColorSamplerHelper::slotAddSamplingJob(const QPointF &docPoint)
{
/**
diff --git a/libs/ui/tool/KisAsyncColorSamplerHelper.h b/libs/ui/tool/KisAsyncColorSamplerHelper.h
index b16c909a57d..4b0513f8b32 100644
--- a/libs/ui/tool/KisAsyncColorSamplerHelper.h
+++ b/libs/ui/tool/KisAsyncColorSamplerHelper.h
@@ -75,13 +75,15 @@ private Q_SLOTS:
void slotAddSamplingJob(const QPointF &docPoint);
void slotColorSamplingFinished(const KoColor &rawColor);
void slotUpdateBgColor();
+ void slotCanvasZoomPreviewUpdated(const QImage &canvasImage, QRect canvasRect);
private:
void activatePreview();
void paintRectangle(QPainter &gc, const QRectF &viewRectF, const QColor ¤tColor, const QColor &baseColor);
void paintCircle(QPainter &gc, const QRectF &viewRectF, const QColor ¤tColor, const QColor &baseColor);
- void paintCircleCanvasPreview(QPainter &gc, const QRectF &viewRectF, const QRectF &zoomDocRectF, const QPainterPath &clip);
+ void paintCircleCrosshair(QPainter &gc, const QRectF &viewRectF, const QColor ¤tColor);
+ bool paintCircleCanvasPreview(QPainter &gc, const QRectF &viewRectF, const QRectF &zoomDocRectF, const QPainterPath &clip);
// Return true if at least one reference image preview was drawn
void paintCircleReferenceImagePreview(QPainter &gc, const QRectF &viewRectF, const QRectF &zoomDocRectF, const QPainterPath &clip);
diff --git a/libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.cpp b/libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.cpp
index e19fb612e6e..31946545e4e 100644
--- a/libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.cpp
+++ b/libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.cpp
@@ -8,6 +8,7 @@
#include "kis_tool_utils.h"
#include "kis_paint_device.h"
+#include "kis_image.h"
struct KisColorSamplerStrokeStrategy::Private
{
@@ -42,6 +43,7 @@ void KisColorSamplerStrokeStrategy::doStrokeCallback(KisStrokeJobData *data)
Data *d = dynamic_cast<Data*>(data);
FinalizeData *finalize = dynamic_cast<FinalizeData*>(data);
+ GenerateCanvasZoomPreviewData *previewData = dynamic_cast<GenerateCanvasZoomPreviewData*>(data);
if (d) {
KoColor color;
@@ -54,6 +56,10 @@ void KisColorSamplerStrokeStrategy::doStrokeCallback(KisStrokeJobData *data)
if (m_d->lastSelectedColor) {
Q_EMIT sigFinalColorSelected(*m_d->lastSelectedColor);
}
+ } else if (previewData) {
+ QImage image = previewData->canvasImage->convertToQImage(previewData->canvasPixelRect, previewData->colorProfile);
+
+ Q_EMIT sigCanvasZoomPreviewUpdated(image, previewData->canvasPixelRect);
}
}
diff --git a/libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.h b/libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.h
index bc6476304d3..2de6217b603 100644
--- a/libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.h
+++ b/libs/ui/tool/strokes/kis_color_sampler_stroke_strategy.h
@@ -44,6 +44,22 @@ public:
return new FinalizeData();
}
};
+
+ class GenerateCanvasZoomPreviewData : public KisStrokeJobData {
+ public:
+ GenerateCanvasZoomPreviewData(KisImageWSP _image, const QRect &_canvasPixelRect, const KoColorProfile *_colorProfile)
+ : canvasImage(_image), canvasPixelRect(_canvasPixelRect), colorProfile(_colorProfile)
+ {}
+
+ KisStrokeJobData* createLodClone(int levelOfDetail) override {
+ Q_UNUSED(levelOfDetail);
+ return new GenerateCanvasZoomPreviewData(canvasImage, canvasPixelRect, colorProfile);
+ }
+
+ KisImageWSP canvasImage;
+ QRect canvasPixelRect;
+ const KoColorProfile *colorProfile;
+ };
public:
KisColorSamplerStrokeStrategy(int radius, int blend, int lod = 0);
~KisColorSamplerStrokeStrategy() override;
@@ -54,6 +70,7 @@ public:
Q_SIGNALS:
void sigColorUpdated(const KoColor &color);
void sigFinalColorSelected(const KoColor &color);
+ void sigCanvasZoomPreviewUpdated(const QImage &canvasImage, const QRect &canvasRect);
private:
struct Private;