diff --git a/libs/brush/kis_auto_brush.cpp b/libs/brush/kis_auto_brush.cpp index 510f9821e4..a16407c20d 100644 --- a/libs/brush/kis_auto_brush.cpp +++ b/libs/brush/kis_auto_brush.cpp @@ -1,413 +1,398 @@ /* * Copyright (c) 2004,2007-2009 Cyrille Berger * Copyright (c) 2010 Lukáš Tvrdý * Copyright (c) 2012 Sven Langkamp * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include //MSVC requires that Vc come first #include "kis_auto_brush.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) || defined(_WIN64) #include #define srand48 srand inline double drand48() { return double(rand()) / RAND_MAX; } #endif struct KisAutoBrush::Private { Private() : randomness(0), density(1.0), idealThreadCountCached(1) {} Private(const Private &rhs) : shape(rhs.shape->clone()), randomness(rhs.randomness), density(rhs.density), idealThreadCountCached(rhs.idealThreadCountCached) { } QScopedPointer shape; qreal randomness; qreal density; int idealThreadCountCached; }; KisAutoBrush::KisAutoBrush(KisMaskGenerator* as, qreal angle, qreal randomness, qreal density) : KisBrush(), d(new Private) { d->shape.reset(as); d->randomness = randomness; d->density = density; d->idealThreadCountCached = QThread::idealThreadCount(); setBrushType(MASK); setWidth(qMax(qreal(1.0), d->shape->width())); setHeight(qMax(qreal(1.0), d->shape->height())); QImage image = createBrushPreview(); setBrushTipImage(image); // Set angle here so brush tip image is generated unrotated setAngle(angle); image = createBrushPreview(); setImage(image); } KisAutoBrush::~KisAutoBrush() { } qreal KisAutoBrush::userEffectiveSize() const { return d->shape->diameter(); } void KisAutoBrush::setUserEffectiveSize(qreal value) { d->shape->setDiameter(value); } KisAutoBrush::KisAutoBrush(const KisAutoBrush& rhs) : KisBrush(rhs), d(new Private(*rhs.d)) { } KisBrush* KisAutoBrush::clone() const { return new KisAutoBrush(*this); } /* It's difficult to predict the mask height when exaclty when there are * more than 2 spikes, so we return an upperbound instead. */ static KisDabShape lieAboutDabShape(KisDabShape const& shape) { return KisDabShape(shape.scale(), 1.0, shape.rotation()); } qint32 KisAutoBrush::maskHeight(KisDabShape const& shape, qreal subPixelX, qreal subPixelY, const KisPaintInformation& info) const { return KisBrush::maskHeight( lieAboutDabShape(shape), subPixelX, subPixelY, info); } qint32 KisAutoBrush::maskWidth(KisDabShape const& shape, qreal subPixelX, qreal subPixelY, const KisPaintInformation& info) const { return KisBrush::maskWidth( lieAboutDabShape(shape), subPixelX, subPixelY, info); } QSizeF KisAutoBrush::characteristicSize(KisDabShape const& shape) const { return KisBrush::characteristicSize(lieAboutDabShape(shape)); } inline void fillPixelOptimized_4bytes(quint8 *color, quint8 *buf, int size) { /** * This version of filling uses low granularity of data transfers * (32-bit chunks) and internal processor's parallelism. It reaches * 25% better performance in KisStrokeBenchmark in comparison to * per-pixel memcpy version (tested on Sandy Bridge). */ int block1 = size / 8; int block2 = size % 8; quint32 *src = reinterpret_cast(color); quint32 *dst = reinterpret_cast(buf); // check whether all buffers are 4 bytes aligned // (uncomment if experience some problems) // Q_ASSERT(((qint64)src & 3) == 0); // Q_ASSERT(((qint64)dst & 3) == 0); for (int i = 0; i < block1; i++) { *dst = *src; *(dst + 1) = *src; *(dst + 2) = *src; *(dst + 3) = *src; *(dst + 4) = *src; *(dst + 5) = *src; *(dst + 6) = *src; *(dst + 7) = *src; dst += 8; } for (int i = 0; i < block2; i++) { *dst = *src; dst++; } } inline void fillPixelOptimized_general(quint8 *color, quint8 *buf, int size, int pixelSize) { /** * This version uses internal processor's parallelism and gives * 20% better performance in KisStrokeBenchmark in comparison to * per-pixel memcpy version (tested on Sandy Bridge (+20%) and * on Merom (+10%)). */ int block1 = size / 8; int block2 = size % 8; for (int i = 0; i < block1; i++) { quint8 *d1 = buf; quint8 *d2 = buf + pixelSize; quint8 *d3 = buf + 2 * pixelSize; quint8 *d4 = buf + 3 * pixelSize; quint8 *d5 = buf + 4 * pixelSize; quint8 *d6 = buf + 5 * pixelSize; quint8 *d7 = buf + 6 * pixelSize; quint8 *d8 = buf + 7 * pixelSize; for (int j = 0; j < pixelSize; j++) { *(d1 + j) = color[j]; *(d2 + j) = color[j]; *(d3 + j) = color[j]; *(d4 + j) = color[j]; *(d5 + j) = color[j]; *(d6 + j) = color[j]; *(d7 + j) = color[j]; *(d8 + j) = color[j]; } buf += 8 * pixelSize; } for (int i = 0; i < block2; i++) { memcpy(buf, color, pixelSize); buf += pixelSize; } } void KisAutoBrush::generateMaskAndApplyMaskOrCreateDab(KisFixedPaintDeviceSP dst, KisBrush::ColoringInformation* coloringInformation, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX , double subPixelY, qreal softnessFactor) const { Q_UNUSED(info); // Generate the paint device from the mask const KoColorSpace* cs = dst->colorSpace(); quint32 pixelSize = cs->pixelSize(); // mask dimension methods already includes KisBrush::angle() int dstWidth = maskWidth(shape, subPixelX, subPixelY, info); int dstHeight = maskHeight(shape, subPixelX, subPixelY, info); QPointF hotSpot = this->hotSpot(shape, info); // mask size and hotSpot function take the KisBrush rotation into account qreal angle = shape.rotation() + KisBrush::angle(); // if there's coloring information, we merely change the alpha: in that case, // the dab should be big enough! if (coloringInformation) { - - // old bounds - QRect oldBounds = dst->bounds(); - // new bounds. we don't care if there is some extra memory occcupied. dst->setRect(QRect(0, 0, dstWidth, dstHeight)); - - if (dstWidth * dstHeight <= oldBounds.width() * oldBounds.height()) { - // just clear the data in dst, - memset(dst->data(), OPACITY_TRANSPARENT_U8, dstWidth * dstHeight * dst->pixelSize()); - } - else { - // enlarge the data - dst->initialize(); - } + dst->lazyGrowBufferWithoutInitialization(); } else { - if (dst->data() == 0 || dst->bounds().isEmpty()) { - warnKrita << "Creating a default black dab: no coloring info and no initialized paint device to mask"; - dst->clear(QRect(0, 0, dstWidth, dstHeight)); - } - Q_ASSERT(dst->bounds().width() >= dstWidth && dst->bounds().height() >= dstHeight); + KIS_SAFE_ASSERT_RECOVER_RETURN(dst->bounds().width() >= dstWidth && + dst->bounds().height() >= dstHeight); } quint8* dabPointer = dst->data(); quint8* color = 0; if (coloringInformation) { if (dynamic_cast(coloringInformation)) { color = const_cast(coloringInformation->color()); } } double centerX = hotSpot.x() - 0.5 + subPixelX; double centerY = hotSpot.y() - 0.5 + subPixelY; d->shape->setScale(shape.scaleX(), shape.scaleY()); d->shape->setSoftness(softnessFactor); if (coloringInformation) { if (color && pixelSize == 4) { fillPixelOptimized_4bytes(color, dabPointer, dstWidth * dstHeight); } else if (color) { fillPixelOptimized_general(color, dabPointer, dstWidth * dstHeight, pixelSize); } else { for (int y = 0; y < dstHeight; y++) { for (int x = 0; x < dstWidth; x++) { memcpy(dabPointer, coloringInformation->color(), pixelSize); coloringInformation->nextColumn(); dabPointer += pixelSize; } coloringInformation->nextRow(); } } } MaskProcessingData data(dst, cs, d->randomness, d->density, centerX, centerY, angle); KisBrushMaskApplicatorBase *applicator = d->shape->applicator(); applicator->initializeData(&data); int jobs = d->idealThreadCountCached; - if (dstHeight > 100 && jobs >= 4) { + if (threadingAllowed() && dstHeight > 100 && jobs >= 4) { int splitter = dstHeight / jobs; QVector rects; for (int i = 0; i < jobs - 1; i++) { rects << QRect(0, i * splitter, dstWidth, splitter); } rects << QRect(0, (jobs - 1)*splitter, dstWidth, dstHeight - (jobs - 1)*splitter); OperatorWrapper wrapper(applicator); QtConcurrent::blockingMap(rects, wrapper); } else { QRect rect(0, 0, dstWidth, dstHeight); applicator->process(rect); } } void KisAutoBrush::toXML(QDomDocument& doc, QDomElement& e) const { QDomElement shapeElt = doc.createElement("MaskGenerator"); d->shape->toXML(doc, shapeElt); e.appendChild(shapeElt); e.setAttribute("type", "auto_brush"); e.setAttribute("spacing", QString::number(spacing())); e.setAttribute("useAutoSpacing", QString::number(autoSpacingActive())); e.setAttribute("autoSpacingCoeff", QString::number(autoSpacingCoeff())); e.setAttribute("angle", QString::number(KisBrush::angle())); e.setAttribute("randomness", QString::number(d->randomness)); e.setAttribute("density", QString::number(d->density)); KisBrush::toXML(doc, e); } QImage KisAutoBrush::createBrushPreview() { int width = maskWidth(KisDabShape(), 0.0, 0.0, KisPaintInformation()); int height = maskHeight(KisDabShape(), 0.0, 0.0, KisPaintInformation()); KisPaintInformation info(QPointF(width * 0.5, height * 0.5), 0.5, 0, 0, angle(), 0, 0, 0, 0); KisFixedPaintDeviceSP fdev = new KisFixedPaintDevice(KoColorSpaceRegistry::instance()->rgb8()); fdev->setRect(QRect(0, 0, width, height)); fdev->initialize(); mask(fdev, KoColor(Qt::black, fdev->colorSpace()), KisDabShape(), info); return fdev->convertToQImage(0); } const KisMaskGenerator* KisAutoBrush::maskGenerator() const { return d->shape.data(); } qreal KisAutoBrush::density() const { return d->density; } qreal KisAutoBrush::randomness() const { return d->randomness; } QPainterPath KisAutoBrush::outline() const { bool simpleOutline = (d->density < 1.0); if (simpleOutline) { QPainterPath path; QRectF brushBoundingbox(0, 0, width(), height()); if (maskGenerator()->type() == KisMaskGenerator::CIRCLE) { path.addEllipse(brushBoundingbox); } else { // if (maskGenerator()->type() == KisMaskGenerator::RECTANGLE) path.addRect(brushBoundingbox); } return path; } return KisBrush::boundary()->path(); } void KisAutoBrush::lodLimitations(KisPaintopLodLimitations *l) const { KisBrush::lodLimitations(l); if (!qFuzzyCompare(density(), 1.0)) { l->limitations << KoID("auto-brush-density", i18nc("PaintOp instant preview limitation", "Brush Density recommended value 100.0")); } if (!qFuzzyCompare(randomness(), 0.0)) { l->limitations << KoID("auto-brush-randomness", i18nc("PaintOp instant preview limitation", "Brush Randomness recommended value 0.0")); } } diff --git a/libs/brush/kis_brush.cpp b/libs/brush/kis_brush.cpp index 91660b2e72..10ba3b128b 100644 --- a/libs/brush/kis_brush.cpp +++ b/libs/brush/kis_brush.cpp @@ -1,638 +1,653 @@ /* * Copyright (c) 1999 Matthias Elter * Copyright (c) 2003 Patrick Julien * Copyright (c) 2004-2008 Boudewijn Rempt * Copyright (c) 2004 Adrian Page * Copyright (c) 2005 Bart Coppens * Copyright (c) 2007 Cyrille Berger * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_brush.h" #include #include #include #include #include #include #include #include #include #include #include "kis_datamanager.h" #include "kis_paint_device.h" #include "kis_global.h" #include "kis_boundary.h" #include "kis_image.h" #include "kis_iterator_ng.h" #include "kis_brush_registry.h" #include #include #include #include KisBrush::ColoringInformation::~ColoringInformation() { } KisBrush::PlainColoringInformation::PlainColoringInformation(const quint8* color) : m_color(color) { } KisBrush::PlainColoringInformation::~PlainColoringInformation() { } const quint8* KisBrush::PlainColoringInformation::color() const { return m_color; } void KisBrush::PlainColoringInformation::nextColumn() { } void KisBrush::PlainColoringInformation::nextRow() { } KisBrush::PaintDeviceColoringInformation::PaintDeviceColoringInformation(const KisPaintDeviceSP source, int width) : m_source(source) , m_iterator(m_source->createHLineConstIteratorNG(0, 0, width)) { } KisBrush::PaintDeviceColoringInformation::~PaintDeviceColoringInformation() { } const quint8* KisBrush::PaintDeviceColoringInformation::color() const { return m_iterator->oldRawData(); } void KisBrush::PaintDeviceColoringInformation::nextColumn() { m_iterator->nextPixel(); } void KisBrush::PaintDeviceColoringInformation::nextRow() { m_iterator->nextRow(); } struct KisBrush::Private { Private() : boundary(0) , angle(0) , scale(1.0) , hasColor(false) , brushType(INVALID) , autoSpacingActive(false) , autoSpacingCoeff(1.0) + , threadingAllowed(true) {} ~Private() { delete boundary; } mutable KisBoundary* boundary; qreal angle; qreal scale; bool hasColor; enumBrushType brushType; qint32 width; qint32 height; double spacing; QPointF hotSpot; mutable QSharedPointer brushPyramid; QImage brushTipImage; bool autoSpacingActive; qreal autoSpacingCoeff; + + bool threadingAllowed; }; KisBrush::KisBrush() : KoResource(QString()) , d(new Private) { } KisBrush::KisBrush(const QString& filename) : KoResource(filename) , d(new Private) { } KisBrush::KisBrush(const KisBrush& rhs) : KoResource(QString()) , KisShared() , d(new Private) { setBrushTipImage(rhs.brushTipImage()); d->brushType = rhs.d->brushType; d->width = rhs.d->width; d->height = rhs.d->height; d->spacing = rhs.d->spacing; d->hotSpot = rhs.d->hotSpot; d->hasColor = rhs.d->hasColor; d->angle = rhs.d->angle; d->scale = rhs.d->scale; d->autoSpacingActive = rhs.d->autoSpacingActive; d->autoSpacingCoeff = rhs.d->autoSpacingCoeff; + d->threadingAllowed = rhs.d->threadingAllowed; setFilename(rhs.filename()); /** * Be careful! The pyramid is shared between two brush objects, * therefore you cannot change it, only recreate! That i sthe * reason why it is defined as const! */ d->brushPyramid = rhs.d->brushPyramid; // don't copy the boundary, it will be regenerated -- see bug 291910 } KisBrush::~KisBrush() { clearBrushPyramid(); delete d; } QImage KisBrush::brushTipImage() const { if (d->brushTipImage.isNull()) { const_cast(this)->load(); } return d->brushTipImage; } qint32 KisBrush::width() const { return d->width; } void KisBrush::setWidth(qint32 width) { d->width = width; } qint32 KisBrush::height() const { return d->height; } void KisBrush::setHeight(qint32 height) { d->height = height; } void KisBrush::setHotSpot(QPointF pt) { double x = pt.x(); double y = pt.y(); if (x < 0) x = 0; else if (x >= width()) x = width() - 1; if (y < 0) y = 0; else if (y >= height()) y = height() - 1; d->hotSpot = QPointF(x, y); } QPointF KisBrush::hotSpot(KisDabShape const& shape, const KisPaintInformation& info) const { Q_UNUSED(info); QSizeF metric = characteristicSize(shape); qreal w = metric.width(); qreal h = metric.height(); // The smallest brush we can produce is a single pixel. if (w < 1) { w = 1; } if (h < 1) { h = 1; } // XXX: This should take d->hotSpot into account, though it // isn't specified by gimp brushes so it would default to the center // anyway. QPointF p(w / 2, h / 2); return p; } bool KisBrush::hasColor() const { return d->hasColor; } void KisBrush::setHasColor(bool hasColor) { d->hasColor = hasColor; } bool KisBrush::isPiercedApprox() const { QImage image = brushTipImage(); qreal w = image.width(); qreal h = image.height(); qreal xPortion = qMin(0.1, 5.0 / w); qreal yPortion = qMin(0.1, 5.0 / h); int x0 = std::floor((0.5 - xPortion) * w); int x1 = std::ceil((0.5 + xPortion) * w); int y0 = std::floor((0.5 - yPortion) * h); int y1 = std::ceil((0.5 + yPortion) * h); const int maxNumSamples = (x1 - x0 + 1) * (y1 - y0 + 1); const int failedPixelsThreshold = 0.1 * maxNumSamples; const int thresholdValue = 0.95 * 255; int failedPixels = 0; for (int y = y0; y <= y1; y++) { for (int x = x0; x <= x1; x++) { QRgb pixel = image.pixel(x,y); if (qRed(pixel) > thresholdValue) { failedPixels++; } } } return failedPixels > failedPixelsThreshold; } bool KisBrush::canPaintFor(const KisPaintInformation& /*info*/) { return true; } void KisBrush::setBrushTipImage(const QImage& image) { //Q_ASSERT(!image.isNull()); d->brushTipImage = image; if (!image.isNull()) { if (image.width() > 128 || image.height() > 128) { KoResource::setImage(image.scaled(128, 128, Qt::KeepAspectRatio, Qt::SmoothTransformation)); } else { KoResource::setImage(image); } setWidth(image.width()); setHeight(image.height()); } clearBrushPyramid(); } void KisBrush::setBrushType(enumBrushType type) { d->brushType = type; } enumBrushType KisBrush::brushType() const { return d->brushType; } void KisBrush::predefinedBrushToXML(const QString &type, QDomElement& e) const { e.setAttribute("type", type); e.setAttribute("filename", shortFilename()); e.setAttribute("spacing", QString::number(spacing())); e.setAttribute("useAutoSpacing", QString::number(autoSpacingActive())); e.setAttribute("autoSpacingCoeff", QString::number(autoSpacingCoeff())); e.setAttribute("angle", QString::number(angle())); e.setAttribute("scale", QString::number(scale())); } void KisBrush::toXML(QDomDocument& /*document*/ , QDomElement& element) const { element.setAttribute("BrushVersion", "2"); } KisBrushSP KisBrush::fromXML(const QDomElement& element, bool forceCopy) { KisBrushSP brush = KisBrushRegistry::instance()->getOrCreateBrush(element, forceCopy); if (brush && element.attribute("BrushVersion", "1") == "1") { brush->setScale(brush->scale() * 2.0); } return brush; } QSizeF KisBrush::characteristicSize(KisDabShape const& shape) const { KisDabShape normalizedShape( shape.scale() * d->scale, shape.ratio(), normalizeAngle(shape.rotation() + d->angle)); return KisQImagePyramid::characteristicSize( QSize(width(), height()), normalizedShape); } qint32 KisBrush::maskWidth(KisDabShape const& shape, qreal subPixelX, qreal subPixelY, const KisPaintInformation& info) const { Q_UNUSED(info); qreal angle = normalizeAngle(shape.rotation() + d->angle); qreal scale = shape.scale() * d->scale; return KisQImagePyramid::imageSize(QSize(width(), height()), KisDabShape(scale, shape.ratio(), angle), subPixelX, subPixelY).width(); } qint32 KisBrush::maskHeight(KisDabShape const& shape, qreal subPixelX, qreal subPixelY, const KisPaintInformation& info) const { Q_UNUSED(info); qreal angle = normalizeAngle(shape.rotation() + d->angle); qreal scale = shape.scale() * d->scale; return KisQImagePyramid::imageSize(QSize(width(), height()), KisDabShape(scale, shape.ratio(), angle), subPixelX, subPixelY).height(); } double KisBrush::maskAngle(double angle) const { return normalizeAngle(angle + d->angle); } quint32 KisBrush::brushIndex(const KisPaintInformation& info) const { Q_UNUSED(info); return 0; } void KisBrush::setSpacing(double s) { if (s < 0.02) s = 0.02; d->spacing = s; } double KisBrush::spacing() const { return d->spacing; } void KisBrush::setAutoSpacing(bool active, qreal coeff) { d->autoSpacingCoeff = coeff; d->autoSpacingActive = active; } bool KisBrush::autoSpacingActive() const { return d->autoSpacingActive; } qreal KisBrush::autoSpacingCoeff() const { return d->autoSpacingCoeff; } void KisBrush::notifyStrokeStarted() { } void KisBrush::notifyCachedDabPainted(const KisPaintInformation& info) { Q_UNUSED(info); } +void KisBrush::prepareForSeqNo(const KisPaintInformation &info, int seqNo) +{ + Q_UNUSED(info); + Q_UNUSED(seqNo); +} + +void KisBrush::setThreadingAllowed(bool value) +{ + d->threadingAllowed = value; +} + +bool KisBrush::threadingAllowed() const +{ + return d->threadingAllowed; +} + void KisBrush::prepareBrushPyramid() const { if (!d->brushPyramid) { d->brushPyramid = toQShared(new KisQImagePyramid(brushTipImage())); } } void KisBrush::clearBrushPyramid() { d->brushPyramid.clear(); } -void KisBrush::mask(KisFixedPaintDeviceSP dst, KisDabShape const& shape, const KisPaintInformation& info , double subPixelX, double subPixelY, qreal softnessFactor) const -{ - generateMaskAndApplyMaskOrCreateDab(dst, 0, shape, info, subPixelX, subPixelY, softnessFactor); -} - void KisBrush::mask(KisFixedPaintDeviceSP dst, const KoColor& color, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX, double subPixelY, qreal softnessFactor) const { PlainColoringInformation pci(color.data()); generateMaskAndApplyMaskOrCreateDab(dst, &pci, shape, info, subPixelX, subPixelY, softnessFactor); } void KisBrush::mask(KisFixedPaintDeviceSP dst, const KisPaintDeviceSP src, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX, double subPixelY, qreal softnessFactor) const { PaintDeviceColoringInformation pdci(src, maskWidth(shape, subPixelX, subPixelY, info)); generateMaskAndApplyMaskOrCreateDab(dst, &pdci, shape, info, subPixelX, subPixelY, softnessFactor); } void KisBrush::generateMaskAndApplyMaskOrCreateDab(KisFixedPaintDeviceSP dst, ColoringInformation* coloringInformation, KisDabShape const& shape, const KisPaintInformation& info_, double subPixelX, double subPixelY, qreal softnessFactor) const { Q_ASSERT(valid()); Q_UNUSED(info_); Q_UNUSED(softnessFactor); prepareBrushPyramid(); QImage outputImage = d->brushPyramid->createImage(KisDabShape( shape.scale() * d->scale, shape.ratio(), -normalizeAngle(shape.rotation() + d->angle)), subPixelX, subPixelY); qint32 maskWidth = outputImage.width(); qint32 maskHeight = outputImage.height(); dst->setRect(QRect(0, 0, maskWidth, maskHeight)); - dst->initialize(); + dst->lazyGrowBufferWithoutInitialization(); quint8* color = 0; if (coloringInformation) { if (dynamic_cast(coloringInformation)) { color = const_cast(coloringInformation->color()); } } const KoColorSpace *cs = dst->colorSpace(); qint32 pixelSize = cs->pixelSize(); quint8 *dabPointer = dst->data(); quint8 *rowPointer = dabPointer; quint8 *alphaArray = new quint8[maskWidth]; bool hasColor = this->hasColor(); for (int y = 0; y < maskHeight; y++) { const quint8* maskPointer = outputImage.constScanLine(y); if (coloringInformation) { for (int x = 0; x < maskWidth; x++) { if (color) { memcpy(dabPointer, color, pixelSize); } else { memcpy(dabPointer, coloringInformation->color(), pixelSize); coloringInformation->nextColumn(); } dabPointer += pixelSize; } } if (hasColor) { const quint8 *src = maskPointer; quint8 *dst = alphaArray; for (int x = 0; x < maskWidth; x++) { const QRgb *c = reinterpret_cast(src); *dst = KoColorSpaceMaths::multiply(255 - qGray(*c), qAlpha(*c)); src += 4; dst++; } } else { const quint8 *src = maskPointer; quint8 *dst = alphaArray; for (int x = 0; x < maskWidth; x++) { const QRgb *c = reinterpret_cast(src); *dst = KoColorSpaceMaths::multiply(255 - *src, qAlpha(*c)); src += 4; dst++; } } cs->applyAlphaU8Mask(rowPointer, alphaArray, maskWidth); rowPointer += maskWidth * pixelSize; dabPointer = rowPointer; if (!color && coloringInformation) { coloringInformation->nextRow(); } } delete[] alphaArray; } KisFixedPaintDeviceSP KisBrush::paintDevice(const KoColorSpace * colorSpace, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX, double subPixelY) const { Q_ASSERT(valid()); Q_UNUSED(info); double angle = normalizeAngle(shape.rotation() + d->angle); double scale = shape.scale() * d->scale; prepareBrushPyramid(); QImage outputImage = d->brushPyramid->createImage( KisDabShape(scale, shape.ratio(), -angle), subPixelX, subPixelY); KisFixedPaintDeviceSP dab = new KisFixedPaintDevice(colorSpace); Q_CHECK_PTR(dab); dab->convertFromQImage(outputImage, ""); return dab; } void KisBrush::resetBoundary() { delete d->boundary; d->boundary = 0; } void KisBrush::generateBoundary() const { KisFixedPaintDeviceSP dev; KisDabShape inverseTransform(1.0 / scale(), 1.0, -angle()); if (brushType() == IMAGE || brushType() == PIPE_IMAGE) { dev = paintDevice(KoColorSpaceRegistry::instance()->rgb8(), inverseTransform, KisPaintInformation()); } else { const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); dev = new KisFixedPaintDevice(cs); mask(dev, KoColor(Qt::black, cs), inverseTransform, KisPaintInformation()); } d->boundary = new KisBoundary(dev); d->boundary->generateBoundary(); } const KisBoundary* KisBrush::boundary() const { if (!d->boundary) generateBoundary(); return d->boundary; } void KisBrush::setScale(qreal _scale) { d->scale = _scale; } qreal KisBrush::scale() const { return d->scale; } void KisBrush::setAngle(qreal _rotation) { d->angle = _rotation; } qreal KisBrush::angle() const { return d->angle; } QPainterPath KisBrush::outline() const { return boundary()->path(); } void KisBrush::lodLimitations(KisPaintopLodLimitations *l) const { if (spacing() > 0.5) { l->limitations << KoID("huge-spacing", i18nc("PaintOp instant preview limitation", "Spacing > 0.5, consider disabling Instant Preview")); } } diff --git a/libs/brush/kis_brush.h b/libs/brush/kis_brush.h index 203d2824f0..c336216c59 100644 --- a/libs/brush/kis_brush.h +++ b/libs/brush/kis_brush.h @@ -1,379 +1,396 @@ /* * Copyright (c) 1999 Matthias Elter * Copyright (c) 2002 Patrick Julien * Copyright (c) 2004 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_BRUSH_ #define KIS_BRUSH_ #include #include #include #include #include #include class KisQImagemask; typedef KisSharedPtr KisQImagemaskSP; class QString; class KoColor; class KoColorSpace; class KisPaintInformation; class KisBoundary; class KisPaintopLodLimitations; enum enumBrushType { INVALID, MASK, IMAGE, PIPE_MASK, PIPE_IMAGE }; static const qreal DEFAULT_SOFTNESS_FACTOR = 1.0; class KisBrush; typedef KisSharedPtr KisBrushSP; /** * KisBrush is the base class for brush resources. A brush resource * defines one or more images that are used to potato-stamp along * the drawn path. The brush type defines how this brush is used -- * the important difference is between masks (which take the current * painting color) and images (which do not). It is up to the paintop * to make use of this feature. * * Brushes must be serializable to an xml representation and provide * a factory class that can recreate or retrieve the brush based on * this representation. * * XXX: This api is still a big mess -- it needs a good refactoring. * And the whole KoResource architecture is way over-designed. */ class BRUSH_EXPORT KisBrush : public KoResource, public KisShared { public: class ColoringInformation { public: virtual ~ColoringInformation(); virtual const quint8* color() const = 0; virtual void nextColumn() = 0; virtual void nextRow() = 0; }; protected: class PlainColoringInformation : public ColoringInformation { public: PlainColoringInformation(const quint8* color); ~PlainColoringInformation() override; const quint8* color() const override ; void nextColumn() override; void nextRow() override; private: const quint8* m_color; }; class PaintDeviceColoringInformation : public ColoringInformation { public: PaintDeviceColoringInformation(const KisPaintDeviceSP source, int width); ~PaintDeviceColoringInformation() override; const quint8* color() const override ; void nextColumn() override; void nextRow() override; private: const KisPaintDeviceSP m_source; KisHLineConstIteratorSP m_iterator; }; public: KisBrush(); KisBrush(const QString& filename); ~KisBrush() override; virtual qreal userEffectiveSize() const = 0; virtual void setUserEffectiveSize(qreal value) = 0; bool load() override { return false; } bool loadFromDevice(QIODevice *) override { return false; } bool save() override { return false; } bool saveToDevice(QIODevice* ) const override { return false; } /** * @brief brushImage the image the brush tip can paint with. Not all brush types have a single * image. * @return a valid QImage. */ virtual QImage brushTipImage() const; /** * Change the spacing of the brush. * @param spacing a spacing of 1.0 means that strokes will be separated from one time the size * of the brush. */ virtual void setSpacing(double spacing); /** * @return the spacing between two strokes for this brush */ double spacing() const; void setAutoSpacing(bool active, qreal coeff); bool autoSpacingActive() const; qreal autoSpacingCoeff() const; /** * @return the width (for scale == 1.0) */ qint32 width() const; /** * @return the height (for scale == 1.0) */ qint32 height() const; /** * @return the width of the mask for the given scale and angle */ virtual qint32 maskWidth(KisDabShape const&, qreal subPixelX, qreal subPixelY, const KisPaintInformation& info) const; /** * @return the height of the mask for the given scale and angle */ virtual qint32 maskHeight(KisDabShape const&, qreal subPixelX, qreal subPixelY, const KisPaintInformation& info) const; /** * @return the logical size of the brush, that is the size measured * in floating point value. * * This value should not be used for calculating future dab sizes * because it doesn't take any rounding into account. The only use * of this metric is calculation of brush-size derivatives like * hotspots and spacing. */ virtual QSizeF characteristicSize(KisDabShape const&) const; /** * @return the angle of the mask adding the given angle */ double maskAngle(double angle = 0) const; /** * @return the index of the brush * if the brush consists of multiple images */ virtual quint32 brushIndex(const KisPaintInformation& info) const; /** * The brush type defines how the brush is used. */ virtual enumBrushType brushType() const; QPointF hotSpot(KisDabShape const&, const KisPaintInformation& info) const; /** * Returns true if this brush can return something useful for the info. This is used * by Pipe Brushes that can't paint sometimes **/ virtual bool canPaintFor(const KisPaintInformation& /*info*/); /** * Is called by the paint op when a paintop starts a stroke. The * point is that we store brushes a server while the paint ops are * are recreated all the time. Is means that upon a stroke start * the brushes may need to clear its state. */ virtual void notifyStrokeStarted(); /** * Is called by the cache, when cache hit has happened. * Having got this notification the brush can update the counters * of dabs, generate some new random values if needed. * + * * NOTE: one should use **either** notifyCachedDabPainted() or prepareForSeqNo() + * * Currently, this is used by pipe'd brushes to implement * incremental and random parasites */ virtual void notifyCachedDabPainted(const KisPaintInformation& info); + /** + * Is called by the multithreaded queue to prepare a specific brush + * tip for the particular seqNo. + * + * NOTE: one should use **either** notifyCachedDabPainted() or prepareForSeqNo() + * + * Currently, this is used by pipe'd brushes to implement + * incremental and random parasites + */ + virtual void prepareForSeqNo(const KisPaintInformation& info, int seqNo); + + /** + * Notify the brush if it can use QtConcurrent's threading capabilities in its + * internal routines. By default it is allowed, but some paintops (who do their + * own multithreading) may ask the brush to avoid internal threading. + */ + void setThreadingAllowed(bool value); + + /** + * \see setThreadingAllowed() for details + */ + bool threadingAllowed() const; + /** * Return a fixed paint device that contains a correctly scaled image dab. */ virtual KisFixedPaintDeviceSP paintDevice(const KoColorSpace * colorSpace, KisDabShape const&, const KisPaintInformation& info, double subPixelX = 0, double subPixelY = 0) const; - /** - * Apply the brush mask to the pixels in dst. Dst should be big enough! - */ - void mask(KisFixedPaintDeviceSP dst, - KisDabShape const& shape, - const KisPaintInformation& info, - double subPixelX = 0, double subPixelY = 0, qreal softnessFactor = DEFAULT_SOFTNESS_FACTOR) const; - /** * clear dst fill it with a mask colored with KoColor */ void mask(KisFixedPaintDeviceSP dst, const KoColor& color, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX = 0, double subPixelY = 0, qreal softnessFactor = DEFAULT_SOFTNESS_FACTOR) const; /** * clear dst and fill it with a mask colored with the corresponding colors of src */ void mask(KisFixedPaintDeviceSP dst, const KisPaintDeviceSP src, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX = 0, double subPixelY = 0, qreal softnessFactor = DEFAULT_SOFTNESS_FACTOR) const; virtual bool hasColor() const; /** * Create a mask and either mask dst (that is, change all alpha values of the * existing pixels to those of the mask) or, if coloringInfo is present, clear * dst and fill dst with pixels according to coloringInfo, masked according to the * generated mask. * * @param dst the destination that will be draw on the image, and this function * will edit its alpha channel * @param coloringInfo coloring information that will be copied on the dab, it can be null * @param scale a scale applied on the alpha mask * @param angle a rotation applied on the alpha mask * @param info the painting information (this is only and should only be used by * KisImagePipeBrush and only to be backward compatible with the Gimp, * KisImagePipeBrush is ignoring scale and angle information) * @param subPixelX sub position of the brush (contained between 0.0 and 1.0) * @param subPixelY sub position of the brush (contained between 0.0 and 1.0) * * @return a mask computed from the grey-level values of the * pixels in the brush. */ virtual void generateMaskAndApplyMaskOrCreateDab(KisFixedPaintDeviceSP dst, ColoringInformation* coloringInfo, KisDabShape const&, const KisPaintInformation& info, double subPixelX = 0, double subPixelY = 0, qreal softnessFactor = DEFAULT_SOFTNESS_FACTOR) const; /** * Serialize this brush to XML. */ virtual void toXML(QDomDocument& , QDomElement&) const; static KisBrushSP fromXML(const QDomElement& element, bool forceCopy = false); virtual const KisBoundary* boundary() const; virtual QPainterPath outline() const; virtual void setScale(qreal _scale); qreal scale() const; virtual void setAngle(qreal _angle); qreal angle() const; void prepareBrushPyramid() const; void clearBrushPyramid(); virtual void lodLimitations(KisPaintopLodLimitations *l) const; virtual KisBrush* clone() const = 0; //protected: KisBrush(const KisBrush& rhs); void setWidth(qint32 width); void setHeight(qint32 height); void setHotSpot(QPointF); /** * The image is used to represent the brush in the gui, and may also, depending on the brush type * be used to define the actual brush instance. */ virtual void setBrushTipImage(const QImage& image); /** * XXX */ virtual void setBrushType(enumBrushType type); virtual void setHasColor(bool hasColor); /** * Returns true if the brush has a bunch of pixels almost * fully transparent in the very center. If the brush is pierced, * then dulling mode may not work correctly due to empty samples. * * WARNING: this method is relatively expensive since it iterates * up to 100 pixels of the brush. */ bool isPiercedApprox() const; protected: void resetBoundary(); void predefinedBrushToXML(const QString &type, QDomElement& e) const; private: // Initialize our boundary void generateBoundary() const; struct Private; Private* const d; }; #endif // KIS_BRUSH_ diff --git a/libs/brush/kis_brushes_pipe.h b/libs/brush/kis_brushes_pipe.h index 458770b586..5855282ad5 100644 --- a/libs/brush/kis_brushes_pipe.h +++ b/libs/brush/kis_brushes_pipe.h @@ -1,175 +1,182 @@ /* * Copyright (c) 2012 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_BRUSHES_PIPE_H #define __KIS_BRUSHES_PIPE_H #include template class KisBrushesPipe { public: KisBrushesPipe() { } KisBrushesPipe(const KisBrushesPipe &rhs) { qDeleteAll(m_brushes); m_brushes.clear(); Q_FOREACH (BrushType * brush, rhs.m_brushes) { BrushType *clonedBrush = dynamic_cast(brush->clone()); KIS_ASSERT_RECOVER(clonedBrush) {continue;} m_brushes.append(clonedBrush); } } virtual ~KisBrushesPipe() { qDeleteAll(m_brushes); } virtual void clear() { qDeleteAll(m_brushes); m_brushes.clear(); } BrushType* firstBrush() const { return m_brushes.first(); } BrushType* lastBrush() const { return m_brushes.last(); } BrushType* currentBrush(const KisPaintInformation& info) { return !m_brushes.isEmpty() ? m_brushes.at(chooseNextBrush(info)) : 0; } int brushIndex(const KisPaintInformation& info) { return chooseNextBrush(info); } qint32 maskWidth(KisDabShape const& shape, double subPixelX, double subPixelY, const KisPaintInformation& info) { BrushType *brush = currentBrush(info); return brush ? brush->maskWidth(shape, subPixelX, subPixelY, info) : 0; } qint32 maskHeight(KisDabShape const& shape, double subPixelX, double subPixelY, const KisPaintInformation& info) { BrushType *brush = currentBrush(info); return brush ? brush->maskHeight(shape, subPixelX, subPixelY, info) : 0; } void setAngle(qreal angle) { Q_FOREACH (BrushType * brush, m_brushes) { brush->setAngle(angle); } } void setScale(qreal scale) { Q_FOREACH (BrushType * brush, m_brushes) { brush->setScale(scale); } } void setSpacing(double spacing) { Q_FOREACH (BrushType * brush, m_brushes) { brush->setSpacing(spacing); } } bool hasColor() const { Q_FOREACH (BrushType * brush, m_brushes) { if (brush->hasColor()) return true; } return false; } void notifyCachedDabPainted(const KisPaintInformation& info) { - updateBrushIndexes(info); + updateBrushIndexes(info, -1); + } + + void prepareForSeqNo(const KisPaintInformation& info, int seqNo) { + updateBrushIndexes(info, seqNo); } void generateMaskAndApplyMaskOrCreateDab(KisFixedPaintDeviceSP dst, KisBrush::ColoringInformation* coloringInformation, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX , double subPixelY, qreal softnessFactor) { BrushType *brush = currentBrush(info); if (!brush) return; brush->generateMaskAndApplyMaskOrCreateDab(dst, coloringInformation, shape, info, subPixelX, subPixelY, softnessFactor); - updateBrushIndexes(info); + notifyCachedDabPainted(info); } KisFixedPaintDeviceSP paintDevice(const KoColorSpace * colorSpace, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX, double subPixelY) { BrushType *brush = currentBrush(info); if (!brush) return 0; KisFixedPaintDeviceSP device = brush->paintDevice(colorSpace, shape, info, subPixelX, subPixelY); - updateBrushIndexes(info); + notifyCachedDabPainted(info); return device; } QVector brushes() { return m_brushes; } void testingSelectNextBrush(const KisPaintInformation& info) { (void) chooseNextBrush(info); - updateBrushIndexes(info); + notifyCachedDabPainted(info); } /** * Is called by the paint op when a paintop starts a stroke. The * brushes are shared among different strokes, so sometimes the * brush should be reset. */ virtual void notifyStrokeStarted() = 0; protected: void addBrush(BrushType *brush) { m_brushes.append(brush); } /** * Returns the index of the brush that corresponds to the current * values of \p info. This method is called *before* the dab is * actually painted. * * The method is const, so no internal counters of the brush should * change during its execution */ virtual int chooseNextBrush(const KisPaintInformation& info) = 0; /** * Updates internal counters of the brush *after* a dab has been * painted on the canvas. Some incremental switching of the brushes * may me implemented in this method. + * + * If \p seqNo is equal or greater than zero, then incremental iteration is + * overriden by this seqNo value */ - virtual void updateBrushIndexes(const KisPaintInformation& info) = 0; + virtual void updateBrushIndexes(const KisPaintInformation& info, int seqNo) = 0; protected: QVector m_brushes; }; #endif /* __KIS_BRUSHES_PIPE_H */ diff --git a/libs/brush/kis_dab_shape.h b/libs/brush/kis_dab_shape.h index 04dbedb861..6bd548bf32 100644 --- a/libs/brush/kis_dab_shape.h +++ b/libs/brush/kis_dab_shape.h @@ -1,44 +1,52 @@ /* * Copyright (c) 2016 Nishant Rodrigues * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #pragma once #include class KisDabShape { qreal m_scale; qreal m_ratio; qreal m_rotation; public: KisDabShape() : m_scale(1.0) , m_ratio(1.0) , m_rotation(0.0) {} KisDabShape(qreal scale, qreal ratio, qreal rotation) : m_scale(scale) , m_ratio(ratio) , m_rotation(rotation) {} + + bool operator==(const KisDabShape &rhs) const { + return + qFuzzyCompare(m_scale, rhs.m_scale) && + qFuzzyCompare(m_ratio, rhs.m_ratio) && + qFuzzyCompare(m_rotation, rhs.m_rotation); + } + qreal scale() const { return m_scale; } qreal scaleX() const { return scale(); } qreal scaleY() const { return m_scale * m_ratio; } qreal ratio() const { return m_ratio; } qreal rotation() const { return m_rotation; } }; diff --git a/libs/brush/kis_gbr_brush.cpp b/libs/brush/kis_gbr_brush.cpp index d25aa1d341..77a9beeddd 100644 --- a/libs/brush/kis_gbr_brush.cpp +++ b/libs/brush/kis_gbr_brush.cpp @@ -1,508 +1,509 @@ /* * Copyright (c) 1999 Matthias Elter * Copyright (c) 2003 Patrick Julien * Copyright (c) 2004 Boudewijn Rempt * Copyright (c) 2004 Adrian Page * Copyright (c) 2005 Bart Coppens * Copyright (c) 2007 Cyrille Berger * Copyright (c) 2010 Lukáš Tvrdý * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include "kis_gbr_brush.h" #include #include #include #include #include #include #include #include #include "kis_datamanager.h" #include "kis_paint_device.h" #include "kis_global.h" #include "kis_image.h" struct GimpBrushV1Header { quint32 header_size; /* header_size = sizeof (BrushHeader) + brush name */ quint32 version; /* brush file version # */ quint32 width; /* width of brush */ quint32 height; /* height of brush */ quint32 bytes; /* depth of brush in bytes */ }; /// All fields are in MSB on disk! struct GimpBrushHeader { quint32 header_size; /* header_size = sizeof (BrushHeader) + brush name */ quint32 version; /* brush file version # */ quint32 width; /* width of brush */ quint32 height; /* height of brush */ quint32 bytes; /* depth of brush in bytes */ /* The following are only defined in version 2 */ quint32 magic_number; /* GIMP brush magic number */ quint32 spacing; /* brush spacing as % of width & height, 0 - 1000 */ }; // Needed, or the GIMP won't open it! quint32 const GimpV2BrushMagic = ('G' << 24) + ('I' << 16) + ('M' << 8) + ('P' << 0); struct KisGbrBrush::Private { QByteArray data; bool ownData; /* seems to indicate that @ref data is owned by the brush, but in Qt4.x this is already guaranteed... so in reality it seems more to indicate whether the data is loaded from file (ownData = true) or memory (ownData = false) */ bool useColorAsMask; quint32 header_size; /* header_size = sizeof (BrushHeader) + brush name */ quint32 version; /* brush file version # */ quint32 bytes; /* depth of brush in bytes */ quint32 magic_number; /* GIMP brush magic number */ }; #define DEFAULT_SPACING 0.25 KisGbrBrush::KisGbrBrush(const QString& filename) : KisScalingSizeBrush(filename) , d(new Private) { d->ownData = true; d->useColorAsMask = false; setHasColor(false); setSpacing(DEFAULT_SPACING); } KisGbrBrush::KisGbrBrush(const QString& filename, const QByteArray& data, qint32 & dataPos) : KisScalingSizeBrush(filename) , d(new Private) { d->ownData = false; d->useColorAsMask = false; setHasColor(false); setSpacing(DEFAULT_SPACING); d->data = QByteArray::fromRawData(data.data() + dataPos, data.size() - dataPos); init(); d->data.clear(); dataPos += d->header_size + (width() * height() * d->bytes); } KisGbrBrush::KisGbrBrush(KisPaintDeviceSP image, int x, int y, int w, int h) : KisScalingSizeBrush() , d(new Private) { d->ownData = true; d->useColorAsMask = false; setHasColor(false); setSpacing(DEFAULT_SPACING); initFromPaintDev(image, x, y, w, h); } KisGbrBrush::KisGbrBrush(const QImage& image, const QString& name) : KisScalingSizeBrush() , d(new Private) { d->ownData = false; d->useColorAsMask = false; setHasColor(false); setSpacing(DEFAULT_SPACING); setBrushTipImage(image); setName(name); } KisGbrBrush::KisGbrBrush(const KisGbrBrush& rhs) : KisScalingSizeBrush(rhs) , d(new Private(*rhs.d)) { setName(rhs.name()); + setBrushTipImage(rhs.brushTipImage()); d->data = QByteArray(); setValid(rhs.valid()); } KisGbrBrush::~KisGbrBrush() { delete d; } bool KisGbrBrush::load() { QFile file(filename()); if (file.size() == 0) return false; file.open(QIODevice::ReadOnly); bool res = loadFromDevice(&file); file.close(); return res; } bool KisGbrBrush::loadFromDevice(QIODevice *dev) { if (d->ownData) { d->data = dev->readAll(); } return init(); } bool KisGbrBrush::init() { GimpBrushHeader bh; if (sizeof(GimpBrushHeader) > (uint)d->data.size()) { return false; } memcpy(&bh, d->data, sizeof(GimpBrushHeader)); bh.header_size = qFromBigEndian(bh.header_size); d->header_size = bh.header_size; bh.version = qFromBigEndian(bh.version); d->version = bh.version; bh.width = qFromBigEndian(bh.width); bh.height = qFromBigEndian(bh.height); bh.bytes = qFromBigEndian(bh.bytes); d->bytes = bh.bytes; bh.magic_number = qFromBigEndian(bh.magic_number); d->magic_number = bh.magic_number; if (bh.version == 1) { // No spacing in version 1 files so use Gimp default bh.spacing = static_cast(DEFAULT_SPACING * 100); } else { bh.spacing = qFromBigEndian(bh.spacing); if (bh.spacing > 1000) { return false; } } setSpacing(bh.spacing / 100.0); if (bh.header_size > (uint)d->data.size() || bh.header_size == 0) { return false; } QString name; if (bh.version == 1) { // Version 1 has no magic number or spacing, so the name // is at a different offset. Character encoding is undefined. const char *text = d->data.constData() + sizeof(GimpBrushV1Header); name = QString::fromLatin1(text, bh.header_size - sizeof(GimpBrushV1Header) - 1); } else { // ### Version = 3->cinepaint; may be float16 data! // Version >=2: UTF-8 encoding is used name = QString::fromUtf8(d->data.constData() + sizeof(GimpBrushHeader), bh.header_size - sizeof(GimpBrushHeader) - 1); } setName(name); if (bh.width == 0 || bh.height == 0) { return false; } QImage::Format imageFormat; if (bh.bytes == 1) { imageFormat = QImage::Format_Indexed8; } else { imageFormat = QImage::Format_ARGB32; } QImage image(QImage(bh.width, bh.height, imageFormat)); if (image.isNull()) { return false; } qint32 k = bh.header_size; if (bh.bytes == 1) { QVector table; for (int i = 0; i < 256; ++i) table.append(qRgb(i, i, i)); image.setColorTable(table); // Grayscale if (static_cast(k + bh.width * bh.height) > d->data.size()) { return false; } setHasColor(false); for (quint32 y = 0; y < bh.height; y++) { uchar *pixel = reinterpret_cast(image.scanLine(y)); for (quint32 x = 0; x < bh.width; x++, k++) { qint32 val = 255 - static_cast(d->data[k]); *pixel = val; ++pixel; } } } else if (bh.bytes == 4) { // RGBA if (static_cast(k + (bh.width * bh.height * 4)) > d->data.size()) { return false; } setHasColor(true); for (quint32 y = 0; y < bh.height; y++) { QRgb *pixel = reinterpret_cast(image.scanLine(y)); for (quint32 x = 0; x < bh.width; x++, k += 4) { *pixel = qRgba(d->data[k], d->data[k + 1], d->data[k + 2], d->data[k + 3]); ++pixel; } } } else { warnKrita << "WARNING: loading of GBR brushes with" << bh.bytes << "bytes per pixel is not supported"; return false; } setWidth(image.width()); setHeight(image.height()); if (d->ownData) { d->data.resize(0); // Save some memory, we're using enough of it as it is. } setValid(image.width() != 0 && image.height() != 0); setBrushTipImage(image); return true; } bool KisGbrBrush::initFromPaintDev(KisPaintDeviceSP image, int x, int y, int w, int h) { // Forcefully convert to RGBA8 // XXX profile and exposure? setBrushTipImage(image->convertToQImage(0, x, y, w, h, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags())); setName(image->objectName()); setHasColor(true); return true; } bool KisGbrBrush::save() { QFile file(filename()); file.open(QIODevice::WriteOnly | QIODevice::Truncate); bool ok = saveToDevice(&file); file.close(); return ok; } bool KisGbrBrush::saveToDevice(QIODevice* dev) const { GimpBrushHeader bh; QByteArray utf8Name = name().toUtf8(); // Names in v2 brushes are in UTF-8 char const* name = utf8Name.data(); int nameLength = qstrlen(name); int wrote; bh.header_size = qToBigEndian((quint32)sizeof(GimpBrushHeader) + nameLength + 1); bh.version = qToBigEndian((quint32)2); // Only RGBA8 data needed atm, no cinepaint stuff bh.width = qToBigEndian((quint32)width()); bh.height = qToBigEndian((quint32)height()); // Hardcoded, 4 bytes RGBA or 1 byte GREY if (!hasColor()) { bh.bytes = qToBigEndian((quint32)1); } else { bh.bytes = qToBigEndian((quint32)4); } bh.magic_number = qToBigEndian((quint32)GimpV2BrushMagic); bh.spacing = qToBigEndian(static_cast(spacing() * 100.0)); // Write header: first bh, then the name QByteArray bytes = QByteArray::fromRawData(reinterpret_cast(&bh), sizeof(GimpBrushHeader)); wrote = dev->write(bytes); bytes.clear(); if (wrote == -1) { return false; } wrote = dev->write(name, nameLength + 1); if (wrote == -1) { return false; } int k = 0; QImage image = brushTipImage(); if (!hasColor()) { bytes.resize(width() * height()); for (qint32 y = 0; y < height(); y++) { for (qint32 x = 0; x < width(); x++) { QRgb c = image.pixel(x, y); bytes[k++] = static_cast(255 - qRed(c)); // red == blue == green } } } else { bytes.resize(width() * height() * 4); for (qint32 y = 0; y < height(); y++) { for (qint32 x = 0; x < width(); x++) { // order for gimp brushes, v2 is: RGBA QRgb pixel = image.pixel(x, y); bytes[k++] = static_cast(qRed(pixel)); bytes[k++] = static_cast(qGreen(pixel)); bytes[k++] = static_cast(qBlue(pixel)); bytes[k++] = static_cast(qAlpha(pixel)); } } } wrote = dev->write(bytes); if (wrote == -1) { return false; } KoResource::saveToDevice(dev); return true; } QImage KisGbrBrush::brushTipImage() const { QImage image = KisBrush::brushTipImage(); if (hasColor() && useColorAsMask()) { for (int y = 0; y < image.height(); y++) { QRgb *pixel = reinterpret_cast(image.scanLine(y)); for (int x = 0; x < image.width(); x++) { QRgb c = pixel[x]; int a = qGray(c); pixel[x] = qRgba(a, a, a, qAlpha(c)); } } } return image; } enumBrushType KisGbrBrush::brushType() const { return !hasColor() || useColorAsMask() ? MASK : IMAGE; } void KisGbrBrush::setBrushType(enumBrushType type) { Q_UNUSED(type); qFatal("FATAL: protected member setBrushType has no meaning for KisGbrBrush"); } void KisGbrBrush::setBrushTipImage(const QImage& image) { KisBrush::setBrushTipImage(image); setValid(true); } void KisGbrBrush::makeMaskImage() { if (!hasColor()) { return; } QImage brushTip = brushTipImage(); if (brushTip.width() == width() && brushTip.height() == height()) { int imageWidth = width(); int imageHeight = height(); QImage image(imageWidth, imageHeight, QImage::Format_Indexed8); QVector table; for (int i = 0; i < 256; ++i) { table.append(qRgb(i, i, i)); } image.setColorTable(table); for (int y = 0; y < imageHeight; y++) { QRgb *pixel = reinterpret_cast(brushTip.scanLine(y)); uchar * dstPixel = image.scanLine(y); for (int x = 0; x < imageWidth; x++) { QRgb c = pixel[x]; float alpha = qAlpha(c) / 255.0f; // linear interpolation with maximum gray value which is transparent in the mask //int a = (qGray(c) * alpha) + ((1.0 - alpha) * 255); // single multiplication version int a = 255 + alpha * (qGray(c) - 255); dstPixel[x] = (uchar)a; } } setBrushTipImage(image); } setHasColor(false); setUseColorAsMask(false); resetBoundary(); clearBrushPyramid(); } KisBrush* KisGbrBrush::clone() const { return new KisGbrBrush(*this); } void KisGbrBrush::toXML(QDomDocument& d, QDomElement& e) const { predefinedBrushToXML("gbr_brush", e); e.setAttribute("ColorAsMask", QString::number((int)useColorAsMask())); KisBrush::toXML(d, e); } void KisGbrBrush::setUseColorAsMask(bool useColorAsMask) { /** * WARNING: There is a problem in the brush server, since it * returns not copies of brushes, but direct pointers to them. It * means that the brushes are shared among all the currently * present paintops, which might be a problem for e.g. Multihand * Brush Tool. * * Right now, all the instances of Multihand Brush Tool share the * same brush, so there is no problem in this sharing, unless we * reset the internal state of the brush on our way. */ if (useColorAsMask != d->useColorAsMask) { d->useColorAsMask = useColorAsMask; resetBoundary(); clearBrushPyramid(); } } bool KisGbrBrush::useColorAsMask() const { return d->useColorAsMask; } QString KisGbrBrush::defaultFileExtension() const { return QString(".gbr"); } diff --git a/libs/brush/kis_gbr_brush.h b/libs/brush/kis_gbr_brush.h index 6d578e9e96..1f2257847f 100644 --- a/libs/brush/kis_gbr_brush.h +++ b/libs/brush/kis_gbr_brush.h @@ -1,122 +1,121 @@ /* * Copyright (c) 1999 Matthias Elter * Copyright (c) 2002 Patrick Julien * Copyright (c) 2004 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_GBR_BRUSH_ #define KIS_GBR_BRUSH_ #include #include #include "kis_scaling_size_brush.h" #include #include #include #include "kritabrush_export.h" class KisQImagemask; typedef KisSharedPtr KisQImagemaskSP; class QString; class QIODevice; class BRUSH_EXPORT KisGbrBrush : public KisScalingSizeBrush { protected: public: /// Construct brush to load filename later as brush KisGbrBrush(const QString& filename); /// Load brush from the specified data, at position dataPos, and set the filename KisGbrBrush(const QString& filename, const QByteArray & data, qint32 & dataPos); /// Load brush from the specified paint device, in the specified region KisGbrBrush(KisPaintDeviceSP image, int x, int y, int w, int h); /// Load brush as a copy from the specified QImage (handy when you need to copy a brush!) KisGbrBrush(const QImage& image, const QString& name = QString()); + KisGbrBrush(const KisGbrBrush& rhs); + ~KisGbrBrush() override; bool load() override; bool loadFromDevice(QIODevice *dev) override; bool save() override; bool saveToDevice(QIODevice* dev) const override; /** * @return a preview of the brush */ QImage brushTipImage() const override; /** * If the brush image data are colorful (e.g. you created the brush from the canvas with custom brush) * and you want to paint with it as with masks, set to true. */ virtual void setUseColorAsMask(bool useColorAsMask); virtual bool useColorAsMask() const; /** * Convert the mask to inverted gray scale, so it is alpha mask. * It can be used as MASK brush type. This operates on the date of the brush, * so it destruct the original brush data */ virtual void makeMaskImage(); enumBrushType brushType() const override; /** * Makes a copy of this brush. */ KisBrush* clone() const override; /** * @return default file extension for saving the brush */ QString defaultFileExtension() const override; protected: /** * save the content of this brush to an IO device */ friend class KisImageBrushesPipe; - - KisGbrBrush(const KisGbrBrush& rhs); - void setBrushType(enumBrushType type) override; void setBrushTipImage(const QImage& image) override; void toXML(QDomDocument& d, QDomElement& e) const override; private: bool init(); bool initFromPaintDev(KisPaintDeviceSP image, int x, int y, int w, int h); struct Private; Private* const d; }; #endif // KIS_GBR_BRUSH_ diff --git a/libs/brush/kis_imagepipe_brush.cpp b/libs/brush/kis_imagepipe_brush.cpp index 8a11cd2d57..1189170c55 100644 --- a/libs/brush/kis_imagepipe_brush.cpp +++ b/libs/brush/kis_imagepipe_brush.cpp @@ -1,502 +1,509 @@ /* * Copyright (c) 2004 Boudewijn Rempt * Copyright (c) 2005 Bart Coppens * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_imagepipe_brush.h" #include "kis_pipebrush_parasite.h" #include "kis_brushes_pipe.h" class KisImageBrushesPipe : public KisBrushesPipe { public: KisImageBrushesPipe() : m_isInitialized(false) { } /* pre and post are split because: 21:12:20 < dmitryK> boud: i guess it was somehow related to the fact that the maskWidth/maskHeight should correspond to the size of the mask returned by paintDevice() 21:13:33 < dmitryK> boud: the random stuff is called once per brush->paintDevice() call, after the device is returned to the paint op, that is "preparing the randomness for the next call" 21:14:16 < dmitryK> boud: and brushesPipe->currentBrush() always returning the same brush for any particular paintInfo. */ protected: static int selectPre(KisParasite::SelectionMode mode, int index, int rank, const KisPaintInformation& info) { qreal angle; switch (mode) { case KisParasite::Constant: case KisParasite::Incremental: case KisParasite::Random: break; case KisParasite::Pressure: index = static_cast(info.pressure() * (rank - 1) + 0.5); break; case KisParasite::Angular: // + m_d->PI_2 to be compatible with the gimp angle = info.drawingAngle() + M_PI_2; angle = normalizeAngle(angle); index = static_cast(angle / (2.0 * M_PI) * rank); break; case KisParasite::TiltX: index = qRound(info.xTilt() / 2.0 * rank) + rank / 2; break; case KisParasite::TiltY: index = qRound(info.yTilt() / 2.0 * rank) + rank / 2; break; default: warnImage << "Parasite" << mode << "is not implemented"; index = 0; } return index; } static int selectPost(KisParasite::SelectionMode mode, int index, int rank, - const KisPaintInformation& info) { + const KisPaintInformation& info, + int seqNo) { switch (mode) { case KisParasite::Constant: break; case KisParasite::Incremental: - index = (index + 1) % rank; + index = (seqNo >= 0 ? seqNo : (index + 1)) % rank; break; case KisParasite::Random: index = info.randomSource()->generate(0, rank); break; case KisParasite::Pressure: case KisParasite::Angular: break; case KisParasite::TiltX: case KisParasite::TiltY: break; default: warnImage << "Parasite" << mode << "is not implemented"; index = 0; } return index; } int chooseNextBrush(const KisPaintInformation& info) override { quint32 brushIndex = 0; if (!m_isInitialized) { /** * Reset all the indexes to the initial values and do the * generation based on parameters. */ for (int i = 0; i < m_parasite.dim; i++) { m_parasite.index[i] = 0; } - updateBrushIndexes(info); + updateBrushIndexes(info, 0); m_isInitialized = true; } for (int i = 0; i < m_parasite.dim; i++) { int index = selectPre(m_parasite.selection[i], m_parasite.index[i], m_parasite.rank[i], info); brushIndex += m_parasite.brushesCount[i] * index; } brushIndex %= m_brushes.size(); return brushIndex; } - void updateBrushIndexes(const KisPaintInformation& info) override { + void updateBrushIndexes(const KisPaintInformation& info, int seqNo) override { for (int i = 0; i < m_parasite.dim; i++) { m_parasite.index[i] = selectPost(m_parasite.selection[i], m_parasite.index[i], m_parasite.rank[i], - info); + info, + seqNo); } } public: using KisBrushesPipe::addBrush; void setParasite(const KisPipeBrushParasite& parasite) { m_parasite = parasite; } const KisPipeBrushParasite& parasite() const { return m_parasite; } void setUseColorAsMask(bool useColorAsMask) { Q_FOREACH (KisGbrBrush * brush, m_brushes) { brush->setUseColorAsMask(useColorAsMask); } } void makeMaskImage() { Q_FOREACH (KisGbrBrush * brush, m_brushes) { brush->makeMaskImage(); } } bool saveToDevice(QIODevice* dev) const { Q_FOREACH (KisGbrBrush * brush, m_brushes) { if (!brush->saveToDevice(dev)) { return false; } } return true; } void notifyStrokeStarted() override { m_isInitialized = false; } private: KisPipeBrushParasite m_parasite; bool m_isInitialized; }; struct KisImagePipeBrush::Private { public: KisImageBrushesPipe brushesPipe; }; KisImagePipeBrush::KisImagePipeBrush(const QString& filename) : KisGbrBrush(filename) , m_d(new Private()) { } KisImagePipeBrush::KisImagePipeBrush(const QString& name, int w, int h, QVector< QVector > devices, QVector modes) : KisGbrBrush(QString()) , m_d(new Private()) { Q_ASSERT(devices.count() == modes.count()); Q_ASSERT(devices.count() > 0); Q_ASSERT(devices.count() < 2); // XXX Multidimensionals not supported yet, change to MaxDim! setName(name); KisPipeBrushParasite parasite; parasite.dim = devices.count(); // XXX Change for multidim! : parasite.ncells = devices.at(0).count(); parasite.rank[0] = parasite.ncells; // ### This can masquerade some bugs, be careful here in the future parasite.selection[0] = modes.at(0); // XXX needsmovement! parasite.setBrushesCount(); setParasite(parasite); setDevices(devices, w, h); setBrushTipImage(m_d->brushesPipe.firstBrush()->brushTipImage()); } KisImagePipeBrush::KisImagePipeBrush(const KisImagePipeBrush& rhs) : KisGbrBrush(rhs), m_d(new Private(*rhs.m_d)) { } KisImagePipeBrush::~KisImagePipeBrush() { delete m_d; } bool KisImagePipeBrush::load() { QFile file(filename()); file.open(QIODevice::ReadOnly); bool res = loadFromDevice(&file); file.close(); return res; } bool KisImagePipeBrush::loadFromDevice(QIODevice *dev) { QByteArray data = dev->readAll(); return initFromData(data); } bool KisImagePipeBrush::initFromData(const QByteArray &data) { if (data.size() == 0) return false; // XXX: this doesn't correctly load the image pipe brushes yet. // XXX: This stuff is in utf-8, too. // The first line contains the name -- this means we look until we arrive at the first newline QByteArray line1; qint32 i = 0; while (data[i] != '\n' && i < data.size()) { line1.append(data[i]); i++; } setName(QString::fromUtf8(line1, line1.size())); i++; // Skip past the first newline // The second line contains the number of brushes, separated by a space from the parasite // XXX: This stuff is in utf-8, too. QByteArray line2; while (data[i] != '\n' && i < data.size()) { line2.append(data[i]); i++; } QString paramline = QString::fromUtf8(line2, line2.size()); qint32 numOfBrushes = paramline.left(paramline.indexOf(' ')).toUInt(); QString parasiteString = paramline.mid(paramline.indexOf(' ') + 1); KisPipeBrushParasite parasite = KisPipeBrushParasite(parasiteString); parasite.sanitize(); m_d->brushesPipe.setParasite(parasite); i++; // Skip past the second newline for (int brushIndex = 0; brushIndex < numOfBrushes && i < data.size(); brushIndex++) { KisGbrBrush* brush = new KisGbrBrush(name() + '_' + QString().setNum(brushIndex), data, i); m_d->brushesPipe.addBrush(brush); } if (numOfBrushes > 0) { setValid(true); setSpacing(m_d->brushesPipe.lastBrush()->spacing()); setWidth(m_d->brushesPipe.firstBrush()->width()); setHeight(m_d->brushesPipe.firstBrush()->height()); setBrushTipImage(m_d->brushesPipe.firstBrush()->brushTipImage()); } return true; } bool KisImagePipeBrush::save() { QFile file(filename()); file.open(QIODevice::WriteOnly | QIODevice::Truncate); bool ok = saveToDevice(&file); file.close(); return ok; } bool KisImagePipeBrush::saveToDevice(QIODevice* dev) const { QByteArray utf8Name = name().toUtf8(); // Names in v2 brushes are in UTF-8 char const* name = utf8Name.data(); int len = qstrlen(name); if (m_d->brushesPipe.parasite().dim != 1) { warnImage << "Save to file for pipe brushes with dim != not yet supported!"; return false; } // Save this pipe brush: first the header, and then all individual brushes consecutively // XXX: this needs some care for when we have > 1 dimension) // Gimp Pipe Brush header format: Name\n \n // The name\n if (dev->write(name, len) == -1) return false; if (!dev->putChar('\n')) return false; // Write the parasite (also writes number of brushes) if (!m_d->brushesPipe.parasite().saveToDevice(dev)) return false; if (!dev->putChar('\n')) return false; KoResource::saveToDevice(dev); // return m_d->brushesPipe.saveToDevice(dev); } void KisImagePipeBrush::notifyStrokeStarted() { m_d->brushesPipe.notifyStrokeStarted(); } void KisImagePipeBrush::notifyCachedDabPainted(const KisPaintInformation& info) { m_d->brushesPipe.notifyCachedDabPainted(info); } +void KisImagePipeBrush::prepareForSeqNo(const KisPaintInformation &info, int seqNo) +{ + m_d->brushesPipe.prepareForSeqNo(info, seqNo); +} + void KisImagePipeBrush::generateMaskAndApplyMaskOrCreateDab(KisFixedPaintDeviceSP dst, KisBrush::ColoringInformation* coloringInformation, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX , double subPixelY, qreal softnessFactor) const { m_d->brushesPipe.generateMaskAndApplyMaskOrCreateDab(dst, coloringInformation, shape, info, subPixelX, subPixelY, softnessFactor); } QVector KisImagePipeBrush::brushes() const { return m_d->brushesPipe.brushes(); } KisFixedPaintDeviceSP KisImagePipeBrush::paintDevice( const KoColorSpace * colorSpace, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX, double subPixelY) const { return m_d->brushesPipe.paintDevice(colorSpace, shape, info, subPixelX, subPixelY); } enumBrushType KisImagePipeBrush::brushType() const { return !hasColor() || useColorAsMask() ? PIPE_MASK : PIPE_IMAGE; } bool KisImagePipeBrush::hasColor() const { return m_d->brushesPipe.hasColor(); } void KisImagePipeBrush::makeMaskImage() { m_d->brushesPipe.makeMaskImage(); setUseColorAsMask(false); } void KisImagePipeBrush::setUseColorAsMask(bool useColorAsMask) { KisGbrBrush::setUseColorAsMask(useColorAsMask); m_d->brushesPipe.setUseColorAsMask(useColorAsMask); } const KisBoundary* KisImagePipeBrush::boundary() const { KisGbrBrush *brush = m_d->brushesPipe.firstBrush(); Q_ASSERT(brush); return brush->boundary(); } bool KisImagePipeBrush::canPaintFor(const KisPaintInformation& info) { return (!m_d->brushesPipe.parasite().needsMovement || info.drawingDistance() >= 0.5); } KisBrush* KisImagePipeBrush::clone() const { return new KisImagePipeBrush(*this); } QString KisImagePipeBrush::defaultFileExtension() const { return QString(".gih"); } quint32 KisImagePipeBrush::brushIndex(const KisPaintInformation& info) const { return m_d->brushesPipe.brushIndex(info); } qint32 KisImagePipeBrush::maskWidth(KisDabShape const& shape, double subPixelX, double subPixelY, const KisPaintInformation& info) const { return m_d->brushesPipe.maskWidth(shape, subPixelX, subPixelY, info); } qint32 KisImagePipeBrush::maskHeight(KisDabShape const& shape, double subPixelX, double subPixelY, const KisPaintInformation& info) const { return m_d->brushesPipe.maskHeight(shape, subPixelX, subPixelY, info); } void KisImagePipeBrush::setAngle(qreal _angle) { KisGbrBrush::setAngle(_angle); m_d->brushesPipe.setAngle(_angle); } void KisImagePipeBrush::setScale(qreal _scale) { KisGbrBrush::setScale(_scale); m_d->brushesPipe.setScale(_scale); } void KisImagePipeBrush::setSpacing(double _spacing) { KisGbrBrush::setSpacing(_spacing); m_d->brushesPipe.setSpacing(_spacing); } void KisImagePipeBrush::setBrushType(enumBrushType type) { Q_UNUSED(type); qFatal("FATAL: protected member setBrushType has no meaning for KisImagePipeBrush"); // brushType() is a function of hasColor() and useColorAsMask() } void KisImagePipeBrush::setHasColor(bool hasColor) { Q_UNUSED(hasColor); qFatal("FATAL: protected member setHasColor has no meaning for KisImagePipeBrush"); // hasColor() is a function of the underlying brushes } KisGbrBrush* KisImagePipeBrush::testingGetCurrentBrush(const KisPaintInformation& info) const { return m_d->brushesPipe.currentBrush(info); } void KisImagePipeBrush::testingSelectNextBrush(const KisPaintInformation& info) const { return m_d->brushesPipe.testingSelectNextBrush(info); } const KisPipeBrushParasite& KisImagePipeBrush::parasite() const { return m_d->brushesPipe.parasite(); } void KisImagePipeBrush::setParasite(const KisPipeBrushParasite ¶site) { m_d->brushesPipe.setParasite(parasite); } void KisImagePipeBrush::setDevices(QVector > devices, int w, int h) { for (int i = 0; i < devices.at(0).count(); i++) { m_d->brushesPipe.addBrush(new KisGbrBrush(devices.at(0).at(i), 0, 0, w, h)); } } diff --git a/libs/brush/kis_imagepipe_brush.h b/libs/brush/kis_imagepipe_brush.h index 9679fdf4f6..59141d8094 100644 --- a/libs/brush/kis_imagepipe_brush.h +++ b/libs/brush/kis_imagepipe_brush.h @@ -1,142 +1,143 @@ /* * Copyright (c) 2004 Boudewijn Rempt * Copyright (c) 2005 Bart Coppens * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_IMAGEPIPE_BRUSH_ #define KIS_IMAGEPIPE_BRUSH_ #include #include #include #include #include "kis_gbr_brush.h" #include "kis_global.h" class KisPipeBrushParasite; /** * Velocity won't be supported, atm Tilt isn't either, * but have chances of implementation */ namespace KisParasite { enum SelectionMode { Constant, Incremental, Angular, Velocity, Random, Pressure, TiltX, TiltY }; } class BRUSH_EXPORT KisImagePipeBrush : public KisGbrBrush { public: KisImagePipeBrush(const QString& filename); /** * Specialized constructor that makes a new pipe brush from a sequence of samesize * devices. The fact that it's a vector of a vector, is to support multidimensional * brushes (not yet supported!) */ KisImagePipeBrush(const QString& name, int w, int h, QVector< QVector > devices, QVector modes); ~KisImagePipeBrush() override; bool load() override; bool loadFromDevice(QIODevice *dev) override; bool save() override; bool saveToDevice(QIODevice* dev) const override; /** * @return the next image in the pipe. */ KisFixedPaintDeviceSP paintDevice(const KoColorSpace * colorSpace, KisDabShape const&, const KisPaintInformation& info, double subPixelX = 0, double subPixelY = 0) const override; void setUseColorAsMask(bool useColorAsMask) override; bool hasColor() const override; enumBrushType brushType() const override; const KisBoundary* boundary() const override; bool canPaintFor(const KisPaintInformation& info) override; void makeMaskImage() override; KisBrush* clone() const override; QString defaultFileExtension() const override; void setAngle(qreal _angle) override; void setScale(qreal _scale) override; void setSpacing(double _spacing) override; quint32 brushIndex(const KisPaintInformation& info) const override; qint32 maskWidth(KisDabShape const&, double subPixelX, double subPixelY, const KisPaintInformation& info) const override; qint32 maskHeight(KisDabShape const&, double subPixelX, double subPixelY, const KisPaintInformation& info) const override; void notifyStrokeStarted() override; void notifyCachedDabPainted(const KisPaintInformation& info) override; + void prepareForSeqNo(const KisPaintInformation& info, int seqNo) override; void generateMaskAndApplyMaskOrCreateDab(KisFixedPaintDeviceSP dst, KisBrush::ColoringInformation* coloringInformation, KisDabShape const&, const KisPaintInformation& info, double subPixelX = 0, double subPixelY = 0, qreal softnessFactor = DEFAULT_SOFTNESS_FACTOR) const override; QVector brushes() const; const KisPipeBrushParasite ¶site() const; void setParasite(const KisPipeBrushParasite& parasite); void setDevices(QVector< QVector > devices, int w, int h); protected: void setBrushType(enumBrushType type) override; void setHasColor(bool hasColor) override; /// Will call KisBrush's saveToDevice as well KisImagePipeBrush(const KisImagePipeBrush& rhs); private: friend class KisImagePipeBrushTest; KisGbrBrush* testingGetCurrentBrush(const KisPaintInformation& info) const; void testingSelectNextBrush(const KisPaintInformation& info) const; bool initFromData(const QByteArray &data); private: struct Private; Private * const m_d; }; #endif // KIS_IMAGEPIPE_BRUSH_ diff --git a/libs/brush/kis_text_brush.cpp b/libs/brush/kis_text_brush.cpp index f39e5c3e24..6a73c97f01 100644 --- a/libs/brush/kis_text_brush.cpp +++ b/libs/brush/kis_text_brush.cpp @@ -1,310 +1,334 @@ /* * Copyright (c) 2004 Cyrille Berger * Copyright (c) 2011 Lukáš Tvrdý * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_text_brush.h" #include #include #include #include #include "kis_gbr_brush.h" #include "kis_brushes_pipe.h" #include #include #ifdef HAVE_THREADED_TEXT_RENDERING_WORKAROUND #include #include #include #endif /* HAVE_THREADED_TEXT_RENDERING_WORKAROUND */ class KisTextBrushesPipe : public KisBrushesPipe { public: KisTextBrushesPipe() { m_charIndex = 0; m_currentBrushIndex = 0; } KisTextBrushesPipe(const KisTextBrushesPipe &rhs) - : KisBrushesPipe(rhs) { + : KisBrushesPipe(), // no copy here! + m_text(rhs.m_text), + m_charIndex(rhs.m_charIndex), + m_currentBrushIndex(rhs.m_currentBrushIndex) + { m_brushesMap.clear(); QMapIterator iter(rhs.m_brushesMap); while (iter.hasNext()) { iter.next(); - m_brushesMap.insert(iter.key(), iter.value()); + KisGbrBrush *brush = new KisGbrBrush(*iter.value()); + m_brushesMap.insert(iter.key(), brush); + KisBrushesPipe::addBrush(brush); } } void setText(const QString &text, const QFont &font) { m_text = text; + m_charIndex = 0; clear(); for (int i = 0; i < m_text.length(); i++) { - QChar letter = m_text.at(i); + + const QChar letter = m_text.at(i); + + // skip letters that are already present in the brushes pipe + if (m_brushesMap.contains(letter)) continue; + QImage image = renderChar(letter, font); KisGbrBrush *brush = new KisGbrBrush(image, letter); brush->setSpacing(0.1); // support for letter spacing? brush->makeMaskImage(); m_brushesMap.insert(letter, brush); KisBrushesPipe::addBrush(brush); } } static QImage renderChar(const QString& text, const QFont &font) { #ifdef HAVE_THREADED_TEXT_RENDERING_WORKAROUND QWidget *focusWidget = qApp->focusWidget(); if (focusWidget) { QThread *guiThread = focusWidget->thread(); if (guiThread != QThread::currentThread()) { warnKrita << "WARNING: Rendering text in non-GUI thread!" << "That may lead to hangups and crashes on some" << "versions of X11/Qt!"; } } #endif /* HAVE_THREADED_TEXT_RENDERING_WORKAROUND */ QFontMetrics metric(font); QRect rect = metric.boundingRect(text); if (rect.isEmpty()) { rect = QRect(0, 0, 1, 1); // paint at least something } QRect paintingRect = rect.translated(-rect.x(), -rect.y()); QImage renderedChar(paintingRect.size(), QImage::Format_ARGB32); QPainter p; p.begin(&renderedChar); p.setFont(font); p.fillRect(paintingRect, Qt::white); p.setPen(Qt::black); p.drawText(-rect.x(), -rect.y(), text); p.end(); return renderedChar; } void clear() override { m_brushesMap.clear(); KisBrushesPipe::clear(); } KisGbrBrush* firstBrush() const { Q_ASSERT(m_text.size() > 0); Q_ASSERT(m_brushesMap.size() > 0); return m_brushesMap.value(m_text.at(0)); } void notifyStrokeStarted() override { m_charIndex = 0; updateBrushIndexesImpl(); } protected: int chooseNextBrush(const KisPaintInformation& info) override { Q_UNUSED(info); return m_currentBrushIndex; } - void updateBrushIndexes(const KisPaintInformation& info) override { + void updateBrushIndexes(const KisPaintInformation& info, int seqNo) override { Q_UNUSED(info); - m_charIndex++; + if (m_text.size()) { + m_charIndex = (seqNo >= 0 ? seqNo : (m_charIndex + 1)) % m_text.size(); + } else { + m_charIndex = 0; + } + updateBrushIndexesImpl(); } private: void updateBrushIndexesImpl() { if (m_text.isEmpty()) return; if (m_charIndex >= m_text.size()) { m_charIndex = 0; } QChar letter = m_text.at(m_charIndex); Q_ASSERT(m_brushesMap.contains(letter)); m_currentBrushIndex = m_brushes.indexOf(m_brushesMap.value(letter)); } private: QMap m_brushesMap; QString m_text; int m_charIndex; int m_currentBrushIndex; }; KisTextBrush::KisTextBrush() : m_brushesPipe(new KisTextBrushesPipe()) { setPipeMode(false); } KisTextBrush::KisTextBrush(const KisTextBrush &rhs) : KisScalingSizeBrush(rhs), + m_font(rhs.m_font), + m_text(rhs.m_text), m_brushesPipe(new KisTextBrushesPipe(*rhs.m_brushesPipe)) { } KisTextBrush::~KisTextBrush() { delete m_brushesPipe; } void KisTextBrush::setPipeMode(bool pipe) { setBrushType(pipe ? PIPE_MASK : MASK); } bool KisTextBrush::pipeMode() const { return brushType() == PIPE_MASK; } void KisTextBrush::setText(const QString& txt) { m_text = txt; } QString KisTextBrush::text(void) const { return m_text; } void KisTextBrush::setFont(const QFont& font) { m_font = font; } QFont KisTextBrush::font() { return m_font; } void KisTextBrush::notifyStrokeStarted() { m_brushesPipe->notifyStrokeStarted(); } void KisTextBrush::notifyCachedDabPainted(const KisPaintInformation& info) { m_brushesPipe->notifyCachedDabPainted(info); } +void KisTextBrush::prepareForSeqNo(const KisPaintInformation &info, int seqNo) +{ + m_brushesPipe->prepareForSeqNo(info, seqNo); +} + void KisTextBrush::generateMaskAndApplyMaskOrCreateDab( KisFixedPaintDeviceSP dst, KisBrush::ColoringInformation* coloringInformation, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX, double subPixelY, qreal softnessFactor) const { if (brushType() == MASK) { KisBrush::generateMaskAndApplyMaskOrCreateDab(dst, coloringInformation, shape, info, subPixelX, subPixelY, softnessFactor); } else { /* if (brushType() == PIPE_MASK)*/ m_brushesPipe->generateMaskAndApplyMaskOrCreateDab(dst, coloringInformation, shape, info, subPixelX, subPixelY, softnessFactor); } } KisFixedPaintDeviceSP KisTextBrush::paintDevice(const KoColorSpace * colorSpace, KisDabShape const& shape, const KisPaintInformation& info, double subPixelX, double subPixelY) const { if (brushType() == MASK) { return KisBrush::paintDevice(colorSpace, shape, info, subPixelX, subPixelY); } else { /* if (brushType() == PIPE_MASK)*/ return m_brushesPipe->paintDevice(colorSpace, shape, info, subPixelX, subPixelY); } } void KisTextBrush::toXML(QDomDocument& doc, QDomElement& e) const { Q_UNUSED(doc); e.setAttribute("type", "kis_text_brush"); e.setAttribute("spacing", KisDomUtils::toString(spacing())); e.setAttribute("text", m_text); e.setAttribute("font", m_font.toString()); e.setAttribute("pipe", (brushType() == PIPE_MASK) ? "true" : "false"); KisBrush::toXML(doc, e); } void KisTextBrush::updateBrush() { Q_ASSERT((brushType() == PIPE_MASK) || (brushType() == MASK)); if (brushType() == PIPE_MASK) { m_brushesPipe->setText(m_text, m_font); setBrushTipImage(m_brushesPipe->firstBrush()->brushTipImage()); } else { /* if (brushType() == MASK)*/ setBrushTipImage(KisTextBrushesPipe::renderChar(m_text, m_font)); } resetBoundary(); setValid(true); } quint32 KisTextBrush::brushIndex(const KisPaintInformation& info) const { return brushType() == MASK ? 0 : 1 + m_brushesPipe->brushIndex(info); } qint32 KisTextBrush::maskWidth(KisDabShape const& shape, double subPixelX, double subPixelY, const KisPaintInformation& info) const { return brushType() == MASK ? KisBrush::maskWidth(shape, subPixelX, subPixelY, info) : m_brushesPipe->maskWidth(shape, subPixelX, subPixelY, info); } qint32 KisTextBrush::maskHeight(KisDabShape const& shape, double subPixelX, double subPixelY, const KisPaintInformation& info) const { return brushType() == MASK ? KisBrush::maskHeight(shape, subPixelX, subPixelY, info) : m_brushesPipe->maskHeight(shape, subPixelX, subPixelY, info); } void KisTextBrush::setAngle(qreal _angle) { KisBrush::setAngle(_angle); m_brushesPipe->setAngle(_angle); } void KisTextBrush::setScale(qreal _scale) { KisBrush::setScale(_scale); m_brushesPipe->setScale(_scale); } void KisTextBrush::setSpacing(double _spacing) { KisBrush::setSpacing(_spacing); m_brushesPipe->setSpacing(_spacing); } KisBrush* KisTextBrush::clone() const { return new KisTextBrush(*this); } diff --git a/libs/brush/kis_text_brush.h b/libs/brush/kis_text_brush.h index 4c13b17b89..01dfed2854 100644 --- a/libs/brush/kis_text_brush.h +++ b/libs/brush/kis_text_brush.h @@ -1,95 +1,96 @@ /* * Copyright (c) 2004 Cyrille Berger * Copyright (c) 2011 Lukáš Tvrdý * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_TEXT_BRUSH_H_ #define _KIS_TEXT_BRUSH_H_ #include #include "kis_scaling_size_brush.h" #include "kritabrush_export.h" class KisTextBrushesPipe; class BRUSH_EXPORT KisTextBrush : public KisScalingSizeBrush { public: KisTextBrush(); KisTextBrush(const KisTextBrush &rhs); ~KisTextBrush() override; void notifyStrokeStarted() override; void notifyCachedDabPainted(const KisPaintInformation& info) override; + void prepareForSeqNo(const KisPaintInformation& info, int seqNo) override; void generateMaskAndApplyMaskOrCreateDab(KisFixedPaintDeviceSP dst, KisBrush::ColoringInformation* coloringInformation, KisDabShape const&, const KisPaintInformation& info, double subPixelX = 0, double subPixelY = 0, qreal softnessFactor = DEFAULT_SOFTNESS_FACTOR) const override; KisFixedPaintDeviceSP paintDevice(const KoColorSpace * colorSpace, KisDabShape const&, const KisPaintInformation& info, double subPixelX, double subPixelY) const override; bool load() override { return false; } bool loadFromDevice(QIODevice *) override { return false; } bool save() override { return false; } bool saveToDevice(QIODevice* ) const override { return false; } void setText(const QString& txt); QString text(void) const; QFont font(); void setFont(const QFont& font); void setPipeMode(bool pipe); bool pipeMode() const; void updateBrush(); void toXML(QDomDocument& , QDomElement&) const override; quint32 brushIndex(const KisPaintInformation& info) const override; qint32 maskWidth(KisDabShape const&, double subPixelX, double subPixelY, const KisPaintInformation& info) const override; qint32 maskHeight(KisDabShape const&, double subPixelX, double subPixelY, const KisPaintInformation& info) const override; void setAngle(qreal _angle) override; void setScale(qreal _scale) override; void setSpacing(double _spacing) override; KisBrush* clone() const override; private: QFont m_font; QString m_text; private: KisTextBrushesPipe *m_brushesPipe; }; #endif diff --git a/libs/brush/tests/kis_gbr_brush_test.cpp b/libs/brush/tests/kis_gbr_brush_test.cpp index c027e9dc88..e5994faef5 100644 --- a/libs/brush/tests/kis_gbr_brush_test.cpp +++ b/libs/brush/tests/kis_gbr_brush_test.cpp @@ -1,361 +1,297 @@ /* * Copyright (c) 2007 Boudewijn Rempt boud@valdyas.org * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_gbr_brush_test.h" #include #include #include #include #include #include #include "testutil.h" #include "../kis_gbr_brush.h" #include "kis_types.h" #include "kis_paint_device.h" #include "brushengine/kis_paint_information.h" #include #include "kis_qimage_pyramid.h" -void KisGbrBrushTest::testMaskGenerationNoColor() -{ - KisGbrBrush* brush = new KisGbrBrush(QString(FILES_DATA_DIR) + QDir::separator() + "brush.gbr"); - brush->load(); - Q_ASSERT(brush->valid()); - const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); - - KisPaintInformation info(QPointF(100.0, 100.0), 0.5); - - // check masking an existing paint device - KisFixedPaintDeviceSP fdev = new KisFixedPaintDevice(cs); - fdev->setRect(QRect(0, 0, 100, 100)); - fdev->initialize(); - cs->setOpacity(fdev->data(), OPACITY_OPAQUE_U8, 100 * 100); - - QPoint errpoint; - QImage result(QString(FILES_DATA_DIR) + QDir::separator() + "result_brush_1.png"); - QImage image = fdev->convertToQImage(0); - - if (!TestUtil::compareQImages(errpoint, image, result)) { - image.save("kis_gbr_brush_test_1.png"); - QFAIL(QString("Failed to create identical image, first different pixel: %1,%2 \n").arg(errpoint.x()).arg(errpoint.y()).toLatin1()); - } - - brush->mask(fdev, KisDabShape(), info); - - result = QImage(QString(FILES_DATA_DIR) + QDir::separator() + "result_brush_2.png"); - image = fdev->convertToQImage(0); - if (!TestUtil::compareQImages(errpoint, image, result)) { - image.save("kis_gbr_brush_test_2.png"); - QFAIL(QString("Failed to create identical image, first different pixel: %1,%2 \n").arg(errpoint.x()).arg(errpoint.y()).toLatin1()); - } -} void KisGbrBrushTest::testMaskGenerationSingleColor() { KisGbrBrush* brush = new KisGbrBrush(QString(FILES_DATA_DIR) + QDir::separator() + "brush.gbr"); brush->load(); Q_ASSERT(brush->valid()); const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); KisPaintInformation info(QPointF(100.0, 100.0), 0.5); // check masking an existing paint device KisFixedPaintDeviceSP fdev = new KisFixedPaintDevice(cs); fdev->setRect(QRect(0, 0, 100, 100)); fdev->initialize(); cs->setOpacity(fdev->data(), OPACITY_OPAQUE_U8, 100 * 100); // Check creating a mask dab with a single color fdev = new KisFixedPaintDevice(cs); brush->mask(fdev, KoColor(Qt::black, cs), KisDabShape(), info); QPoint errpoint; QImage result = QImage(QString(FILES_DATA_DIR) + QDir::separator() + "result_brush_3.png"); QImage image = fdev->convertToQImage(0); if (!TestUtil::compareQImages(errpoint, image, result)) { image.save("kis_gbr_brush_test_3.png"); QFAIL(QString("Failed to create identical image, first different pixel: %1,%2 \n").arg(errpoint.x()).arg(errpoint.y()).toLatin1()); } } void KisGbrBrushTest::testMaskGenerationDevColor() { KisGbrBrush* brush = new KisGbrBrush(QString(FILES_DATA_DIR) + QDir::separator() + "brush.gbr"); brush->load(); Q_ASSERT(brush->valid()); const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); KisPaintInformation info(QPointF(100.0, 100.0), 0.5); // check masking an existing paint device KisFixedPaintDeviceSP fdev = new KisFixedPaintDevice(cs); fdev->setRect(QRect(0, 0, 100, 100)); fdev->initialize(); cs->setOpacity(fdev->data(), OPACITY_OPAQUE_U8, 100 * 100); // Check creating a mask dab with a color taken from a paint device KoColor red(Qt::red, cs); cs->setOpacity(red.data(), quint8(128), 1); KisPaintDeviceSP dev = new KisPaintDevice(cs); dev->fill(0, 0, 100, 100, red.data()); fdev = new KisFixedPaintDevice(cs); brush->mask(fdev, dev, KisDabShape(), info); QPoint errpoint; QImage result = QImage(QString(FILES_DATA_DIR) + QDir::separator() + "result_brush_4.png"); QImage image = fdev->convertToQImage(0); if (!TestUtil::compareQImages(errpoint, image, result)) { image.save("kis_gbr_brush_test_4.png"); QFAIL(QString("Failed to create identical image, first different pixel: %1,%2 \n").arg(errpoint.x()).arg(errpoint.y()).toLatin1()); } } -void KisGbrBrushTest::testMaskGenerationDefaultColor() -{ - KisGbrBrush* brush = new KisGbrBrush(QString(FILES_DATA_DIR) + QDir::separator() + "brush.gbr"); - brush->load(); - Q_ASSERT(brush->valid()); - const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); - - KisPaintInformation info(QPointF(100.0, 100.0), 0.5); - - // check masking an existing paint device - KisFixedPaintDeviceSP fdev = new KisFixedPaintDevice(cs); - fdev->setRect(QRect(0, 0, 100, 100)); - fdev->initialize(); - cs->setOpacity(fdev->data(), OPACITY_OPAQUE_U8, 100 * 100); - - // check creating a mask dab with a default color - fdev = new KisFixedPaintDevice(cs); - brush->mask(fdev, KisDabShape(), info); - - QPoint errpoint; - QImage result = QImage(QString(FILES_DATA_DIR) + QDir::separator() + "result_brush_3.png"); - QImage image = fdev->convertToQImage(0); - if (!TestUtil::compareQImages(errpoint, image, result)) { - image.save("kis_gbr_brush_test_5.png"); - QFAIL(QString("Failed to create identical image, first different pixel: %1,%2 \n").arg(errpoint.x()).arg(errpoint.y()).toLatin1()); - } - - delete brush; -} - - void KisGbrBrushTest::testImageGeneration() { KisGbrBrush* brush = new KisGbrBrush(QString(FILES_DATA_DIR) + QDir::separator() + "testing_brush_512_bars.gbr"); bool res = brush->load(); Q_UNUSED(res); Q_ASSERT(res); QVERIFY(!brush->brushTipImage().isNull()); brush->prepareBrushPyramid(); qsrand(1); const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); KisPaintInformation info(QPointF(100.0, 100.0), 0.5); KisFixedPaintDeviceSP dab; for (int i = 0; i < 200; i++) { qreal scale = qreal(qrand()) / RAND_MAX * 2.0; qreal rotation = qreal(qrand()) / RAND_MAX * 2 * M_PI; qreal subPixelX = qreal(qrand()) / RAND_MAX * 0.5; QString testName = QString("brush_%1_sc_%2_rot_%3_sub_%4") .arg(i).arg(scale).arg(rotation).arg(subPixelX); dab = brush->paintDevice(cs, KisDabShape(scale, 1.0, rotation), info, subPixelX); /** * Compare first 10 images. Others are tested for asserts only */ if (i < 10) { QImage result = dab->convertToQImage(0); TestUtil::checkQImage(result, "brush_masks", "", testName); } } } void KisGbrBrushTest::benchmarkPyramidCreation() { KisGbrBrush* brush = new KisGbrBrush(QString(FILES_DATA_DIR) + QDir::separator() + "testing_brush_512_bars.gbr"); brush->load(); QVERIFY(!brush->brushTipImage().isNull()); QBENCHMARK { brush->prepareBrushPyramid(); brush->clearBrushPyramid(); } } void KisGbrBrushTest::benchmarkScaling() { KisGbrBrush* brush = new KisGbrBrush(QString(FILES_DATA_DIR) + QDir::separator() + "testing_brush_512_bars.gbr"); brush->load(); QVERIFY(!brush->brushTipImage().isNull()); brush->prepareBrushPyramid(); qsrand(1); const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); KisPaintInformation info(QPointF(100.0, 100.0), 0.5); KisFixedPaintDeviceSP dab; QBENCHMARK { dab = brush->paintDevice(cs, KisDabShape(qreal(qrand()) / RAND_MAX * 2.0, 1.0, 0.0), info); //dab->convertToQImage(0).save(QString("dab_%1_new_smooth.png").arg(i++)); } } void KisGbrBrushTest::benchmarkRotation() { KisGbrBrush* brush = new KisGbrBrush(QString(FILES_DATA_DIR) + QDir::separator() + "testing_brush_512_bars.gbr"); brush->load(); QVERIFY(!brush->brushTipImage().isNull()); brush->prepareBrushPyramid(); qsrand(1); const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); KisPaintInformation info(QPointF(100.0, 100.0), 0.5); KisFixedPaintDeviceSP dab; QBENCHMARK { dab = brush->paintDevice(cs, KisDabShape(1.0, 1.0, qreal(qrand()) / RAND_MAX * 2 * M_PI), info); } } void KisGbrBrushTest::benchmarkMaskScaling() { KisGbrBrush* brush = new KisGbrBrush(QString(FILES_DATA_DIR) + QDir::separator() + "testing_brush_512_bars.gbr"); brush->load(); QVERIFY(!brush->brushTipImage().isNull()); brush->prepareBrushPyramid(); qsrand(1); const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); KisPaintInformation info(QPointF(100.0, 100.0), 0.5); KisFixedPaintDeviceSP dab = new KisFixedPaintDevice(cs); QBENCHMARK { KoColor c(Qt::black, cs); qreal scale = qreal(qrand()) / RAND_MAX * 2.0; brush->mask(dab, c, KisDabShape(scale, 1.0, 0.0), info, 0.0, 0.0, 1.0); } } void KisGbrBrushTest::testPyramidLevelRounding() { QSize imageSize(41, 41); QImage image(imageSize, QImage::Format_ARGB32); image.fill(0); KisQImagePyramid pyramid(image); qreal baseScale; int baseLevel; baseLevel = pyramid.findNearestLevel(1.0, &baseScale); QCOMPARE(baseScale, 1.0); QCOMPARE(baseLevel, 3); baseLevel = pyramid.findNearestLevel(2.0, &baseScale); QCOMPARE(baseScale, 2.0); QCOMPARE(baseLevel, 2); baseLevel = pyramid.findNearestLevel(4.0, &baseScale); QCOMPARE(baseScale, 4.0); QCOMPARE(baseLevel, 1); baseLevel = pyramid.findNearestLevel(0.5, &baseScale); QCOMPARE(baseScale, 0.5); QCOMPARE(baseLevel, 4); baseLevel = pyramid.findNearestLevel(0.25, &baseScale); QCOMPARE(baseScale, 0.25); QCOMPARE(baseLevel, 5); baseLevel = pyramid.findNearestLevel(0.25 + 1e-7, &baseScale); QCOMPARE(baseScale, 0.25); QCOMPARE(baseLevel, 5); } static QSize dabTransformHelper(KisDabShape const& shape) { QSize const testSize(150, 150); qreal const subPixelX = 0.0, subPixelY = 0.0; return KisQImagePyramid::imageSize(testSize, shape, subPixelX, subPixelY); } void KisGbrBrushTest::testPyramidDabTransform() { QCOMPARE(dabTransformHelper(KisDabShape(1.0, 1.0, 0.0)), QSize(150, 150)); QCOMPARE(dabTransformHelper(KisDabShape(1.0, 0.5, 0.0)), QSize(150, 75)); QCOMPARE(dabTransformHelper(KisDabShape(1.0, 1.0, M_PI / 4)), QSize(213, 213)); QCOMPARE(dabTransformHelper(KisDabShape(1.0, 0.5, M_PI / 4)), QSize(160, 160)); } // see comment in KisQImagePyramid::appendPyramidLevel void KisGbrBrushTest::testQPainterTransformationBorder() { QImage image1(10, 10, QImage::Format_ARGB32); QImage image2(12, 12, QImage::Format_ARGB32); image1.fill(0); image2.fill(0); { QPainter gc(&image1); gc.fillRect(QRect(0, 0, 10, 10), Qt::black); } { QPainter gc(&image2); gc.fillRect(QRect(1, 1, 10, 10), Qt::black); } image1.save("src1.png"); image2.save("src2.png"); { QImage canvas(100, 100, QImage::Format_ARGB32); canvas.fill(0); QPainter gc(&canvas); QTransform transform; transform.rotate(15); gc.setTransform(transform); gc.setRenderHints(QPainter::SmoothPixmapTransform); gc.drawImage(QPointF(50, 50), image1); gc.end(); canvas.save("canvas1.png"); } { QImage canvas(100, 100, QImage::Format_ARGB32); canvas.fill(0); QPainter gc(&canvas); QTransform transform; transform.rotate(15); gc.setTransform(transform); gc.setRenderHints(QPainter::SmoothPixmapTransform); gc.drawImage(QPointF(50, 50), image2); gc.end(); canvas.save("canvas2.png"); } } QTEST_MAIN(KisGbrBrushTest) diff --git a/libs/brush/tests/kis_gbr_brush_test.h b/libs/brush/tests/kis_gbr_brush_test.h index 070a03b3b7..fa630e9a16 100644 --- a/libs/brush/tests/kis_gbr_brush_test.h +++ b/libs/brush/tests/kis_gbr_brush_test.h @@ -1,49 +1,47 @@ /* * Copyright (c) 2007 Boudewijn Rempt boud@valdyas.org * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_BRUSH_TEST_H #define KIS_BRUSH_TEST_H #include class KisGbrBrushTest : public QObject { Q_OBJECT // XXX disabled until I figure out why they don't work from here, while the brushes do work from Krita - void testMaskGenerationNoColor(); void testMaskGenerationSingleColor(); void testMaskGenerationDevColor(); - void testMaskGenerationDefaultColor(); private Q_SLOTS: void testImageGeneration(); void benchmarkPyramidCreation(); void benchmarkScaling(); void benchmarkRotation(); void benchmarkMaskScaling(); void testPyramidLevelRounding(); void testPyramidDabTransform(); void testQPainterTransformationBorder(); }; #endif diff --git a/libs/brush/tests/kis_imagepipe_brush_test.cpp b/libs/brush/tests/kis_imagepipe_brush_test.cpp index ea47af1816..5a54e81fb4 100644 --- a/libs/brush/tests/kis_imagepipe_brush_test.cpp +++ b/libs/brush/tests/kis_imagepipe_brush_test.cpp @@ -1,278 +1,277 @@ /* * Copyright (c) 2012 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_imagepipe_brush_test.h" #include #include #include #include #include #include #include #include #include "kis_imagepipe_brush.h" #include #include #define COMPARE_ALL(brush, method) \ Q_FOREACH (KisGbrBrush *child, brush->brushes()) { \ if(brush->method() != child->method()) { \ dbgKrita << "Failing method:" << #method \ << "brush index:" \ << brush->brushes().indexOf(child); \ QCOMPARE(brush->method(), child->method()); \ } \ } inline void KisImagePipeBrushTest::checkConsistency(KisImagePipeBrush *brush) { qreal scale = 0.5; Q_UNUSED(scale); KisGbrBrush *firstBrush = brush->brushes().first(); /** * This set of values is supposed to be constant, so * it is just set to the corresponding values of the * first brush */ QCOMPARE(brush->width(), firstBrush->width()); QCOMPARE(brush->height(), firstBrush->height()); QCOMPARE(brush->boundary(), firstBrush->boundary()); /** * These values should be spread over the children brushes */ COMPARE_ALL(brush, maskAngle); COMPARE_ALL(brush, scale); COMPARE_ALL(brush, angle); COMPARE_ALL(brush, spacing); /** * Check mask size values, they depend on current brush */ KisPaintInformation info; KisBrush *oldBrush = brush->testingGetCurrentBrush(info); QVERIFY(oldBrush); qreal realScale = 1; qreal realAngle = 0; qreal subPixelX = 0; qreal subPixelY = 0; int maskWidth = brush->maskWidth(KisDabShape(realScale, 1.0, realAngle), subPixelX, subPixelY, info); int maskHeight = brush->maskHeight(KisDabShape(realScale, 1.0, realAngle), subPixelX, subPixelY, info); const KoColorSpace *cs = KoColorSpaceRegistry::instance()->rgb8(); KisFixedPaintDeviceSP dev = brush->testingGetCurrentBrush(info)->paintDevice( cs, KisDabShape(realScale, 1.0, realAngle), info, subPixelX, subPixelY); QCOMPARE(maskWidth, dev->bounds().width()); QCOMPARE(maskHeight, dev->bounds().height()); KisBrush *newBrush = brush->testingGetCurrentBrush(info); QCOMPARE(oldBrush, newBrush); } void KisImagePipeBrushTest::testLoading() { KisImagePipeBrush *brush = new KisImagePipeBrush(QString(FILES_DATA_DIR) + QDir::separator() + "C_Dirty_Spot.gih"); brush->load(); QVERIFY(brush->valid()); checkConsistency(brush); delete brush; } void KisImagePipeBrushTest::testChangingBrushes() { KisImagePipeBrush *brush = new KisImagePipeBrush(QString(FILES_DATA_DIR) + QDir::separator() + "C_Dirty_Spot.gih"); brush->load(); QVERIFY(brush->valid()); qreal rotation = 0; KisPaintInformation info(QPointF(100.0, 100.0), 0.5, 0, 0, rotation); for (int i = 0; i < 100; i++) { checkConsistency(brush); brush->testingSelectNextBrush(info); } delete brush; } void checkIncrementalPainting(KisBrush *brush, const QString &prefix) { qreal realScale = 1; qreal realAngle = 0; const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); KoColor fillColor(Qt::red, cs); KisFixedPaintDeviceSP fixedDab = new KisFixedPaintDevice(cs); qreal rotation = 0; qreal subPixelX = 0.0; qreal subPixelY = 0.0; KisPaintInformation info(QPointF(100.0, 100.0), 0.5, 0, 0, rotation); for (int i = 0; i < 20; i++) { int maskWidth = brush->maskWidth(KisDabShape(realScale, 1.0, realAngle), subPixelX, subPixelY, info); int maskHeight = brush->maskHeight(KisDabShape(realScale, 1.0, realAngle), subPixelX, subPixelY, info); QRect fillRect(0, 0, maskWidth, maskHeight); fixedDab->setRect(fillRect); - fixedDab->initialize(); - fixedDab->fill(fillRect.x(), fillRect.y(), fillRect.width(), fillRect.height(), fillColor.data()); + fixedDab->lazyGrowBufferWithoutInitialization(); - brush->mask(fixedDab, KisDabShape(realScale, 1.0, realAngle), info); + brush->mask(fixedDab, fillColor, KisDabShape(realScale, 1.0, realAngle), info); QCOMPARE(fixedDab->bounds(), fillRect); QImage result = fixedDab->convertToQImage(0); result.save(QString("fixed_dab_%1_%2.png").arg(prefix).arg(i)); } } void KisImagePipeBrushTest::testSimpleDabApplication() { KisImagePipeBrush *brush = new KisImagePipeBrush(QString(FILES_DATA_DIR) + QDir::separator() + "C_Dirty_Spot.gih"); brush->load(); QVERIFY(brush->valid()); checkConsistency(brush); checkIncrementalPainting(brush, "simple"); delete brush; } void KisImagePipeBrushTest::testColoredDab() { KisImagePipeBrush *brush = new KisImagePipeBrush(QString(FILES_DATA_DIR) + QDir::separator() + "G_Sparks.gih"); brush->load(); QVERIFY(brush->valid()); checkConsistency(brush); QCOMPARE(brush->useColorAsMask(), false); QCOMPARE(brush->hasColor(), true); QCOMPARE(brush->brushType(), PIPE_IMAGE); // let it be the mask (should be revertible) brush->setUseColorAsMask(true); QCOMPARE(brush->useColorAsMask(), true); QCOMPARE(brush->hasColor(), true); QCOMPARE(brush->brushType(), PIPE_MASK); // revert back brush->setUseColorAsMask(false); QCOMPARE(brush->useColorAsMask(), false); QCOMPARE(brush->hasColor(), true); QCOMPARE(brush->brushType(), PIPE_IMAGE); // convert to the mask (irreversible) brush->makeMaskImage(); QCOMPARE(brush->useColorAsMask(), false); QCOMPARE(brush->hasColor(), false); QCOMPARE(brush->brushType(), PIPE_MASK); checkConsistency(brush); delete brush; } void KisImagePipeBrushTest::testColoredDabWash() { KisImagePipeBrush *brush = new KisImagePipeBrush(QString(FILES_DATA_DIR) + QDir::separator() + "G_Sparks.gih"); brush->load(); QVERIFY(brush->valid()); const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); qreal rotation = 0; KisPaintInformation info(QPointF(100.0, 100.0), 0.5, 0, 0, rotation); KisPaintDeviceSP layer = new KisPaintDevice(cs); KisPainter painter(layer); painter.setCompositeOp(COMPOSITE_ALPHA_DARKEN); const QVector gbrs = brush->brushes(); KisFixedPaintDeviceSP dab = gbrs.at(0)->paintDevice(cs, KisDabShape(2.0, 1.0, 0.0), info); painter.bltFixed(0, 0, dab, 0, 0, dab->bounds().width(), dab->bounds().height()); painter.bltFixed(80, 60, dab, 0, 0, dab->bounds().width(), dab->bounds().height()); painter.end(); QRect rc = layer->exactBounds(); QImage result = layer->convertToQImage(0, rc.x(), rc.y(), rc.width(), rc.height()); #if 0 // if you want to see the result on white background, set #if 1 QImage bg(result.size(), result.format()); bg.fill(Qt::white); QPainter qPainter(&bg); qPainter.drawImage(0, 0, result); result = bg; #endif result.save("z_spark_alpha_darken.png"); delete brush; } #include "kis_text_brush.h" void KisImagePipeBrushTest::testTextBrushNoPipes() { KisTextBrush *brush = new KisTextBrush(); brush->setPipeMode(false); brush->setFont(QApplication::font()); brush->setText("The_Quick_Brown_Fox_Jumps_Over_The_Lazy_Dog"); brush->updateBrush(); checkIncrementalPainting(brush, "text_no_incremental"); delete brush; } void KisImagePipeBrushTest::testTextBrushPiped() { KisTextBrush *brush = new KisTextBrush(); brush->setPipeMode(true); brush->setFont(QApplication::font()); brush->setText("The_Quick_Brown_Fox_Jumps_Over_The_Lazy_Dog"); brush->updateBrush(); checkIncrementalPainting(brush, "text_incremental"); delete brush; } QTEST_MAIN(KisImagePipeBrushTest) diff --git a/libs/global/CMakeLists.txt b/libs/global/CMakeLists.txt index f329f874c3..c5bf72bd8a 100644 --- a/libs/global/CMakeLists.txt +++ b/libs/global/CMakeLists.txt @@ -1,45 +1,50 @@ +add_subdirectory( tests ) + include(CheckFunctionExists) check_function_exists(backtrace HAVE_BACKTRACE) configure_file(config-debug.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-debug.h) option(HAVE_MEMORY_LEAK_TRACKER "Enable memory leak tracker (always disabled in release build)" OFF) option(HAVE_BACKTRACE_SUPPORT "Enable recording of backtrace in memory leak tracker" OFF) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config-memory-leak-tracker.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-memory-leak-tracker.h) ### WRONG PLACE??? set(kritaglobal_LIB_SRCS kis_assert.cpp kis_debug.cpp kis_algebra_2d.cpp kis_memory_leak_tracker.cpp kis_shared.cpp kis_dom_utils.cpp kis_painting_tweaks.cpp KisHandlePainterHelper.cpp KisHandleStyle.cpp kis_signal_compressor.cpp kis_signal_compressor_with_param.cpp kis_acyclic_signal_connector.cpp kis_latency_tracker.cpp KisQPainterStateSaver.cpp + KisSharedThreadPoolAdapter.cpp + KisSharedRunnable.cpp + KisRollingMeanAccumulatorWrapper.cpp KisLoggingManager.cpp ) add_library(kritaglobal SHARED ${kritaglobal_LIB_SRCS} ) generate_export_header(kritaglobal BASE_NAME kritaglobal) target_link_libraries(kritaglobal PUBLIC Qt5::Concurrent Qt5::Core Qt5::Gui Qt5::Widgets Qt5::Xml KF5::I18n ) set_target_properties(kritaglobal PROPERTIES VERSION ${GENERIC_KRITA_LIB_VERSION} SOVERSION ${GENERIC_KRITA_LIB_SOVERSION} ) install(TARGETS kritaglobal ${INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/libs/global/KisRollingMeanAccumulatorWrapper.cpp b/libs/global/KisRollingMeanAccumulatorWrapper.cpp new file mode 100644 index 0000000000..0ae502a78b --- /dev/null +++ b/libs/global/KisRollingMeanAccumulatorWrapper.cpp @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisRollingMeanAccumulatorWrapper.h" + +#include +#include +#include + +using namespace boost::accumulators; + +struct KisRollingMeanAccumulatorWrapper::Private { + Private(int windowSize) + : accumulator(tag::rolling_window::window_size = windowSize) + { + } + + accumulator_set > accumulator; +}; + + +KisRollingMeanAccumulatorWrapper::KisRollingMeanAccumulatorWrapper(int windowSize) + : m_d(new Private(windowSize)) +{ +} + +KisRollingMeanAccumulatorWrapper::~KisRollingMeanAccumulatorWrapper() +{ +} + +void KisRollingMeanAccumulatorWrapper::operator()(qreal value) +{ + m_d->accumulator(value); +} + +qreal KisRollingMeanAccumulatorWrapper::rollingMean() const +{ + return boost::accumulators::rolling_mean(m_d->accumulator); +} + +void KisRollingMeanAccumulatorWrapper::reset(int windowSize) +{ + m_d->accumulator = + accumulator_set>( + tag::rolling_window::window_size = windowSize); +} diff --git a/libs/global/KisRollingMeanAccumulatorWrapper.h b/libs/global/KisRollingMeanAccumulatorWrapper.h new file mode 100644 index 0000000000..0d696b565e --- /dev/null +++ b/libs/global/KisRollingMeanAccumulatorWrapper.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KISROLLINGMEANACCUMULATORWRAPPER_H +#define KISROLLINGMEANACCUMULATORWRAPPER_H + +#include +#include +#include "kritaglobal_export.h" + +/** + * @brief A simple wrapper class that hides boost includes from QtCreator preventing it + * from crashing when one adds boost's accumulator into a file + */ + +class KRITAGLOBAL_EXPORT KisRollingMeanAccumulatorWrapper +{ +public: + /** + * Create a rolling mean accumulator with window \p windowSize + */ + KisRollingMeanAccumulatorWrapper(int windowSize); + ~KisRollingMeanAccumulatorWrapper(); + + /** + * Add \p value to a set of numbers + */ + void operator()(qreal value); + + /** + * Get rolling mean of the numbers passed to the operator + */ + qreal rollingMean() const; + + /** + * Reset accumulator and any stored value + */ + void reset(int windowSize); + +private: + struct Private; + const QScopedPointer m_d; +}; + +#endif // KISROLLINGMEANACCUMULATORWRAPPER_H diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/global/KisSharedRunnable.cpp similarity index 63% copy from libs/image/kis_projection_updates_filter.cpp copy to libs/global/KisSharedRunnable.cpp index e1493ec367..f1874dfa72 100644 --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/global/KisSharedRunnable.cpp @@ -1,36 +1,37 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#include "KisSharedRunnable.h" +#include "KisSharedThreadPoolAdapter.h" +#include "kis_assert.h" -#include -#include - -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +void KisSharedRunnable::run() { + runShared(); + + if (m_adapter) { + m_adapter->notifyJobCompleted(); + } } -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) +void KisSharedRunnable::setSharedThreadPoolAdapter(KisSharedThreadPoolAdapter *adapter) { - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; + m_adapter = adapter; } + diff --git a/libs/ui/opengl/kis_opengl_canvas_debugger.h b/libs/global/KisSharedRunnable.h similarity index 58% copy from libs/ui/opengl/kis_opengl_canvas_debugger.h copy to libs/global/KisSharedRunnable.h index 8d7e605e96..50dbf7d9c5 100644 --- a/libs/ui/opengl/kis_opengl_canvas_debugger.h +++ b/libs/global/KisSharedRunnable.h @@ -1,45 +1,42 @@ /* - * Copyright (c) 2015 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_OPENGL_CANVAS_DEBUGGER_H -#define __KIS_OPENGL_CANVAS_DEBUGGER_H +#ifndef KISSHAREDRUNNABLE_H +#define KISSHAREDRUNNABLE_H -#include +#include +#include +class KisSharedThreadPoolAdapter; -class KisOpenglCanvasDebugger +class KRITAGLOBAL_EXPORT KisSharedRunnable : public QRunnable { public: - KisOpenglCanvasDebugger(); - ~KisOpenglCanvasDebugger(); + virtual void runShared() = 0; + void run() final; - static KisOpenglCanvasDebugger* instance(); - - bool showFpsOnCanvas() const; - - void nofityPaintRequested(); - void nofitySyncStatus(bool value); - qreal accumulatedFps(); +private: + friend class KisSharedThreadPoolAdapter; + void setSharedThreadPoolAdapter(KisSharedThreadPoolAdapter *adapter); private: - struct Private; - const QScopedPointer m_d; + KisSharedThreadPoolAdapter *m_adapter = 0; }; -#endif /* __KIS_OPENGL_CANVAS_DEBUGGER_H */ +#endif // KISSHAREDRUNNABLE_H diff --git a/libs/global/KisSharedThreadPoolAdapter.cpp b/libs/global/KisSharedThreadPoolAdapter.cpp new file mode 100644 index 0000000000..888274f106 --- /dev/null +++ b/libs/global/KisSharedThreadPoolAdapter.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisSharedThreadPoolAdapter.h" + +#include "kis_assert.h" +#include +#include + + +KisSharedThreadPoolAdapter::KisSharedThreadPoolAdapter(QThreadPool *parentPool) + : m_parentPool(parentPool), + m_numRunningJobs(0) +{ +} + +KisSharedThreadPoolAdapter::~KisSharedThreadPoolAdapter() +{ + waitForDone(); + KIS_SAFE_ASSERT_RECOVER_NOOP(!m_numRunningJobs); +} + +void KisSharedThreadPoolAdapter::start(KisSharedRunnable *runnable, int priority) +{ + QMutexLocker l(&m_mutex); + + runnable->setSharedThreadPoolAdapter(this); + m_parentPool->start(runnable, priority); + + m_numRunningJobs++; +} + +bool KisSharedThreadPoolAdapter::tryStart(KisSharedRunnable *runnable) +{ + QMutexLocker l(&m_mutex); + + runnable->setSharedThreadPoolAdapter(this); + const bool result = m_parentPool->tryStart(runnable); + + if (result) { + m_numRunningJobs++; + } + + return result; +} + +bool KisSharedThreadPoolAdapter::waitForDone(int msecs) +{ + QElapsedTimer t; + t.start(); + + while (1) { + QMutexLocker l(&m_mutex); + + if (!m_numRunningJobs) break; + + const qint64 elapsed = t.elapsed(); + if (msecs >= 0 && msecs < elapsed) return false; + + const unsigned long timeout = msecs < 0 ? ULONG_MAX : msecs - elapsed; + + m_waitCondition.wait(&m_mutex, timeout); + } + + return true; +} + +void KisSharedThreadPoolAdapter::notifyJobCompleted() +{ + QMutexLocker l(&m_mutex); + + KIS_SAFE_ASSERT_RECOVER (m_numRunningJobs > 0) { + m_waitCondition.wakeAll(); + return; + } + + m_numRunningJobs--; + if (!m_numRunningJobs) { + m_waitCondition.wakeAll(); + } +} + + diff --git a/libs/global/KisSharedThreadPoolAdapter.h b/libs/global/KisSharedThreadPoolAdapter.h new file mode 100644 index 0000000000..eadf83fab1 --- /dev/null +++ b/libs/global/KisSharedThreadPoolAdapter.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KISSHAREDTHREADPOOLADAPTER_H +#define KISSHAREDTHREADPOOLADAPTER_H + +#include +#include + +#include + +class QThreadPool; + +class KRITAGLOBAL_EXPORT KisSharedThreadPoolAdapter +{ +public: + KisSharedThreadPoolAdapter(QThreadPool *parentPool); + ~KisSharedThreadPoolAdapter(); + + void start(KisSharedRunnable *runnable, int priority = 0); + bool tryStart(KisSharedRunnable *runnable); + + bool waitForDone(int msecs = -1); + +private: + friend class KisSharedRunnable; + void notifyJobCompleted(); + + KisSharedThreadPoolAdapter(KisSharedThreadPoolAdapter &rhs) = delete; + +private: + QThreadPool *m_parentPool; + QMutex m_mutex; + QWaitCondition m_waitCondition; + int m_numRunningJobs; +}; + +#endif // KISSHAREDTHREADPOOLADAPTER_H diff --git a/libs/global/tests/CMakeLists.txt b/libs/global/tests/CMakeLists.txt new file mode 100644 index 0000000000..3cc4776465 --- /dev/null +++ b/libs/global/tests/CMakeLists.txt @@ -0,0 +1,8 @@ +include(ECMAddTests) +include(KritaAddBrokenUnitTest) + +macro_add_unittest_definitions() + +ecm_add_test(KisSharedThreadPoolAdapterTest.cpp + TEST_NAME KisSharedThreadPoolAdapter + LINK_LIBRARIES kritaglobal Qt5::Test) diff --git a/libs/global/tests/KisSharedThreadPoolAdapterTest.cpp b/libs/global/tests/KisSharedThreadPoolAdapterTest.cpp new file mode 100644 index 0000000000..436ee4c01a --- /dev/null +++ b/libs/global/tests/KisSharedThreadPoolAdapterTest.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisSharedThreadPoolAdapterTest.h" + +#include + +#include +#include +#include + +#include "kis_debug.h" + +struct NastyCounter : public KisSharedRunnable +{ + NastyCounter(QAtomicInt *value) + : m_value(value) + { + } + + void runShared() override { + for (int i = 0; i < numCycles(); i++) { + QThread::msleep(qrand() % 10); + m_value->ref(); + } + } + + static int numCycles() { + return 100; + } + +private: + QAtomicInt *m_value; +}; + + +void KisSharedThreadPoolAdapterTest::test() +{ + QAtomicInt value; + + KisSharedThreadPoolAdapter adapter(QThreadPool::globalInstance()); + + const int numThreads = 30; + + for (int i = 0; i < numThreads; i++) { + adapter.start(new NastyCounter(&value)); + } + + adapter.waitForDone(); + + QCOMPARE(int(value), numThreads * NastyCounter::numCycles()); +} + +// TODO: test waitForDone on empty queue!!!! + + +QTEST_MAIN(KisSharedThreadPoolAdapterTest) diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/global/tests/KisSharedThreadPoolAdapterTest.h similarity index 62% copy from libs/image/kis_projection_updates_filter.cpp copy to libs/global/tests/KisSharedThreadPoolAdapterTest.h index e1493ec367..8daba54d5e 100644 --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/global/tests/KisSharedThreadPoolAdapterTest.h @@ -1,36 +1,34 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#ifndef KISSHAREDTHREADPOOLADAPTERTEST_H +#define KISSHAREDTHREADPOOLADAPTERTEST_H +#include -#include -#include - -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +class KisSharedThreadPoolAdapterTest : public QObject { -} + Q_OBJECT -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} +private Q_SLOTS: + + void test(); + +}; + +#endif // KISSHAREDTHREADPOOLADAPTERTEST_H diff --git a/libs/image/CMakeLists.txt b/libs/image/CMakeLists.txt index 4423928930..9556869232 100644 --- a/libs/image/CMakeLists.txt +++ b/libs/image/CMakeLists.txt @@ -1,391 +1,400 @@ add_subdirectory( tests ) add_subdirectory( tiles3 ) include_directories( ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/metadata ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty ${CMAKE_CURRENT_SOURCE_DIR}/brushengine ${CMAKE_CURRENT_SOURCE_DIR}/commands ${CMAKE_CURRENT_SOURCE_DIR}/commands_new ${CMAKE_CURRENT_SOURCE_DIR}/filter ${CMAKE_CURRENT_SOURCE_DIR}/floodfill ${CMAKE_CURRENT_SOURCE_DIR}/generator ${CMAKE_CURRENT_SOURCE_DIR}/layerstyles ${CMAKE_CURRENT_SOURCE_DIR}/processing ${CMAKE_CURRENT_SOURCE_DIR}/recorder ${CMAKE_SOURCE_DIR}/sdk/tests ) include_directories(SYSTEM ${EIGEN3_INCLUDE_DIR} ${Boost_INCLUDE_DIRS} ) if(FFTW3_FOUND) include_directories(${FFTW3_INCLUDE_DIR}) endif() if(HAVE_VC) include_directories(SYSTEM ${Vc_INCLUDE_DIR} ${Qt5Core_INCLUDE_DIRS} ${Qt5Gui_INCLUDE_DIRS}) ko_compile_for_all_implementations(__per_arch_circle_mask_generator_objs kis_brush_mask_applicator_factories.cpp) else() set(__per_arch_circle_mask_generator_objs kis_brush_mask_applicator_factories.cpp) endif() set(kritaimage_LIB_SRCS tiles3/kis_tile.cc tiles3/kis_tile_data.cc tiles3/kis_tile_data_store.cc tiles3/kis_tile_data_pooler.cc tiles3/kis_tiled_data_manager.cc tiles3/kis_memento_manager.cc tiles3/kis_hline_iterator.cpp tiles3/kis_vline_iterator.cpp tiles3/kis_random_accessor.cc tiles3/swap/kis_abstract_compression.cpp tiles3/swap/kis_lzf_compression.cpp tiles3/swap/kis_abstract_tile_compressor.cpp tiles3/swap/kis_legacy_tile_compressor.cpp tiles3/swap/kis_tile_compressor_2.cpp tiles3/swap/kis_chunk_allocator.cpp tiles3/swap/kis_memory_window.cpp tiles3/swap/kis_swapped_data_store.cpp tiles3/swap/kis_tile_data_swapper.cpp kis_distance_information.cpp kis_painter.cc + kis_painter_blt_multi_fixed.cpp kis_marker_painter.cpp kis_progress_updater.cpp brushengine/kis_paint_information.cc brushengine/kis_random_source.cpp + brushengine/KisPerStrokeRandomSource.cpp brushengine/kis_stroke_random_source.cpp brushengine/kis_paintop.cc brushengine/kis_paintop_factory.cpp brushengine/kis_paintop_preset.cpp brushengine/kis_paintop_registry.cc brushengine/kis_paintop_settings.cpp brushengine/kis_paintop_settings_update_proxy.cpp + brushengine/kis_paintop_utils.cpp brushengine/kis_no_size_paintop_settings.cpp brushengine/kis_locked_properties.cc brushengine/kis_locked_properties_proxy.cpp brushengine/kis_locked_properties_server.cpp brushengine/kis_paintop_config_widget.cpp brushengine/kis_uniform_paintop_property.cpp brushengine/kis_combo_based_paintop_property.cpp brushengine/kis_slider_based_paintop_property.cpp brushengine/kis_standard_uniform_properties_factory.cpp + brushengine/KisStrokeSpeedMeasurer.cpp commands/kis_deselect_global_selection_command.cpp commands/kis_image_change_layers_command.cpp commands/kis_image_change_visibility_command.cpp commands/kis_image_command.cpp commands/kis_image_set_projection_color_space_command.cpp commands/kis_image_layer_add_command.cpp commands/kis_image_layer_move_command.cpp commands/kis_image_layer_remove_command.cpp commands/kis_image_layer_remove_command_impl.cpp commands/kis_image_lock_command.cpp commands/kis_layer_command.cpp commands/kis_node_command.cpp commands/kis_node_compositeop_command.cpp commands/kis_node_opacity_command.cpp commands/kis_node_property_list_command.cpp commands/kis_reselect_global_selection_command.cpp commands/kis_set_global_selection_command.cpp commands_new/kis_saved_commands.cpp commands_new/kis_processing_command.cpp commands_new/kis_image_resize_command.cpp commands_new/kis_image_set_resolution_command.cpp commands_new/kis_node_move_command2.cpp commands_new/kis_set_layer_style_command.cpp commands_new/kis_selection_move_command2.cpp commands_new/kis_update_command.cpp commands_new/kis_switch_current_time_command.cpp commands_new/kis_change_projection_color_command.cpp commands_new/kis_activate_selection_mask_command.cpp processing/kis_do_nothing_processing_visitor.cpp processing/kis_simple_processing_visitor.cpp processing/kis_crop_processing_visitor.cpp processing/kis_crop_selections_processing_visitor.cpp processing/kis_transform_processing_visitor.cpp processing/kis_mirror_processing_visitor.cpp filter/kis_filter.cc filter/kis_filter_configuration.cc filter/kis_color_transformation_configuration.cc filter/kis_filter_registry.cc filter/kis_color_transformation_filter.cc generator/kis_generator.cpp generator/kis_generator_layer.cpp generator/kis_generator_registry.cpp floodfill/kis_fill_interval_map.cpp floodfill/kis_scanline_fill.cpp lazybrush/kis_min_cut_worker.cpp lazybrush/kis_lazy_fill_tools.cpp lazybrush/kis_multiway_cut.cpp lazybrush/kis_colorize_mask.cpp lazybrush/kis_colorize_stroke_strategy.cpp KisDelayedUpdateNodeInterface.cpp kis_adjustment_layer.cc kis_selection_based_layer.cpp kis_node_filter_interface.cpp kis_base_accessor.cpp kis_base_node.cpp kis_base_processor.cpp kis_bookmarked_configuration_manager.cc kis_clone_info.cpp kis_clone_layer.cpp kis_colorspace_convert_visitor.cpp kis_config_widget.cpp kis_convolution_kernel.cc kis_convolution_painter.cc kis_gaussian_kernel.cpp kis_edge_detection_kernel.cpp kis_cubic_curve.cpp kis_default_bounds.cpp kis_default_bounds_base.cpp kis_effect_mask.cc kis_fast_math.cpp kis_fill_painter.cc kis_filter_mask.cpp kis_filter_strategy.cc kis_transform_mask.cpp kis_transform_mask_params_interface.cpp kis_recalculate_transform_mask_job.cpp kis_recalculate_generator_layer_job.cpp kis_transform_mask_params_factory_registry.cpp kis_safe_transform.cpp kis_gradient_painter.cc kis_gradient_shape_strategy.cpp kis_cached_gradient_shape_strategy.cpp kis_polygonal_gradient_shape_strategy.cpp kis_iterator_ng.cpp kis_async_merger.cpp kis_merge_walker.cc kis_updater_context.cpp kis_update_job_item.cpp kis_stroke_strategy_undo_command_based.cpp kis_simple_stroke_strategy.cpp + KisRunnableBasedStrokeStrategy.cpp + KisRunnableStrokeJobData.cpp + KisRunnableStrokeJobsInterface.cpp + KisFakeRunnableStrokeJobsExecutor.cpp kis_stroke_job_strategy.cpp kis_stroke_strategy.cpp kis_stroke.cpp kis_strokes_queue.cpp + KisStrokesQueueMutatedJobInterface.cpp kis_simple_update_queue.cpp kis_update_scheduler.cpp kis_queues_progress_updater.cpp kis_composite_progress_proxy.cpp kis_sync_lod_cache_stroke_strategy.cpp kis_lod_capable_layer_offset.cpp kis_update_time_monitor.cpp KisUpdateSchedulerConfigNotifier.cpp kis_group_layer.cc kis_count_visitor.cpp kis_histogram.cc kis_image_interfaces.cpp kis_image_animation_interface.cpp kis_time_range.cpp kis_node_graph_listener.cpp kis_image.cc kis_image_signal_router.cpp kis_image_config.cpp kis_projection_updates_filter.cpp kis_suspend_projection_updates_stroke_strategy.cpp kis_regenerate_frame_stroke_strategy.cpp kis_switch_time_stroke_strategy.cpp kis_crop_saved_extra_data.cpp kis_thread_safe_signal_compressor.cpp kis_timed_signal_threshold.cpp kis_layer.cc kis_indirect_painting_support.cpp kis_abstract_projection_plane.cpp kis_layer_projection_plane.cpp kis_layer_utils.cpp kis_mask_projection_plane.cpp kis_projection_leaf.cpp kis_mask.cc kis_base_mask_generator.cpp kis_rect_mask_generator.cpp kis_circle_mask_generator.cpp kis_gauss_circle_mask_generator.cpp kis_gauss_rect_mask_generator.cpp ${__per_arch_circle_mask_generator_objs} kis_curve_circle_mask_generator.cpp kis_curve_rect_mask_generator.cpp kis_math_toolbox.cpp kis_memory_statistics_server.cpp kis_name_server.cpp kis_node.cpp kis_node_facade.cpp kis_node_progress_proxy.cpp kis_busy_progress_indicator.cpp kis_node_visitor.cpp kis_paint_device.cc kis_paint_device_debug_utils.cpp kis_fixed_paint_device.cpp kis_paint_layer.cc kis_perspective_math.cpp kis_pixel_selection.cpp kis_processing_information.cpp kis_properties_configuration.cc kis_random_accessor_ng.cpp kis_random_generator.cc kis_random_sub_accessor.cpp kis_wrapped_random_accessor.cpp kis_selection.cc kis_selection_mask.cpp kis_update_outline_job.cpp kis_update_selection_job.cpp kis_serializable_configuration.cc kis_transaction_data.cpp kis_transform_worker.cc kis_perspectivetransform_worker.cpp bsplines/kis_bspline_1d.cpp bsplines/kis_bspline_2d.cpp bsplines/kis_nu_bspline_2d.cpp kis_warptransform_worker.cc kis_cage_transform_worker.cpp kis_liquify_transform_worker.cpp kis_green_coordinates_math.cpp kis_transparency_mask.cc kis_undo_adapter.cpp kis_macro_based_undo_store.cpp kis_surrogate_undo_adapter.cpp kis_legacy_undo_adapter.cpp kis_post_execution_undo_adapter.cpp kis_processing_visitor.cpp kis_processing_applicator.cpp krita_utils.cpp kis_outline_generator.cpp kis_layer_composition.cpp kis_selection_filters.cpp KisProofingConfiguration.h metadata/kis_meta_data_entry.cc metadata/kis_meta_data_filter.cc metadata/kis_meta_data_filter_p.cc metadata/kis_meta_data_filter_registry.cc metadata/kis_meta_data_filter_registry_model.cc metadata/kis_meta_data_io_backend.cc metadata/kis_meta_data_merge_strategy.cc metadata/kis_meta_data_merge_strategy_p.cc metadata/kis_meta_data_merge_strategy_registry.cc metadata/kis_meta_data_parser.cc metadata/kis_meta_data_schema.cc metadata/kis_meta_data_schema_registry.cc metadata/kis_meta_data_store.cc metadata/kis_meta_data_type_info.cc metadata/kis_meta_data_validator.cc metadata/kis_meta_data_value.cc recorder/kis_action_recorder.cc recorder/kis_macro.cc recorder/kis_macro_player.cc recorder/kis_node_query_path.cc recorder/kis_play_info.cc recorder/kis_recorded_action.cc recorder/kis_recorded_action_factory_registry.cc recorder/kis_recorded_action_load_context.cpp recorder/kis_recorded_action_save_context.cpp recorder/kis_recorded_filter_action.cpp recorder/kis_recorded_fill_paint_action.cpp recorder/kis_recorded_node_action.cc recorder/kis_recorded_paint_action.cpp recorder/kis_recorded_path_paint_action.cpp recorder/kis_recorded_shape_paint_action.cpp kis_keyframe.cpp kis_keyframe_channel.cpp kis_keyframe_commands.cpp kis_scalar_keyframe_channel.cpp kis_raster_keyframe_channel.cpp kis_onion_skin_compositor.cpp kis_onion_skin_cache.cpp kis_idle_watcher.cpp kis_psd_layer_style.cpp kis_layer_properties_icons.cpp layerstyles/kis_multiple_projection.cpp layerstyles/kis_layer_style_filter.cpp layerstyles/kis_layer_style_filter_environment.cpp layerstyles/kis_layer_style_filter_projection_plane.cpp layerstyles/kis_layer_style_projection_plane.cpp layerstyles/kis_ls_drop_shadow_filter.cpp layerstyles/kis_ls_satin_filter.cpp layerstyles/kis_ls_stroke_filter.cpp layerstyles/kis_ls_bevel_emboss_filter.cpp layerstyles/kis_ls_overlay_filter.cpp layerstyles/kis_ls_utils.cpp layerstyles/gimp_bump_map.cpp KisProofingConfiguration.cpp ) set(einspline_SRCS 3rdparty/einspline/bspline_create.cpp 3rdparty/einspline/bspline_data.cpp 3rdparty/einspline/multi_bspline_create.cpp 3rdparty/einspline/nubasis.cpp 3rdparty/einspline/nubspline_create.cpp 3rdparty/einspline/nugrid.cpp ) add_library(kritaimage SHARED ${kritaimage_LIB_SRCS} ${einspline_SRCS}) generate_export_header(kritaimage BASE_NAME kritaimage) target_link_libraries(kritaimage PUBLIC kritaversion kritawidgets kritaglobal kritapsd kritaodf kritapigment kritacommand kritawidgetutils Qt5::Concurrent ) target_link_libraries(kritaimage PUBLIC ${Boost_SYSTEM_LIBRARY}) if(OPENEXR_FOUND) target_link_libraries(kritaimage PUBLIC ${OPENEXR_LIBRARIES}) endif() if(FFTW3_FOUND) target_link_libraries(kritaimage PRIVATE ${FFTW3_LIBRARIES}) endif() if(HAVE_VC) target_link_libraries(kritaimage PUBLIC ${Vc_LIBRARIES}) endif() if (NOT GSL_FOUND) message (WARNING "KRITA WARNING! No GNU Scientific Library was found! Krita's Shaped Gradients might be non-normalized! Please install GSL library.") else () target_link_libraries(kritaimage PRIVATE ${GSL_LIBRARIES} ${GSL_CBLAS_LIBRARIES}) endif () target_include_directories(kritaimage PUBLIC $ $ $ $ $ $ $ ) set_target_properties(kritaimage PROPERTIES VERSION ${GENERIC_KRITA_LIB_VERSION} SOVERSION ${GENERIC_KRITA_LIB_SOVERSION} ) install(TARGETS kritaimage ${INSTALL_TARGETS_DEFAULT_ARGS}) ########### install schemas ############# install( FILES metadata/schemas/dc.schema metadata/schemas/exif.schema metadata/schemas/tiff.schema metadata/schemas/mkn.schema metadata/schemas/xmp.schema metadata/schemas/xmpmm.schema metadata/schemas/xmprights.schema DESTINATION ${DATA_INSTALL_DIR}/krita/metadata/schemas) diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/image/KisFakeRunnableStrokeJobsExecutor.cpp similarity index 51% copy from libs/image/kis_projection_updates_filter.cpp copy to libs/image/KisFakeRunnableStrokeJobsExecutor.cpp index e1493ec367..38ef1c4b59 100644 --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/image/KisFakeRunnableStrokeJobsExecutor.cpp @@ -1,36 +1,36 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#include "KisFakeRunnableStrokeJobsExecutor.h" +#include +#include -#include -#include +#include -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +void KisFakeRunnableStrokeJobsExecutor::addRunnableJobs(const QVector &list) { -} + Q_FOREACH (KisRunnableStrokeJobData *data, list) { + KIS_SAFE_ASSERT_RECOVER_NOOP(data->sequentiality() != KisStrokeJobData::BARRIER && "barrier jobs are not supported on the fake executor"); + KIS_SAFE_ASSERT_RECOVER_NOOP(data->exclusivity() != KisStrokeJobData::EXCLUSIVE && "exclusive jobs are not supported on the fake executor"); -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; + data->run(); + } + + qDeleteAll(list); } diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/image/KisFakeRunnableStrokeJobsExecutor.h similarity index 62% copy from libs/image/kis_projection_updates_filter.cpp copy to libs/image/KisFakeRunnableStrokeJobsExecutor.h index e1493ec367..5a7fd07af9 100644 --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/image/KisFakeRunnableStrokeJobsExecutor.h @@ -1,36 +1,31 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#ifndef KISFAKERUNNABLESTROKEJOBSEXECUTOR_H +#define KISFAKERUNNABLESTROKEJOBSEXECUTOR_H +#include "KisRunnableStrokeJobsInterface.h" -#include -#include -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +class KRITAIMAGE_EXPORT KisFakeRunnableStrokeJobsExecutor : public KisRunnableStrokeJobsInterface { -} +public: + void addRunnableJobs(const QVector &list); +}; -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} +#endif // KISFAKERUNNABLESTROKEJOBSEXECUTOR_H diff --git a/libs/ui/opengl/kis_opengl_canvas_debugger.h b/libs/image/KisRenderedDab.h similarity index 53% copy from libs/ui/opengl/kis_opengl_canvas_debugger.h copy to libs/image/KisRenderedDab.h index 8d7e605e96..92f7f9a7f2 100644 --- a/libs/ui/opengl/kis_opengl_canvas_debugger.h +++ b/libs/image/KisRenderedDab.h @@ -1,45 +1,46 @@ /* - * Copyright (c) 2015 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_OPENGL_CANVAS_DEBUGGER_H -#define __KIS_OPENGL_CANVAS_DEBUGGER_H +#ifndef KISRENDEREDDAB_H +#define KISRENDEREDDAB_H -#include +#include "kis_types.h" +#include "kis_fixed_paint_device.h" - - -class KisOpenglCanvasDebugger +struct KisRenderedDab { -public: - KisOpenglCanvasDebugger(); - ~KisOpenglCanvasDebugger(); - - static KisOpenglCanvasDebugger* instance(); - - bool showFpsOnCanvas() const; - - void nofityPaintRequested(); - void nofitySyncStatus(bool value); - qreal accumulatedFps(); - -private: - struct Private; - const QScopedPointer m_d; + KisRenderedDab() {} + KisRenderedDab(KisFixedPaintDeviceSP _device) + : device(_device), + offset(_device->bounds().topLeft()) + { + } + + KisFixedPaintDeviceSP device; + QPoint offset; + + qreal opacity = OPACITY_OPAQUE_F; + qreal flow = OPACITY_OPAQUE_F; + qreal averageOpacity = OPACITY_TRANSPARENT_F; + + inline QRect realBounds() const { + return QRect(offset, device->bounds().size()); + } }; -#endif /* __KIS_OPENGL_CANVAS_DEBUGGER_H */ +#endif // KISRENDEREDDAB_H diff --git a/libs/image/KisRunnableBasedStrokeStrategy.cpp b/libs/image/KisRunnableBasedStrokeStrategy.cpp new file mode 100644 index 0000000000..45829d73c6 --- /dev/null +++ b/libs/image/KisRunnableBasedStrokeStrategy.cpp @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisRunnableBasedStrokeStrategy.h" + +#include +#include + +#include "KisRunnableStrokeJobData.h" +#include "KisRunnableStrokeJobsInterface.h" + +struct KisRunnableBasedStrokeStrategy::JobsInterface : public KisRunnableStrokeJobsInterface +{ + JobsInterface(KisRunnableBasedStrokeStrategy *q) + : m_q(q) + { + } + + + void addRunnableJobs(const QVector &list) { + QVector newList; + + Q_FOREACH (KisRunnableStrokeJobData *item, list) { + newList.append(item); + } + + m_q->addMutatedJobs(newList); + } + +private: + KisRunnableBasedStrokeStrategy *m_q; +}; + + +KisRunnableBasedStrokeStrategy::KisRunnableBasedStrokeStrategy(QString id, const KUndo2MagicString &name) + : KisSimpleStrokeStrategy(id, name), + m_jobsInterface(new JobsInterface(this)) +{ +} + +KisRunnableBasedStrokeStrategy::KisRunnableBasedStrokeStrategy(const KisRunnableBasedStrokeStrategy &rhs) + : KisSimpleStrokeStrategy(rhs), + m_jobsInterface(new JobsInterface(this)) +{ +} + +KisRunnableBasedStrokeStrategy::~KisRunnableBasedStrokeStrategy() +{ +} + +void KisRunnableBasedStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) +{ + if (!data) return; + + KisRunnableStrokeJobData *runnable = dynamic_cast(data); + if (!runnable) return; + + runnable->run(); +} + +KisRunnableStrokeJobsInterface *KisRunnableBasedStrokeStrategy::runnableJobsInterface() const +{ + return m_jobsInterface.data(); +} diff --git a/libs/image/KisRunnableBasedStrokeStrategy.h b/libs/image/KisRunnableBasedStrokeStrategy.h new file mode 100644 index 0000000000..6346ecafe0 --- /dev/null +++ b/libs/image/KisRunnableBasedStrokeStrategy.h @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KISRUNNABLEBASEDSTROKESTRATEGY_H +#define KISRUNNABLEBASEDSTROKESTRATEGY_H + +#include "kis_simple_stroke_strategy.h" + +class KisRunnableStrokeJobsInterface; + +class KRITAIMAGE_EXPORT KisRunnableBasedStrokeStrategy : public KisSimpleStrokeStrategy +{ +private: + struct JobsInterface; + +public: + KisRunnableBasedStrokeStrategy(QString id, const KUndo2MagicString &name); + KisRunnableBasedStrokeStrategy(const KisRunnableBasedStrokeStrategy &rhs); + ~KisRunnableBasedStrokeStrategy(); + + void doStrokeCallback(KisStrokeJobData *data) override; + + KisRunnableStrokeJobsInterface *runnableJobsInterface() const; + +private: + const QScopedPointer m_jobsInterface; +}; + +#endif // KISRUNNABLEBASEDSTROKESTRATEGY_H diff --git a/libs/image/KisRunnableStrokeJobData.cpp b/libs/image/KisRunnableStrokeJobData.cpp new file mode 100644 index 0000000000..a79fd504a0 --- /dev/null +++ b/libs/image/KisRunnableStrokeJobData.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisRunnableStrokeJobData.h" + +#include +#include + +KisRunnableStrokeJobData::KisRunnableStrokeJobData(QRunnable *runnable, KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity) + : KisStrokeJobData(sequentiality, exclusivity), + m_runnable(runnable) +{ +} + +KisRunnableStrokeJobData::KisRunnableStrokeJobData(std::function func, KisStrokeJobData::Sequentiality sequentiality, KisStrokeJobData::Exclusivity exclusivity) + : KisStrokeJobData(sequentiality, exclusivity), + m_func(func) +{ +} + +KisRunnableStrokeJobData::~KisRunnableStrokeJobData() { + if (m_runnable && m_runnable->autoDelete()) { + delete m_runnable; + } +} + +void KisRunnableStrokeJobData::run() { + if (m_runnable) { + m_runnable->run(); + } else if (m_func) { + m_func(); + } +} diff --git a/libs/image/KisRunnableStrokeJobData.h b/libs/image/KisRunnableStrokeJobData.h new file mode 100644 index 0000000000..1afbffed40 --- /dev/null +++ b/libs/image/KisRunnableStrokeJobData.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KISRUNNABLESTROKEJOBDATA_H +#define KISRUNNABLESTROKEJOBDATA_H + +#include "kritaimage_export.h" +#include "kis_stroke_job_strategy.h" +#include + +class QRunnable; + +class KRITAIMAGE_EXPORT KisRunnableStrokeJobData : public KisStrokeJobData { +public: + KisRunnableStrokeJobData(QRunnable *runnable, KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL, + KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL); + + KisRunnableStrokeJobData(std::function func, KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL, + KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL); + + ~KisRunnableStrokeJobData(); + + void run(); + +private: + QRunnable *m_runnable = 0; + std::function m_func; +}; + +#endif // KISRUNNABLESTROKEJOBDATA_H diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/image/KisRunnableStrokeJobsInterface.cpp similarity index 63% copy from libs/image/kis_projection_updates_filter.cpp copy to libs/image/KisRunnableStrokeJobsInterface.cpp index e1493ec367..76c667c89d 100644 --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/image/KisRunnableStrokeJobsInterface.cpp @@ -1,36 +1,31 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#include "KisRunnableStrokeJobsInterface.h" +#include -#include -#include - -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +KisRunnableStrokeJobsInterface::~KisRunnableStrokeJobsInterface() { + } -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) +void KisRunnableStrokeJobsInterface::addRunnableJob(KisRunnableStrokeJobData *data) { - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; + addRunnableJobs({data}); } diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/image/KisRunnableStrokeJobsInterface.h similarity index 60% copy from libs/image/kis_projection_updates_filter.cpp copy to libs/image/KisRunnableStrokeJobsInterface.h index e1493ec367..6f522f3690 100644 --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/image/KisRunnableStrokeJobsInterface.h @@ -1,36 +1,37 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" - +#ifndef KISRUNNABLESTROKEJOBSINTERFACE_H +#define KISRUNNABLESTROKEJOBSINTERFACE_H +#include "kritaimage_export.h" #include -#include -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() -{ -} +class KisRunnableStrokeJobData; + -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) +class KRITAIMAGE_EXPORT KisRunnableStrokeJobsInterface { - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} +public: + virtual ~KisRunnableStrokeJobsInterface(); + + void addRunnableJob(KisRunnableStrokeJobData *data); + virtual void addRunnableJobs(const QVector &list) = 0; +}; + +#endif // KISRUNNABLESTROKEJOBSINTERFACE_H diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/image/KisStrokesQueueMutatedJobInterface.cpp similarity index 62% copy from libs/image/kis_projection_updates_filter.cpp copy to libs/image/KisStrokesQueueMutatedJobInterface.cpp index e1493ec367..7caad5a7a8 100644 --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/image/KisStrokesQueueMutatedJobInterface.cpp @@ -1,36 +1,24 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#include "KisStrokesQueueMutatedJobInterface.h" - -#include -#include - -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +KisStrokesQueueMutatedJobInterface::~KisStrokesQueueMutatedJobInterface() { } -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/image/KisStrokesQueueMutatedJobInterface.h similarity index 62% copy from libs/image/kis_projection_updates_filter.cpp copy to libs/image/KisStrokesQueueMutatedJobInterface.h index e1493ec367..b479c72fcf 100644 --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/image/KisStrokesQueueMutatedJobInterface.h @@ -1,36 +1,34 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#ifndef KISSTROKESQUEUEMUTATEDJOBINTERFACE_H +#define KISSTROKESQUEUEMUTATEDJOBINTERFACE_H +#include "kis_types.h" -#include -#include +class KisStrokeJobData; -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +class KisStrokesQueueMutatedJobInterface { -} +public: + virtual ~KisStrokesQueueMutatedJobInterface(); -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} + virtual void addMutatedJobs(KisStrokeId strokeId, const QVector list) = 0; +}; + +#endif // KISSTROKESQUEUEMUTATEDJOBINTERFACE_H diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/image/KisUpdaterContextSnapshotEx.h similarity index 58% copy from libs/image/kis_projection_updates_filter.cpp copy to libs/image/KisUpdaterContextSnapshotEx.h index e1493ec367..dd6608af9e 100644 --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/image/KisUpdaterContextSnapshotEx.h @@ -1,36 +1,34 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#ifndef KISUPDATERCONTEXTSNAPSHOTEX_H +#define KISUPDATERCONTEXTSNAPSHOTEX_H +enum KisUpdaterContextSnapshotExTag { + ContextEmpty = 0x00, + HasSequentialJob = 0x01, + HasUniquelyConcurrentJob = 0x02, + HasConcurrentJob = 0x04, + HasBarrierJob = 0x08, + HasMergeJob = 0x10 +}; -#include -#include +Q_DECLARE_FLAGS(KisUpdaterContextSnapshotEx, KisUpdaterContextSnapshotExTag); +Q_DECLARE_OPERATORS_FOR_FLAGS(KisUpdaterContextSnapshotEx); -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() -{ -} - -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} +#endif // KISUPDATERCONTEXTSNAPSHOTEX_H diff --git a/libs/image/brushengine/KisPerStrokeRandomSource.cpp b/libs/image/brushengine/KisPerStrokeRandomSource.cpp new file mode 100644 index 0000000000..2b552d22ac --- /dev/null +++ b/libs/image/brushengine/KisPerStrokeRandomSource.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisPerStrokeRandomSource.h" + +#include +#include +#include + +#include +#include +#include + +struct KisPerStrokeRandomSource::Private +{ + Private(int _seed) + : seed(_seed) + { + boost::taus88 tempGenerator(seed); + generatorMax = tempGenerator.max(); + } + + Private(const Private &rhs) + : seed(rhs.seed), + generatorMax(rhs.generatorMax), + valuesCache(rhs.valuesCache) + { + } + + qint64 fetchInt(const QString &key); + + int seed = 0; + qint64 generatorMax = 0; + QHash valuesCache; + QMutex mutex; +}; + +KisPerStrokeRandomSource::KisPerStrokeRandomSource() + : m_d(new Private(qrand())) +{ + +} + +KisPerStrokeRandomSource::KisPerStrokeRandomSource(const KisPerStrokeRandomSource &rhs) + : KisShared(), + m_d(new Private(*rhs.m_d)) +{ +} + +KisPerStrokeRandomSource::~KisPerStrokeRandomSource() +{ +} + + +qint64 KisPerStrokeRandomSource::Private::fetchInt(const QString &key) +{ + QMutexLocker l(&mutex); + + auto it = valuesCache.find(key); + if (it != valuesCache.end()) { + return it.value(); + } + + boost::taus88 oneTimeGenerator(seed + qHash(key)); + const qint64 newValue = oneTimeGenerator(); + + valuesCache.insert(key, newValue); + + return newValue; +} + +int KisPerStrokeRandomSource::generate(const QString &key, int min, int max) const +{ + return min + m_d->fetchInt(key) % (max - min); +} + +qreal KisPerStrokeRandomSource::generateNormalized(const QString &key) const +{ + return qreal(m_d->fetchInt(key)) / m_d->generatorMax; +} + diff --git a/libs/image/brushengine/kis_stroke_random_source.h b/libs/image/brushengine/KisPerStrokeRandomSource.h similarity index 50% copy from libs/image/brushengine/kis_stroke_random_source.h copy to libs/image/brushengine/KisPerStrokeRandomSource.h index bbc4bd1fd2..9721eb51e8 100644 --- a/libs/image/brushengine/kis_stroke_random_source.h +++ b/libs/image/brushengine/KisPerStrokeRandomSource.h @@ -1,54 +1,56 @@ /* - * Copyright (c) 2015 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_STROKE_RANDOM_SOURCE_H -#define __KIS_STROKE_RANDOM_SOURCE_H +#ifndef KISPERSTROKERANDOMSOURCE_H +#define KISPERSTROKERANDOMSOURCE_H #include +#include "kis_shared.h" +#include "kis_shared_ptr.h" #include "kritaimage_export.h" -#include "kis_random_source.h" -/** - * A helper class to handle multiple KisRandomSource objects in a - * stroke strategies. It creates two identical random sources in the - * beginning of the stroke, so, when copied through copy-ctor and set - * another level of detail starts returning the same sequence of - * numbers as was returned for the first stroke. - */ -class KRITAIMAGE_EXPORT KisStrokeRandomSource +class KRITAIMAGE_EXPORT KisPerStrokeRandomSource : public KisShared { public: - KisStrokeRandomSource(); - KisStrokeRandomSource(const KisStrokeRandomSource &rhs); - KisStrokeRandomSource& operator=(const KisStrokeRandomSource &rhs); + KisPerStrokeRandomSource(); + KisPerStrokeRandomSource(const KisPerStrokeRandomSource &rhs); - ~KisStrokeRandomSource(); + ~KisPerStrokeRandomSource(); - KisRandomSourceSP source() const; + /** + * Generates a random number in a range from \p min to \p max + */ + int generate(const QString &key, int min, int max) const; - int levelOfDetail() const; - void setLevelOfDetail(int value); + /** + * Generates a random number in a closed range [0; 1.0] + */ + qreal generateNormalized(const QString &key) const; private: struct Private; const QScopedPointer m_d; }; -#endif /* __KIS_STROKE_RANDOM_SOURCE_H */ +class KisPerStrokeRandomSource; +typedef KisSharedPtr KisPerStrokeRandomSourceSP; +typedef KisWeakSharedPtr KisPerStrokeRandomSourceWSP; + +#endif // KISPERSTROKERANDOMSOURCE_H diff --git a/libs/image/brushengine/KisStrokeSpeedMeasurer.cpp b/libs/image/brushengine/KisStrokeSpeedMeasurer.cpp new file mode 100644 index 0000000000..6edd13841c --- /dev/null +++ b/libs/image/brushengine/KisStrokeSpeedMeasurer.cpp @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisStrokeSpeedMeasurer.h" + +#include +#include + +#include "kis_global.h" + +struct KisStrokeSpeedMeasurer::Private +{ + struct StrokeSample { + StrokeSample() {} + StrokeSample(int _time, qreal _distance) : time(_time), distance(_distance) {} + + int time = 0; /* ms */ + qreal distance = 0; + }; + + int timeSmoothWindow = 0; + + QList samples; + QPointF lastSamplePos; + int startTime = 0; + + qreal maxSpeed = 0; + + void purgeOldSamples(); + void addSampleImpl(const QPointF &pt, int time); +}; + +KisStrokeSpeedMeasurer::KisStrokeSpeedMeasurer(int timeSmoothWindow) + : m_d(new Private()) +{ + m_d->timeSmoothWindow = timeSmoothWindow; +} + +KisStrokeSpeedMeasurer::~KisStrokeSpeedMeasurer() +{ +} + +void KisStrokeSpeedMeasurer::Private::addSampleImpl(const QPointF &pt, int time) +{ + if (samples.isEmpty()) { + lastSamplePos = pt; + startTime = time; + samples.append(Private::StrokeSample(time, 0)); + } else { + Private::StrokeSample &lastSample = samples.last(); + + const qreal newStrokeDistance = lastSample.distance + kisDistance(lastSamplePos, pt); + lastSamplePos = pt; + + if (lastSample.time >= time) { + lastSample.distance = newStrokeDistance; + } else { + samples.append(Private::StrokeSample(time, newStrokeDistance)); + } + } +} + +void KisStrokeSpeedMeasurer::addSample(const QPointF &pt, int time) +{ + m_d->addSampleImpl(pt, time); + m_d->purgeOldSamples(); + sampleMaxSpeed(); +} + +void KisStrokeSpeedMeasurer::addSamples(const QVector &points, int time) +{ + const int lastSampleTime = !m_d->samples.isEmpty() ? m_d->samples.last().time : 0; + + const int timeSmoothBase = qMin(lastSampleTime, time); + const qreal timeSmoothStep = qreal(time - timeSmoothBase) / points.size(); + + for (int i = 0; i < points.size(); i++) { + const int sampleTime = timeSmoothBase + timeSmoothStep * (i + 1); + m_d->addSampleImpl(points[i], sampleTime); + } + + m_d->purgeOldSamples(); + sampleMaxSpeed(); +} + +qreal KisStrokeSpeedMeasurer::averageSpeed() const +{ + if (m_d->samples.isEmpty()) return 0; + + const Private::StrokeSample &lastSample = m_d->samples.last(); + + const int timeDiff = lastSample.time - m_d->startTime; + if (!timeDiff) return 0; + + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(timeDiff > 0, 0); + + return lastSample.distance / timeDiff; +} + +void KisStrokeSpeedMeasurer::Private::purgeOldSamples() +{ + if (samples.size() <= 1) return; + + const Private::StrokeSample lastSample = samples.last(); + + auto lastValueToKeep = samples.end(); + + for (auto it = samples.begin(); it != samples.end(); ++it) { + KIS_SAFE_ASSERT_RECOVER_RETURN(lastSample.time - it->time >= 0); + + if (lastSample.time - it->time < timeSmoothWindow) break; + lastValueToKeep = it; + } + + if (lastValueToKeep != samples.begin() && + lastValueToKeep != samples.end()) { + + samples.erase(samples.begin(), lastValueToKeep - 1); + } +} + +qreal KisStrokeSpeedMeasurer::currentSpeed() const +{ + if (m_d->samples.size() <= 1) return 0; + + const Private::StrokeSample firstSample = m_d->samples.first(); + const Private::StrokeSample lastSample = m_d->samples.last(); + + const int timeDiff = lastSample.time - firstSample.time; + if (!timeDiff) return 0; + + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(timeDiff > 0, 0); + + return (lastSample.distance - firstSample.distance) / timeDiff; +} + +qreal KisStrokeSpeedMeasurer::maxSpeed() const +{ + return m_d->maxSpeed; +} + +void KisStrokeSpeedMeasurer::reset() +{ + m_d->samples.clear(); + m_d->lastSamplePos = QPointF(); + m_d->startTime = 0; + m_d->maxSpeed = 0; +} + +void KisStrokeSpeedMeasurer::sampleMaxSpeed() +{ + if (m_d->samples.size() <= 1) return; + + const Private::StrokeSample firstSample = m_d->samples.first(); + const Private::StrokeSample lastSample = m_d->samples.last(); + + const int timeDiff = lastSample.time - firstSample.time; + if (timeDiff < m_d->timeSmoothWindow) return; + + const qreal speed = currentSpeed(); + if (speed > m_d->maxSpeed) { + m_d->maxSpeed = speed; + } +} diff --git a/libs/ui/opengl/kis_opengl_canvas_debugger.h b/libs/image/brushengine/KisStrokeSpeedMeasurer.h similarity index 58% copy from libs/ui/opengl/kis_opengl_canvas_debugger.h copy to libs/image/brushengine/KisStrokeSpeedMeasurer.h index 8d7e605e96..3dacd68cd8 100644 --- a/libs/ui/opengl/kis_opengl_canvas_debugger.h +++ b/libs/image/brushengine/KisStrokeSpeedMeasurer.h @@ -1,45 +1,53 @@ /* - * Copyright (c) 2015 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_OPENGL_CANVAS_DEBUGGER_H -#define __KIS_OPENGL_CANVAS_DEBUGGER_H +#ifndef KISSTROKESPEEDMEASURER_H +#define KISSTROKESPEEDMEASURER_H +#include "kritaimage_export.h" #include +#include +class QPointF; -class KisOpenglCanvasDebugger + +class KRITAIMAGE_EXPORT KisStrokeSpeedMeasurer { public: - KisOpenglCanvasDebugger(); - ~KisOpenglCanvasDebugger(); + KisStrokeSpeedMeasurer(int timeSmoothWindow); + ~KisStrokeSpeedMeasurer(); + + void addSample(const QPointF &pt, int time); + void addSamples(const QVector &points, int time); - static KisOpenglCanvasDebugger* instance(); + qreal averageSpeed() const; + qreal currentSpeed() const; + qreal maxSpeed() const; - bool showFpsOnCanvas() const; + void reset(); - void nofityPaintRequested(); - void nofitySyncStatus(bool value); - qreal accumulatedFps(); +private: + void sampleMaxSpeed(); private: struct Private; const QScopedPointer m_d; }; -#endif /* __KIS_OPENGL_CANVAS_DEBUGGER_H */ +#endif // KISSTROKESPEEDMEASURER_H diff --git a/libs/image/brushengine/kis_paint_information.cc b/libs/image/brushengine/kis_paint_information.cc index 25583e910c..bf238ba88e 100644 --- a/libs/image/brushengine/kis_paint_information.cc +++ b/libs/image/brushengine/kis_paint_information.cc @@ -1,595 +1,635 @@ /* * Copyright (c) 2007,2010 Cyrille Berger * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include "kis_paintop.h" #include "kis_algebra_2d.h" #include "kis_lod_transform.h" #include "kis_spacing_information.h" #include struct KisPaintInformation::Private { Private(const QPointF & pos_, qreal pressure_, qreal xTilt_, qreal yTilt_, qreal rotation_, qreal tangentialPressure_, qreal perspective_, qreal time_, qreal speed_, bool isHoveringMode_) : pos(pos_), pressure(pressure_), xTilt(xTilt_), yTilt(yTilt_), rotation(rotation_), tangentialPressure(tangentialPressure_), perspective(perspective_), time(time_), speed(speed_), isHoveringMode(isHoveringMode_), randomSource(0), - currentDistanceInfo(0), + perStrokeRandomSource(0), levelOfDetail(0) { } ~Private() { - KIS_ASSERT_RECOVER_NOOP(!currentDistanceInfo); + KIS_ASSERT_RECOVER_NOOP(!sanityIsRegistered); } Private(const Private &rhs) { copy(rhs); } Private& operator=(const Private &rhs) { copy(rhs); return *this; } void copy(const Private &rhs) { pos = rhs.pos; pressure = rhs.pressure; xTilt = rhs.xTilt; yTilt = rhs.yTilt; rotation = rhs.rotation; tangentialPressure = rhs.tangentialPressure; perspective = rhs.perspective; time = rhs.time; speed = rhs.speed; isHoveringMode = rhs.isHoveringMode; randomSource = rhs.randomSource; - currentDistanceInfo = rhs.currentDistanceInfo; + perStrokeRandomSource = rhs.perStrokeRandomSource; + sanityIsRegistered = false; // HINT: we do not copy registration mark! + directionHistoryInfo = rhs.directionHistoryInfo; canvasRotation = rhs.canvasRotation; canvasMirroredH = rhs.canvasMirroredH; if (rhs.drawingAngleOverride) { drawingAngleOverride = *rhs.drawingAngleOverride; } levelOfDetail = rhs.levelOfDetail; } QPointF pos; qreal pressure; qreal xTilt; qreal yTilt; qreal rotation; qreal tangentialPressure; qreal perspective; qreal time; qreal speed; bool isHoveringMode; KisRandomSourceSP randomSource; + KisPerStrokeRandomSourceSP perStrokeRandomSource; int canvasRotation; bool canvasMirroredH; boost::optional drawingAngleOverride; - KisDistanceInformation *currentDistanceInfo; + bool sanityIsRegistered = false; + + struct DirectionHistoryInfo { + DirectionHistoryInfo() {} + DirectionHistoryInfo(qreal _totalDistance, + int _currentDabSeqNo, + qreal _lastAngle, + QPointF _lastPosition, + boost::optional _lockedDrawingAngle) + : totalStrokeLength(_totalDistance), + currentDabSeqNo(_currentDabSeqNo), + lastAngle(_lastAngle), + lastPosition(_lastPosition), + lockedDrawingAngle(_lockedDrawingAngle) + { + } + + qreal totalStrokeLength = 0.0; + int currentDabSeqNo = 0; + qreal lastAngle = 0.0; + QPointF lastPosition; + boost::optional lockedDrawingAngle; + }; + boost::optional directionHistoryInfo; int levelOfDetail; void registerDistanceInfo(KisDistanceInformation *di) { - currentDistanceInfo = di; + directionHistoryInfo = DirectionHistoryInfo(di->scalarDistanceApprox(), + di->currentDabSeqNo(), + di->lastDrawingAngle(), + di->lastPosition(), + di->lockedDrawingAngleOptional()); + + + KIS_SAFE_ASSERT_RECOVER_NOOP(!sanityIsRegistered); + sanityIsRegistered = true; } void unregisterDistanceInfo() { - currentDistanceInfo = 0; + sanityIsRegistered = false; } }; KisPaintInformation::DistanceInformationRegistrar:: DistanceInformationRegistrar(KisPaintInformation *_p, KisDistanceInformation *distanceInfo) : p(_p) { p->d->registerDistanceInfo(distanceInfo); } KisPaintInformation::DistanceInformationRegistrar:: ~DistanceInformationRegistrar() { p->d->unregisterDistanceInfo(); } KisPaintInformation::KisPaintInformation(const QPointF & pos, qreal pressure, qreal xTilt, qreal yTilt, qreal rotation, qreal tangentialPressure, qreal perspective, qreal time, qreal speed) : d(new Private(pos, pressure, xTilt, yTilt, rotation, tangentialPressure, perspective, time, speed, false)) { } KisPaintInformation::KisPaintInformation(const QPointF & pos, qreal pressure, qreal xTilt, qreal yTilt, qreal rotation) : d(new Private(pos, pressure, xTilt, yTilt, rotation, 0.0, 1.0, 0.0, 0.0, false)) { } KisPaintInformation::KisPaintInformation(const QPointF &pos, qreal pressure) : d(new Private(pos, pressure, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, false)) { } KisPaintInformation::KisPaintInformation(const KisPaintInformation& rhs) : d(new Private(*rhs.d)) { } void KisPaintInformation::operator=(const KisPaintInformation & rhs) { *d = *rhs.d; } KisPaintInformation::~KisPaintInformation() { delete d; } bool KisPaintInformation::isHoveringMode() const { return d->isHoveringMode; } KisPaintInformation KisPaintInformation::createHoveringModeInfo(const QPointF &pos, qreal pressure, qreal xTilt, qreal yTilt, qreal rotation, qreal tangentialPressure, qreal perspective, qreal speed, int canvasrotation, bool canvasMirroredH) { KisPaintInformation info(pos, pressure, xTilt, yTilt, rotation, tangentialPressure, perspective, 0, speed); info.d->isHoveringMode = true; info.d->canvasRotation = canvasrotation; info.d->canvasMirroredH = canvasMirroredH; return info; } int KisPaintInformation::canvasRotation() const { return d->canvasRotation; } void KisPaintInformation::setCanvasRotation(int rotation) { if (rotation < 0) { d->canvasRotation= 360- abs(rotation % 360); } else { d->canvasRotation= rotation % 360; } } bool KisPaintInformation::canvasMirroredH() const { return d->canvasMirroredH; } void KisPaintInformation::setCanvasHorizontalMirrorState(bool mir) { d->canvasMirroredH = mir; } void KisPaintInformation::toXML(QDomDocument&, QDomElement& e) const { // hovering mode infos are not supposed to be saved KIS_ASSERT_RECOVER_NOOP(!d->isHoveringMode); e.setAttribute("pointX", QString::number(pos().x(), 'g', 15)); e.setAttribute("pointY", QString::number(pos().y(), 'g', 15)); e.setAttribute("pressure", QString::number(pressure(), 'g', 15)); e.setAttribute("xTilt", QString::number(xTilt(), 'g', 15)); e.setAttribute("yTilt", QString::number(yTilt(), 'g', 15)); e.setAttribute("rotation", QString::number(rotation(), 'g', 15)); e.setAttribute("tangentialPressure", QString::number(tangentialPressure(), 'g', 15)); e.setAttribute("perspective", QString::number(perspective(), 'g', 15)); e.setAttribute("time", QString::number(d->time, 'g', 15)); e.setAttribute("speed", QString::number(d->speed, 'g', 15)); } KisPaintInformation KisPaintInformation::fromXML(const QDomElement& e) { qreal pointX = qreal(KisDomUtils::toDouble(e.attribute("pointX", "0.0"))); qreal pointY = qreal(KisDomUtils::toDouble(e.attribute("pointY", "0.0"))); qreal pressure = qreal(KisDomUtils::toDouble(e.attribute("pressure", "0.0"))); qreal rotation = qreal(KisDomUtils::toDouble(e.attribute("rotation", "0.0"))); qreal tangentialPressure = qreal(KisDomUtils::toDouble(e.attribute("tangentialPressure", "0.0"))); qreal perspective = qreal(KisDomUtils::toDouble(e.attribute("perspective", "0.0"))); qreal xTilt = qreal(KisDomUtils::toDouble(e.attribute("xTilt", "0.0"))); qreal yTilt = qreal(KisDomUtils::toDouble(e.attribute("yTilt", "0.0"))); qreal time = KisDomUtils::toDouble(e.attribute("time", "0")); qreal speed = KisDomUtils::toDouble(e.attribute("speed", "0")); return KisPaintInformation(QPointF(pointX, pointY), pressure, xTilt, yTilt, rotation, tangentialPressure, perspective, time, speed); } const QPointF& KisPaintInformation::pos() const { return d->pos; } void KisPaintInformation::setPos(const QPointF& p) { d->pos = p; } qreal KisPaintInformation::pressure() const { return d->pressure; } void KisPaintInformation::setPressure(qreal p) { d->pressure = p; } qreal KisPaintInformation::xTilt() const { return d->xTilt; } qreal KisPaintInformation::yTilt() const { return d->yTilt; } void KisPaintInformation::overrideDrawingAngle(qreal angle) { d->drawingAngleOverride = angle; } qreal KisPaintInformation::drawingAngleSafe(const KisDistanceInformation &distance) const { - if (d->drawingAngleOverride) return *d->drawingAngleOverride; + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!d->directionHistoryInfo, 0.0); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(distance.hasLastDabInformation(), 0.0); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!d->drawingAngleOverride, 0.0); - if (!distance.hasLastDabInformation()) { - warnKrita << "KisPaintInformation::drawingAngleSafe()" << "Cannot access Distance Info last dab data"; - return 0.0; - } + return KisAlgebra2D::directionBetweenPoints(distance.lastPosition(), + pos(), + distance.lastDrawingAngle()); - return distance.nextDrawingAngle(pos()); } KisPaintInformation::DistanceInformationRegistrar KisPaintInformation::registerDistanceInformation(KisDistanceInformation *distance) { return DistanceInformationRegistrar(this, distance); } -qreal KisPaintInformation::drawingAngle() const +qreal KisPaintInformation::drawingAngle(bool considerLockedAngle) const { if (d->drawingAngleOverride) return *d->drawingAngleOverride; - if (!d->currentDistanceInfo || !d->currentDistanceInfo->hasLastDabInformation()) { - warnKrita << "KisPaintInformation::drawingAngle()" << "Cannot access Distance Info last dab data"; + if (!d->directionHistoryInfo) { + warnKrita << "KisPaintInformation::drawingAngleSafe()" << "DirectionHistoryInfo object is not available"; return 0.0; } - return d->currentDistanceInfo->nextDrawingAngle(pos()); -} - -void KisPaintInformation::lockCurrentDrawingAngle(qreal alpha_unused) const -{ - Q_UNUSED(alpha_unused); - - if (!d->currentDistanceInfo || !d->currentDistanceInfo->hasLastDabInformation()) { - warnKrita << "KisPaintInformation::lockCurrentDrawingAngle()" << "Cannot access Distance Info last dab data"; - return; - } - - qreal angle = d->currentDistanceInfo->nextDrawingAngle(pos(), false); - - qreal newAngle = angle; + if (considerLockedAngle && + d->directionHistoryInfo->lockedDrawingAngle) { - if (d->currentDistanceInfo->hasLockedDrawingAngle()) { - const qreal stabilizingCoeff = 20.0; - const qreal dist = stabilizingCoeff * d->currentDistanceInfo->currentSpacing().scalarApprox(); - const qreal alpha = qMax(0.0, dist - d->currentDistanceInfo->scalarDistanceApprox()) / dist; - - const qreal oldAngle = d->currentDistanceInfo->lockedDrawingAngle(); - - if (shortestAngularDistance(oldAngle, newAngle) < M_PI / 6) { - newAngle = (1.0 - alpha) * oldAngle + alpha * newAngle; - } else { - newAngle = oldAngle; - } + return *d->directionHistoryInfo->lockedDrawingAngle; } - d->currentDistanceInfo->setLockedDrawingAngle(newAngle); + // If the start and end positions are the same, we can't compute an angle. In that case, use the + // provided default. + return KisAlgebra2D::directionBetweenPoints(d->directionHistoryInfo->lastPosition, + pos(), + d->directionHistoryInfo->lastAngle); } QPointF KisPaintInformation::drawingDirectionVector() const { - if (d->drawingAngleOverride) { - qreal angle = *d->drawingAngleOverride; - return QPointF(cos(angle), sin(angle)); - } - - if (!d->currentDistanceInfo || !d->currentDistanceInfo->hasLastDabInformation()) { - warnKrita << "KisPaintInformation::drawingDirectionVector()" << "Cannot access Distance Info last dab data"; - return QPointF(1.0, 0.0); - } - - return d->currentDistanceInfo->nextDrawingDirectionVector(pos()); + const qreal angle = drawingAngle(false); + return QPointF(cos(angle), sin(angle)); } qreal KisPaintInformation::drawingDistance() const { - if (!d->currentDistanceInfo || !d->currentDistanceInfo->hasLastDabInformation()) { - warnKrita << "KisPaintInformation::drawingDistance()" << "Cannot access Distance Info last dab data"; + if (!d->directionHistoryInfo) { + warnKrita << "KisPaintInformation::drawingDistance()" << "DirectionHistoryInfo object is not available"; return 1.0; } - QVector2D diff(pos() - d->currentDistanceInfo->lastPosition()); + QVector2D diff(pos() - d->directionHistoryInfo->lastPosition); qreal length = diff.length(); if (d->levelOfDetail) { length *= KisLodTransform::lodToInvScale(d->levelOfDetail); } return length; } qreal KisPaintInformation::drawingSpeed() const { return d->speed; } qreal KisPaintInformation::rotation() const { return d->rotation; } qreal KisPaintInformation::tangentialPressure() const { return d->tangentialPressure; } qreal KisPaintInformation::perspective() const { return d->perspective; } qreal KisPaintInformation::currentTime() const { return d->time; } +int KisPaintInformation::currentDabSeqNo() const +{ + if (!d->directionHistoryInfo) { + warnKrita << "KisPaintInformation::currentDabSeqNo()" << "DirectionHistoryInfo object is not available"; + return 0; + } + + return d->directionHistoryInfo->currentDabSeqNo; +} + +qreal KisPaintInformation::totalStrokeLength() const +{ + if (!d->directionHistoryInfo) { + warnKrita << "KisPaintInformation::totalStrokeLength()" << "DirectionHistoryInfo object is not available"; + return 0; + } + + return d->directionHistoryInfo->totalStrokeLength; +} + KisRandomSourceSP KisPaintInformation::randomSource() const { if (!d->randomSource) { + qWarning() << "Accessing uninitialized random source!"; d->randomSource = new KisRandomSource(); } return d->randomSource; } void KisPaintInformation::setRandomSource(KisRandomSourceSP value) { d->randomSource = value; } +KisPerStrokeRandomSourceSP KisPaintInformation::perStrokeRandomSource() const +{ + if (!d->perStrokeRandomSource) { + qWarning() << "Accessing uninitialized per stroke random source!"; + d->perStrokeRandomSource = new KisPerStrokeRandomSource(); + } + + return d->perStrokeRandomSource; +} + +void KisPaintInformation::setPerStrokeRandomSource(KisPerStrokeRandomSourceSP value) +{ + d->perStrokeRandomSource = value; +} + void KisPaintInformation::setLevelOfDetail(int levelOfDetail) { d->levelOfDetail = levelOfDetail; } QDebug operator<<(QDebug dbg, const KisPaintInformation &info) { #ifdef NDEBUG Q_UNUSED(info); #else dbg.nospace() << "Position: " << info.pos(); dbg.nospace() << ", Pressure: " << info.pressure(); dbg.nospace() << ", X Tilt: " << info.xTilt(); dbg.nospace() << ", Y Tilt: " << info.yTilt(); dbg.nospace() << ", Rotation: " << info.rotation(); dbg.nospace() << ", Tangential Pressure: " << info.tangentialPressure(); dbg.nospace() << ", Perspective: " << info.perspective(); dbg.nospace() << ", Drawing Angle: " << info.drawingAngle(); dbg.nospace() << ", Drawing Speed: " << info.drawingSpeed(); dbg.nospace() << ", Drawing Distance: " << info.drawingDistance(); dbg.nospace() << ", Time: " << info.currentTime(); #endif return dbg.space(); } KisPaintInformation KisPaintInformation::mixOnlyPosition(qreal t, const KisPaintInformation& mixedPi, const KisPaintInformation& basePi) { QPointF pt = (1 - t) * mixedPi.pos() + t * basePi.pos(); return mixImpl(pt, t, mixedPi, basePi, true, false); } KisPaintInformation KisPaintInformation::mix(qreal t, const KisPaintInformation& pi1, const KisPaintInformation& pi2) { QPointF pt = (1 - t) * pi1.pos() + t * pi2.pos(); return mix(pt, t, pi1, pi2); } KisPaintInformation KisPaintInformation::mix(const QPointF& p, qreal t, const KisPaintInformation& pi1, const KisPaintInformation& pi2) { return mixImpl(p, t, pi1, pi2, false, true); } KisPaintInformation KisPaintInformation::mixWithoutTime(qreal t, const KisPaintInformation& pi1, const KisPaintInformation& pi2) { QPointF pt = (1 - t) * pi1.pos() + t * pi2.pos(); return mixWithoutTime(pt, t, pi1, pi2); } KisPaintInformation KisPaintInformation::mixWithoutTime(const QPointF& p, qreal t, const KisPaintInformation& pi1, const KisPaintInformation& pi2) { return mixImpl(p, t, pi1, pi2, false, false); } void KisPaintInformation::mixOtherOnlyPosition(qreal t, const KisPaintInformation& other) { QPointF pt = (1 - t) * other.pos() + t * this->pos(); this->mixOtherImpl(pt, t, other, true, false); } void KisPaintInformation::mixOtherWithoutTime(qreal t, const KisPaintInformation& other) { QPointF pt = (1 - t) * other.pos() + t * this->pos(); this->mixOtherImpl(pt, t, other, false, false); } KisPaintInformation KisPaintInformation::mixImpl(const QPointF &p, qreal t, const KisPaintInformation &pi1, const KisPaintInformation &pi2, bool posOnly, bool mixTime) { KisPaintInformation result(pi2); result.mixOtherImpl(p, t, pi1, posOnly, mixTime); return result; } void KisPaintInformation::mixOtherImpl(const QPointF &p, qreal t, const KisPaintInformation &other, bool posOnly, bool mixTime) { if (posOnly) { this->d->pos = p; this->d->isHoveringMode = false; - this->d->currentDistanceInfo = 0; this->d->levelOfDetail = 0; return; } else { qreal pressure = (1 - t) * other.pressure() + t * this->pressure(); qreal xTilt = (1 - t) * other.xTilt() + t * this->xTilt(); qreal yTilt = (1 - t) * other.yTilt() + t * this->yTilt(); qreal rotation = other.rotation(); if (other.rotation() != this->rotation()) { qreal a1 = kisDegreesToRadians(other.rotation()); qreal a2 = kisDegreesToRadians(this->rotation()); qreal distance = shortestAngularDistance(a2, a1); rotation = kisRadiansToDegrees(incrementInDirection(a1, t * distance, a2)); } qreal tangentialPressure = (1 - t) * other.tangentialPressure() + t * this->tangentialPressure(); qreal perspective = (1 - t) * other.perspective() + t * this->perspective(); qreal time = mixTime ? ((1 - t) * other.currentTime() + t * this->currentTime()) : this->currentTime(); qreal speed = (1 - t) * other.drawingSpeed() + t * this->drawingSpeed(); KIS_ASSERT_RECOVER_NOOP(other.isHoveringMode() == this->isHoveringMode()); *(this->d) = Private(p, pressure, xTilt, yTilt, rotation, tangentialPressure, perspective, time, speed, other.isHoveringMode()); this->d->randomSource = other.d->randomSource; + this->d->perStrokeRandomSource = other.d->perStrokeRandomSource; // this->d->isHoveringMode = other.isHoveringMode(); - this->d->currentDistanceInfo = 0; this->d->levelOfDetail = other.d->levelOfDetail; } } qreal KisPaintInformation::tiltDirection(const KisPaintInformation& info, bool normalize) { qreal xTilt = info.xTilt(); qreal yTilt = info.yTilt(); // radians -PI, PI qreal tiltDirection = atan2(-xTilt, yTilt); // if normalize is true map to 0.0..1.0 return normalize ? (tiltDirection / (2 * M_PI) + 0.5) : tiltDirection; } qreal KisPaintInformation::tiltElevation(const KisPaintInformation& info, qreal maxTiltX, qreal maxTiltY, bool normalize) { qreal xTilt = qBound(qreal(-1.0), info.xTilt() / maxTiltX , qreal(1.0)); qreal yTilt = qBound(qreal(-1.0), info.yTilt() / maxTiltY , qreal(1.0)); qreal e; if (fabs(xTilt) > fabs(yTilt)) { e = sqrt(qreal(1.0) + yTilt * yTilt); } else { e = sqrt(qreal(1.0) + xTilt * xTilt); } qreal cosAlpha = sqrt(xTilt * xTilt + yTilt * yTilt) / e; qreal tiltElevation = acos(cosAlpha); // in radians in [0, 0.5 * PI] // mapping to 0.0..1.0 if normalize is true return normalize ? (tiltElevation / (M_PI * qreal(0.5))) : tiltElevation; } diff --git a/libs/image/brushengine/kis_paint_information.h b/libs/image/brushengine/kis_paint_information.h index 7a6ab7a274..2ff0a1548a 100644 --- a/libs/image/brushengine/kis_paint_information.h +++ b/libs/image/brushengine/kis_paint_information.h @@ -1,295 +1,309 @@ /* * Copyright (c) 2004 Cyrille Berger * Copyright (c) 2006 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_PAINT_INFORMATION_ #define _KIS_PAINT_INFORMATION_ #include #include #include "kis_global.h" #include "kritaimage_export.h" #include #include "kis_random_source.h" +#include "KisPerStrokeRandomSource.h" #include "kis_spacing_information.h" #include "kis_timing_information.h" class QDomDocument; class QDomElement; class KisDistanceInformation; /** * KisPaintInformation contains information about the input event that * causes the brush action to happen to the brush engine's paint * methods. * * XXX: we directly pass the KoPointerEvent x and y tilt to * KisPaintInformation, and their range is -60 to +60! * * @param pos: the position of the paint event in subpixel accuracy * @param pressure: the pressure of the stylus * @param xTilt: the angle between the device (a pen, for example) and * the perpendicular in the direction of the x axis. Positive values * are towards the bottom of the tablet. The angle is within the range * 0 to 1 * @param yTilt: the angle between the device (a pen, for example) and * the perpendicular in the direction of the y axis. Positive values * are towards the bottom of the tablet. The angle is within the range * 0 to . * @param movement: current position minus the last position of the call to paintAt * @param rotation * @param tangentialPressure * @param perspective **/ class KRITAIMAGE_EXPORT KisPaintInformation { public: /** * Note, that this class is relied on the compiler optimization * of the return value. So if it doesn't work for some reason, * please implement a proper copy c-tor */ class KRITAIMAGE_EXPORT DistanceInformationRegistrar { public: DistanceInformationRegistrar(KisPaintInformation *_p, KisDistanceInformation *distanceInfo); ~DistanceInformationRegistrar(); private: KisPaintInformation *p; }; public: /** * Create a new KisPaintInformation object. */ KisPaintInformation(const QPointF & pos, qreal pressure, qreal xTilt, qreal yTilt, qreal rotation, qreal tangentialPressure, qreal perspective, qreal time, qreal speed); KisPaintInformation(const QPointF & pos, qreal pressure, qreal xTilt, qreal yTilt, qreal rotation); KisPaintInformation(const QPointF & pos = QPointF(), qreal pressure = PRESSURE_DEFAULT); KisPaintInformation(const KisPaintInformation& rhs); void operator=(const KisPaintInformation& rhs); ~KisPaintInformation(); template void paintAt(PaintOp &op, KisDistanceInformation *distanceInfo) { KisSpacingInformation spacingInfo; KisTimingInformation timingInfo; { DistanceInformationRegistrar r = registerDistanceInformation(distanceInfo); spacingInfo = op.paintAt(*this); timingInfo = op.updateTimingImpl(*this); + + // Initiate the process of locking the drawing angle. The locked value will + // always be present in the internals, but it will be requested but the users + // with a special parameter of drawingAngle() only. + if (!this->isHoveringMode()) { + distanceInfo->lockCurrentDrawingAngle(*this); + } } distanceInfo->registerPaintedDab(*this, spacingInfo, timingInfo); } const QPointF& pos() const; void setPos(const QPointF& p); /// The pressure of the value (from 0.0 to 1.0) qreal pressure() const; /// Set the pressure void setPressure(qreal p); /// The tilt of the pen on the horizontal axis (from 0.0 to 1.0) qreal xTilt() const; /// The tilt of the pen on the vertical axis (from 0.0 to 1.0) qreal yTilt() const; /// XXX !!! :-| Please add dox! void overrideDrawingAngle(qreal angle); /// XXX !!! :-| Please add dox! qreal drawingAngleSafe(const KisDistanceInformation &distance) const; /** * Causes the specified distance information to be temporarily registered with this * KisPaintInformation object, so that the KisPaintInformation can compute certain values that * may be needed at painting time, such as the drawing direction. When the returned object is * destroyed, the KisDistanceInformation will be unregistered. At most one * KisDistanceInformation can be registered with a given KisPaintInformation at a time. */ DistanceInformationRegistrar registerDistanceInformation(KisDistanceInformation *distance); /** * Current brush direction computed from the cursor movement * * WARNING: this method is available *only* inside paintAt() call, * that is when the distance information is registered. */ - qreal drawingAngle() const; - - /** - * Lock current drawing angle for the rest of the stroke. If some - * value has already been locked, \p alpha shown the coefficient - * with which the new velue should be blended in. - */ - void lockCurrentDrawingAngle(qreal alpha) const; + qreal drawingAngle(bool considerLockedAngle = false) const; /** * Current brush direction vector computed from the cursor movement * * WARNING: this method is available *only* inside paintAt() call, * that is when the distance information is registered. */ QPointF drawingDirectionVector() const; /** * Current brush speed computed from the cursor movement * * WARNING: this method is available *only* inside paintAt() call, * that is when the distance information is registered. */ qreal drawingSpeed() const; /** * Current distance from the previous dab * * WARNING: this method is available *only* inside paintAt() call, * that is when the distance information is registered. */ qreal drawingDistance() const; /// rotation as given by the tablet event qreal rotation() const; /// tangential pressure (i.e., rate for an airbrush device) qreal tangentialPressure() const; /// reciprocal of distance on the perspective grid qreal perspective() const; /// Number of ms since the beginning of the stroke qreal currentTime() const; + /// Number of dabs painted since the beginning of the stroke + int currentDabSeqNo() const; + + /// The length of the stroke **before** painting the curent dab + qreal totalStrokeLength() const; + // random source for generating in-stroke effects KisRandomSourceSP randomSource() const; // the stroke should initialize random source of all the used // paint info objects, otherwise it shows a warning void setRandomSource(KisRandomSourceSP value); + // random source for generating in-stroke effects, generates one(!) value per stroke + KisPerStrokeRandomSourceSP perStrokeRandomSource() const; + + // the stroke should initialize per stroke random source of all the used + // paint info objects, otherwise it shows a warning + void setPerStrokeRandomSource(KisPerStrokeRandomSourceSP value); + // set level of detail which info object has been generated for void setLevelOfDetail(int levelOfDetail); /** * The paint information may be generated not only during real * stroke when the actual painting is happening, but also when the * cursor is hovering the canvas. In this mode some of the sensors * work a bit differently. The most outstanding example is Fuzzy * sensor, which returns unit value in this mode, otherwise it is * too irritating for a user. * * This value is true only for paint information objects created with * createHoveringModeInfo() constructor. * * \see createHoveringModeInfo() */ bool isHoveringMode() const; /** * Create a fake info object with isHoveringMode() property set to * true. * * \see isHoveringMode() */ static KisPaintInformation createHoveringModeInfo(const QPointF &pos, qreal pressure = PRESSURE_DEFAULT, qreal xTilt = 0.0, qreal yTilt = 0.0, qreal rotation = 0.0, qreal tangentialPressure = 0.0, qreal perspective = 1.0, qreal speed = 0.0, int canvasrotation = 0, bool canvasMirroredH = false); /** *Returns the canvas rotation if that has been given to the kispaintinformation. */ int canvasRotation() const; /** *set the canvas rotation. */ void setCanvasRotation(int rotation); /* *Whether the canvas is mirrored for the paint-operation. */ bool canvasMirroredH() const; /* *Set whether the canvas is mirrored for the paint-operation. */ void setCanvasHorizontalMirrorState(bool mir); void toXML(QDomDocument&, QDomElement&) const; static KisPaintInformation fromXML(const QDomElement&); // TODO: Refactor the static mix functions to non-static in-place mutation // versions like mixOtherOnlyPosition and mixOtherWithoutTime. // Heap allocation on Windows is awfully slow and will fragment the memory // badly. Since KisPaintInformation allocates on the heap, we should re-use // existing instance whenever possible, especially in loops. // Ref: https://phabricator.kde.org/D6578 /// (1-t) * p1 + t * p2 static KisPaintInformation mixOnlyPosition(qreal t, const KisPaintInformation& mixedPi, const KisPaintInformation& basePi); static KisPaintInformation mix(const QPointF& p, qreal t, const KisPaintInformation& p1, const KisPaintInformation& p2); static KisPaintInformation mix(qreal t, const KisPaintInformation& pi1, const KisPaintInformation& pi2); static KisPaintInformation mixWithoutTime(const QPointF &p, qreal t, const KisPaintInformation &p1, const KisPaintInformation &p2); static KisPaintInformation mixWithoutTime(qreal t, const KisPaintInformation &pi1, const KisPaintInformation &pi2); void mixOtherOnlyPosition(qreal t, const KisPaintInformation& other); void mixOtherWithoutTime(qreal t, const KisPaintInformation& other); static qreal tiltDirection(const KisPaintInformation& info, bool normalize = true); static qreal tiltElevation(const KisPaintInformation& info, qreal maxTiltX = 60.0, qreal maxTiltY = 60.0, bool normalize = true); private: static KisPaintInformation mixImpl(const QPointF &p, qreal t, const KisPaintInformation &p1, const KisPaintInformation &p2, bool posOnly, bool mixTime); void mixOtherImpl(const QPointF &p, qreal t, const KisPaintInformation &other, bool posOnly, bool mixTime); private: struct Private; Private* const d; }; KRITAIMAGE_EXPORT QDebug operator<<(QDebug debug, const KisPaintInformation& info); #endif diff --git a/libs/image/brushengine/kis_paintop.cc b/libs/image/brushengine/kis_paintop.cc index 24b1df7d35..6c6571c618 100644 --- a/libs/image/brushengine/kis_paintop.cc +++ b/libs/image/brushengine/kis_paintop.cc @@ -1,205 +1,211 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2004 Boudewijn Rempt * Copyright (c) 2004 Clarence Dang * Copyright (c) 2004 Adrian Page * Copyright (c) 2004,2007,2010 Cyrille Berger * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_paintop.h" #include #include #include #include #include "kis_painter.h" #include "kis_layer.h" #include "kis_image.h" #include "kis_paint_device.h" #include "kis_global.h" #include "kis_datamanager.h" #include #include #include "kis_vec.h" #include "kis_perspective_math.h" #include "kis_fixed_paint_device.h" #include "kis_paintop_utils.h" #define BEZIER_FLATNESS_THRESHOLD 0.5 #include #include struct Q_DECL_HIDDEN KisPaintOp::Private { Private(KisPaintOp *_q) : q(_q), dab(0), fanCornersEnabled(false), fanCornersStep(1.0) {} KisPaintOp *q; KisFixedPaintDeviceSP dab; KisPainter* painter; bool fanCornersEnabled; qreal fanCornersStep; }; KisPaintOp::KisPaintOp(KisPainter * painter) : d(new Private(this)) { d->painter = painter; } KisPaintOp::~KisPaintOp() { d->dab.clear(); delete d; } KisFixedPaintDeviceSP KisPaintOp::cachedDab() { return cachedDab(d->painter->device()->colorSpace()); } KisFixedPaintDeviceSP KisPaintOp::cachedDab(const KoColorSpace *cs) { if (!d->dab || *d->dab->colorSpace() != *cs) { d->dab = new KisFixedPaintDevice(cs); } return d->dab; } void KisPaintOp::setFanCornersInfo(bool fanCornersEnabled, qreal fanCornersStep) { d->fanCornersEnabled = fanCornersEnabled; d->fanCornersStep = fanCornersStep; } void KisPaintOp::splitCoordinate(qreal coordinate, qint32 *whole, qreal *fraction) { const qint32 i = std::floor(coordinate); const qreal f = coordinate - i; *whole = i; *fraction = f; } +int KisPaintOp::doAsyncronousUpdate(QVector &jobs) +{ + Q_UNUSED(jobs); + return 40; +} + static void paintBezierCurve(KisPaintOp *paintOp, const KisPaintInformation &pi1, const KisVector2D &control1, const KisVector2D &control2, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance) { LineEquation line = LineEquation::Through(toKisVector2D(pi1.pos()), toKisVector2D(pi2.pos())); qreal d1 = line.absDistance(control1); qreal d2 = line.absDistance(control2); if ((d1 < BEZIER_FLATNESS_THRESHOLD && d2 < BEZIER_FLATNESS_THRESHOLD) || qIsNaN(d1) || qIsNaN(d2)) { paintOp->paintLine(pi1, pi2, currentDistance); } else { // Midpoint subdivision. See Foley & Van Dam Computer Graphics P.508 KisVector2D l2 = (toKisVector2D(pi1.pos()) + control1) / 2; KisVector2D h = (control1 + control2) / 2; KisVector2D l3 = (l2 + h) / 2; KisVector2D r3 = (control2 + toKisVector2D(pi2.pos())) / 2; KisVector2D r2 = (h + r3) / 2; KisVector2D l4 = (l3 + r2) / 2; KisPaintInformation middlePI = KisPaintInformation::mix(toQPointF(l4), 0.5, pi1, pi2); paintBezierCurve(paintOp, pi1, l2, l3, middlePI, currentDistance); paintBezierCurve(paintOp, middlePI, r2, r3, pi2, currentDistance); } } void KisPaintOp::paintBezierCurve(const KisPaintInformation &pi1, const QPointF &control1, const QPointF &control2, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance) { return ::paintBezierCurve(this, pi1, toKisVector2D(control1), toKisVector2D(control2), pi2, currentDistance); } void KisPaintOp::paintLine(const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance) { KisPaintOpUtils::paintLine(*this, pi1, pi2, currentDistance, d->fanCornersEnabled, d->fanCornersStep); } void KisPaintOp::paintAt(const KisPaintInformation& info, KisDistanceInformation *currentDistance) { Q_ASSERT(currentDistance); KisPaintInformation pi(info); pi.paintAt(*this, currentDistance); } void KisPaintOp::updateSpacing(const KisPaintInformation &info, KisDistanceInformation ¤tDistance) const { KisPaintInformation pi(info); KisSpacingInformation spacingInfo; { KisPaintInformation::DistanceInformationRegistrar r = pi.registerDistanceInformation(¤tDistance); spacingInfo = updateSpacingImpl(pi); } currentDistance.updateSpacing(spacingInfo); } void KisPaintOp::updateTiming(const KisPaintInformation &info, KisDistanceInformation ¤tDistance) const { KisPaintInformation pi(info); KisTimingInformation timingInfo; { KisPaintInformation::DistanceInformationRegistrar r = pi.registerDistanceInformation(¤tDistance); timingInfo = updateTimingImpl(pi); } currentDistance.updateTiming(timingInfo); } KisTimingInformation KisPaintOp::updateTimingImpl(const KisPaintInformation &info) const { Q_UNUSED(info); return KisTimingInformation(); } KisPainter* KisPaintOp::painter() const { return d->painter; } KisPaintDeviceSP KisPaintOp::source() const { return d->painter->device(); } diff --git a/libs/image/brushengine/kis_paintop.h b/libs/image/brushengine/kis_paintop.h index e97639249b..f1365270a9 100644 --- a/libs/image/brushengine/kis_paintop.h +++ b/libs/image/brushengine/kis_paintop.h @@ -1,155 +1,165 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2004 Boudewijn Rempt * Copyright (c) 2004 Clarence Dang * Copyright (c) 2004 Adrian Page * Copyright (c) 2004,2010 Cyrille Berger * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_PAINTOP_H_ #define KIS_PAINTOP_H_ #include #include "kis_shared.h" #include "kis_types.h" #include class QPointF; class KoColorSpace; class KisPainter; class KisPaintInformation; +class KisRunnableStrokeJobData; /** * KisPaintOp are use by tools to draw on a paint device. A paintop takes settings * and input information, like pressure, tilt or motion and uses that to draw pixels */ class KRITAIMAGE_EXPORT KisPaintOp : public KisShared { struct Private; public: KisPaintOp(KisPainter * painter); virtual ~KisPaintOp(); /** * Paint at the subpixel point pos using the specified paint information.. * * The distance/time between two calls of the paintAt is always specified by spacing and timing, * which are automatically saved into the current distance information object. */ void paintAt(const KisPaintInformation& info, KisDistanceInformation *currentDistance); /** * Updates the spacing settings in currentDistance based on the provided information. Note that * the spacing is updated automatically in the paintAt method, so there is no need to call this * method if paintAt has just been called. */ void updateSpacing(const KisPaintInformation &info, KisDistanceInformation ¤tDistance) const; /** * Updates the timing settings in currentDistance based on the provided information. Note that * the timing is updated automatically in the paintAt method, so there is no need to call this * method if paintAt has just been called. */ void updateTiming(const KisPaintInformation &info, KisDistanceInformation ¤tDistance) const; /** * Draw a line between pos1 and pos2 using the currently set brush and color. * If savedDist is less than zero, the brush is painted at pos1 before being * painted along the line using the spacing setting. * * @return the drag distance, that is the remains of the distance * between p1 and p2 not covered because the currenlty set brush * has a spacing greater than that distance. */ virtual void paintLine(const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance); /** * Draw a Bezier curve between pos1 and pos2 using control points 1 and 2. * If savedDist is less than zero, the brush is painted at pos1 before being * painted along the curve using the spacing setting. * @return the drag distance, that is the remains of the distance between p1 and p2 not covered * because the currenlty set brush has a spacing greater than that distance. */ virtual void paintBezierCurve(const KisPaintInformation &pi1, const QPointF &control1, const QPointF &control2, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance); /** * Whether this paintop can paint. Can be false in case that some setting isn't read correctly. * @return if paintop is ready for painting, default is true */ virtual bool canPaint() const { return true; } /** * Split the coordinate into whole + fraction, where fraction is always >= 0. */ static void splitCoordinate(qreal coordinate, qint32 *whole, qreal *fraction); + /** + * If the preset supports asynchronous updates, then the stroke execution core will + * call this method with a desured frame rate. The jobs that should be run to prepare the update + * are returned via \p jobs + * + * @return the desired FPS rate (period of updates) + */ + virtual int doAsyncronousUpdate(QVector &jobs); + protected: friend class KisPaintInformation; /** * The implementation of painting of a dab and updating spacing. This does NOT need to update * the timing information. */ virtual KisSpacingInformation paintAt(const KisPaintInformation& info) = 0; /** * Implementation of a spacing update */ virtual KisSpacingInformation updateSpacingImpl(const KisPaintInformation &info) const = 0; /** * Implementation of a timing update. The default implementation always disables timing. This is * suitable for paintops that do not support airbrushing. */ virtual KisTimingInformation updateTimingImpl(const KisPaintInformation &info) const; KisFixedPaintDeviceSP cachedDab(); KisFixedPaintDeviceSP cachedDab(const KoColorSpace *cs); /** * Return the painter this paintop is owned by */ KisPainter* painter() const; /** * Return the paintdevice the painter this paintop is owned by */ KisPaintDeviceSP source() const; private: friend class KisPressureRotationOption; void setFanCornersInfo(bool fanCornersEnabled, qreal fanCornersStep); private: Private* const d; }; #endif // KIS_PAINTOP_H_ diff --git a/libs/image/brushengine/kis_paintop_settings.cpp b/libs/image/brushengine/kis_paintop_settings.cpp index c6680e195a..6702f91497 100644 --- a/libs/image/brushengine/kis_paintop_settings.cpp +++ b/libs/image/brushengine/kis_paintop_settings.cpp @@ -1,456 +1,461 @@ /* * Copyright (c) 2007 Boudewijn Rempt * Copyright (c) 2008 Lukáš Tvrdý * Copyright (c) 2014 Mohit Goyal * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include #include #include "kis_paint_layer.h" #include "kis_image.h" #include "kis_painter.h" #include "kis_paint_device.h" #include "kis_paintop_registry.h" #include "kis_timing_information.h" #include #include "kis_paintop_config_widget.h" #include #include "kis_paintop_settings_update_proxy.h" #include #include #include #include #include struct Q_DECL_HIDDEN KisPaintOpSettings::Private { Private() : disableDirtyNotifications(false) {} QPointer settingsWidget; QString modelName; KisPaintOpPresetWSP preset; QList uniformProperties; bool disableDirtyNotifications; class DirtyNotificationsLocker { public: DirtyNotificationsLocker(KisPaintOpSettings::Private *d) : m_d(d), m_oldNotificationsState(d->disableDirtyNotifications) { m_d->disableDirtyNotifications = true; } ~DirtyNotificationsLocker() { m_d->disableDirtyNotifications = m_oldNotificationsState; } private: KisPaintOpSettings::Private *m_d; bool m_oldNotificationsState; Q_DISABLE_COPY(DirtyNotificationsLocker) }; KisPaintopSettingsUpdateProxy* updateProxyNoCreate() const { auto presetSP = preset.toStrongRef(); return presetSP ? presetSP->updateProxyNoCreate() : 0; } KisPaintopSettingsUpdateProxy* updateProxyCreate() const { auto presetSP = preset.toStrongRef(); return presetSP ? presetSP->updateProxy() : 0; } }; KisPaintOpSettings::KisPaintOpSettings() : d(new Private) { d->preset = 0; } KisPaintOpSettings::~KisPaintOpSettings() { } KisPaintOpSettings::KisPaintOpSettings(const KisPaintOpSettings &rhs) : KisPropertiesConfiguration(rhs) , d(new Private) { d->settingsWidget = 0; d->preset = rhs.preset(); d->modelName = rhs.modelName(); } void KisPaintOpSettings::setOptionsWidget(KisPaintOpConfigWidget* widget) { d->settingsWidget = widget; } void KisPaintOpSettings::setPreset(KisPaintOpPresetWSP preset) { d->preset = preset; } KisPaintOpPresetWSP KisPaintOpSettings::preset() const { return d->preset; } bool KisPaintOpSettings::mousePressEvent(const KisPaintInformation &paintInformation, Qt::KeyboardModifiers modifiers, KisNodeWSP currentNode) { Q_UNUSED(modifiers); Q_UNUSED(currentNode); setRandomOffset(paintInformation); return true; // ignore the event by default } void KisPaintOpSettings::setRandomOffset(const KisPaintInformation &paintInformation) { if (getBool("Texture/Pattern/Enabled")) { if (getBool("Texture/Pattern/isRandomOffsetX")) { setProperty("Texture/Pattern/OffsetX", paintInformation.randomSource()->generate(0, KisPropertiesConfiguration::getInt("Texture/Pattern/MaximumOffsetX"))); } if (getBool("Texture/Pattern/isRandomOffsetY")) { setProperty("Texture/Pattern/OffsetY", paintInformation.randomSource()->generate(0, KisPropertiesConfiguration::getInt("Texture/Pattern/MaximumOffsetY"))); } } } KisPaintOpSettingsSP KisPaintOpSettings::clone() const { QString paintopID = getString("paintop"); if (paintopID.isEmpty()) return 0; KisPaintOpSettingsSP settings = KisPaintOpRegistry::instance()->settings(KoID(paintopID, "")); QMapIterator i(getProperties()); while (i.hasNext()) { i.next(); settings->setProperty(i.key(), QVariant(i.value())); } settings->setPreset(this->preset()); return settings; } void KisPaintOpSettings::activate() { } void KisPaintOpSettings::setPaintOpOpacity(qreal value) { KisLockedPropertiesProxySP proxy( KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(this)); proxy->setProperty("OpacityValue", value); } void KisPaintOpSettings::setPaintOpFlow(qreal value) { KisLockedPropertiesProxySP proxy( KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(this)); proxy->setProperty("FlowValue", value); } void KisPaintOpSettings::setPaintOpCompositeOp(const QString &value) { KisLockedPropertiesProxySP proxy( KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(this)); proxy->setProperty("CompositeOp", value); } qreal KisPaintOpSettings::paintOpOpacity() { KisLockedPropertiesProxySP proxy = KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(this); return proxy->getDouble("OpacityValue", 1.0); } qreal KisPaintOpSettings::paintOpFlow() { KisLockedPropertiesProxySP proxy( KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(this)); return proxy->getDouble("FlowValue", 1.0); } QString KisPaintOpSettings::paintOpCompositeOp() { KisLockedPropertiesProxySP proxy( KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(this)); return proxy->getString("CompositeOp", COMPOSITE_OVER); } void KisPaintOpSettings::setEraserMode(bool value) { KisLockedPropertiesProxySP proxy( KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(this)); proxy->setProperty("EraserMode", value); } bool KisPaintOpSettings::eraserMode() { KisLockedPropertiesProxySP proxy( KisLockedPropertiesServer::instance()->createLockedPropertiesProxy(this)); return proxy->getBool("EraserMode", false); } QString KisPaintOpSettings::effectivePaintOpCompositeOp() { return !eraserMode() ? paintOpCompositeOp() : COMPOSITE_ERASE; } qreal KisPaintOpSettings::savedEraserSize() const { return getDouble("SavedEraserSize", 0.0); } void KisPaintOpSettings::setSavedEraserSize(qreal value) { setProperty("SavedEraserSize", value); setPropertyNotSaved("SavedEraserSize"); } qreal KisPaintOpSettings::savedBrushSize() const { return getDouble("SavedBrushSize", 0.0); } void KisPaintOpSettings::setSavedBrushSize(qreal value) { setProperty("SavedBrushSize", value); setPropertyNotSaved("SavedBrushSize"); } qreal KisPaintOpSettings::savedEraserOpacity() const { return getDouble("SavedEraserOpacity", 0.0); } void KisPaintOpSettings::setSavedEraserOpacity(qreal value) { setProperty("SavedEraserOpacity", value); setPropertyNotSaved("SavedEraserOpacity"); } qreal KisPaintOpSettings::savedBrushOpacity() const { return getDouble("SavedBrushOpacity", 0.0); } void KisPaintOpSettings::setSavedBrushOpacity(qreal value) { setProperty("SavedBrushOpacity", value); setPropertyNotSaved("SavedBrushOpacity"); } QString KisPaintOpSettings::modelName() const { return d->modelName; } void KisPaintOpSettings::setModelName(const QString & modelName) { d->modelName = modelName; } KisPaintOpConfigWidget* KisPaintOpSettings::optionsWidget() const { if (d->settingsWidget.isNull()) return 0; return d->settingsWidget.data(); } bool KisPaintOpSettings::isValid() const { return true; } bool KisPaintOpSettings::isLoadable() { return isValid(); } QString KisPaintOpSettings::indirectPaintingCompositeOp() const { return COMPOSITE_ALPHA_DARKEN; } bool KisPaintOpSettings::isAirbrushing() const { return getBool(AIRBRUSH_ENABLED, false); } qreal KisPaintOpSettings::airbrushInterval() const { qreal rate = getDouble(AIRBRUSH_RATE, 1.0); if (rate == 0.0) { return LONG_TIME; } else { return 1000.0 / rate; } } bool KisPaintOpSettings::useSpacingUpdates() const { return getBool(SPACING_USE_UPDATES, false); } +bool KisPaintOpSettings::needsAsynchronousUpdates() const +{ + return false; +} + QPainterPath KisPaintOpSettings::brushOutline(const KisPaintInformation &info, OutlineMode mode) { QPainterPath path; if (mode == CursorIsOutline || mode == CursorIsCircleOutline || mode == CursorTiltOutline) { path = ellipseOutline(10, 10, 1.0, 0); if (mode == CursorTiltOutline) { path.addPath(makeTiltIndicator(info, QPointF(0.0, 0.0), 0.0, 2.0)); } path.translate(info.pos()); } return path; } QPainterPath KisPaintOpSettings::ellipseOutline(qreal width, qreal height, qreal scale, qreal rotation) { QPainterPath path; QRectF ellipse(0, 0, width * scale, height * scale); ellipse.translate(-ellipse.center()); path.addEllipse(ellipse); QTransform m; m.reset(); m.rotate(rotation); path = m.map(path); return path; } QPainterPath KisPaintOpSettings::makeTiltIndicator(KisPaintInformation const& info, QPointF const& start, qreal maxLength, qreal angle) { if (maxLength == 0.0) maxLength = 50.0; maxLength = qMax(maxLength, 50.0); qreal const length = maxLength * (1 - info.tiltElevation(info, 60.0, 60.0, true)); qreal const baseAngle = 360.0 - fmod(KisPaintInformation::tiltDirection(info, true) * 360.0 + 270.0, 360.0); QLineF guideLine = QLineF::fromPolar(length, baseAngle + angle); guideLine.translate(start); QPainterPath ret; ret.moveTo(guideLine.p1()); ret.lineTo(guideLine.p2()); guideLine.setAngle(baseAngle - angle); ret.lineTo(guideLine.p2()); ret.lineTo(guideLine.p1()); return ret; } void KisPaintOpSettings::setCanvasRotation(qreal angle) { Private::DirtyNotificationsLocker locker(d.data()); setProperty("runtimeCanvasRotation", angle); setPropertyNotSaved("runtimeCanvasRotation"); } void KisPaintOpSettings::setCanvasMirroring(bool xAxisMirrored, bool yAxisMirrored) { Private::DirtyNotificationsLocker locker(d.data()); setProperty("runtimeCanvasMirroredX", xAxisMirrored); setPropertyNotSaved("runtimeCanvasMirroredX"); setProperty("runtimeCanvasMirroredY", yAxisMirrored); setPropertyNotSaved("runtimeCanvasMirroredY"); } void KisPaintOpSettings::setProperty(const QString & name, const QVariant & value) { if (value != KisPropertiesConfiguration::getProperty(name) && !d->disableDirtyNotifications) { KisPaintOpPresetSP presetSP = preset().toStrongRef(); if (presetSP) { presetSP->setPresetDirty(true); } } KisPropertiesConfiguration::setProperty(name, value); onPropertyChanged(); } void KisPaintOpSettings::onPropertyChanged() { KisPaintopSettingsUpdateProxy *proxy = d->updateProxyNoCreate(); if (proxy) { proxy->notifySettingsChanged(); } } bool KisPaintOpSettings::isLodUserAllowed(const KisPropertiesConfigurationSP config) { return config->getBool("lodUserAllowed", true); } void KisPaintOpSettings::setLodUserAllowed(KisPropertiesConfigurationSP config, bool value) { config->setProperty("lodUserAllowed", value); } #include "kis_standard_uniform_properties_factory.h" QList KisPaintOpSettings::uniformProperties(KisPaintOpSettingsSP settings) { QList props = listWeakToStrong(d->uniformProperties); if (props.isEmpty()) { using namespace KisStandardUniformPropertiesFactory; props.append(createProperty(opacity, settings, d->updateProxyCreate())); props.append(createProperty(size, settings, d->updateProxyCreate())); props.append(createProperty(flow, settings, d->updateProxyCreate())); d->uniformProperties = listStrongToWeak(props); } return props; } diff --git a/libs/image/brushengine/kis_paintop_settings.h b/libs/image/brushengine/kis_paintop_settings.h index 7ed5fb2e5c..bef12bea17 100644 --- a/libs/image/brushengine/kis_paintop_settings.h +++ b/libs/image/brushengine/kis_paintop_settings.h @@ -1,315 +1,321 @@ /* * Copyright (c) 2007 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_PAINTOP_SETTINGS_H_ #define KIS_PAINTOP_SETTINGS_H_ #include "kis_types.h" #include "kritaimage_export.h" #include #include #include "kis_properties_configuration.h" #include #include class KisPaintOpConfigWidget; class KisPaintopSettingsUpdateProxy; /** * Configuration property used to control whether airbrushing is enabled. */ const QString AIRBRUSH_ENABLED = "PaintOpSettings/isAirbrushing"; /** * Configuration property used to control airbrushing rate. The value should be in dabs per second. */ const QString AIRBRUSH_RATE = "PaintOpSettings/rate"; /** * Configuration property used to control whether airbrushing is configured to ignore distance-based * spacing. */ const QString AIRBRUSH_IGNORE_SPACING = "PaintOpSettings/ignoreSpacing"; /** * Configuration property used to control whether the spacing settings can be updated between * painted dabs. */ const QString SPACING_USE_UPDATES = "PaintOpSettings/updateSpacingBetweenDabs"; /** * This class is used to cache the settings for a paintop * between two creations. There is one KisPaintOpSettings per input device (mouse, tablet, * etc...). * * The settings may be stored in a preset or a recorded brush stroke. Note that if your * paintop's settings subclass has data that is not stored as a property, that data is not * saved and restored. * * The object also contains a pointer to its parent KisPaintOpPreset object.This is to control the DirtyPreset * property of KisPaintOpPreset. Whenever the settings are changed/modified from the original -- the preset is * set to dirty. */ class KRITAIMAGE_EXPORT KisPaintOpSettings : public KisPropertiesConfiguration { public: KisPaintOpSettings(); ~KisPaintOpSettings() override; KisPaintOpSettings(const KisPaintOpSettings &rhs); /** * */ virtual void setOptionsWidget(KisPaintOpConfigWidget* widget); /** * This function is called by a tool when the mouse is pressed. It's useful if * the paintop needs mouse interaction for instance in the case of the clone op. * If the tool is supposed to ignore the event, the paint op should return false * and if the tool is supposed to use the event, return true. */ virtual bool mousePressEvent(const KisPaintInformation &paintInformation, Qt::KeyboardModifiers modifiers, KisNodeWSP currentNode); /** * Clone the current settings object. Override this if your settings instance doesn't * store everything as properties. */ virtual KisPaintOpSettingsSP clone() const; /** * @return the node the paintop is working on. */ KisNodeSP node() const; /** * Call this function when the paint op is selected or the tool is activated */ virtual void activate(); /** * XXX: Remove this after 2.0, when the paint operation (incremental/non incremental) will * be completely handled in the paintop, not in the tool. This is a filthy hack to move * the option to the right place, at least. * @return true if we paint incrementally, false if we paint like Photoshop. By default, paintops * do not support non-incremental. */ virtual bool paintIncremental() { return true; } /** * @return the composite op it to which the indirect painting device * should be initialized to. This is used by clone op to reset * the composite op to COMPOSITE_COPY */ virtual QString indirectPaintingCompositeOp() const; /** * Whether this paintop wants to deposit paint even when not moving, i.e. the tool needs to * activate its timer. If this is true, painting updates need to be generated at regular * intervals even in the absence of input device events, e.g. when the cursor is not moving. * * The default implementation checks the property AIRBRUSH_ENABLED, defaulting to false if the * property is not found. This should be suitable for most paintops. */ virtual bool isAirbrushing() const; /** * Indicates the minimum time interval that might be needed between airbrush dabs, in * milliseconds. A lower value means painting updates need to happen more frequently. This value * should be ignored if isAirbrushing() is false. * * The default implementation uses the property AIRBRUSH_RATE, defaulting to an interval of * one second if the property is not found. This should be suitable for most paintops. */ virtual qreal airbrushInterval() const; /** * Indicates whether this configuration allows spacing information to be updated between painted * dabs during a stroke. */ virtual bool useSpacingUpdates() const; + /** + * Indicates if the tool should call paintOp->doAsynchronousUpdate() inbetween + * paintAt() calls to do the asynchronous rendering + */ + virtual bool needsAsynchronousUpdates() const; + /** * This enum defines the current mode for painting an outline. */ enum OutlineMode { CursorIsOutline = 1, ///< When this mode is set, an outline is painted around the cursor CursorIsCircleOutline, CursorNoOutline, CursorTiltOutline, CursorColorOutline }; /** * Returns the brush outline in pixel coordinates. Tool is responsible for conversion into view coordinates. * Outline mode has to be passed to the paintop which builds the outline as some paintops have to paint outline * always like clone paintop indicating the duplicate position */ virtual QPainterPath brushOutline(const KisPaintInformation &info, OutlineMode mode); /** * Helpers for drawing the brush outline */ static QPainterPath ellipseOutline(qreal width, qreal height, qreal scale, qreal rotation); /** * Helper for drawing a triangle representing the tilt of the stylus. * * @param start is the offset from the brush's outline's bounding box * @param lengthScale is used for deciding the size of the triangle. * Brush diameter or width are common choices for this. * @param angle is the angle between the two sides of the triangle. */ static QPainterPath makeTiltIndicator(KisPaintInformation const& info, QPointF const& start, qreal lengthScale, qreal angle); /** * Set paintop opacity directly in the properties */ void setPaintOpOpacity(qreal value); /** * Set paintop flow directly in the properties */ void setPaintOpFlow(qreal value); /** * Set paintop composite mode directly in the properties */ void setPaintOpCompositeOp(const QString &value); /** * @return opacity saved in the properties */ qreal paintOpOpacity(); /** * @return flow saved in the properties */ qreal paintOpFlow(); /** * @return composite mode saved in the properties */ QString paintOpCompositeOp(); /** * Set paintop size directly in the properties */ virtual void setPaintOpSize(qreal value) = 0; /** * @return size saved in the properties */ virtual qreal paintOpSize() const = 0; void setEraserMode(bool value); bool eraserMode(); qreal savedEraserSize() const; void setSavedEraserSize(qreal value); qreal savedBrushSize() const; void setSavedBrushSize(qreal value); qreal savedEraserOpacity() const; void setSavedEraserOpacity(qreal value); qreal savedBrushOpacity() const; void setSavedBrushOpacity(qreal value); QString effectivePaintOpCompositeOp(); void setPreset(KisPaintOpPresetWSP preset); KisPaintOpPresetWSP preset() const; /** * @return filename of the 3D brush model, empty if no brush is set */ virtual QString modelName() const; /** * Set filename of 3D brush model. By default no brush is set */ void setModelName(const QString & modelName); /// Check if the settings are valid, setting might be invalid through missing brushes etc /// Overwrite if the settings of a paintop can be invalid /// @return state of the settings, default implementation is true virtual bool isValid() const; /// Check if the settings are loadable, that might the case if we can fallback to something /// Overwrite if the settings can do some kind of fallback /// @return loadable state of the settings, by default implementation return the same as isValid() virtual bool isLoadable(); /** * These methods are populating properties with runtime * information about canvas rotation/mirroring. This information * is set directly by KisToolFreehand. Later the data is accessed * by the pressure options to make a final decision. */ void setCanvasRotation(qreal angle); void setCanvasMirroring(bool xAxisMirrored, bool yAxisMirrored); /** * Overrides the method in KisPropertiesCofiguration to allow * onPropertyChanged() callback */ void setProperty(const QString & name, const QVariant & value) override; virtual QList uniformProperties(KisPaintOpSettingsSP settings); static bool isLodUserAllowed(const KisPropertiesConfigurationSP config); static void setLodUserAllowed(KisPropertiesConfigurationSP config, bool value); /** * @return the option widget of the paintop (can be 0 is no option widgets is set) */ KisPaintOpConfigWidget* optionsWidget() const; /** * This function is called to set random offsets to the brush whenever the mouse is clicked. It is * specific to when the pattern option is set. * */ virtual void setRandomOffset(const KisPaintInformation &paintInformation); protected: /** * The callback is called every time when a property changes */ virtual void onPropertyChanged(); private: struct Private; const QScopedPointer d; }; #endif diff --git a/libs/image/brushengine/kis_paintop_utils.cpp b/libs/image/brushengine/kis_paintop_utils.cpp new file mode 100644 index 0000000000..b78833b9ab --- /dev/null +++ b/libs/image/brushengine/kis_paintop_utils.cpp @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kis_paintop_utils.h" + +#include "krita_utils.h" +#include "krita_container_utils.h" +#include + +namespace KisPaintOpUtils { + + +KisSpacingInformation effectiveSpacing(qreal dabWidth, qreal dabHeight, qreal extraScale, bool distanceSpacingEnabled, bool isotropicSpacing, qreal rotation, bool axesFlipped, qreal spacingVal, bool autoSpacingActive, qreal autoSpacingCoeff, qreal lodScale) +{ + QPointF spacing; + + if (!isotropicSpacing) { + if (autoSpacingActive) { + spacing = calcAutoSpacing(QPointF(dabWidth, dabHeight), autoSpacingCoeff, lodScale); + } else { + spacing = QPointF(dabWidth, dabHeight); + spacing *= spacingVal; + } + } + else { + qreal significantDimension = qMax(dabWidth, dabHeight); + if (autoSpacingActive) { + significantDimension = calcAutoSpacing(significantDimension, autoSpacingCoeff); + } else { + significantDimension *= spacingVal; + } + spacing = QPointF(significantDimension, significantDimension); + rotation = 0.0; + axesFlipped = false; + } + + spacing *= extraScale; + + return KisSpacingInformation(distanceSpacingEnabled, spacing, rotation, axesFlipped); +} + +KisTimingInformation effectiveTiming(bool timingEnabled, qreal timingInterval, qreal rateExtraScale) +{ + + if (!timingEnabled) { + return KisTimingInformation(); + } + else { + qreal scaledInterval = rateExtraScale <= 0.0 ? LONG_TIME : timingInterval / rateExtraScale; + return KisTimingInformation(scaledInterval); + } +} + +QVector splitAndFilterDabRect(const QRect &totalRect, const QList &dabs, int idealPatchSize) +{ + QVector rects = KritaUtils::splitRectIntoPatches(totalRect, QSize(idealPatchSize,idealPatchSize)); + + KritaUtils::filterContainer(rects, + [dabs] (const QRect &rc) { + Q_FOREACH (const KisRenderedDab &dab, dabs) { + if (dab.realBounds().intersects(rc)) { + return true; + } + } + return false; + }); + return rects; +} + +QVector splitDabsIntoRects(const QList &dabs, int idealNumRects, int diameter, qreal spacing) +{ + QRect totalRect; + + Q_FOREACH (const KisRenderedDab &dab, dabs) { + const QRect rc = dab.realBounds(); + totalRect |= rc; + } + + constexpr int minPatchSize = 128; + constexpr int maxPatchSize = 512; + constexpr int patchStep = 64; + constexpr int halfPatchStep= patchStep >> 1; + + + int idealPatchSize = qBound(minPatchSize, + (int(diameter * (2.0 - spacing)) + halfPatchStep) & ~(patchStep - 1), + maxPatchSize); + + + QVector rects = splitAndFilterDabRect(totalRect, dabs, idealPatchSize); + + while (rects.size() < idealNumRects && idealPatchSize >minPatchSize) { + idealPatchSize = qMax(minPatchSize, idealPatchSize - patchStep); + rects = splitAndFilterDabRect(totalRect, dabs, idealPatchSize); + } + + return rects; +} + + + +} diff --git a/libs/image/brushengine/kis_paintop_utils.h b/libs/image/brushengine/kis_paintop_utils.h index ca53683483..2e1acc84ea 100644 --- a/libs/image/brushengine/kis_paintop_utils.h +++ b/libs/image/brushengine/kis_paintop_utils.h @@ -1,228 +1,203 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_PAINTOP_UTILS_H #define __KIS_PAINTOP_UTILS_H #include "kis_global.h" #include "kis_paint_information.h" #include "kis_distance_information.h" #include "kis_spacing_information.h" #include "kis_timing_information.h" +#include "kritaimage_export.h" + +class KisRenderedDab; + namespace KisPaintOpUtils { template bool paintFan(PaintOp &op, const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance, qreal fanCornersStep) { const qreal angleStep = fanCornersStep; const qreal initialAngle = currentDistance->lastDrawingAngle(); const qreal finalAngle = pi2.drawingAngleSafe(*currentDistance); const qreal fullDistance = shortestAngularDistance(initialAngle, finalAngle); qreal lastAngle = initialAngle; int i = 0; while (shortestAngularDistance(lastAngle, finalAngle) > angleStep) { lastAngle = incrementInDirection(lastAngle, angleStep, finalAngle); qreal t = angleStep * i++ / fullDistance; QPointF pt = pi1.pos() + t * (pi2.pos() - pi1.pos()); KisPaintInformation pi = KisPaintInformation::mix(pt, t, pi1, pi2); pi.overrideDrawingAngle(lastAngle); pi.paintAt(op, currentDistance); } return i; } template void paintLine(PaintOp &op, const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance, bool fanCornersEnabled, qreal fanCornersStep) { QPointF end = pi2.pos(); qreal endTime = pi2.currentTime(); KisPaintInformation pi = pi1; qreal t = 0.0; while ((t = currentDistance->getNextPointPosition(pi.pos(), end, pi.currentTime(), endTime)) >= 0.0) { pi = KisPaintInformation::mix(t, pi, pi2); if (fanCornersEnabled && currentDistance->hasLastPaintInformation()) { paintFan(op, currentDistance->lastPaintInformation(), pi, currentDistance, fanCornersStep); } /** * A bit complicated part to ensure the registration * of the distance information is done in right order */ pi.paintAt(op, currentDistance); } /* * Perform spacing and/or timing updates between dabs if appropriate. Typically, this will not * happen if the above loop actually painted anything. This is because the * getNextPointPosition() call before the paint operation will reset the accumulators in * currentDistance and therefore make needsSpacingUpdate() and needsTimingUpdate() false. The * temporal distance between pi1 and pi2 is typically too small for the accumulators to build * back up enough to require a spacing or timing update after that. (The accumulated time values * are updated not during the paint operation, but during the call to getNextPointPosition(), * that is, updated during every paintLine() call.) */ if (currentDistance->needsSpacingUpdate()) { op.updateSpacing(pi2, *currentDistance); } if (currentDistance->needsTimingUpdate()) { op.updateTiming(pi2, *currentDistance); } } /** * A special class containing the previous position of the cursor for * the sake of painting the outline of the paint op. The main purpose * of this class is to ensure that the saved point does not equal to * the current one, which would cause a outline flicker. To echieve * this the class stores two previosly requested points instead of the * last one. */ -class PositionHistory +class KRITAIMAGE_EXPORT PositionHistory { public: /** * \return the previously used point, which is guaranteed not to * be equal to \p pt and updates the history if needed */ QPointF pushThroughHistory(const QPointF &pt) { QPointF result; const qreal pointSwapThreshold = 7.0; /** * We check x *and* y separately, because events generated by * a mouse device tend to come separately for x and y offsets. * Efficienty generating the 'stairs' pattern. */ if (qAbs(pt.x() - m_second.x()) > pointSwapThreshold && qAbs(pt.y() - m_second.y()) > pointSwapThreshold) { result = m_second; m_first = m_second; m_second = pt; } else { result = m_first; } return result; } private: QPointF m_first; QPointF m_second; }; -bool checkSizeTooSmall(qreal scale, qreal width, qreal height) +inline bool checkSizeTooSmall(qreal scale, qreal width, qreal height) { return scale * width < 0.01 || scale * height < 0.01; } inline qreal calcAutoSpacing(qreal value, qreal coeff) { return coeff * (value < 1.0 ? value : sqrt(value)); } -QPointF calcAutoSpacing(const QPointF &pt, qreal coeff, qreal lodScale) +inline QPointF calcAutoSpacing(const QPointF &pt, qreal coeff, qreal lodScale) { const qreal invLodScale = 1.0 / lodScale; const QPointF lod0Point = invLodScale * pt; return lodScale * QPointF(calcAutoSpacing(lod0Point.x(), coeff), calcAutoSpacing(lod0Point.y(), coeff)); } +KRITAIMAGE_EXPORT KisSpacingInformation effectiveSpacing(qreal dabWidth, qreal dabHeight, qreal extraScale, bool distanceSpacingEnabled, bool isotropicSpacing, qreal rotation, bool axesFlipped, qreal spacingVal, bool autoSpacingActive, qreal autoSpacingCoeff, - qreal lodScale) -{ - QPointF spacing; - - if (!isotropicSpacing) { - if (autoSpacingActive) { - spacing = calcAutoSpacing(QPointF(dabWidth, dabHeight), autoSpacingCoeff, lodScale); - } else { - spacing = QPointF(dabWidth, dabHeight); - spacing *= spacingVal; - } - } - else { - qreal significantDimension = qMax(dabWidth, dabHeight); - if (autoSpacingActive) { - significantDimension = calcAutoSpacing(significantDimension, autoSpacingCoeff); - } else { - significantDimension *= spacingVal; - } - spacing = QPointF(significantDimension, significantDimension); - rotation = 0.0; - axesFlipped = false; - } - - spacing *= extraScale; - - return KisSpacingInformation(distanceSpacingEnabled, spacing, rotation, axesFlipped); -} + qreal lodScale); +KRITAIMAGE_EXPORT KisTimingInformation effectiveTiming(bool timingEnabled, qreal timingInterval, - qreal rateExtraScale) -{ + qreal rateExtraScale); - if (!timingEnabled) { - return KisTimingInformation(); - } - else { - qreal scaledInterval = rateExtraScale <= 0.0 ? LONG_TIME : timingInterval / rateExtraScale; - return KisTimingInformation(scaledInterval); - } -} +KRITAIMAGE_EXPORT +QVector splitAndFilterDabRect(const QRect &totalRect, const QList &dabs, int idealPatchSize); + +KRITAIMAGE_EXPORT +QVector splitDabsIntoRects(const QList &dabs, int idealNumRects, int diameter, qreal spacing); } #endif /* __KIS_PAINTOP_UTILS_H */ diff --git a/libs/image/brushengine/kis_random_source.cpp b/libs/image/brushengine/kis_random_source.cpp index 8a17fecc46..9b28ed3512 100644 --- a/libs/image/brushengine/kis_random_source.cpp +++ b/libs/image/brushengine/kis_random_source.cpp @@ -1,96 +1,97 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_random_source.h" #include #include #include struct KisRandomSource::Private { Private() : uniformSource(qrand()) {} Private(int seed) : uniformSource(seed) {} /** * Taus88's numbers are not too random, but it works fast and it * can be copied very quickly (three 32-bit integers only). * * Average cycle: 2^88 steps */ boost::taus88 uniformSource; }; KisRandomSource::KisRandomSource() : m_d(new Private) { } KisRandomSource::KisRandomSource(int seed) : m_d(new Private(seed)) { } KisRandomSource::KisRandomSource(const KisRandomSource &rhs) : KisShared(), m_d(new Private(*rhs.m_d)) { } KisRandomSource& KisRandomSource::operator=(const KisRandomSource &rhs) { if (this != &rhs) { *m_d = *rhs.m_d; } return *this; } KisRandomSource::~KisRandomSource() { } qint64 KisRandomSource::generate() const { return m_d->uniformSource(); } int KisRandomSource::generate(int min, int max) const { boost::uniform_smallint smallint(min, max); return smallint(m_d->uniformSource); } qreal KisRandomSource::generateNormalized() const { const qint64 v = m_d->uniformSource(); const qint64 max = m_d->uniformSource.max(); + // we don't have min, because taus88 is always positive return qreal(v) / max; } qreal KisRandomSource::generateGaussian(qreal mean, qreal sigma) const { boost::normal_distribution normal(mean, sigma); return normal(m_d->uniformSource); } diff --git a/libs/image/brushengine/kis_stroke_random_source.cpp b/libs/image/brushengine/kis_stroke_random_source.cpp index ce88e3018d..4bdeef23c1 100644 --- a/libs/image/brushengine/kis_stroke_random_source.cpp +++ b/libs/image/brushengine/kis_stroke_random_source.cpp @@ -1,73 +1,83 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_stroke_random_source.h" struct KisStrokeRandomSource::Private { Private() : levelOfDetail(0), lod0RandomSource(new KisRandomSource()), - lodNRandomSource(new KisRandomSource(*lod0RandomSource)) + lodNRandomSource(new KisRandomSource(*lod0RandomSource)), + lod0PerStrokeRandomSource(new KisPerStrokeRandomSource()), + lodNPerStrokeRandomSource(new KisPerStrokeRandomSource(*lod0PerStrokeRandomSource)) { } int levelOfDetail; KisRandomSourceSP lod0RandomSource; KisRandomSourceSP lodNRandomSource; + + KisPerStrokeRandomSourceSP lod0PerStrokeRandomSource; + KisPerStrokeRandomSourceSP lodNPerStrokeRandomSource; }; KisStrokeRandomSource::KisStrokeRandomSource() : m_d(new Private) { } KisStrokeRandomSource::KisStrokeRandomSource(const KisStrokeRandomSource &rhs) : m_d(new Private(*rhs.m_d)) { } KisStrokeRandomSource& KisStrokeRandomSource::operator=(const KisStrokeRandomSource &rhs) { if (&rhs != this) { *m_d = *rhs.m_d; } return *this; } KisStrokeRandomSource::~KisStrokeRandomSource() { } KisRandomSourceSP KisStrokeRandomSource::source() const { return m_d->levelOfDetail ? m_d->lodNRandomSource : m_d->lod0RandomSource; } +KisPerStrokeRandomSourceSP KisStrokeRandomSource::perStrokeSource() const +{ + return m_d->levelOfDetail ? m_d->lodNPerStrokeRandomSource : m_d->lod0PerStrokeRandomSource; +} + int KisStrokeRandomSource::levelOfDetail() const { return m_d->levelOfDetail; } void KisStrokeRandomSource::setLevelOfDetail(int value) { m_d->levelOfDetail = value; } diff --git a/libs/image/brushengine/kis_stroke_random_source.h b/libs/image/brushengine/kis_stroke_random_source.h index bbc4bd1fd2..c6484351b6 100644 --- a/libs/image/brushengine/kis_stroke_random_source.h +++ b/libs/image/brushengine/kis_stroke_random_source.h @@ -1,54 +1,56 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_STROKE_RANDOM_SOURCE_H #define __KIS_STROKE_RANDOM_SOURCE_H #include #include "kritaimage_export.h" #include "kis_random_source.h" +#include "KisPerStrokeRandomSource.h" /** * A helper class to handle multiple KisRandomSource objects in a * stroke strategies. It creates two identical random sources in the * beginning of the stroke, so, when copied through copy-ctor and set * another level of detail starts returning the same sequence of * numbers as was returned for the first stroke. */ class KRITAIMAGE_EXPORT KisStrokeRandomSource { public: KisStrokeRandomSource(); KisStrokeRandomSource(const KisStrokeRandomSource &rhs); KisStrokeRandomSource& operator=(const KisStrokeRandomSource &rhs); ~KisStrokeRandomSource(); KisRandomSourceSP source() const; + KisPerStrokeRandomSourceSP perStrokeSource() const; int levelOfDetail() const; void setLevelOfDetail(int value); private: struct Private; const QScopedPointer m_d; }; #endif /* __KIS_STROKE_RANDOM_SOURCE_H */ diff --git a/libs/image/generator/kis_generator_layer.cpp b/libs/image/generator/kis_generator_layer.cpp index 66a68f52ed..6faf9a7272 100644 --- a/libs/image/generator/kis_generator_layer.cpp +++ b/libs/image/generator/kis_generator_layer.cpp @@ -1,167 +1,167 @@ /* * Copyright (c) 2008 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_generator_layer.h" #include #include "kis_debug.h" #include #include #include "kis_selection.h" #include "filter/kis_filter_configuration.h" #include "kis_processing_information.h" #include "generator/kis_generator_registry.h" #include "generator/kis_generator.h" #include "kis_node_visitor.h" #include "kis_processing_visitor.h" #include "kis_thread_safe_signal_compressor.h" #include "kis_recalculate_generator_layer_job.h" #define UPDATE_DELAY 100 /*ms */ struct Q_DECL_HIDDEN KisGeneratorLayer::Private { Private() : updateSignalCompressor(UPDATE_DELAY, KisSignalCompressor::FIRST_INACTIVE) { } KisThreadSafeSignalCompressor updateSignalCompressor; }; KisGeneratorLayer::KisGeneratorLayer(KisImageWSP image, const QString &name, KisFilterConfigurationSP kfc, KisSelectionSP selection) : KisSelectionBasedLayer(image, name, selection, kfc, true), m_d(new Private) { connect(&m_d->updateSignalCompressor, SIGNAL(timeout()), SLOT(slotDelayedStaticUpdate())); update(); } KisGeneratorLayer::KisGeneratorLayer(const KisGeneratorLayer& rhs) : KisSelectionBasedLayer(rhs), m_d(new Private) { connect(&m_d->updateSignalCompressor, SIGNAL(timeout()), SLOT(slotDelayedStaticUpdate())); } KisGeneratorLayer::~KisGeneratorLayer() { } void KisGeneratorLayer::setFilter(KisFilterConfigurationSP filterConfig) { KisSelectionBasedLayer::setFilter(filterConfig); update(); } void KisGeneratorLayer::slotDelayedStaticUpdate() { /** * The mask might have been deleted from the layers stack in the * meanwhile. Just ignore the updates in the case. */ KisLayerSP parentLayer(qobject_cast(parent().data())); if (!parentLayer) return; KisImageSP image = parentLayer->image(); if (image) { image->addSpontaneousJob(new KisRecalculateGeneratorLayerJob(KisGeneratorLayerSP(this))); } } void KisGeneratorLayer::update() { KisFilterConfigurationSP filterConfig = filter(); if (!filterConfig) { warnImage << "BUG: No Filter configuration in KisGeneratorLayer"; return; } KisGeneratorSP f = KisGeneratorRegistry::instance()->value(filterConfig->name()); if (!f) return; QRect processRect = exactBounds(); resetCache(); KisPaintDeviceSP originalDevice = original(); KisProcessingInformation dstCfg(originalDevice, processRect.topLeft(), KisSelectionSP()); f->generate(dstCfg, processRect.size(), filterConfig.data()); // hack alert! // this avoids cyclic loop with KisRecalculateGeneratorLayerJob::run() KisSelectionBasedLayer::setDirty(extent()); } bool KisGeneratorLayer::accept(KisNodeVisitor & v) { return v.visit(this); } void KisGeneratorLayer::accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) { return visitor.visit(this, undoAdapter); } QIcon KisGeneratorLayer::icon() const { return KisIconUtils::loadIcon("fillLayer"); } KisBaseNode::PropertyList KisGeneratorLayer::sectionModelProperties() const { KisFilterConfigurationSP filterConfig = filter(); KisBaseNode::PropertyList l = KisLayer::sectionModelProperties(); l << KisBaseNode::Property(KoID("generator", i18n("Generator")), KisGeneratorRegistry::instance()->value(filterConfig->name())->name()); return l; } void KisGeneratorLayer::setX(qint32 x) { KisSelectionBasedLayer::setX(x); m_d->updateSignalCompressor.start(); } void KisGeneratorLayer::setY(qint32 y) { KisSelectionBasedLayer::setY(y); m_d->updateSignalCompressor.start(); } -void KisGeneratorLayer::setDirty(const QRect & rect) +void KisGeneratorLayer::setDirty(const QVector &rects) { - KisSelectionBasedLayer::setDirty(rect); + KisSelectionBasedLayer::setDirty(rects); m_d->updateSignalCompressor.start(); } diff --git a/libs/image/generator/kis_generator_layer.h b/libs/image/generator/kis_generator_layer.h index eab61dabf4..757b464d52 100644 --- a/libs/image/generator/kis_generator_layer.h +++ b/libs/image/generator/kis_generator_layer.h @@ -1,90 +1,90 @@ /* * Copyright (c) 2008 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_GENERATOR_LAYER_H_ #define KIS_GENERATOR_LAYER_H_ #include "kis_selection_based_layer.h" #include #include class KisFilterConfiguration; /** * A generator layer is a special kind of layer that can be prefilled * with some pixel pattern generated by a KisGenerator plugin. * A KisGenerator is similar to a filter, but doesn't take * input pixel data and creates new pixel data. * * It is not possible to destructively paint on a generator layer. * * XXX: what about threadedness? */ class KRITAIMAGE_EXPORT KisGeneratorLayer : public KisSelectionBasedLayer { Q_OBJECT public: /** * Create a new Generator layer with the given configuration * and selection. Note that the selection will be _copied_ * (using COW, though). */ KisGeneratorLayer(KisImageWSP image, const QString &name, KisFilterConfigurationSP kfc, KisSelectionSP selection); KisGeneratorLayer(const KisGeneratorLayer& rhs); ~KisGeneratorLayer() override; KisNodeSP clone() const override { return KisNodeSP(new KisGeneratorLayer(*this)); } void setFilter(KisFilterConfigurationSP filterConfig) override; bool accept(KisNodeVisitor &) override; void accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) override; QIcon icon() const override; KisBaseNode::PropertyList sectionModelProperties() const override; /** * re-run the generator. This happens over the bounds * of the associated selection. */ void update(); using KisSelectionBasedLayer::setDirty; - void setDirty(const QRect & rect) override; + void setDirty(const QVector &rects) override; void setX(qint32 x) override; void setY(qint32 y) override; private Q_SLOTS: void slotDelayedStaticUpdate(); public: // KisIndirectPaintingSupport KisLayer* layer() { return this; } private: struct Private; const QScopedPointer m_d; }; #endif diff --git a/libs/image/kis_brush_mask_applicators.h b/libs/image/kis_brush_mask_applicators.h index 4c1a15494e..4ecd348da9 100644 --- a/libs/image/kis_brush_mask_applicators.h +++ b/libs/image/kis_brush_mask_applicators.h @@ -1,214 +1,202 @@ /* * Copyright (c) 2012 Sven Langkamp * Copyright (c) 2012 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_BRUSH_MASK_APPLICATORS_H #define __KIS_BRUSH_MASK_APPLICATORS_H #include "kis_brush_mask_applicator_base.h" #include "kis_global.h" +#include "kis_random_source.h" // 3x3 supersampling #define SUPERSAMPLING 3 -#if defined(_WIN32) || defined(_WIN64) -#include -#define srand48 srand -inline double drand48() { - return double(rand()) / RAND_MAX; -} -#endif - -#include - template struct KisBrushMaskScalarApplicator : public KisBrushMaskApplicatorBase { KisBrushMaskScalarApplicator(MaskGenerator *maskGenerator) : m_maskGenerator(maskGenerator) { } void process(const QRect &rect) override { processScalar(rect); } protected: void processScalar(const QRect &rect); protected: MaskGenerator *m_maskGenerator; - std::random_device m_rand_dev; + KisRandomSource m_randomSource; // TODO: make it more deterministic for LoD }; #if defined HAVE_VC template struct KisBrushMaskVectorApplicator : public KisBrushMaskScalarApplicator { KisBrushMaskVectorApplicator(MaskGenerator *maskGenerator) : KisBrushMaskScalarApplicator(maskGenerator) { } void process(const QRect &rect) { startProcessing(rect, TypeHelper()); } protected: void processVector(const QRect &rect); private: template struct TypeHelper {}; private: template inline void startProcessing(const QRect &rect, TypeHelper) { KisBrushMaskScalarApplicator::processScalar(rect); } template inline void startProcessing(const QRect &rect, TypeHelper) { MaskGenerator *m_maskGenerator = KisBrushMaskScalarApplicator::m_maskGenerator; if (m_maskGenerator->shouldVectorize()) { processVector(rect); } else { KisBrushMaskScalarApplicator::processScalar(rect); } } }; template void KisBrushMaskVectorApplicator::processVector(const QRect &rect) { const MaskProcessingData *m_d = KisBrushMaskApplicatorBase::m_d; MaskGenerator *m_maskGenerator = KisBrushMaskScalarApplicator::m_maskGenerator; qreal random = 1.0; quint8* dabPointer = m_d->device->data() + rect.y() * rect.width() * m_d->pixelSize; quint8 alphaValue = OPACITY_TRANSPARENT_U8; // this offset is needed when brush size is smaller then fixed device size int offset = (m_d->device->bounds().width() - rect.width()) * m_d->pixelSize; int width = rect.width(); // We need to calculate with a multiple of the width of the simd register int alignOffset = 0; if (width % Vc::float_v::size() != 0) { alignOffset = Vc::float_v::size() - (width % Vc::float_v::size()); } int simdWidth = width + alignOffset; float *buffer = Vc::malloc(simdWidth); typename MaskGenerator::FastRowProcessor processor(m_maskGenerator); for (int y = rect.y(); y < rect.y() + rect.height(); y++) { processor.template process<_impl>(buffer, simdWidth, y, m_d->cosa, m_d->sina, m_d->centerX, m_d->centerY); if (m_d->randomness != 0.0 || m_d->density != 1.0) { for (int x = 0; x < width; x++) { if (m_d->randomness!= 0.0){ - random = (1.0 - m_d->randomness) + m_d->randomness * float(rand()) / RAND_MAX; + random = (1.0 - m_d->randomness) + m_d->randomness * KisBrushMaskScalarApplicator::m_randomSource.generateNormalized(); } alphaValue = quint8( (OPACITY_OPAQUE_U8 - buffer[x]*255) * random); // avoid computation of random numbers if density is full if (m_d->density != 1.0){ // compute density only for visible pixels of the mask if (alphaValue != OPACITY_TRANSPARENT_U8){ - if ( !(m_d->density >= drand48()) ){ + if ( !(m_d->density >= KisBrushMaskScalarApplicator::m_randomSource.generateNormalized()) ){ alphaValue = OPACITY_TRANSPARENT_U8; } } } m_d->colorSpace->applyAlphaU8Mask(dabPointer, &alphaValue, 1); dabPointer += m_d->pixelSize; } } else { m_d->colorSpace->applyInverseNormedFloatMask(dabPointer, buffer, width); dabPointer += width * m_d->pixelSize; }//endfor x dabPointer += offset; }//endfor y Vc::free(buffer); } #endif /* defined HAVE_VC */ template void KisBrushMaskScalarApplicator::processScalar(const QRect &rect) { const MaskProcessingData *m_d = KisBrushMaskApplicatorBase::m_d; MaskGenerator *m_maskGenerator = KisBrushMaskScalarApplicator::m_maskGenerator; - std::default_random_engine rand_engine{m_rand_dev()}; - std::uniform_real_distribution<> rand_distr(0.0f, 1.0f); - qreal random = 1.0; quint8* dabPointer = m_d->device->data() + rect.y() * rect.width() * m_d->pixelSize; quint8 alphaValue = OPACITY_TRANSPARENT_U8; // this offset is needed when brush size is smaller then fixed device size int offset = (m_d->device->bounds().width() - rect.width()) * m_d->pixelSize; int supersample = (m_maskGenerator->shouldSupersample() ? SUPERSAMPLING : 1); double invss = 1.0 / supersample; int samplearea = pow2(supersample); for (int y = rect.y(); y < rect.y() + rect.height(); y++) { for (int x = rect.x(); x < rect.x() + rect.width(); x++) { int value = 0; for (int sy = 0; sy < supersample; sy++) { for (int sx = 0; sx < supersample; sx++) { double x_ = x + sx * invss - m_d->centerX; double y_ = y + sy * invss - m_d->centerY; double maskX = m_d->cosa * x_ - m_d->sina * y_; double maskY = m_d->sina * x_ + m_d->cosa * y_; value += m_maskGenerator->valueAt(maskX, maskY); } } if (supersample != 1) value /= samplearea; if (m_d->randomness!= 0.0){ - random = (1.0 - m_d->randomness) + m_d->randomness * rand_distr(rand_engine); + random = (1.0 - m_d->randomness) + m_d->randomness * m_randomSource.generateNormalized(); } alphaValue = quint8( (OPACITY_OPAQUE_U8 - value) * random); // avoid computation of random numbers if density is full if (m_d->density != 1.0){ // compute density only for visible pixels of the mask if (alphaValue != OPACITY_TRANSPARENT_U8){ - if ( !(m_d->density >= rand_distr(rand_engine)) ){ + if ( !(m_d->density >= m_randomSource.generateNormalized()) ){ alphaValue = OPACITY_TRANSPARENT_U8; } } } m_d->colorSpace->applyAlphaU8Mask(dabPointer, &alphaValue, 1); dabPointer += m_d->pixelSize; }//endfor x dabPointer += offset; }//endfor y } #endif /* __KIS_BRUSH_MASK_APPLICATORS_H */ diff --git a/libs/image/kis_distance_information.cpp b/libs/image/kis_distance_information.cpp index 76643df404..b7060c1257 100644 --- a/libs/image/kis_distance_information.cpp +++ b/libs/image/kis_distance_information.cpp @@ -1,649 +1,625 @@ /* * Copyright (c) 2010 Cyrille Berger * Copyright (c) 2013 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include "kis_spacing_information.h" #include "kis_timing_information.h" #include "kis_debug.h" #include #include #include #include "kis_algebra_2d.h" #include "kis_dom_utils.h" #include "kis_lod_transform.h" const qreal MIN_DISTANCE_SPACING = 0.5; // Smallest allowed interval when timed spacing is enabled, in milliseconds. const qreal MIN_TIMED_INTERVAL = 0.5; // Largest allowed interval when timed spacing is enabled, in milliseconds. const qreal MAX_TIMED_INTERVAL = LONG_TIME; struct Q_DECL_HIDDEN KisDistanceInformation::Private { Private() : accumDistance(), accumTime(0.0), spacingUpdateInterval(LONG_TIME), timeSinceSpacingUpdate(0.0), timingUpdateInterval(LONG_TIME), timeSinceTimingUpdate(0.0), lastDabInfoValid(false), lastPaintInfoValid(false), - lockedDrawingAngle(0.0), - hasLockedDrawingAngle(false), - totalDistance(0.0) {} + totalDistance(0.0), + currentDabSeqNo(0), + levelOfDetail(0) + { + } // Accumulators of time/distance passed since the last painted dab QPointF accumDistance; qreal accumTime; KisSpacingInformation spacing; qreal spacingUpdateInterval; // Accumulator of time passed since the last spacing update qreal timeSinceSpacingUpdate; KisTimingInformation timing; qreal timingUpdateInterval; // Accumulator of time passed since the last timing update qreal timeSinceTimingUpdate; // Information about the last position considered (not necessarily a painted dab) QPointF lastPosition; - qreal lastTime; qreal lastAngle; bool lastDabInfoValid; // Information about the last painted dab KisPaintInformation lastPaintInformation; bool lastPaintInfoValid; - qreal lockedDrawingAngle; - bool hasLockedDrawingAngle; qreal totalDistance; + boost::optional lockedDrawingAngleOptional; + + int currentDabSeqNo; + int levelOfDetail; }; struct Q_DECL_HIDDEN KisDistanceInitInfo::Private { Private() : hasLastInfo(false), lastPosition(), - lastTime(0.0), lastAngle(0.0), spacingUpdateInterval(LONG_TIME), - timingUpdateInterval(LONG_TIME) {} + timingUpdateInterval(LONG_TIME), + currentDabSeqNo(0) + { + } - // Indicates whether lastPosition, lastTime, and lastAngle are valid or not. + // Indicates whether lastPosition, and lastAngle are valid or not. bool hasLastInfo; QPointF lastPosition; - qreal lastTime; qreal lastAngle; qreal spacingUpdateInterval; qreal timingUpdateInterval; + + int currentDabSeqNo; }; KisDistanceInitInfo::KisDistanceInitInfo() : m_d(new Private) { } -KisDistanceInitInfo::KisDistanceInitInfo(qreal spacingUpdateInterval, qreal timingUpdateInterval) +KisDistanceInitInfo::KisDistanceInitInfo(qreal spacingUpdateInterval, qreal timingUpdateInterval, int currentDabSeqNo) : m_d(new Private) { m_d->spacingUpdateInterval = spacingUpdateInterval; m_d->timingUpdateInterval = timingUpdateInterval; + m_d->currentDabSeqNo = currentDabSeqNo; } -KisDistanceInitInfo::KisDistanceInitInfo(const QPointF &lastPosition, qreal lastTime, - qreal lastAngle) +KisDistanceInitInfo::KisDistanceInitInfo(const QPointF &lastPosition, + qreal lastAngle, int currentDabSeqNo) : m_d(new Private) { m_d->hasLastInfo = true; m_d->lastPosition = lastPosition; - m_d->lastTime = lastTime; m_d->lastAngle = lastAngle; + m_d->currentDabSeqNo = currentDabSeqNo; } -KisDistanceInitInfo::KisDistanceInitInfo(const QPointF &lastPosition, qreal lastTime, +KisDistanceInitInfo::KisDistanceInitInfo(const QPointF &lastPosition, qreal lastAngle, qreal spacingUpdateInterval, - qreal timingUpdateInterval) + qreal timingUpdateInterval, + int currentDabSeqNo) : m_d(new Private) { m_d->hasLastInfo = true; m_d->lastPosition = lastPosition; - m_d->lastTime = lastTime; m_d->lastAngle = lastAngle; m_d->spacingUpdateInterval = spacingUpdateInterval; m_d->timingUpdateInterval = timingUpdateInterval; + m_d->currentDabSeqNo = currentDabSeqNo; } KisDistanceInitInfo::KisDistanceInitInfo(const KisDistanceInitInfo &rhs) : m_d(new Private(*rhs.m_d)) { } KisDistanceInitInfo::~KisDistanceInitInfo() { delete m_d; } bool KisDistanceInitInfo::operator==(const KisDistanceInitInfo &other) const { if (m_d->spacingUpdateInterval != other.m_d->spacingUpdateInterval || m_d->timingUpdateInterval != other.m_d->timingUpdateInterval || m_d->hasLastInfo != other.m_d->hasLastInfo) { return false; } if (m_d->hasLastInfo) { - if (m_d->lastPosition != other.m_d->lastPosition || m_d->lastTime != other.m_d->lastTime + if (m_d->lastPosition != other.m_d->lastPosition || m_d->lastAngle != other.m_d->lastAngle) { return false; } } + if (m_d->currentDabSeqNo != other.m_d->currentDabSeqNo) { + return false; + } + return true; } bool KisDistanceInitInfo::operator!=(const KisDistanceInitInfo &other) const { return !(*this == other); } KisDistanceInitInfo &KisDistanceInitInfo::operator=(const KisDistanceInitInfo &rhs) { *m_d = *rhs.m_d; return *this; } KisDistanceInformation KisDistanceInitInfo::makeDistInfo() { if (m_d->hasLastInfo) { - return KisDistanceInformation(m_d->lastPosition, m_d->lastTime, m_d->lastAngle, - m_d->spacingUpdateInterval, m_d->timingUpdateInterval); + return KisDistanceInformation(m_d->lastPosition, m_d->lastAngle, + m_d->spacingUpdateInterval, m_d->timingUpdateInterval, + m_d->currentDabSeqNo); } else { - return KisDistanceInformation(m_d->spacingUpdateInterval, m_d->timingUpdateInterval); + return KisDistanceInformation(m_d->spacingUpdateInterval, m_d->timingUpdateInterval, m_d->currentDabSeqNo); } } void KisDistanceInitInfo::toXML(QDomDocument &doc, QDomElement &elt) const { elt.setAttribute("spacingUpdateInterval", QString::number(m_d->spacingUpdateInterval, 'g', 15)); elt.setAttribute("timingUpdateInterval", QString::number(m_d->timingUpdateInterval, 'g', 15)); + elt.setAttribute("currentDabSeqNo", QString::number(m_d->currentDabSeqNo)); if (m_d->hasLastInfo) { QDomElement lastInfoElt = doc.createElement("LastInfo"); lastInfoElt.setAttribute("lastPosX", QString::number(m_d->lastPosition.x(), 'g', 15)); lastInfoElt.setAttribute("lastPosY", QString::number(m_d->lastPosition.y(), 'g', 15)); - lastInfoElt.setAttribute("lastTime", QString::number(m_d->lastTime, 'g', 15)); lastInfoElt.setAttribute("lastAngle", QString::number(m_d->lastAngle, 'g', 15)); elt.appendChild(lastInfoElt); } } KisDistanceInitInfo KisDistanceInitInfo::fromXML(const QDomElement &elt) { const qreal spacingUpdateInterval = qreal(KisDomUtils::toDouble(elt.attribute("spacingUpdateInterval", QString::number(LONG_TIME, 'g', 15)))); const qreal timingUpdateInterval = qreal(KisDomUtils::toDouble(elt.attribute("timingUpdateInterval", QString::number(LONG_TIME, 'g', 15)))); + const qreal currentDabSeqNo = KisDomUtils::toInt(elt.attribute("currentDabSeqNo", "0")); + const QDomElement lastInfoElt = elt.firstChildElement("LastInfo"); const bool hasLastInfo = !lastInfoElt.isNull(); if (hasLastInfo) { const qreal lastPosX = qreal(KisDomUtils::toDouble(lastInfoElt.attribute("lastPosX", "0.0"))); const qreal lastPosY = qreal(KisDomUtils::toDouble(lastInfoElt.attribute("lastPosY", "0.0"))); - const qreal lastTime = qreal(KisDomUtils::toDouble(lastInfoElt.attribute("lastTime", - "0.0"))); const qreal lastAngle = qreal(KisDomUtils::toDouble(lastInfoElt.attribute("lastAngle", "0.0"))); - return KisDistanceInitInfo(QPointF(lastPosX, lastPosY), lastTime, lastAngle, - spacingUpdateInterval, timingUpdateInterval); + return KisDistanceInitInfo(QPointF(lastPosX, lastPosY), lastAngle, + spacingUpdateInterval, timingUpdateInterval, + currentDabSeqNo); } else { - return KisDistanceInitInfo(spacingUpdateInterval, timingUpdateInterval); + return KisDistanceInitInfo(spacingUpdateInterval, timingUpdateInterval, + currentDabSeqNo); } } KisDistanceInformation::KisDistanceInformation() : m_d(new Private) { } KisDistanceInformation::KisDistanceInformation(qreal spacingUpdateInterval, - qreal timingUpdateInterval) + qreal timingUpdateInterval, + int currentDabSeqNo) : m_d(new Private) { m_d->spacingUpdateInterval = spacingUpdateInterval; m_d->timingUpdateInterval = timingUpdateInterval; + m_d->currentDabSeqNo = currentDabSeqNo; } KisDistanceInformation::KisDistanceInformation(const QPointF &lastPosition, - qreal lastTime, qreal lastAngle) : m_d(new Private) { m_d->lastPosition = lastPosition; - m_d->lastTime = lastTime; m_d->lastAngle = lastAngle; m_d->lastDabInfoValid = true; } KisDistanceInformation::KisDistanceInformation(const QPointF &lastPosition, - qreal lastTime, qreal lastAngle, qreal spacingUpdateInterval, - qreal timingUpdateInterval) - : KisDistanceInformation(lastPosition, lastTime, lastAngle) + qreal timingUpdateInterval, + int currentDabSeqNo) + : KisDistanceInformation(lastPosition, lastAngle) { m_d->spacingUpdateInterval = spacingUpdateInterval; m_d->timingUpdateInterval = timingUpdateInterval; + m_d->currentDabSeqNo = currentDabSeqNo; } KisDistanceInformation::KisDistanceInformation(const KisDistanceInformation &rhs) : m_d(new Private(*rhs.m_d)) { } KisDistanceInformation::KisDistanceInformation(const KisDistanceInformation &rhs, int levelOfDetail) : m_d(new Private(*rhs.m_d)) { KIS_ASSERT_RECOVER_NOOP(!m_d->lastPaintInfoValid && "The distance information " "should be cloned before the " "actual painting is started"); + m_d->levelOfDetail = levelOfDetail; + KisLodTransform t(levelOfDetail); m_d->lastPosition = t.map(m_d->lastPosition); } KisDistanceInformation& KisDistanceInformation::operator=(const KisDistanceInformation &rhs) { *m_d = *rhs.m_d; return *this; } -void KisDistanceInformation::overrideLastValues(const QPointF &lastPosition, qreal lastTime, +void KisDistanceInformation::overrideLastValues(const QPointF &lastPosition, qreal lastAngle) { m_d->lastPosition = lastPosition; - m_d->lastTime = lastTime; m_d->lastAngle = lastAngle; m_d->lastDabInfoValid = true; } KisDistanceInformation::~KisDistanceInformation() { delete m_d; } const KisSpacingInformation& KisDistanceInformation::currentSpacing() const { return m_d->spacing; } void KisDistanceInformation::updateSpacing(const KisSpacingInformation &spacing) { m_d->spacing = spacing; m_d->timeSinceSpacingUpdate = 0.0; } bool KisDistanceInformation::needsSpacingUpdate() const { return m_d->timeSinceSpacingUpdate >= m_d->spacingUpdateInterval; } const KisTimingInformation &KisDistanceInformation::currentTiming() const { return m_d->timing; } void KisDistanceInformation::updateTiming(const KisTimingInformation &timing) { m_d->timing = timing; m_d->timeSinceTimingUpdate = 0.0; } bool KisDistanceInformation::needsTimingUpdate() const { return m_d->timeSinceTimingUpdate >= m_d->timingUpdateInterval; } bool KisDistanceInformation::hasLastDabInformation() const { return m_d->lastDabInfoValid; } QPointF KisDistanceInformation::lastPosition() const { return m_d->lastPosition; } -qreal KisDistanceInformation::lastTime() const -{ - return m_d->lastTime; -} - qreal KisDistanceInformation::lastDrawingAngle() const { return m_d->lastAngle; } bool KisDistanceInformation::hasLastPaintInformation() const { return m_d->lastPaintInfoValid; } const KisPaintInformation& KisDistanceInformation::lastPaintInformation() const { return m_d->lastPaintInformation; } +int KisDistanceInformation::currentDabSeqNo() const +{ + return m_d->currentDabSeqNo; +} + bool KisDistanceInformation::isStarted() const { return m_d->lastPaintInfoValid; } void KisDistanceInformation::registerPaintedDab(const KisPaintInformation &info, const KisSpacingInformation &spacing, const KisTimingInformation &timing) { - m_d->totalDistance += KisAlgebra2D::norm(info.pos() - m_d->lastPosition); + m_d->totalDistance += + KisAlgebra2D::norm(info.pos() - m_d->lastPosition) * + KisLodTransform::lodToInvScale(m_d->levelOfDetail); m_d->lastPaintInformation = info; m_d->lastPaintInfoValid = true; - m_d->lastAngle = nextDrawingAngle(info.pos()); + m_d->lastAngle = info.drawingAngle(false); m_d->lastPosition = info.pos(); - m_d->lastTime = info.currentTime(); m_d->lastDabInfoValid = true; m_d->spacing = spacing; m_d->timing = timing; + + m_d->currentDabSeqNo++; } qreal KisDistanceInformation::getNextPointPosition(const QPointF &start, const QPointF &end, qreal startTime, qreal endTime) { // Compute interpolation factor based on distance. qreal distanceFactor = -1.0; if (m_d->spacing.isDistanceSpacingEnabled()) { distanceFactor = m_d->spacing.isIsotropic() ? getNextPointPositionIsotropic(start, end) : getNextPointPositionAnisotropic(start, end); } // Compute interpolation factor based on time. qreal timeFactor = -1.0; if (m_d->timing.isTimedSpacingEnabled()) { timeFactor = getNextPointPositionTimed(startTime, endTime); } // Return the distance-based or time-based factor, whichever is smallest. qreal t = -1.0; if (distanceFactor < 0.0) { t = timeFactor; } else if (timeFactor < 0.0) { t = distanceFactor; } else { t = qMin(distanceFactor, timeFactor); } // If we aren't ready to paint a dab, accumulate time for the spacing/timing updates that might // be needed between dabs. if (t < 0.0) { m_d->timeSinceSpacingUpdate += endTime - startTime; m_d->timeSinceTimingUpdate += endTime - startTime; } // If we are ready to paint a dab, reset the accumulated time for spacing/timing updates. else { m_d->timeSinceSpacingUpdate = 0.0; m_d->timeSinceTimingUpdate = 0.0; } return t; } qreal KisDistanceInformation::getNextPointPositionIsotropic(const QPointF &start, const QPointF &end) { qreal distance = m_d->accumDistance.x(); qreal spacing = qMax(MIN_DISTANCE_SPACING, m_d->spacing.distanceSpacing().x()); if (start == end) { return -1; } qreal dragVecLength = QVector2D(end - start).length(); qreal nextPointDistance = spacing - distance; qreal t; // nextPointDistance can sometimes be negative if the spacing info has been modified since the // last interpolation attempt. In that case, have a point painted immediately. if (nextPointDistance <= 0.0) { resetAccumulators(); t = 0.0; } else if (nextPointDistance <= dragVecLength) { t = nextPointDistance / dragVecLength; resetAccumulators(); } else { t = -1; m_d->accumDistance.rx() += dragVecLength; } return t; } qreal KisDistanceInformation::getNextPointPositionAnisotropic(const QPointF &start, const QPointF &end) { if (start == end) { return -1; } qreal a_rev = 1.0 / qMax(MIN_DISTANCE_SPACING, m_d->spacing.distanceSpacing().x()); qreal b_rev = 1.0 / qMax(MIN_DISTANCE_SPACING, m_d->spacing.distanceSpacing().y()); qreal x = m_d->accumDistance.x(); qreal y = m_d->accumDistance.y(); qreal gamma = pow2(x * a_rev) + pow2(y * b_rev) - 1; // If the distance accumulator is already past the spacing ellipse, have a point painted // immediately. This can happen if the spacing info has been modified since the last // interpolation attempt. if (gamma >= 0.0) { resetAccumulators(); return 0.0; } static const qreal eps = 2e-3; // < 0.2 deg qreal currentRotation = m_d->spacing.rotation(); if (m_d->spacing.coordinateSystemFlipped()) { currentRotation = 2 * M_PI - currentRotation; } QPointF diff = end - start; if (currentRotation > eps) { QTransform rot; // since the ellipse is symmetrical, the sign // of rotation doesn't matter rot.rotateRadians(currentRotation); diff = rot.map(diff); } qreal dx = qAbs(diff.x()); qreal dy = qAbs(diff.y()); qreal alpha = pow2(dx * a_rev) + pow2(dy * b_rev); qreal beta = x * dx * a_rev * a_rev + y * dy * b_rev * b_rev; qreal D_4 = pow2(beta) - alpha * gamma; qreal t = -1.0; if (D_4 >= 0) { qreal k = (-beta + qSqrt(D_4)) / alpha; if (k >= 0.0 && k <= 1.0) { t = k; resetAccumulators(); } else { m_d->accumDistance += KisAlgebra2D::abs(diff); } } else { warnKrita << "BUG: No solution for elliptical spacing equation has been found. This shouldn't have happened."; } return t; } qreal KisDistanceInformation::getNextPointPositionTimed(qreal startTime, qreal endTime) { // If start time is not before end time, do not interpolate. if (!(startTime < endTime)) { return -1.0; } qreal timedSpacingInterval = qBound(MIN_TIMED_INTERVAL, m_d->timing.timedSpacingInterval(), MAX_TIMED_INTERVAL); qreal nextPointInterval = timedSpacingInterval - m_d->accumTime; qreal t = -1.0; // nextPointInterval can sometimes be negative if the spacing info has been modified since the // last interpolation attempt. In that case, have a point painted immediately. if (nextPointInterval <= 0.0) { resetAccumulators(); t = 0.0; } else if (nextPointInterval <= endTime - startTime) { resetAccumulators(); t = nextPointInterval / (endTime - startTime); } else { m_d->accumTime += endTime - startTime; t = -1.0; } return t; } void KisDistanceInformation::resetAccumulators() { m_d->accumDistance = QPointF(); m_d->accumTime = 0.0; } -bool KisDistanceInformation::hasLockedDrawingAngle() const +boost::optional KisDistanceInformation::lockedDrawingAngleOptional() const { - return m_d->hasLockedDrawingAngle; + return m_d->lockedDrawingAngleOptional; } -qreal KisDistanceInformation::lockedDrawingAngle() const +void KisDistanceInformation::lockCurrentDrawingAngle(const KisPaintInformation &info) const { - return m_d->lockedDrawingAngle; -} + const qreal angle = info.drawingAngle(false); -void KisDistanceInformation::setLockedDrawingAngle(qreal angle) -{ - m_d->hasLockedDrawingAngle = true; - m_d->lockedDrawingAngle = angle; -} + qreal newAngle = angle; -qreal KisDistanceInformation::nextDrawingAngle(const QPointF &nextPos, - bool considerLockedAngle) const -{ - if (!m_d->lastDabInfoValid) { - warnKrita << "KisDistanceInformation::nextDrawingAngle()" << "No last dab data"; - return 0.0; - } + if (m_d->lockedDrawingAngleOptional) { + const qreal stabilizingCoeff = 20.0; + const qreal dist = stabilizingCoeff * m_d->spacing.scalarApprox(); + const qreal alpha = qMax(0.0, dist - scalarDistanceApprox()) / dist; - // Compute the drawing angle. If the new position is the same as the previous position, an angle - // can't be computed. In that case, act as if the angle is the same as in the previous dab. - return drawingAngleImpl(m_d->lastPosition, nextPos, considerLockedAngle, m_d->lastAngle); -} + const qreal oldAngle = *m_d->lockedDrawingAngleOptional; -QPointF KisDistanceInformation::nextDrawingDirectionVector(const QPointF &nextPos, - bool considerLockedAngle) const -{ - if (!m_d->lastDabInfoValid) { - warnKrita << "KisDistanceInformation::nextDrawingDirectionVector()" << "No last dab data"; - return QPointF(1.0, 0.0); + if (shortestAngularDistance(oldAngle, newAngle) < M_PI / 6) { + newAngle = (1.0 - alpha) * oldAngle + alpha * newAngle; + } else { + newAngle = oldAngle; + } } - // Compute the direction vector. If the new position is the same as the previous position, a - // direction can't be computed. In that case, act as if the direction is the same as in the - // previous dab. - return drawingDirectionVectorImpl(m_d->lastPosition, nextPos, considerLockedAngle, - m_d->lastAngle); + m_d->lockedDrawingAngleOptional = newAngle; } + qreal KisDistanceInformation::scalarDistanceApprox() const { return m_d->totalDistance; } -qreal KisDistanceInformation::drawingAngleImpl(const QPointF &start, const QPointF &end, - bool considerLockedAngle, qreal defaultAngle) const -{ - if (m_d->hasLockedDrawingAngle && considerLockedAngle) { - return m_d->lockedDrawingAngle; - } - - // If the start and end positions are the same, we can't compute an angle. In that case, use the - // provided default. - return KisAlgebra2D::directionBetweenPoints(start, end, defaultAngle); -} - -QPointF KisDistanceInformation::drawingDirectionVectorImpl(const QPointF &start, const QPointF &end, - bool considerLockedAngle, - qreal defaultAngle) const -{ - if (m_d->hasLockedDrawingAngle && considerLockedAngle) { - return QPointF(cos(m_d->lockedDrawingAngle), sin(m_d->lockedDrawingAngle)); - } - - // If the start and end positions are the same, we can't compute a drawing direction. In that - // case, use the provided default. - if (KisAlgebra2D::fuzzyPointCompare(start, end)) { - return QPointF(cos(defaultAngle), sin(defaultAngle)); - } - - const QPointF diff(end - start); - return KisAlgebra2D::normalize(diff); -} diff --git a/libs/image/kis_distance_information.h b/libs/image/kis_distance_information.h index 5c8462e2a4..2082aa59d6 100644 --- a/libs/image/kis_distance_information.h +++ b/libs/image/kis_distance_information.h @@ -1,203 +1,190 @@ /* * Copyright (c) 2010 Cyrille Berger * Copyright (c) 2013 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_DISTANCE_INFORMATION_H_ #define _KIS_DISTANCE_INFORMATION_H_ #include #include #include #include #include "kritaimage_export.h" +#include class KisPaintInformation; class KisSpacingInformation; class KisTimingInformation; class KisDistanceInformation; /** * Represents some information that can be used to initialize a KisDistanceInformation object. The * main purpose of this class is to allow serialization of KisDistanceInformation initial settings * to XML. */ class KRITAIMAGE_EXPORT KisDistanceInitInfo { public: /** * Creates a KisDistanceInitInfo with no initial last dab information, and spacing and timing * update intervals set to LONG_TIME. */ explicit KisDistanceInitInfo(); /** * Creates a KisDistanceInitInfo with no initial last dab information, and the specified spacing * and timing update intervals. */ - explicit KisDistanceInitInfo(qreal spacingUpdateInterval, qreal timingUpdateInterval); + explicit KisDistanceInitInfo(qreal spacingUpdateInterval, qreal timingUpdateInterval, int currentDabSeqNo); /** * Creates a KisDistanceInitInfo with the specified last dab information, and spacing and timing * update intervals set to LONG_TIME. */ - explicit KisDistanceInitInfo(const QPointF &lastPosition, qreal lastTime, qreal lastAngle); + explicit KisDistanceInitInfo(const QPointF &lastPosition, qreal lastAngle, int currentDabSeqNo); /** * Creates a KisDistanceInitInfo with the specified last dab information and spacing and timing * update intervals. */ - explicit KisDistanceInitInfo(const QPointF &lastPosition, qreal lastTime, qreal lastAngle, - qreal spacingUpdateInterval, qreal timingUpdateInterval); + explicit KisDistanceInitInfo(const QPointF &lastPosition, qreal lastAngle, + qreal spacingUpdateInterval, qreal timingUpdateInterval, int currentDabSeqNo); KisDistanceInitInfo(const KisDistanceInitInfo &rhs); ~KisDistanceInitInfo(); bool operator==(const KisDistanceInitInfo &other) const; bool operator!=(const KisDistanceInitInfo &other) const; KisDistanceInitInfo &operator=(const KisDistanceInitInfo &rhs); /** * Constructs a KisDistanceInformation with initial settings based on this object. */ KisDistanceInformation makeDistInfo(); void toXML(QDomDocument &doc, QDomElement &elt) const; static KisDistanceInitInfo fromXML(const QDomElement &elt); private: struct Private; Private * const m_d; }; /** * This structure keeps track of distance and timing information during a stroke, e.g. the time or * distance moved since the last dab. */ class KRITAIMAGE_EXPORT KisDistanceInformation { public: KisDistanceInformation(); - KisDistanceInformation(qreal spacingUpdateInterval, qreal timingUpdateInterval); - KisDistanceInformation(const QPointF &lastPosition, qreal lastTime, qreal lastAngle); + KisDistanceInformation(qreal spacingUpdateInterval, qreal timingUpdateInterval, int currentDabSeqNo = 0); + KisDistanceInformation(const QPointF &lastPosition, qreal lastAngle); /** * @param spacingUpdateInterval The amount of time allowed between spacing updates, in * milliseconds. Use LONG_TIME to only allow spacing updates when a * dab is painted. * @param timingUpdateInterval The amount of time allowed between time-based spacing updates, in * milliseconds. Use LONG_TIME to only allow timing updates when a * dab is painted. */ - KisDistanceInformation(const QPointF &lastPosition, qreal lastTime, qreal lastAngle, - qreal spacingUpdateInterval, qreal timingUpdateInterval); + KisDistanceInformation(const QPointF &lastPosition, qreal lastAngle, + qreal spacingUpdateInterval, qreal timingUpdateInterval, int currentDabSeqNo); KisDistanceInformation(const KisDistanceInformation &rhs); KisDistanceInformation(const KisDistanceInformation &rhs, int levelOfDetail); KisDistanceInformation& operator=(const KisDistanceInformation &rhs); ~KisDistanceInformation(); const KisSpacingInformation& currentSpacing() const; void updateSpacing(const KisSpacingInformation &spacing); /** * Returns true if this KisDistanceInformation should have its spacing information updated * immediately (regardless of whether a dab is ready to be painted). */ bool needsSpacingUpdate() const; const KisTimingInformation ¤tTiming() const; void updateTiming(const KisTimingInformation &timing); /** * Returns true if this KisDistanceInformation should have its timing information updated * immediately (regardless of whether a dab is ready to be painted). */ bool needsTimingUpdate() const; bool hasLastDabInformation() const; QPointF lastPosition() const; - qreal lastTime() const; qreal lastDrawingAngle() const; bool hasLastPaintInformation() const; const KisPaintInformation& lastPaintInformation() const; + int currentDabSeqNo() const; + /** * @param spacing The new effective spacing after the dab. (Painting a dab is always supposed to * cause a spacing update.) * @param timing The new effective timing after the dab. (Painting a dab is always supposed to * cause a timing update.) */ void registerPaintedDab(const KisPaintInformation &info, const KisSpacingInformation &spacing, const KisTimingInformation &timing); qreal getNextPointPosition(const QPointF &start, const QPointF &end, qreal startTime, qreal endTime); /** * \return true if at least one dab has been painted with this * distance information */ bool isStarted() const; - bool hasLockedDrawingAngle() const; - qreal lockedDrawingAngle() const; - void setLockedDrawingAngle(qreal angle); + boost::optional lockedDrawingAngleOptional() const; /** - * Computes the next drawing angle assuming that the next painting position will be nextPos. - * This method should not be called when hasLastDabInformation() is false. + * Lock current drawing angle for the rest of the stroke. The new value is blended + * into the result proportional to the length of the stroke. */ - qreal nextDrawingAngle(const QPointF &nextPos, bool considerLockedAngle = true) const; - - /** - * Returns a unit vector pointing in the direction that would have been indicated by a call to - * nextDrawingAngle. This method should not be called when hasLastDabInformation() is false. - */ - QPointF nextDrawingDirectionVector(const QPointF &nextPos, - bool considerLockedAngle = true) const; + void lockCurrentDrawingAngle(const KisPaintInformation &info) const; qreal scalarDistanceApprox() const; - void overrideLastValues(const QPointF &lastPosition, qreal lastTime, qreal lastAngle); + void overrideLastValues(const QPointF &lastPosition, qreal lastAngle); private: qreal getNextPointPositionIsotropic(const QPointF &start, const QPointF &end); qreal getNextPointPositionAnisotropic(const QPointF &start, const QPointF &end); qreal getNextPointPositionTimed(qreal startTime, qreal endTime); void resetAccumulators(); - qreal drawingAngleImpl(const QPointF &start, const QPointF &end, - bool considerLockedAngle = true, qreal defaultAngle = 0.0) const; - QPointF drawingDirectionVectorImpl(const QPointF &start, const QPointF &end, - bool considerLockedAngle = true, - qreal defaultAngle = 0.0) const; - private: struct Private; Private * const m_d; }; #endif diff --git a/libs/image/kis_fixed_paint_device.cpp b/libs/image/kis_fixed_paint_device.cpp index 2f8b44638e..514c3fa76d 100644 --- a/libs/image/kis_fixed_paint_device.cpp +++ b/libs/image/kis_fixed_paint_device.cpp @@ -1,317 +1,351 @@ /* * Copyright (c) 2009 Boudewijn Rempt * Copyright (c) 2010 Lukáš Tvrdý * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_fixed_paint_device.h" #include #include #include #include "kis_debug.h" KisFixedPaintDevice::KisFixedPaintDevice(const KoColorSpace* colorSpace) : m_colorSpace(colorSpace) { } KisFixedPaintDevice::~KisFixedPaintDevice() { } KisFixedPaintDevice::KisFixedPaintDevice(const KisFixedPaintDevice& rhs) : KisShared() { m_bounds = rhs.m_bounds; m_colorSpace = rhs.m_colorSpace; m_data = rhs.m_data; } KisFixedPaintDevice& KisFixedPaintDevice::operator=(const KisFixedPaintDevice& rhs) { m_bounds = rhs.m_bounds; m_colorSpace = rhs.m_colorSpace; - m_data = rhs.m_data; + + + const int referenceSize = m_bounds.height() * m_bounds.width() * pixelSize(); + + if (m_data.size() >= referenceSize) { + memcpy(m_data.data(), rhs.m_data.data(), referenceSize); + } else { + m_data = rhs.m_data; + } + return *this; } void KisFixedPaintDevice::setRect(const QRect& rc) { m_bounds = rc; } QRect KisFixedPaintDevice::bounds() const { return m_bounds; } int KisFixedPaintDevice::allocatedPixels() const { return m_data.size() / m_colorSpace->pixelSize(); } quint32 KisFixedPaintDevice::pixelSize() const { return m_colorSpace->pixelSize(); } bool KisFixedPaintDevice::initialize(quint8 defaultValue) { m_data.fill(defaultValue, m_bounds.height() * m_bounds.width() * pixelSize()); return true; } +void KisFixedPaintDevice::reallocateBufferWithoutInitialization() +{ + const int referenceSize = m_bounds.height() * m_bounds.width() * pixelSize(); + + if (referenceSize != m_data.size()) { + m_data.resize(m_bounds.height() * m_bounds.width() * pixelSize()); + } +} + +void KisFixedPaintDevice::lazyGrowBufferWithoutInitialization() +{ + const int referenceSize = m_bounds.height() * m_bounds.width() * pixelSize(); + + if (m_data.size() < referenceSize) { + m_data.resize(referenceSize); + } +} + quint8* KisFixedPaintDevice::data() { - return m_data.data(); + return (quint8*) m_data.data(); +} + +const quint8 *KisFixedPaintDevice::constData() const +{ + return (const quint8*) m_data.constData(); } quint8* KisFixedPaintDevice::data() const { - return const_cast(m_data.data()); + return const_cast((quint8*)m_data.data()); } void KisFixedPaintDevice::convertTo(const KoColorSpace* dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) { if (*m_colorSpace == *dstColorSpace) { return; } quint32 size = m_bounds.width() * m_bounds.height(); - QVector dstData(size * dstColorSpace->pixelSize()); + QByteArray dstData; + + // make sure that we are not initializing the destination pixels! + dstData.resize(size * dstColorSpace->pixelSize()); - m_colorSpace->convertPixelsTo(data(), dstData.data(), + m_colorSpace->convertPixelsTo(constData(), (quint8*)dstData.data(), dstColorSpace, size, renderingIntent, conversionFlags); m_colorSpace = dstColorSpace; m_data = dstData; - } void KisFixedPaintDevice::convertFromQImage(const QImage& _image, const QString &srcProfileName) { QImage image = _image; if (image.format() != QImage::Format_ARGB32) { image = image.convertToFormat(QImage::Format_ARGB32); } setRect(image.rect()); - initialize(); + lazyGrowBufferWithoutInitialization(); // Don't convert if not no profile is given and both paint dev and qimage are rgba. if (srcProfileName.isEmpty() && colorSpace()->id() == "RGBA") { memcpy(data(), image.constBits(), image.byteCount()); } else { KoColorSpaceRegistry::instance() ->colorSpace( RGBAColorModelID.id(), Integer8BitsColorDepthID.id(), srcProfileName) ->convertPixelsTo(image.constBits(), data(), colorSpace(), image.width() * image.height(), KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); } } QImage KisFixedPaintDevice::convertToQImage(const KoColorProfile * dstProfile, KoColorConversionTransformation::Intent intent, KoColorConversionTransformation::ConversionFlags conversionFlags) const { qint32 x1; qint32 y1; qint32 w; qint32 h; x1 = m_bounds.x(); y1 = m_bounds.y(); w = m_bounds.width(); h = m_bounds.height(); return convertToQImage(dstProfile, x1, y1, w, h, intent, conversionFlags); } QImage KisFixedPaintDevice::convertToQImage(const KoColorProfile * dstProfile, qint32 x1, qint32 y1, qint32 w, qint32 h, KoColorConversionTransformation::Intent intent, KoColorConversionTransformation::ConversionFlags conversionFlags) const { Q_ASSERT( m_bounds.contains(QRect(x1,y1,w,h)) ); if (w < 0) return QImage(); if (h < 0) return QImage(); if (QRect(x1, y1, w, h) == m_bounds) { - return colorSpace()->convertToQImage(data(), w, h, dstProfile, + return colorSpace()->convertToQImage(constData(), w, h, dstProfile, intent, conversionFlags); } else { try { // XXX: fill the image row by row! - int pSize = pixelSize(); - int deviceWidth = m_bounds.width(); + const int pSize = pixelSize(); + const int deviceWidth = m_bounds.width(); quint8* newData = new quint8[w * h * pSize]; - quint8* srcPtr = data() + x1 * pSize + y1 * deviceWidth * pSize; + const quint8* srcPtr = constData() + x1 * pSize + y1 * deviceWidth * pSize; quint8* dstPtr = newData; // copy the right area out of the paint device into data for (int row = 0; row < h; row++) { memcpy(dstPtr, srcPtr, w * pSize); srcPtr += deviceWidth * pSize; dstPtr += w * pSize; } QImage image = colorSpace()->convertToQImage(newData, w, h, dstProfile, intent, conversionFlags); return image; } catch(std::bad_alloc) { return QImage(); } } } void KisFixedPaintDevice::clear(const QRect & rc) { KoColor c(Qt::black, m_colorSpace); quint8* black = new quint8[pixelSize()]; memcpy(black, c.data(), m_colorSpace->pixelSize()); m_colorSpace->setOpacity(black, OPACITY_TRANSPARENT_U8, 1); fill(rc.x(), rc.y(), rc.width(), rc.height(), black); delete[] black; } void KisFixedPaintDevice::fill(const QRect &rc, const KoColor &color) { KoColor realColor(color); realColor.convertTo(colorSpace()); fill(rc.x(), rc.y(), rc.width(), rc.height(), realColor.data()); } void KisFixedPaintDevice::fill(qint32 x, qint32 y, qint32 w, qint32 h, const quint8 *fillPixel) { if (m_data.isEmpty() || m_bounds.isEmpty()) { setRect(QRect(x, y, w, h)); - initialize(); + reallocateBufferWithoutInitialization(); } QRect rc(x, y, w, h); if (!m_bounds.contains(rc)) { rc = m_bounds; } quint8 pixelSize = m_colorSpace->pixelSize(); quint8* dabPointer = data(); if (rc.contains(m_bounds)) { for (int i = 0; i < w * h ; ++i) { memcpy(dabPointer, fillPixel, pixelSize); dabPointer += pixelSize; } } else { int deviceWidth = bounds().width(); quint8* rowPointer = dabPointer + ((y - bounds().y()) * deviceWidth + (x - bounds().x())) * pixelSize; for (int row = 0; row < h; row++) { for (int col = 0; col < w; col++) { memcpy(rowPointer + col * pixelSize , fillPixel, pixelSize); } rowPointer += deviceWidth * pixelSize; } } } void KisFixedPaintDevice::readBytes(quint8* dstData, qint32 x, qint32 y, qint32 w, qint32 h) const { if (m_data.isEmpty() || m_bounds.isEmpty()) { return; } QRect rc(x, y, w, h); if (!m_bounds.contains(rc)){ return; } - quint8 pixelSize = m_colorSpace->pixelSize(); - quint8* dabPointer = data(); + const int pixelSize = m_colorSpace->pixelSize(); + const quint8* dabPointer = constData(); if (rc == m_bounds) { - memcpy(dstData, dabPointer, pixelSize * w * h); - } - else - { - int deviceWidth = bounds().width(); - quint8* rowPointer = dabPointer + ((y - bounds().y()) * deviceWidth + (x - bounds().x())) * pixelSize; + memcpy(dstData, dabPointer, pixelSize * w * h); + } else { + int deviceWidth = m_bounds.width(); + const quint8* rowPointer = dabPointer + ((y - bounds().y()) * deviceWidth + (x - bounds().x())) * pixelSize; for (int row = 0; row < h; row++) { - memcpy(dstData,rowPointer, w * pixelSize); + memcpy(dstData, rowPointer, w * pixelSize); rowPointer += deviceWidth * pixelSize; dstData += w * pixelSize; } } } void KisFixedPaintDevice::mirror(bool horizontal, bool vertical) { if (!horizontal && !vertical){ return; } int pixelSize = m_colorSpace->pixelSize(); int w = m_bounds.width(); int h = m_bounds.height(); if (horizontal){ int rowSize = pixelSize * w; quint8 * dabPointer = data(); quint8 * row = new quint8[ rowSize ]; quint8 * mirror = 0; for (int y = 0; y < h ; y++){ + // TODO: implement better flipping of the data + memcpy(row, dabPointer, rowSize); mirror = row; mirror += (w-1) * pixelSize; for (int x = 0; x < w; x++){ memcpy(dabPointer,mirror,pixelSize); dabPointer += pixelSize; mirror -= pixelSize; } } delete [] row; } if (vertical){ int rowsToMove = h / 2; int rowSize = pixelSize * w; quint8 * startRow = data(); quint8 * endRow = data() + (h-1) * w * pixelSize; quint8 * row = new quint8[ rowSize ]; for (int y = 0; y < rowsToMove; y++){ memcpy(row, startRow, rowSize); memcpy(startRow, endRow, rowSize); memcpy(endRow, row, rowSize); startRow += rowSize; endRow -= rowSize; } delete [] row; } } diff --git a/libs/image/kis_fixed_paint_device.h b/libs/image/kis_fixed_paint_device.h index f09337bccc..d46838bac2 100644 --- a/libs/image/kis_fixed_paint_device.h +++ b/libs/image/kis_fixed_paint_device.h @@ -1,187 +1,202 @@ /* * Copyright (c) 2009 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_FIXED_PAINT_DEVICE_H #define KIS_FIXED_PAINT_DEVICE_H #include #include #include "kis_shared.h" #include #include #include #include class KoColor; /** * A fixed paint device is a simple paint device that consists of an array * of bytes and a rectangle. It cannot grow, it cannot shrink, all you can * do is fill the paint device with the right bytes and use it as an argument * to KisPainter or use the bytes as an argument to KoColorSpace functions. */ class KRITAIMAGE_EXPORT KisFixedPaintDevice : public KisShared { public: KisFixedPaintDevice(const KoColorSpace* colorSpace); virtual ~KisFixedPaintDevice(); /** * Deep copy the fixed paint device, including the data. */ KisFixedPaintDevice(const KisFixedPaintDevice& rhs); /** * Deep copy the fixed paint device, including the data. */ KisFixedPaintDevice& operator=(const KisFixedPaintDevice& rhs); /** * setRect sets the rect of the fixed paint device to rect. * This will _not_ create the associated data area. * * @rect the bounds in pixels. The x,y of the rect represent the origin * of the fixed paint device. */ void setRect(const QRect& rc); /** * @return the rect that the data represents */ QRect bounds() const; /** * @return the amount of allocated pixels (you can fake the size with setRect/bounds) * It is useful to know the accumulated memory size in pixels (not in bytes) for optimizations to avoid re-allocation. */ int allocatedPixels() const; /** * @return the pixelSize associated with this fixed paint device. */ quint32 pixelSize() const; const KoColorSpace* colorSpace() const { return m_colorSpace; } /** * initializes the paint device. * * @param defaultValue the default byte with which all pixels will be filled. * @return false if the allocation failed. */ bool initialize(quint8 defaultValue = 0); + /** + * Changed the size of the internal buffer to accomodate the exact number of bytes + * needed to store area bounds(). The allocated data is *not* initialized! + */ + void reallocateBufferWithoutInitialization(); + + /** + * If the size of the internal buffer is smller than the one needed to accomodate + * bounds(), resize the buffer. Otherwise, do nothing. The allocated data is neither + * copying or initialized! + */ + void lazyGrowBufferWithoutInitialization(); + /** * @return a pointer to the beginning of the data associated with this fixed paint device. */ quint8* data(); + const quint8* constData() const; + quint8* data() const; /** * Read the bytes representing the rectangle described by x, y, w, h into * data. If data is not big enough, Krita will gladly overwrite the rest * of your precious memory. * * Since this is a copy, you need to make sure you have enough memory. * * The reading is done only if the rectangular area x,y,w,h is inside the bounds of the device * and the device is not empty */ void readBytes(quint8 * dstData, qint32 x, qint32 y, qint32 w, qint32 h) const; /** * Converts the paint device to a different colorspace */ void convertTo(const KoColorSpace * dstColorSpace = 0, KoColorConversionTransformation::Intent renderingIntent = KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::ConversionFlags conversionFlags = KoColorConversionTransformation::internalConversionFlags()); /** * Fill this paint device with the data from image * * @param srcProfileName name of the RGB profile to interpret the image as. 0 is interpreted as sRGB */ virtual void convertFromQImage(const QImage& image, const QString &srcProfileName); /** * Create an RGBA QImage from a rectangle in the paint device. * * @param x Left coordinate of the rectangle * @param y Top coordinate of the rectangle * @param w Width of the rectangle in pixels * @param h Height of the rectangle in pixels * @param dstProfile RGB profile to use in conversion. May be 0, in which * case it's up to the color strategy to choose a profile (most * like sRGB). */ virtual QImage convertToQImage(const KoColorProfile *dstProfile, qint32 x, qint32 y, qint32 w, qint32 h, KoColorConversionTransformation::Intent renderingIntent = KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::ConversionFlags conversionFlags = KoColorConversionTransformation::internalConversionFlags()) const; /** * Create an RGBA QImage from a rectangle in the paint device. The * rectangle is defined by the parent image's bounds. * * @param dstProfile RGB profile to use in conversion. May be 0, in which * case it's up to the color strategy to choose a profile (most * like sRGB). */ virtual QImage convertToQImage(const KoColorProfile *dstProfile, KoColorConversionTransformation::Intent renderingIntent = KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::ConversionFlags conversionFlags = KoColorConversionTransformation::internalConversionFlags()) const; /** * Clear the given rectangle to transparent black. * * XXX: this will not (yet) expand the paint device to contain the specified rect * but if the paintdevice has not been initialized, it will be. */ void clear(const QRect & rc); /** * Fill the given rectangle with the given pixel. This does not take the * selection into account. * * XXX: this will not (yet) expand the paint device to contain the specified rect * but if the paintdevice has not been initialized, it will be. */ void fill(qint32 x, qint32 y, qint32 w, qint32 h, const quint8 *fillPixel); void fill(const QRect &rc, const KoColor &color); /** * Mirrors the device. */ void mirror(bool horizontal, bool vertical); private: const KoColorSpace* m_colorSpace; QRect m_bounds; - QVector m_data; + QByteArray m_data; }; #endif diff --git a/libs/image/kis_image.cc b/libs/image/kis_image.cc index 2ea1b7a400..e76d7d7ee1 100644 --- a/libs/image/kis_image.cc +++ b/libs/image/kis_image.cc @@ -1,1726 +1,1730 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2007 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_image.h" #include // WORDS_BIGENDIAN #include #include #include #include #include #include #include #include #include #include #include "KoColorSpaceRegistry.h" #include "KoColor.h" #include "KoColorProfile.h" #include #include "KisProofingConfiguration.h" #include "recorder/kis_action_recorder.h" #include "kis_adjustment_layer.h" #include "kis_annotation.h" #include "kis_change_profile_visitor.h" #include "kis_colorspace_convert_visitor.h" #include "kis_count_visitor.h" #include "kis_filter_strategy.h" #include "kis_group_layer.h" #include "commands/kis_image_commands.h" #include "kis_layer.h" #include "kis_meta_data_merge_strategy_registry.h" #include "kis_name_server.h" #include "kis_paint_layer.h" #include "kis_painter.h" #include "kis_selection.h" #include "kis_transaction.h" #include "kis_meta_data_merge_strategy.h" #include "kis_memory_statistics_server.h" #include "kis_image_config.h" #include "kis_update_scheduler.h" #include "kis_image_signal_router.h" #include "kis_image_animation_interface.h" #include "kis_stroke_strategy.h" #include "kis_image_barrier_locker.h" #include "kis_undo_stores.h" #include "kis_legacy_undo_adapter.h" #include "kis_post_execution_undo_adapter.h" #include "kis_transform_worker.h" #include "kis_processing_applicator.h" #include "processing/kis_crop_processing_visitor.h" #include "processing/kis_crop_selections_processing_visitor.h" #include "processing/kis_transform_processing_visitor.h" #include "commands_new/kis_image_resize_command.h" #include "commands_new/kis_image_set_resolution_command.h" #include "commands_new/kis_activate_selection_mask_command.h" #include "kis_composite_progress_proxy.h" #include "kis_layer_composition.h" #include "kis_wrapped_rect.h" #include "kis_crop_saved_extra_data.h" #include "kis_layer_utils.h" #include "kis_lod_transform.h" #include "kis_suspend_projection_updates_stroke_strategy.h" #include "kis_sync_lod_cache_stroke_strategy.h" #include "kis_projection_updates_filter.h" #include "kis_layer_projection_plane.h" #include "kis_update_time_monitor.h" #include "kis_image_barrier_locker.h" #include #include #include "kis_time_range.h" // #define SANITY_CHECKS #ifdef SANITY_CHECKS #define SANITY_CHECK_LOCKED(name) \ if (!locked()) warnKrita() << "Locking policy failed:" << name \ << "has been called without the image" \ "being locked"; #else #define SANITY_CHECK_LOCKED(name) #endif struct KisImageSPStaticRegistrar { KisImageSPStaticRegistrar() { qRegisterMetaType("KisImageSP"); } }; static KisImageSPStaticRegistrar __registrar; class KisImage::KisImagePrivate { public: KisImagePrivate(KisImage *_q, qint32 w, qint32 h, const KoColorSpace *c, KisUndoStore *undo, KisImageAnimationInterface *_animationInterface) : q(_q) , lockedForReadOnly(false) , width(w) , height(h) , colorSpace(c ? c : KoColorSpaceRegistry::instance()->rgb8()) , nserver(1) , undoStore(undo ? undo : new KisDumbUndoStore()) , legacyUndoAdapter(undoStore.data(), _q) , postExecutionUndoAdapter(undoStore.data(), _q) , recorder(_q) , signalRouter(_q) , animationInterface(_animationInterface) , scheduler(_q, _q) , axesCenter(QPointF(0.5, 0.5)) { { KisImageConfig cfg; if (cfg.enableProgressReporting()) { scheduler.setProgressProxy(&compositeProgressProxy); } // Each of these lambdas defines a new factory function. scheduler.setLod0ToNStrokeStrategyFactory( [=](bool forgettable) { return KisLodSyncPair( new KisSyncLodCacheStrokeStrategy(KisImageWSP(q), forgettable), KisSyncLodCacheStrokeStrategy::createJobsData(KisImageWSP(q))); }); scheduler.setSuspendUpdatesStrokeStrategyFactory( [=]() { return KisSuspendResumePair( new KisSuspendProjectionUpdatesStrokeStrategy(KisImageWSP(q), true), KisSuspendProjectionUpdatesStrokeStrategy::createSuspendJobsData(KisImageWSP(q))); }); scheduler.setResumeUpdatesStrokeStrategyFactory( [=]() { return KisSuspendResumePair( new KisSuspendProjectionUpdatesStrokeStrategy(KisImageWSP(q), false), KisSuspendProjectionUpdatesStrokeStrategy::createResumeJobsData(KisImageWSP(q))); }); } connect(q, SIGNAL(sigImageModified()), KisMemoryStatisticsServer::instance(), SLOT(notifyImageChanged())); } ~KisImagePrivate() { /** * Stop animation interface. It may use the rootLayer. */ delete animationInterface; /** * First delete the nodes, while strokes * and undo are still alive */ rootLayer.clear(); } KisImage *q; quint32 lockCount = 0; bool lockedForReadOnly; qint32 width; qint32 height; double xres = 1.0; double yres = 1.0; const KoColorSpace * colorSpace; KisProofingConfigurationSP proofingConfig; KisSelectionSP deselectedGlobalSelection; KisGroupLayerSP rootLayer; // The layers are contained in here QList compositions; KisNodeSP isolatedRootNode; bool wrapAroundModePermitted = false; KisNameServer nserver; QScopedPointer undoStore; KisLegacyUndoAdapter legacyUndoAdapter; KisPostExecutionUndoAdapter postExecutionUndoAdapter; KisActionRecorder recorder; vKisAnnotationSP annotations; QAtomicInt disableUIUpdateSignals; KisProjectionUpdatesFilterSP projectionUpdatesFilter; KisImageSignalRouter signalRouter; KisImageAnimationInterface *animationInterface; KisUpdateScheduler scheduler; QAtomicInt disableDirtyRequests; KisCompositeProgressProxy compositeProgressProxy; bool blockLevelOfDetail = false; QPointF axesCenter; bool tryCancelCurrentStrokeAsync(); void notifyProjectionUpdatedInPatches(const QRect &rc); }; KisImage::KisImage(KisUndoStore *undoStore, qint32 width, qint32 height, const KoColorSpace * colorSpace, const QString& name) : QObject(0) , KisShared() , m_d(new KisImagePrivate(this, width, height, colorSpace, undoStore, new KisImageAnimationInterface(this))) { // make sure KisImage belongs to the GUI thread moveToThread(qApp->thread()); setObjectName(name); setRootLayer(new KisGroupLayer(this, "root", OPACITY_OPAQUE_U8)); } KisImage::~KisImage() { dbgImage << "deleting kisimage" << objectName(); /** * Request the tools to end currently running strokes */ waitForDone(); delete m_d; disconnect(); // in case Qt gets confused } KisImage *KisImage::clone(bool exactCopy) { return new KisImage(*this, 0, exactCopy); } KisImage::KisImage(const KisImage& rhs, KisUndoStore *undoStore, bool exactCopy) : KisNodeFacade(), KisNodeGraphListener(), KisShared(), m_d(new KisImagePrivate(this, rhs.width(), rhs.height(), rhs.colorSpace(), undoStore ? undoStore : new KisDumbUndoStore(), new KisImageAnimationInterface(*rhs.animationInterface(), this))) { // make sure KisImage belongs to the GUI thread moveToThread(qApp->thread()); setObjectName(rhs.objectName()); m_d->xres = rhs.m_d->xres; m_d->yres = rhs.m_d->yres; if (rhs.m_d->proofingConfig) { m_d->proofingConfig = toQShared(new KisProofingConfiguration(*rhs.m_d->proofingConfig)); } KisNodeSP newRoot = rhs.root()->clone(); newRoot->setGraphListener(this); newRoot->setImage(this); m_d->rootLayer = dynamic_cast(newRoot.data()); setRoot(newRoot); if (exactCopy) { QQueue linearizedNodes; KisLayerUtils::recursiveApplyNodes(rhs.root(), [&linearizedNodes](KisNodeSP node) { linearizedNodes.enqueue(node); }); KisLayerUtils::recursiveApplyNodes(newRoot, [&linearizedNodes](KisNodeSP node) { KisNodeSP refNode = linearizedNodes.dequeue(); node->setUuid(refNode->uuid()); }); } Q_FOREACH (KisLayerCompositionSP comp, rhs.m_d->compositions) { m_d->compositions << toQShared(new KisLayerComposition(*comp, this)); } rhs.m_d->nserver = KisNameServer(rhs.m_d->nserver); vKisAnnotationSP newAnnotations; Q_FOREACH (KisAnnotationSP annotation, rhs.m_d->annotations) { newAnnotations << annotation->clone(); } m_d->annotations = newAnnotations; KIS_ASSERT_RECOVER_NOOP(!rhs.m_d->projectionUpdatesFilter); KIS_ASSERT_RECOVER_NOOP(!rhs.m_d->disableUIUpdateSignals); KIS_ASSERT_RECOVER_NOOP(!rhs.m_d->disableDirtyRequests); m_d->blockLevelOfDetail = rhs.m_d->blockLevelOfDetail; } void KisImage::aboutToAddANode(KisNode *parent, int index) { KisNodeGraphListener::aboutToAddANode(parent, index); SANITY_CHECK_LOCKED("aboutToAddANode"); } void KisImage::nodeHasBeenAdded(KisNode *parent, int index) { KisNodeGraphListener::nodeHasBeenAdded(parent, index); SANITY_CHECK_LOCKED("nodeHasBeenAdded"); m_d->signalRouter.emitNodeHasBeenAdded(parent, index); KisNodeSP newNode = parent->at(index); if (!dynamic_cast(newNode.data())) { stopIsolatedMode(); } } void KisImage::aboutToRemoveANode(KisNode *parent, int index) { KisNodeSP deletedNode = parent->at(index); if (!dynamic_cast(deletedNode.data())) { stopIsolatedMode(); } KisNodeGraphListener::aboutToRemoveANode(parent, index); SANITY_CHECK_LOCKED("aboutToRemoveANode"); m_d->signalRouter.emitAboutToRemoveANode(parent, index); } void KisImage::nodeChanged(KisNode* node) { KisNodeGraphListener::nodeChanged(node); requestStrokeEnd(); m_d->signalRouter.emitNodeChanged(node); } void KisImage::invalidateAllFrames() { invalidateFrames(KisTimeRange::infinite(0), QRect()); } KisSelectionSP KisImage::globalSelection() const { KisSelectionMaskSP selectionMask = m_d->rootLayer->selectionMask(); if (selectionMask) { return selectionMask->selection(); } else { return 0; } } void KisImage::setGlobalSelection(KisSelectionSP globalSelection) { KisSelectionMaskSP selectionMask = m_d->rootLayer->selectionMask(); if (!globalSelection) { if (selectionMask) { removeNode(selectionMask); } } else { if (!selectionMask) { selectionMask = new KisSelectionMask(this); selectionMask->initSelection(m_d->rootLayer); addNode(selectionMask); // If we do not set the selection now, the setActive call coming next // can be very, very expensive, depending on the size of the image. selectionMask->setSelection(globalSelection); selectionMask->setActive(true); } else { selectionMask->setSelection(globalSelection); } Q_ASSERT(m_d->rootLayer->childCount() > 0); Q_ASSERT(m_d->rootLayer->selectionMask()); } m_d->deselectedGlobalSelection = 0; m_d->legacyUndoAdapter.emitSelectionChanged(); } void KisImage::deselectGlobalSelection() { KisSelectionSP savedSelection = globalSelection(); setGlobalSelection(0); m_d->deselectedGlobalSelection = savedSelection; } bool KisImage::canReselectGlobalSelection() { return m_d->deselectedGlobalSelection; } void KisImage::reselectGlobalSelection() { if(m_d->deselectedGlobalSelection) { setGlobalSelection(m_d->deselectedGlobalSelection); } } QString KisImage::nextLayerName(const QString &_baseName) const { QString baseName = _baseName; if (m_d->nserver.currentSeed() == 0) { m_d->nserver.number(); return i18n("background"); } if (baseName.isEmpty()) { baseName = i18n("Layer"); } return QString("%1 %2").arg(baseName).arg(m_d->nserver.number()); } void KisImage::rollBackLayerName() { m_d->nserver.rollback(); } KisCompositeProgressProxy* KisImage::compositeProgressProxy() { return &m_d->compositeProgressProxy; } bool KisImage::locked() const { return m_d->lockCount != 0; } void KisImage::barrierLock(bool readOnly) { if (!locked()) { requestStrokeEnd(); m_d->scheduler.barrierLock(); m_d->lockedForReadOnly = readOnly; } else { m_d->lockedForReadOnly &= readOnly; } m_d->lockCount++; } bool KisImage::tryBarrierLock(bool readOnly) { bool result = true; if (!locked()) { result = m_d->scheduler.tryBarrierLock(); m_d->lockedForReadOnly = readOnly; } if (result) { m_d->lockCount++; m_d->lockedForReadOnly &= readOnly; } return result; } bool KisImage::isIdle(bool allowLocked) { return (allowLocked || !locked()) && m_d->scheduler.isIdle(); } void KisImage::lock() { if (!locked()) { requestStrokeEnd(); m_d->scheduler.lock(); } m_d->lockCount++; m_d->lockedForReadOnly = false; } void KisImage::unlock() { Q_ASSERT(locked()); if (locked()) { m_d->lockCount--; if (m_d->lockCount == 0) { m_d->scheduler.unlock(!m_d->lockedForReadOnly); } } } void KisImage::blockUpdates() { m_d->scheduler.blockUpdates(); } void KisImage::unblockUpdates() { m_d->scheduler.unblockUpdates(); } void KisImage::setSize(const QSize& size) { m_d->width = size.width(); m_d->height = size.height(); } void KisImage::resizeImageImpl(const QRect& newRect, bool cropLayers) { if (newRect == bounds() && !cropLayers) return; KUndo2MagicString actionName = cropLayers ? kundo2_i18n("Crop Image") : kundo2_i18n("Resize Image"); KisImageSignalVector emitSignals; emitSignals << ComplexSizeChangedSignal(newRect, newRect.size()); emitSignals << ModifiedSignal; KisCropSavedExtraData *extraData = new KisCropSavedExtraData(cropLayers ? KisCropSavedExtraData::CROP_IMAGE : KisCropSavedExtraData::RESIZE_IMAGE, newRect); KisProcessingApplicator applicator(this, m_d->rootLayer, KisProcessingApplicator::RECURSIVE | KisProcessingApplicator::NO_UI_UPDATES, emitSignals, actionName, extraData); if (cropLayers || !newRect.topLeft().isNull()) { KisProcessingVisitorSP visitor = new KisCropProcessingVisitor(newRect, cropLayers, true); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); } applicator.applyCommand(new KisImageResizeCommand(this, newRect.size())); applicator.end(); } void KisImage::resizeImage(const QRect& newRect) { resizeImageImpl(newRect, false); } void KisImage::cropImage(const QRect& newRect) { resizeImageImpl(newRect, true); } void KisImage::cropNode(KisNodeSP node, const QRect& newRect) { bool isLayer = qobject_cast(node.data()); KUndo2MagicString actionName = isLayer ? kundo2_i18n("Crop Layer") : kundo2_i18n("Crop Mask"); KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisCropSavedExtraData *extraData = new KisCropSavedExtraData(KisCropSavedExtraData::CROP_LAYER, newRect, node); KisProcessingApplicator applicator(this, node, KisProcessingApplicator::RECURSIVE, emitSignals, actionName, extraData); KisProcessingVisitorSP visitor = new KisCropProcessingVisitor(newRect, true, false); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); applicator.end(); } void KisImage::scaleImage(const QSize &size, qreal xres, qreal yres, KisFilterStrategy *filterStrategy) { bool resolutionChanged = xres != xRes() && yres != yRes(); bool sizeChanged = size != this->size(); if (!resolutionChanged && !sizeChanged) return; KisImageSignalVector emitSignals; if (resolutionChanged) emitSignals << ResolutionChangedSignal; if (sizeChanged) emitSignals << ComplexSizeChangedSignal(bounds(), size); emitSignals << ModifiedSignal; KUndo2MagicString actionName = sizeChanged ? kundo2_i18n("Scale Image") : kundo2_i18n("Change Image Resolution"); KisProcessingApplicator::ProcessingFlags signalFlags = (resolutionChanged || sizeChanged) ? KisProcessingApplicator::NO_UI_UPDATES : KisProcessingApplicator::NONE; KisProcessingApplicator applicator(this, m_d->rootLayer, KisProcessingApplicator::RECURSIVE | signalFlags, emitSignals, actionName); qreal sx = qreal(size.width()) / this->size().width(); qreal sy = qreal(size.height()) / this->size().height(); QTransform shapesCorrection; if (resolutionChanged) { shapesCorrection = QTransform::fromScale(xRes() / xres, yRes() / yres); } KisProcessingVisitorSP visitor = new KisTransformProcessingVisitor(sx, sy, 0, 0, QPointF(), 0, 0, 0, filterStrategy, shapesCorrection); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); if (resolutionChanged) { KUndo2Command *parent = new KisResetShapesCommand(m_d->rootLayer); new KisImageSetResolutionCommand(this, xres, yres, parent); applicator.applyCommand(parent); } if (sizeChanged) { applicator.applyCommand(new KisImageResizeCommand(this, size)); } applicator.end(); } void KisImage::scaleNode(KisNodeSP node, qreal scaleX, qreal scaleY, KisFilterStrategy *filterStrategy) { KUndo2MagicString actionName(kundo2_i18n("Scale Layer")); KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisProcessingApplicator applicator(this, node, KisProcessingApplicator::RECURSIVE, emitSignals, actionName); KisProcessingVisitorSP visitor = new KisTransformProcessingVisitor(scaleX, scaleY, 0, 0, QPointF(), 0, 0, 0, filterStrategy); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); applicator.end(); } void KisImage::rotateImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, bool resizeImage, double radians) { QPointF offset; QSize newSize; { KisTransformWorker worker(0, 1.0, 1.0, 0, 0, 0, 0, radians, 0, 0, 0, 0); QTransform transform = worker.transform(); if (resizeImage) { QRect newRect = transform.mapRect(bounds()); newSize = newRect.size(); offset = -newRect.topLeft(); } else { QPointF origin = QRectF(rootNode->exactBounds()).center(); newSize = size(); offset = -(transform.map(origin) - origin); } } bool sizeChanged = resizeImage && (newSize.width() != width() || newSize.height() != height()); // These signals will be emitted after processing is done KisImageSignalVector emitSignals; if (sizeChanged) emitSignals << ComplexSizeChangedSignal(bounds(), newSize); emitSignals << ModifiedSignal; // These flags determine whether updates are transferred to the UI during processing KisProcessingApplicator::ProcessingFlags signalFlags = sizeChanged ? KisProcessingApplicator::NO_UI_UPDATES : KisProcessingApplicator::NONE; KisProcessingApplicator applicator(this, rootNode, KisProcessingApplicator::RECURSIVE | signalFlags, emitSignals, actionName); KisFilterStrategy *filter = KisFilterStrategyRegistry::instance()->value("Bicubic"); KisProcessingVisitorSP visitor = new KisTransformProcessingVisitor(1.0, 1.0, 0.0, 0.0, QPointF(), radians, offset.x(), offset.y(), filter); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); if (sizeChanged) { applicator.applyCommand(new KisImageResizeCommand(this, newSize)); } applicator.end(); } void KisImage::rotateImage(double radians) { rotateImpl(kundo2_i18n("Rotate Image"), root(), true, radians); } void KisImage::rotateNode(KisNodeSP node, double radians) { if (node->inherits("KisMask")) { rotateImpl(kundo2_i18n("Rotate Mask"), node, false, radians); } else { rotateImpl(kundo2_i18n("Rotate Layer"), node, false, radians); } } void KisImage::shearImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, bool resizeImage, double angleX, double angleY, const QPointF &origin) { //angleX, angleY are in degrees const qreal pi = 3.1415926535897932385; const qreal deg2rad = pi / 180.0; qreal tanX = tan(angleX * deg2rad); qreal tanY = tan(angleY * deg2rad); QPointF offset; QSize newSize; { KisTransformWorker worker(0, 1.0, 1.0, tanX, tanY, origin.x(), origin.y(), 0, 0, 0, 0, 0); QRect newRect = worker.transform().mapRect(bounds()); newSize = newRect.size(); if (resizeImage) offset = -newRect.topLeft(); } if (newSize == size()) return; KisImageSignalVector emitSignals; if (resizeImage) emitSignals << ComplexSizeChangedSignal(bounds(), newSize); emitSignals << ModifiedSignal; KisProcessingApplicator::ProcessingFlags signalFlags = KisProcessingApplicator::RECURSIVE; if (resizeImage) signalFlags |= KisProcessingApplicator::NO_UI_UPDATES; KisProcessingApplicator applicator(this, rootNode, signalFlags, emitSignals, actionName); KisFilterStrategy *filter = KisFilterStrategyRegistry::instance()->value("Bilinear"); KisProcessingVisitorSP visitor = new KisTransformProcessingVisitor(1.0, 1.0, tanX, tanY, origin, 0, offset.x(), offset.y(), filter); applicator.applyVisitorAllFrames(visitor, KisStrokeJobData::CONCURRENT); if (resizeImage) { applicator.applyCommand(new KisImageResizeCommand(this, newSize)); } applicator.end(); } void KisImage::shearNode(KisNodeSP node, double angleX, double angleY) { QPointF shearOrigin = QRectF(bounds()).center(); if (node->inherits("KisMask")) { shearImpl(kundo2_i18n("Shear Mask"), node, false, angleX, angleY, shearOrigin); } else { shearImpl(kundo2_i18n("Shear Layer"), node, false, angleX, angleY, shearOrigin); } } void KisImage::shear(double angleX, double angleY) { shearImpl(kundo2_i18n("Shear Image"), m_d->rootLayer, true, angleX, angleY, QPointF()); } void KisImage::convertImageColorSpace(const KoColorSpace *dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) { if (!dstColorSpace) return; const KoColorSpace *srcColorSpace = m_d->colorSpace; undoAdapter()->beginMacro(kundo2_i18n("Convert Image Color Space")); undoAdapter()->addCommand(new KisImageLockCommand(KisImageWSP(this), true)); undoAdapter()->addCommand(new KisImageSetProjectionColorSpaceCommand(KisImageWSP(this), dstColorSpace)); KisColorSpaceConvertVisitor visitor(this, srcColorSpace, dstColorSpace, renderingIntent, conversionFlags); m_d->rootLayer->accept(visitor); undoAdapter()->addCommand(new KisImageLockCommand(KisImageWSP(this), false)); undoAdapter()->endMacro(); setModified(); } bool KisImage::assignImageProfile(const KoColorProfile *profile) { if (!profile) return false; const KoColorSpace *dstCs = KoColorSpaceRegistry::instance()->colorSpace(colorSpace()->colorModelId().id(), colorSpace()->colorDepthId().id(), profile); const KoColorSpace *srcCs = colorSpace(); if (!dstCs) return false; m_d->colorSpace = dstCs; KisChangeProfileVisitor visitor(srcCs, dstCs); bool retval = m_d->rootLayer->accept(visitor); m_d->signalRouter.emitNotification(ProfileChangedSignal); return retval; } void KisImage::convertProjectionColorSpace(const KoColorSpace *dstColorSpace) { if (*m_d->colorSpace == *dstColorSpace) return; undoAdapter()->beginMacro(kundo2_i18n("Convert Projection Color Space")); undoAdapter()->addCommand(new KisImageLockCommand(KisImageWSP(this), true)); undoAdapter()->addCommand(new KisImageSetProjectionColorSpaceCommand(KisImageWSP(this), dstColorSpace)); undoAdapter()->addCommand(new KisImageLockCommand(KisImageWSP(this), false)); undoAdapter()->endMacro(); setModified(); } void KisImage::setProjectionColorSpace(const KoColorSpace * colorSpace) { m_d->colorSpace = colorSpace; m_d->rootLayer->resetCache(); m_d->signalRouter.emitNotification(ColorSpaceChangedSignal); } const KoColorSpace * KisImage::colorSpace() const { return m_d->colorSpace; } const KoColorProfile * KisImage::profile() const { return colorSpace()->profile(); } double KisImage::xRes() const { return m_d->xres; } double KisImage::yRes() const { return m_d->yres; } void KisImage::setResolution(double xres, double yres) { m_d->xres = xres; m_d->yres = yres; m_d->signalRouter.emitNotification(ResolutionChangedSignal); } QPointF KisImage::documentToPixel(const QPointF &documentCoord) const { return QPointF(documentCoord.x() * xRes(), documentCoord.y() * yRes()); } QPoint KisImage::documentToIntPixel(const QPointF &documentCoord) const { QPointF pixelCoord = documentToPixel(documentCoord); return QPoint((int)pixelCoord.x(), (int)pixelCoord.y()); } QRectF KisImage::documentToPixel(const QRectF &documentRect) const { return QRectF(documentToPixel(documentRect.topLeft()), documentToPixel(documentRect.bottomRight())); } QRect KisImage::documentToIntPixel(const QRectF &documentRect) const { return documentToPixel(documentRect).toAlignedRect(); } QPointF KisImage::pixelToDocument(const QPointF &pixelCoord) const { return QPointF(pixelCoord.x() / xRes(), pixelCoord.y() / yRes()); } QPointF KisImage::pixelToDocument(const QPoint &pixelCoord) const { return QPointF((pixelCoord.x() + 0.5) / xRes(), (pixelCoord.y() + 0.5) / yRes()); } QRectF KisImage::pixelToDocument(const QRectF &pixelCoord) const { return QRectF(pixelToDocument(pixelCoord.topLeft()), pixelToDocument(pixelCoord.bottomRight())); } qint32 KisImage::width() const { return m_d->width; } qint32 KisImage::height() const { return m_d->height; } KisGroupLayerSP KisImage::rootLayer() const { Q_ASSERT(m_d->rootLayer); return m_d->rootLayer; } KisPaintDeviceSP KisImage::projection() const { if (m_d->isolatedRootNode) { return m_d->isolatedRootNode->projection(); } Q_ASSERT(m_d->rootLayer); KisPaintDeviceSP projection = m_d->rootLayer->projection(); Q_ASSERT(projection); return projection; } qint32 KisImage::nlayers() const { QStringList list; list << "KisLayer"; KisCountVisitor visitor(list, KoProperties()); m_d->rootLayer->accept(visitor); return visitor.count(); } qint32 KisImage::nHiddenLayers() const { QStringList list; list << "KisLayer"; KoProperties properties; properties.setProperty("visible", false); KisCountVisitor visitor(list, properties); m_d->rootLayer->accept(visitor); return visitor.count(); } void KisImage::flatten() { KisLayerUtils::flattenImage(this); } void KisImage::mergeMultipleLayers(QList mergedNodes, KisNodeSP putAfter) { if (!KisLayerUtils::tryMergeSelectionMasks(this, mergedNodes, putAfter)) { KisLayerUtils::mergeMultipleLayers(this, mergedNodes, putAfter); } } void KisImage::mergeDown(KisLayerSP layer, const KisMetaData::MergeStrategy* strategy) { KisLayerUtils::mergeDown(this, layer, strategy); } void KisImage::flattenLayer(KisLayerSP layer) { KisLayerUtils::flattenLayer(this, layer); } void KisImage::setModified() { m_d->signalRouter.emitNotification(ModifiedSignal); } QImage KisImage::convertToQImage(QRect imageRect, const KoColorProfile * profile) { qint32 x; qint32 y; qint32 w; qint32 h; imageRect.getRect(&x, &y, &w, &h); return convertToQImage(x, y, w, h, profile); } QImage KisImage::convertToQImage(qint32 x, qint32 y, qint32 w, qint32 h, const KoColorProfile * profile) { KisPaintDeviceSP dev = projection(); if (!dev) return QImage(); QImage image = dev->convertToQImage(const_cast(profile), x, y, w, h, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); return image; } QImage KisImage::convertToQImage(const QSize& scaledImageSize, const KoColorProfile *profile) { if (scaledImageSize.isEmpty()) { return QImage(); } KisPaintDeviceSP dev = new KisPaintDevice(colorSpace()); KisPainter gc; gc.copyAreaOptimized(QPoint(0, 0), projection(), dev, bounds()); gc.end(); double scaleX = qreal(scaledImageSize.width()) / width(); double scaleY = qreal(scaledImageSize.height()) / height(); QPointer updater = new KoDummyUpdater(); KisTransformWorker worker(dev, scaleX, scaleY, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, updater, KisFilterStrategyRegistry::instance()->value("Bicubic")); worker.run(); delete updater; return dev->convertToQImage(profile); } void KisImage::notifyLayersChanged() { m_d->signalRouter.emitNotification(LayersChangedSignal); } QRect KisImage::bounds() const { return QRect(0, 0, width(), height()); } QRect KisImage::effectiveLodBounds() const { QRect boundRect = bounds(); const int lod = currentLevelOfDetail(); if (lod > 0) { KisLodTransform t(lod); boundRect = t.map(boundRect); } return boundRect; } KisPostExecutionUndoAdapter* KisImage::postExecutionUndoAdapter() const { const int lod = currentLevelOfDetail(); return lod > 0 ? m_d->scheduler.lodNPostExecutionUndoAdapter() : &m_d->postExecutionUndoAdapter; } void KisImage::setUndoStore(KisUndoStore *undoStore) { m_d->legacyUndoAdapter.setUndoStore(undoStore); m_d->postExecutionUndoAdapter.setUndoStore(undoStore); m_d->undoStore.reset(undoStore); } KisUndoStore* KisImage::undoStore() { return m_d->undoStore.data(); } KisUndoAdapter* KisImage::undoAdapter() const { return &m_d->legacyUndoAdapter; } KisActionRecorder* KisImage::actionRecorder() const { return &m_d->recorder; } void KisImage::setDefaultProjectionColor(const KoColor &color) { KIS_ASSERT_RECOVER_RETURN(m_d->rootLayer); m_d->rootLayer->setDefaultProjectionColor(color); } KoColor KisImage::defaultProjectionColor() const { KIS_ASSERT_RECOVER(m_d->rootLayer) { return KoColor(Qt::transparent, m_d->colorSpace); } return m_d->rootLayer->defaultProjectionColor(); } void KisImage::setRootLayer(KisGroupLayerSP rootLayer) { stopIsolatedMode(); KoColor defaultProjectionColor(Qt::transparent, m_d->colorSpace); if (m_d->rootLayer) { m_d->rootLayer->setGraphListener(0); m_d->rootLayer->disconnect(); KisPaintDeviceSP original = m_d->rootLayer->original(); defaultProjectionColor = original->defaultPixel(); } m_d->rootLayer = rootLayer; m_d->rootLayer->disconnect(); m_d->rootLayer->setGraphListener(this); m_d->rootLayer->setImage(this); KisPaintDeviceSP newOriginal = m_d->rootLayer->original(); newOriginal->setDefaultPixel(defaultProjectionColor); setRoot(m_d->rootLayer.data()); } void KisImage::addAnnotation(KisAnnotationSP annotation) { // Find the icc annotation, if there is one vKisAnnotationSP_it it = m_d->annotations.begin(); while (it != m_d->annotations.end()) { if ((*it)->type() == annotation->type()) { *it = annotation; return; } ++it; } m_d->annotations.push_back(annotation); } KisAnnotationSP KisImage::annotation(const QString& type) { vKisAnnotationSP_it it = m_d->annotations.begin(); while (it != m_d->annotations.end()) { if ((*it)->type() == type) { return *it; } ++it; } return KisAnnotationSP(0); } void KisImage::removeAnnotation(const QString& type) { vKisAnnotationSP_it it = m_d->annotations.begin(); while (it != m_d->annotations.end()) { if ((*it)->type() == type) { m_d->annotations.erase(it); return; } ++it; } } vKisAnnotationSP_it KisImage::beginAnnotations() { return m_d->annotations.begin(); } vKisAnnotationSP_it KisImage::endAnnotations() { return m_d->annotations.end(); } void KisImage::notifyAboutToBeDeleted() { emit sigAboutToBeDeleted(); } KisImageSignalRouter* KisImage::signalRouter() { return &m_d->signalRouter; } void KisImage::waitForDone() { requestStrokeEnd(); m_d->scheduler.waitForDone(); } KisStrokeId KisImage::startStroke(KisStrokeStrategy *strokeStrategy) { /** * Ask open strokes to end gracefully. All the strokes clients * (including the one calling this method right now) will get * a notification that they should probably end their strokes. * However this is purely their choice whether to end a stroke * or not. */ if (strokeStrategy->requestsOtherStrokesToEnd()) { requestStrokeEnd(); } /** * Some of the strokes can cancel their work with undoing all the * changes they did to the paint devices. The problem is that undo * stack will know nothing about it. Therefore, just notify it * explicitly */ if (strokeStrategy->clearsRedoOnStart()) { m_d->undoStore->purgeRedoState(); } return m_d->scheduler.startStroke(strokeStrategy); } void KisImage::KisImagePrivate::notifyProjectionUpdatedInPatches(const QRect &rc) { KisImageConfig imageConfig; int patchWidth = imageConfig.updatePatchWidth(); int patchHeight = imageConfig.updatePatchHeight(); for (int y = 0; y < rc.height(); y += patchHeight) { for (int x = 0; x < rc.width(); x += patchWidth) { QRect patchRect(x, y, patchWidth, patchHeight); patchRect &= rc; QtConcurrent::run(std::bind(&KisImage::notifyProjectionUpdated, q, patchRect)); } } } bool KisImage::startIsolatedMode(KisNodeSP node) { if (!tryBarrierLock()) return false; unlock(); m_d->isolatedRootNode = node; emit sigIsolatedModeChanged(); // the GUI uses our thread to do the color space conversion so we // need to emit this signal in multiple threads m_d->notifyProjectionUpdatedInPatches(bounds()); invalidateAllFrames(); return true; } void KisImage::stopIsolatedMode() { if (!m_d->isolatedRootNode) return; KisNodeSP oldRootNode = m_d->isolatedRootNode; m_d->isolatedRootNode = 0; emit sigIsolatedModeChanged(); // the GUI uses our thread to do the color space conversion so we // need to emit this signal in multiple threads m_d->notifyProjectionUpdatedInPatches(bounds()); invalidateAllFrames(); // TODO: Substitute notifyProjectionUpdated() with this code // when update optimization is implemented // // QRect updateRect = bounds() | oldRootNode->extent(); // oldRootNode->setDirty(updateRect); } KisNodeSP KisImage::isolatedModeRoot() const { return m_d->isolatedRootNode; } void KisImage::addJob(KisStrokeId id, KisStrokeJobData *data) { KisUpdateTimeMonitor::instance()->reportJobStarted(data); m_d->scheduler.addJob(id, data); } void KisImage::endStroke(KisStrokeId id) { m_d->scheduler.endStroke(id); } bool KisImage::cancelStroke(KisStrokeId id) { return m_d->scheduler.cancelStroke(id); } bool KisImage::KisImagePrivate::tryCancelCurrentStrokeAsync() { return scheduler.tryCancelCurrentStrokeAsync(); } void KisImage::requestUndoDuringStroke() { emit sigUndoDuringStrokeRequested(); } void KisImage::requestStrokeCancellation() { if (!m_d->tryCancelCurrentStrokeAsync()) { emit sigStrokeCancellationRequested(); } } UndoResult KisImage::tryUndoUnfinishedLod0Stroke() { return m_d->scheduler.tryUndoLastStrokeAsync(); } void KisImage::requestStrokeEnd() { emit sigStrokeEndRequested(); emit sigStrokeEndRequestedActiveNodeFiltered(); } void KisImage::requestStrokeEndActiveNode() { emit sigStrokeEndRequested(); } void KisImage::refreshGraph(KisNodeSP root) { refreshGraph(root, bounds(), bounds()); } void KisImage::refreshGraph(KisNodeSP root, const QRect &rc, const QRect &cropRect) { if (!root) root = m_d->rootLayer; m_d->animationInterface->notifyNodeChanged(root.data(), rc, true); m_d->scheduler.fullRefresh(root, rc, cropRect); } void KisImage::initialRefreshGraph() { /** * NOTE: Tricky part. We set crop rect to null, so the clones * will not rely on precalculated projections of their sources */ refreshGraphAsync(0, bounds(), QRect()); waitForDone(); } void KisImage::refreshGraphAsync(KisNodeSP root) { refreshGraphAsync(root, bounds(), bounds()); } void KisImage::refreshGraphAsync(KisNodeSP root, const QRect &rc) { refreshGraphAsync(root, rc, bounds()); } void KisImage::refreshGraphAsync(KisNodeSP root, const QRect &rc, const QRect &cropRect) { if (!root) root = m_d->rootLayer; m_d->animationInterface->notifyNodeChanged(root.data(), rc, true); m_d->scheduler.fullRefreshAsync(root, rc, cropRect); } void KisImage::requestProjectionUpdateNoFilthy(KisNodeSP pseudoFilthy, const QRect &rc, const QRect &cropRect) { KIS_ASSERT_RECOVER_RETURN(pseudoFilthy); m_d->animationInterface->notifyNodeChanged(pseudoFilthy.data(), rc, false); m_d->scheduler.updateProjectionNoFilthy(pseudoFilthy, rc, cropRect); } void KisImage::addSpontaneousJob(KisSpontaneousJob *spontaneousJob) { m_d->scheduler.addSpontaneousJob(spontaneousJob); } void KisImage::setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP filter) { // udpate filters are *not* recursive! KIS_ASSERT_RECOVER_NOOP(!filter || !m_d->projectionUpdatesFilter); m_d->projectionUpdatesFilter = filter; } KisProjectionUpdatesFilterSP KisImage::projectionUpdatesFilter() const { return m_d->projectionUpdatesFilter; } void KisImage::disableDirtyRequests() { setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP(new KisDropAllProjectionUpdatesFilter())); } void KisImage::enableDirtyRequests() { setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP()); } void KisImage::disableUIUpdates() { m_d->disableUIUpdateSignals.ref(); } void KisImage::enableUIUpdates() { m_d->disableUIUpdateSignals.deref(); } void KisImage::notifyProjectionUpdated(const QRect &rc) { KisUpdateTimeMonitor::instance()->reportUpdateFinished(rc); if (!m_d->disableUIUpdateSignals) { int lod = currentLevelOfDetail(); QRect dirtyRect = !lod ? rc : KisLodTransform::upscaledRect(rc, lod); if (dirtyRect.isEmpty()) return; emit sigImageUpdated(dirtyRect); } } void KisImage::setWorkingThreadsLimit(int value) { m_d->scheduler.setThreadsLimit(value); } int KisImage::workingThreadsLimit() const { return m_d->scheduler.threadsLimit(); } void KisImage::notifySelectionChanged() { /** * The selection is calculated asynchromously, so it is not * handled by disableUIUpdates() and other special signals of * KisImageSignalRouter */ m_d->legacyUndoAdapter.emitSelectionChanged(); /** * Editing of selection masks doesn't necessary produce a * setDirty() call, so in the end of the stroke we need to request * direct update of the UI's cache. */ if (m_d->isolatedRootNode && dynamic_cast(m_d->isolatedRootNode.data())) { notifyProjectionUpdated(bounds()); } } void KisImage::requestProjectionUpdateImpl(KisNode *node, - const QRect &rect, + const QVector &rects, const QRect &cropRect) { - if (rect.isEmpty()) return; + if (rects.isEmpty()) return; - m_d->scheduler.updateProjection(node, rect, cropRect); + m_d->scheduler.updateProjection(node, rects, cropRect); } -void KisImage::requestProjectionUpdate(KisNode *node, const QRect& rect, bool resetAnimationCache) +void KisImage::requestProjectionUpdate(KisNode *node, const QVector &rects, bool resetAnimationCache) { if (m_d->projectionUpdatesFilter - && m_d->projectionUpdatesFilter->filter(this, node, rect, resetAnimationCache)) { + && m_d->projectionUpdatesFilter->filter(this, node, rects, resetAnimationCache)) { return; } if (resetAnimationCache) { - m_d->animationInterface->notifyNodeChanged(node, rect, false); + m_d->animationInterface->notifyNodeChanged(node, rects, false); } /** * Here we use 'permitted' instead of 'active' intentively, * because the updates may come after the actual stroke has been * finished. And having some more updates for the stroke not * supporting the wrap-around mode will not make much harm. */ if (m_d->wrapAroundModePermitted) { - const QRect boundRect = effectiveLodBounds(); - KisWrappedRect splitRect(rect, boundRect); + QVector allSplitRects; - Q_FOREACH (const QRect &rc, splitRect) { - requestProjectionUpdateImpl(node, rc, boundRect); + const QRect boundRect = effectiveLodBounds(); + Q_FOREACH (const QRect &rc, rects) { + KisWrappedRect splitRect(rc, boundRect); + allSplitRects.append(splitRect); } + + requestProjectionUpdateImpl(node, allSplitRects, boundRect); + } else { - requestProjectionUpdateImpl(node, rect, bounds()); + requestProjectionUpdateImpl(node, rects, bounds()); } - KisNodeGraphListener::requestProjectionUpdate(node, rect, resetAnimationCache); + KisNodeGraphListener::requestProjectionUpdate(node, rects, resetAnimationCache); } void KisImage::invalidateFrames(const KisTimeRange &range, const QRect &rect) { m_d->animationInterface->invalidateFrames(range, rect); } void KisImage::requestTimeSwitch(int time) { m_d->animationInterface->requestTimeSwitchNonGUI(time); } QList KisImage::compositions() { return m_d->compositions; } void KisImage::addComposition(KisLayerCompositionSP composition) { m_d->compositions.append(composition); } void KisImage::removeComposition(KisLayerCompositionSP composition) { m_d->compositions.removeAll(composition); } bool checkMasksNeedConversion(KisNodeSP root, const QRect &bounds) { KisSelectionMask *mask = dynamic_cast(root.data()); if (mask && (!bounds.contains(mask->paintDevice()->exactBounds()) || mask->selection()->hasShapeSelection())) { return true; } KisNodeSP node = root->firstChild(); while (node) { if (checkMasksNeedConversion(node, bounds)) { return true; } node = node->nextSibling(); } return false; } void KisImage::setWrapAroundModePermitted(bool value) { if (m_d->wrapAroundModePermitted != value) { requestStrokeEnd(); } m_d->wrapAroundModePermitted = value; if (m_d->wrapAroundModePermitted && checkMasksNeedConversion(root(), bounds())) { KisProcessingApplicator applicator(this, root(), KisProcessingApplicator::RECURSIVE, KisImageSignalVector() << ModifiedSignal, kundo2_i18n("Crop Selections")); KisProcessingVisitorSP visitor = new KisCropSelectionsProcessingVisitor(bounds()); applicator.applyVisitor(visitor, KisStrokeJobData::CONCURRENT); applicator.end(); } } bool KisImage::wrapAroundModePermitted() const { return m_d->wrapAroundModePermitted; } bool KisImage::wrapAroundModeActive() const { return m_d->wrapAroundModePermitted && m_d->scheduler.wrapAroundModeSupported(); } void KisImage::setDesiredLevelOfDetail(int lod) { if (m_d->blockLevelOfDetail) { qWarning() << "WARNING: KisImage::setDesiredLevelOfDetail()" << "was called while LoD functionality was being blocked!"; return; } m_d->scheduler.setDesiredLevelOfDetail(lod); } int KisImage::currentLevelOfDetail() const { if (m_d->blockLevelOfDetail) { return 0; } return m_d->scheduler.currentLevelOfDetail(); } void KisImage::setLevelOfDetailBlocked(bool value) { KisImageBarrierLockerRaw l(this); if (value && !m_d->blockLevelOfDetail) { m_d->scheduler.setDesiredLevelOfDetail(0); } m_d->blockLevelOfDetail = value; } void KisImage::explicitRegenerateLevelOfDetail() { if (!m_d->blockLevelOfDetail) { m_d->scheduler.explicitRegenerateLevelOfDetail(); } } bool KisImage::levelOfDetailBlocked() const { return m_d->blockLevelOfDetail; } void KisImage::notifyNodeCollpasedChanged() { emit sigNodeCollapsedChanged(); } KisImageAnimationInterface* KisImage::animationInterface() const { return m_d->animationInterface; } void KisImage::setProofingConfiguration(KisProofingConfigurationSP proofingConfig) { m_d->proofingConfig = proofingConfig; emit sigProofingConfigChanged(); } KisProofingConfigurationSP KisImage::proofingConfiguration() const { if (!m_d->proofingConfig) { KisImageConfig cfg; m_d->proofingConfig = cfg.defaultProofingconfiguration(); } return m_d->proofingConfig; } QPointF KisImage::mirrorAxesCenter() const { return m_d->axesCenter; } void KisImage::setMirrorAxesCenter(const QPointF &value) const { m_d->axesCenter = value; } diff --git a/libs/image/kis_image.h b/libs/image/kis_image.h index bfeb5b504c..2f1afab7a8 100644 --- a/libs/image/kis_image.h +++ b/libs/image/kis_image.h @@ -1,995 +1,995 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2007 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_IMAGE_H_ #define KIS_IMAGE_H_ #include #include #include #include #include #include #include #include "kis_paint_device.h" // msvc cannot handle forward declarations, so include kis_paint_device here #include "kis_types.h" #include "kis_shared.h" #include "kis_node_graph_listener.h" #include "kis_node_facade.h" #include "kis_image_interfaces.h" #include "kis_strokes_queue_undo_result.h" #include class KisDocument; class KoColorSpace; class KoColor; class KisCompositeProgressProxy; class KisActionRecorder; class KisUndoStore; class KisUndoAdapter; class KisImageSignalRouter; class KisPostExecutionUndoAdapter; class KisFilterStrategy; class KoColorProfile; class KisLayerComposition; class KisSpontaneousJob; class KisImageAnimationInterface; class KUndo2MagicString; class KisProofingConfiguration; namespace KisMetaData { class MergeStrategy; } /** * This is the image class, it contains a tree of KisLayer stack and * meta information about the image. And it also provides some * functions to manipulate the whole image. */ class KRITAIMAGE_EXPORT KisImage : public QObject, public KisStrokesFacade, public KisStrokeUndoFacade, public KisUpdatesFacade, public KisProjectionUpdateListener, public KisNodeFacade, public KisNodeGraphListener, public KisShared { Q_OBJECT public: /// @param colorSpace can be null. in that case it will be initialised to a default color space. KisImage(KisUndoStore *undoStore, qint32 width, qint32 height, const KoColorSpace *colorSpace, const QString& name); ~KisImage() override; public: // KisNodeGraphListener implementation void aboutToAddANode(KisNode *parent, int index) override; void nodeHasBeenAdded(KisNode *parent, int index) override; void aboutToRemoveANode(KisNode *parent, int index) override; void nodeChanged(KisNode * node) override; void invalidateAllFrames() override; void notifySelectionChanged() override; - void requestProjectionUpdate(KisNode *node, const QRect& rect, bool resetAnimationCache) override; + void requestProjectionUpdate(KisNode *node, const QVector &rects, bool resetAnimationCache) override; void invalidateFrames(const KisTimeRange &range, const QRect &rect) override; void requestTimeSwitch(int time) override; public: // KisProjectionUpdateListener implementation void notifyProjectionUpdated(const QRect &rc) override; public: /** * Set the number of threads used by the image's working threads */ void setWorkingThreadsLimit(int value); /** * Return the number of threads available to the image's working threads */ int workingThreadsLimit() const; /** * Makes a copy of the image with all the layers. If possible, shallow * copies of the layers are made. * * \p exactCopy shows if the copied image should look *exactly* the same as * the other one (according to it's .kra xml representation). It means that * the layers will have the same UUID keys and, therefore, you are not * expected to use the copied image anywhere except for saving. Don't use * this option if you plan to work with the copied image later. */ KisImage *clone(bool exactCopy = false); /** * Render the projection onto a QImage. */ QImage convertToQImage(qint32 x1, qint32 y1, qint32 width, qint32 height, const KoColorProfile * profile); /** * Render the projection onto a QImage. * (this is an overloaded function) */ QImage convertToQImage(QRect imageRect, const KoColorProfile * profile); /** * XXX: docs! */ QImage convertToQImage(const QSize& scaledImageSize, const KoColorProfile *profile); /** * Calls KisUpdateScheduler::lock (XXX: APIDOX -- what does that mean?) */ void lock(); /** * Calls KisUpdateScheduler::unlock (XXX: APIDOX -- what does that mean?) */ void unlock(); /** * Returns true if lock() has been called more often than unlock(). */ bool locked() const; /** * @return the global selection object or 0 if there is none. The * global selection is always read-write. */ KisSelectionSP globalSelection() const; /** * Retrieve the next automatic layername (XXX: fix to add option to return Mask X) */ QString nextLayerName(const QString &baseName = "") const; /** * Set the automatic layer name counter one back. */ void rollBackLayerName(); /** * Resize the image to the specified rect. The resize * method handles the creating on an undo step itself. * * @param newRect the rect describing the new width, height and offset * of the image */ void resizeImage(const QRect& newRect); /** * Crop the image to the specified rect. The crop * method handles the creating on an undo step itself. * * @param newRect the rect describing the new width, height and offset * of the image */ void cropImage(const QRect& newRect); /** * Crop a node to @newRect. The node will *not* be moved anywhere, * it just drops some content */ void cropNode(KisNodeSP node, const QRect& newRect); /// XXX: ApiDox void scaleImage(const QSize &size, qreal xres, qreal yres, KisFilterStrategy *filterStrategy); /// XXX: ApiDox void scaleNode(KisNodeSP node, qreal scaleX, qreal scaleY, KisFilterStrategy *filterStrategy); /** * Execute a rotate transform on all layers in this image. * Image is resized to fit rotated image. */ void rotateImage(double radians); /** * Execute a rotate transform on on a subtree of this image. * Image is not resized. */ void rotateNode(KisNodeSP node, double radians); /** * Execute a shear transform on all layers in this image. */ void shear(double angleX, double angleY); /** * Shear a node and all its children. * @param angleX, @param angleY are given in degrees. */ void shearNode(KisNodeSP node, double angleX, double angleY); /** * Convert the image and all its layers to the dstColorSpace */ void convertImageColorSpace(const KoColorSpace *dstColorSpace, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags); /** * Set the color space of the projection (and the root layer) * to dstColorSpace. No conversion is done for other layers, * their colorspace can differ. * NOTE: Note conversion is done, only regeneration, so no rendering * intent needed */ void convertProjectionColorSpace(const KoColorSpace *dstColorSpace); // Get the profile associated with this image const KoColorProfile * profile() const; /** * Set the profile of the image to the new profile and do the same for * all layers that have the same colorspace and profile of the image. * It doesn't do any pixel conversion. * * This is essential if you have loaded an image that didn't * have an embedded profile to which you want to attach the right profile. * * This does not create an undo action; only call it when creating or * loading an image. * * @returns false if the profile could not be assigned */ bool assignImageProfile(const KoColorProfile *profile); /** * Returns the current undo adapter. You can add new commands to the * undo stack using the adapter. This adapter is used for a backward * compatibility for old commands created before strokes. It blocks * all the porcessing at the scheduler, waits until it's finished * adn executes commands exclusively. */ KisUndoAdapter* undoAdapter() const; /** * This adapter is used by the strokes system. The commands are added * to it *after* redo() is done (in the scheduler context). They are * wrapped into a special command and added to the undo stack. redo() * in not called. */ KisPostExecutionUndoAdapter* postExecutionUndoAdapter() const override; /** * Replace current undo store with the new one. The old store * will be deleted. * This method is used by KisDocument for dropping all the commands * during file loading. */ void setUndoStore(KisUndoStore *undoStore); /** * Return current undo store of the image */ KisUndoStore* undoStore(); /** * @return the action recorder associated with this image */ KisActionRecorder* actionRecorder() const; /** * Tell the image it's modified; this emits the sigImageModified * signal. This happens when the image needs to be saved */ void setModified(); /** * The default colorspace of this image: new layers will have this * colorspace and the projection will have this colorspace. */ const KoColorSpace * colorSpace() const; /** * X resolution in pixels per pt */ double xRes() const; /** * Y resolution in pixels per pt */ double yRes() const; /** * Set the resolution in pixels per pt. */ void setResolution(double xres, double yres); /** * Convert a document coordinate to a pixel coordinate. * * @param documentCoord PostScript Pt coordinate to convert. */ QPointF documentToPixel(const QPointF &documentCoord) const; /** * Convert a document coordinate to an integer pixel coordinate. * * @param documentCoord PostScript Pt coordinate to convert. */ QPoint documentToIntPixel(const QPointF &documentCoord) const; /** * Convert a document rectangle to a pixel rectangle. * * @param documentRect PostScript Pt rectangle to convert. */ QRectF documentToPixel(const QRectF &documentRect) const; /** * Convert a document rectangle to an integer pixel rectangle. * * @param documentRect PostScript Pt rectangle to convert. */ QRect documentToIntPixel(const QRectF &documentRect) const; /** * Convert a pixel coordinate to a document coordinate. * * @param pixelCoord pixel coordinate to convert. */ QPointF pixelToDocument(const QPointF &pixelCoord) const; /** * Convert an integer pixel coordinate to a document coordinate. * The document coordinate is at the centre of the pixel. * * @param pixelCoord pixel coordinate to convert. */ QPointF pixelToDocument(const QPoint &pixelCoord) const; /** * Convert a document rectangle to an integer pixel rectangle. * * @param pixelCoord pixel coordinate to convert. */ QRectF pixelToDocument(const QRectF &pixelCoord) const; /** * Return the width of the image */ qint32 width() const; /** * Return the height of the image */ qint32 height() const; /** * Return the size of the image */ QSize size() const { return QSize(width(), height()); } /** * @return the root node of the image node graph */ KisGroupLayerSP rootLayer() const; /** * Return the projection; that is, the complete, composited * representation of this image. */ KisPaintDeviceSP projection() const; /** * Return the number of layers (not other nodes) that are in this * image. */ qint32 nlayers() const; /** * Return the number of layers (not other node types) that are in * this image and that are hidden. */ qint32 nHiddenLayers() const; /** * Merge all visible layers and discard hidden ones. */ void flatten(); /** * Merge the specified layer with the layer * below this layer, remove the specified layer. */ void mergeDown(KisLayerSP l, const KisMetaData::MergeStrategy* strategy); /** * flatten the layer: that is, the projection becomes the layer * and all subnodes are removed. If this is not a paint layer, it will morph * into a paint layer. */ void flattenLayer(KisLayerSP layer); /** * Merges layers in \p mergedLayers and creates a new layer above * \p putAfter */ void mergeMultipleLayers(QList mergedLayers, KisNodeSP putAfter); /// @return the exact bounds of the image in pixel coordinates. QRect bounds() const; /** * Returns the actual bounds of the image, taking LevelOfDetail * into account. This value is used as a bounds() value of * KisDefaultBounds object. */ QRect effectiveLodBounds() const; /// use if the layers have changed _completely_ (eg. when flattening) void notifyLayersChanged(); /** * Sets the default color of the root layer projection. All the layers * will be merged on top of this very color */ void setDefaultProjectionColor(const KoColor &color); /** * \see setDefaultProjectionColor() */ KoColor defaultProjectionColor() const; void setRootLayer(KisGroupLayerSP rootLayer); /** * Add an annotation for this image. This can be anything: Gamma, EXIF, etc. * Note that the "icc" annotation is reserved for the color strategies. * If the annotation already exists, overwrite it with this one. */ void addAnnotation(KisAnnotationSP annotation); /** get the annotation with the given type, can return 0 */ KisAnnotationSP annotation(const QString& type); /** delete the annotation, if the image contains it */ void removeAnnotation(const QString& type); /** * Start of an iteration over the annotations of this image (including the ICC Profile) */ vKisAnnotationSP_it beginAnnotations(); /** end of an iteration over the annotations of this image */ vKisAnnotationSP_it endAnnotations(); /** * Called before the image is delted and sends the sigAboutToBeDeleted signal */ void notifyAboutToBeDeleted(); KisImageSignalRouter* signalRouter(); /** * Returns whether we can reselect current global selection * * \see reselectGlobalSelection() */ bool canReselectGlobalSelection(); /** * Returns the layer compositions for the image */ QList compositions(); /** * Adds a new layer composition, will be saved with the image */ void addComposition(KisLayerCompositionSP composition); /** * Remove the layer compostion */ void removeComposition(KisLayerCompositionSP composition); /** * Permit or deny the wrap-around mode for all the paint devices * of the image. Note that permitting the wraparound mode will not * necessarily activate it right now. To be activated the wrap * around mode should be 1) permitted; 2) supported by the * currently running stroke. */ void setWrapAroundModePermitted(bool value); /** * \return whether the wrap-around mode is permitted for this * image. If the wrap around mode is permitted and the * currently running stroke supports it, the mode will be * activated for all paint devices of the image. * * \see setWrapAroundMode */ bool wrapAroundModePermitted() const; /** * \return whether the wraparound mode is activated for all the * devices of the image. The mode is activated when both * factors are true: the user permitted it and the stroke * supports it */ bool wrapAroundModeActive() const; /** * \return curent level of detail which is used when processing the image. * Current working zoom = 2 ^ (- currentLevelOfDetail()). Default value is * null, which means we work on the original image. */ int currentLevelOfDetail() const; /** * Notify KisImage which level of detail should be used in the * lod-mode. Setting the mode does not guarantee the LOD to be * used. It will be activated only when the stokes supports it. */ void setDesiredLevelOfDetail(int lod); /** * Relative position of the mirror axis center * 0,0 - topleft corner of the image * 1,1 - bottomright corner of the image */ QPointF mirrorAxesCenter() const; /** * Sets the relative position of the axes center * \see mirrorAxesCenter() for details */ void setMirrorAxesCenter(const QPointF &value) const; public Q_SLOTS: /** * Explicitly start regeneration of LoD planes of all the devices * in the image. This call should be performed when the user is idle, * just to make the quality of image updates better. */ void explicitRegenerateLevelOfDetail(); public: /** * Blocks usage of level of detail functionality. After this method * has been called, no new strokes will use LoD. */ void setLevelOfDetailBlocked(bool value); /** * \see setLevelOfDetailBlocked() */ bool levelOfDetailBlocked() const; /** * Notifies that the node collapsed state has changed */ void notifyNodeCollpasedChanged(); KisImageAnimationInterface *animationInterface() const; /** * @brief setProofingConfiguration, this sets the image's proofing configuration, and signals * the proofingConfiguration has changed. * @param proofingConfig - the kis proofing config that will be used instead. */ void setProofingConfiguration(KisProofingConfigurationSP proofingConfig); /** * @brief proofingConfiguration * @return the proofing configuration of the image. */ KisProofingConfigurationSP proofingConfiguration() const; public: bool startIsolatedMode(KisNodeSP node); void stopIsolatedMode(); KisNodeSP isolatedModeRoot() const; Q_SIGNALS: /** * Emitted whenever an action has caused the image to be * recomposited. * * @param rc The rect that has been recomposited. */ void sigImageUpdated(const QRect &); /** Emitted whenever the image has been modified, so that it doesn't match with the version saved on disk. */ void sigImageModified(); /** * The signal is emitted when the size of the image is changed. * \p oldStillPoint and \p newStillPoint give the receiver the * hint about how the new and old rect of the image correspond to * each other. They specify the point of the image around which * the conversion was done. This point will stay still on the * user's screen. That is the \p newStillPoint of the new image * will be painted at the same screen position, where \p * oldStillPoint of the old image was painted. * * \param oldStillPoint is a still point represented in *old* * image coordinates * * \param newStillPoint is a still point represented in *new* * image coordinates */ void sigSizeChanged(const QPointF &oldStillPoint, const QPointF &newStillPoint); void sigProfileChanged(const KoColorProfile * profile); void sigColorSpaceChanged(const KoColorSpace* cs); void sigResolutionChanged(double xRes, double yRes); void sigRequestNodeReselection(KisNodeSP activeNode, const KisNodeList &selectedNodes); /** * Inform the model that a node was changed */ void sigNodeChanged(KisNodeSP node); /** * Inform that the image is going to be deleted */ void sigAboutToBeDeleted(); /** * The signal is emitted right after a node has been connected * to the graph of the nodes. * * WARNING: you must not request any graph-related information * about the node being run in a not-scheduler thread. If you need * information about the parent/siblings of the node connect * with Qt::DirectConnection, get needed information and then * emit another Qt::AutoConnection signal to pass this information * to your thread. See details of the implementation * in KisDummiesfacadeBase. */ void sigNodeAddedAsync(KisNodeSP node); /** * This signal is emitted right before a node is going to removed * from the graph of the nodes. * * WARNING: you must not request any graph-related information * about the node being run in a not-scheduler thread. * * \see comment in sigNodeAddedAsync() */ void sigRemoveNodeAsync(KisNodeSP node); /** * Emitted when the root node of the image has changed. * It happens, e.g. when we flatten the image. When * this happens the receiver should reload information * about the image */ void sigLayersChangedAsync(); /** * Emitted when the UI has requested the undo of the last stroke's * operation. The point is, we cannot deal with the internals of * the stroke without its creator knowing about it (which most * probably cause a crash), so we just forward this request from * the UI to the creator of the stroke. * * If your tool supports undoing part of its work, just listen to * this signal and undo when it comes */ void sigUndoDuringStrokeRequested(); /** * Emitted when the UI has requested the cancellation of * the stroke. The point is, we cannot cancel the stroke * without its creator knowing about it (which most probably * cause a crash), so we just forward this request from the UI * to the creator of the stroke. * * If your tool supports cancelling of its work in the middle * of operation, just listen to this signal and cancel * the stroke when it comes */ void sigStrokeCancellationRequested(); /** * Emitted when the image decides that the stroke should better * be ended. The point is, we cannot just end the stroke * without its creator knowing about it (which most probably * cause a crash), so we just forward this request from the UI * to the creator of the stroke. * * If your tool supports long strokes that may involve multiple * mouse actions in one stroke, just listen to this signal and * end the stroke when it comes. */ void sigStrokeEndRequested(); /** * Same as sigStrokeEndRequested() but is not emitted when the active node * is changed. */ void sigStrokeEndRequestedActiveNodeFiltered(); /** * Emitted when the isolated mode status has changed. * * Can be used by the receivers to catch a fact of forcefully * stopping the isolated mode by the image when some complex * action was requested */ void sigIsolatedModeChanged(); /** * Emitted when one or more nodes changed the collapsed state * */ void sigNodeCollapsedChanged(); /** * Emitted when the proofing configuration of the image is being changed. * */ void sigProofingConfigChanged(); public Q_SLOTS: KisCompositeProgressProxy* compositeProgressProxy(); bool isIdle(bool allowLocked = false); /** * @brief barrierLock APIDOX * @param readOnly */ void barrierLock(bool readOnly = false); /** * @brief barrierLock APIDOX * @param readOnly */ bool tryBarrierLock(bool readOnly = false); /** * @brief barrierLock APIDOX * @param readOnly */ void waitForDone(); KisStrokeId startStroke(KisStrokeStrategy *strokeStrategy) override; void addJob(KisStrokeId id, KisStrokeJobData *data) override; void endStroke(KisStrokeId id) override; bool cancelStroke(KisStrokeId id) override; /** * @brief blockUpdates block updating the image projection */ void blockUpdates() override; /** * @brief unblockUpdates unblock updating the image project. This * only restarts the scheduler and does not schedule a full refresh. */ void unblockUpdates() override; /** * Disables notification of the UI about the changes in the image. * This feature is used by KisProcessingApplicator. It is needed * when we change the size of the image. In this case, the whole * image will be reloaded into UI by sigSizeChanged(), so there is * no need to inform the UI about individual dirty rects. */ void disableUIUpdates() override; /** * \see disableUIUpdates */ void enableUIUpdates() override; /** * Disables the processing of all the setDirty() requests that * come to the image. The incoming requests are effectively * *dropped*. * * This feature is used by KisProcessingApplicator. For many cases * it provides its own updates interface, which recalculates the * whole subtree of nodes. But while we change any particular * node, it can ask for an update itself. This method is a way of * blocking such intermediate (and excessive) requests. * * NOTE: this is a convenience function for setProjectionUpdatesFilter() * that installs a predefined filter that eats everything. Please * note that these calls are *not* recursive */ void disableDirtyRequests() override; /** * \see disableDirtyRequests() */ void enableDirtyRequests() override; /** * Installs a filter object that will filter all the incoming projection update * requests. If the filter return true, the incoming update is dropped. * * NOTE: you cannot set filters recursively! */ void setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP filter) override; /** * \see setProjectionUpdatesFilter() */ KisProjectionUpdatesFilterSP projectionUpdatesFilter() const override; void refreshGraphAsync(KisNodeSP root = KisNodeSP()) override; void refreshGraphAsync(KisNodeSP root, const QRect &rc) override; void refreshGraphAsync(KisNodeSP root, const QRect &rc, const QRect &cropRect) override; /** * Triggers synchronous recomposition of the projection */ void refreshGraph(KisNodeSP root = KisNodeSP()); void refreshGraph(KisNodeSP root, const QRect& rc, const QRect &cropRect); void initialRefreshGraph(); /** * Initiate a stack regeneration skipping the recalculation of the * filthy node's projection. * * Works exactly as pseudoFilthy->setDirty() with the only * exception that pseudoFilthy::updateProjection() will not be * called. That is used by KisRecalculateTransformMaskJob to avoid * cyclic dependencies. */ void requestProjectionUpdateNoFilthy(KisNodeSP pseudoFilthy, const QRect &rc, const QRect &cropRect); /** * Adds a spontaneous job to the updates queue. * * A spontaneous job may do some trivial tasks in the background, * like updating the outline of selection or purging unused tiles * from the existing paint devices. */ void addSpontaneousJob(KisSpontaneousJob *spontaneousJob); /** * This method is called by the UI (*not* by the creator of the * stroke) when it thinks the current stroke should undo its last * action, for example, when the user presses Ctrl+Z while some * stroke is active. * * If the creator of the stroke supports undoing of intermediate * actions, it will be notified about this request and can undo * its last action. */ void requestUndoDuringStroke(); /** * This method is called by the UI (*not* by the creator of the * stroke) when it thinks current stroke should be cancelled. If * there is a running stroke that has already been detached from * its creator (ended or cancelled), it will be forcefully * cancelled and reverted. If there is an open stroke present, and * if its creator supports cancelling, it will be notified about * the request and the stroke will be cancelled */ void requestStrokeCancellation(); /** * This method requests the last stroke executed on the image to become undone. * If the stroke is not ended, or if all the Lod0 strokes are completed, the method * returns UNDO_FAIL. If the last Lod0 is going to be finished soon, then UNDO_WAIT * is returned and the caller should just wait for its completion and call global undo * instead. UNDO_OK means one unfinished stroke has been undone. */ UndoResult tryUndoUnfinishedLod0Stroke(); /** * This method is called when image or some other part of Krita * (*not* the creator of the stroke) decides that the stroke * should be ended. If the creator of the stroke supports it, it * will be notified and the stroke will be cancelled */ void requestStrokeEnd(); /** * Same as requestStrokeEnd() but is called by view manager when * the current node is changed. Use to dintinguish * sigStrokeEndRequested() and * sigStrokeEndRequestedActiveNodeFiltered() which are used by * KisNodeJugglerCompressed */ void requestStrokeEndActiveNode(); private: KisImage(const KisImage& rhs, KisUndoStore *undoStore, bool exactCopy); KisImage& operator=(const KisImage& rhs); void emitSizeChanged(); void resizeImageImpl(const QRect& newRect, bool cropLayers); void rotateImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, bool resizeImage, double radians); void shearImpl(const KUndo2MagicString &actionName, KisNodeSP rootNode, bool resizeImage, double angleX, double angleY, const QPointF &origin); void safeRemoveTwoNodes(KisNodeSP node1, KisNodeSP node2); void refreshHiddenArea(KisNodeSP rootNode, const QRect &preparedArea); void requestProjectionUpdateImpl(KisNode *node, - const QRect& rect, + const QVector &rects, const QRect &cropRect); friend class KisImageResizeCommand; void setSize(const QSize& size); friend class KisImageSetProjectionColorSpaceCommand; void setProjectionColorSpace(const KoColorSpace * colorSpace); friend class KisDeselectGlobalSelectionCommand; friend class KisReselectGlobalSelectionCommand; friend class KisSetGlobalSelectionCommand; friend class KisImageTest; friend class Document; // For libkis /** * Replaces the current global selection with globalSelection. If * \p globalSelection is empty, removes the selection object, so that * \ref globalSelection() will return 0 after that. */ void setGlobalSelection(KisSelectionSP globalSelection); /** * Deselects current global selection. * \ref globalSelection() will return 0 after that. */ void deselectGlobalSelection(); /** * Reselects current deselected selection * * \see deselectGlobalSelection() */ void reselectGlobalSelection(); private: class KisImagePrivate; KisImagePrivate * m_d; }; #endif // KIS_IMAGE_H_ diff --git a/libs/image/kis_image_animation_interface.cpp b/libs/image/kis_image_animation_interface.cpp index 8636811e55..ceff81c754 100644 --- a/libs/image/kis_image_animation_interface.cpp +++ b/libs/image/kis_image_animation_interface.cpp @@ -1,402 +1,416 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_image_animation_interface.h" #include #include "kis_global.h" #include "kis_image.h" #include "kis_regenerate_frame_stroke_strategy.h" #include "kis_switch_time_stroke_strategy.h" #include "kis_keyframe_channel.h" #include "kis_time_range.h" #include "kis_post_execution_undo_adapter.h" #include "commands_new/kis_switch_current_time_command.h" #include "kis_layer_utils.h" struct KisImageAnimationInterface::Private { Private() : image(0), externalFrameActive(false), frameInvalidationBlocked(false), cachedLastFrameValue(-1), audioChannelMuted(false), audioChannelVolume(0.5), m_currentTime(0), m_currentUITime(0) { } Private(const Private &rhs, KisImage *newImage) : image(newImage), externalFrameActive(false), frameInvalidationBlocked(false), fullClipRange(rhs.fullClipRange), playbackRange(rhs.playbackRange), framerate(rhs.framerate), cachedLastFrameValue(-1), audioChannelFileName(rhs.audioChannelFileName), audioChannelMuted(rhs.audioChannelMuted), audioChannelVolume(rhs.audioChannelVolume), m_currentTime(rhs.m_currentTime), m_currentUITime(rhs.m_currentUITime) { } KisImage *image; bool externalFrameActive; bool frameInvalidationBlocked; KisTimeRange fullClipRange; KisTimeRange playbackRange; int framerate; int cachedLastFrameValue; QString audioChannelFileName; bool audioChannelMuted; qreal audioChannelVolume; KisSwitchTimeStrokeStrategy::SharedTokenWSP switchToken; inline int currentTime() const { return m_currentTime; } inline int currentUITime() const { return m_currentUITime; } inline void setCurrentTime(int value) { m_currentTime = value; } inline void setCurrentUITime(int value) { m_currentUITime = value; } private: int m_currentTime; int m_currentUITime; }; KisImageAnimationInterface::KisImageAnimationInterface(KisImage *image) : QObject(image), m_d(new Private) { m_d->image = image; m_d->framerate = 24; m_d->fullClipRange = KisTimeRange::fromTime(0, 100); connect(this, SIGNAL(sigInternalRequestTimeSwitch(int, bool)), SLOT(switchCurrentTimeAsync(int, bool))); } KisImageAnimationInterface::KisImageAnimationInterface(const KisImageAnimationInterface &rhs, KisImage *newImage) : m_d(new Private(*rhs.m_d, newImage)) { connect(this, SIGNAL(sigInternalRequestTimeSwitch(int, bool)), SLOT(switchCurrentTimeAsync(int, bool))); } KisImageAnimationInterface::~KisImageAnimationInterface() { } bool KisImageAnimationInterface::hasAnimation() const { bool hasAnimation = false; KisLayerUtils::recursiveApplyNodes( m_d->image->root(), [&hasAnimation](KisNodeSP node) { hasAnimation |= node->isAnimated(); }); return hasAnimation; } int KisImageAnimationInterface::currentTime() const { return m_d->currentTime(); } int KisImageAnimationInterface::currentUITime() const { return m_d->currentUITime(); } const KisTimeRange& KisImageAnimationInterface::fullClipRange() const { return m_d->fullClipRange; } void KisImageAnimationInterface::setFullClipRange(const KisTimeRange range) { m_d->fullClipRange = range; emit sigFullClipRangeChanged(); } const KisTimeRange& KisImageAnimationInterface::playbackRange() const { return m_d->playbackRange.isValid() ? m_d->playbackRange : m_d->fullClipRange; } void KisImageAnimationInterface::setPlaybackRange(const KisTimeRange range) { m_d->playbackRange = range; emit sigPlaybackRangeChanged(); } int KisImageAnimationInterface::framerate() const { return m_d->framerate; } QString KisImageAnimationInterface::audioChannelFileName() const { return m_d->audioChannelFileName; } void KisImageAnimationInterface::setAudioChannelFileName(const QString &fileName) { QFileInfo info(fileName); KIS_SAFE_ASSERT_RECOVER_NOOP(fileName.isEmpty() || info.isAbsolute()); m_d->audioChannelFileName = fileName.isEmpty() ? fileName : info.absoluteFilePath(); emit sigAudioChannelChanged(); } bool KisImageAnimationInterface::isAudioMuted() const { return m_d->audioChannelMuted; } void KisImageAnimationInterface::setAudioMuted(bool value) { m_d->audioChannelMuted = value; emit sigAudioChannelChanged(); } qreal KisImageAnimationInterface::audioVolume() const { return m_d->audioChannelVolume; } void KisImageAnimationInterface::setAudioVolume(qreal value) { m_d->audioChannelVolume = value; emit sigAudioVolumeChanged(); } void KisImageAnimationInterface::setFramerate(int fps) { m_d->framerate = fps; emit sigFramerateChanged(); } KisImageWSP KisImageAnimationInterface::image() const { return m_d->image; } bool KisImageAnimationInterface::externalFrameActive() const { return m_d->externalFrameActive; } void KisImageAnimationInterface::requestTimeSwitchWithUndo(int time) { if (currentUITime() == time) return; requestTimeSwitchNonGUI(time, true); } void KisImageAnimationInterface::setDefaultProjectionColor(const KoColor &color) { int savedTime = 0; saveAndResetCurrentTime(currentTime(), &savedTime); m_d->image->setDefaultProjectionColor(color); restoreCurrentTime(&savedTime); } void KisImageAnimationInterface::requestTimeSwitchNonGUI(int time, bool useUndo) { emit sigInternalRequestTimeSwitch(time, useUndo); } void KisImageAnimationInterface::explicitlySetCurrentTime(int frameId) { m_d->setCurrentTime(frameId); } void KisImageAnimationInterface::switchCurrentTimeAsync(int frameId, bool useUndo) { if (currentUITime() == frameId) return; KisTimeRange range = KisTimeRange::infinite(0); KisTimeRange::calculateTimeRangeRecursive(m_d->image->root(), currentUITime(), range, true); const bool needsRegeneration = !range.contains(frameId); KisSwitchTimeStrokeStrategy::SharedTokenSP token = m_d->switchToken.toStrongRef(); if (!token || !token->tryResetDestinationTime(frameId, needsRegeneration)) { { KisPostExecutionUndoAdapter *undoAdapter = useUndo ? m_d->image->postExecutionUndoAdapter() : 0; KisSwitchTimeStrokeStrategy *strategy = new KisSwitchTimeStrokeStrategy(frameId, needsRegeneration, this, undoAdapter); m_d->switchToken = strategy->token(); KisStrokeId stroke = m_d->image->startStroke(strategy); m_d->image->endStroke(stroke); } if (needsRegeneration) { KisStrokeStrategy *strategy = new KisRegenerateFrameStrokeStrategy(this); KisStrokeId strokeId = m_d->image->startStroke(strategy); m_d->image->endStroke(strokeId); } } m_d->setCurrentUITime(frameId); emit sigUiTimeChanged(frameId); } void KisImageAnimationInterface::requestFrameRegeneration(int frameId, const QRegion &dirtyRegion) { KisStrokeStrategy *strategy = new KisRegenerateFrameStrokeStrategy(frameId, dirtyRegion, this); QList jobs = KisRegenerateFrameStrokeStrategy::createJobsData(m_d->image); KisStrokeId stroke = m_d->image->startStroke(strategy); Q_FOREACH (KisStrokeJobData* job, jobs) { m_d->image->addJob(stroke, job); } m_d->image->endStroke(stroke); } void KisImageAnimationInterface::saveAndResetCurrentTime(int frameId, int *savedValue) { m_d->externalFrameActive = true; *savedValue = m_d->currentTime(); m_d->setCurrentTime(frameId); } void KisImageAnimationInterface::restoreCurrentTime(int *savedValue) { m_d->setCurrentTime(*savedValue); m_d->externalFrameActive = false; } void KisImageAnimationInterface::notifyFrameReady() { emit sigFrameReady(m_d->currentTime()); } void KisImageAnimationInterface::notifyFrameCancelled() { emit sigFrameCancelled(); } KisUpdatesFacade* KisImageAnimationInterface::updatesFacade() const { return m_d->image; } void KisImageAnimationInterface::notifyNodeChanged(const KisNode *node, const QRect &rect, bool recursive) +{ + notifyNodeChanged(node, QVector({rect}), recursive); +} + +void KisImageAnimationInterface::notifyNodeChanged(const KisNode *node, + const QVector &rects, + bool recursive) { if (externalFrameActive() || m_d->frameInvalidationBlocked) return; if (node->inherits("KisSelectionMask")) return; KisKeyframeChannel *channel = node->getKeyframeChannel(KisKeyframeChannel::Content.id()); - if (recursive) { - KisTimeRange affectedRange; - KisTimeRange::calculateTimeRangeRecursive(node, currentTime(), affectedRange, false); + KisTimeRange invalidateRange; - invalidateFrames(affectedRange, rect); + if (recursive) { + KisTimeRange::calculateTimeRangeRecursive(node, currentTime(), invalidateRange, false); } else if (channel) { const int currentTime = m_d->currentTime(); - - invalidateFrames(channel->affectedFrames(currentTime), rect); + invalidateRange = channel->affectedFrames(currentTime); } else { - invalidateFrames(KisTimeRange::infinite(0), rect); + invalidateRange = KisTimeRange::infinite(0); + } + + + // we compress the updated rect (atm, noone uses it anyway) + QRect unitedRect; + Q_FOREACH (const QRect &rc, rects) { + unitedRect |= rc; } + + invalidateFrames(invalidateRange, unitedRect); } void KisImageAnimationInterface::invalidateFrames(const KisTimeRange &range, const QRect &rect) { m_d->cachedLastFrameValue = -1; emit sigFramesChanged(range, rect); } void KisImageAnimationInterface::blockFrameInvalidation(bool value) { m_d->frameInvalidationBlocked = value; } int findLastKeyframeTimeRecursive(KisNodeSP node) { int time = 0; KisKeyframeChannel *channel; Q_FOREACH (channel, node->keyframeChannels()) { KisKeyframeSP keyframe = channel->lastKeyframe(); if (keyframe) { time = std::max(time, keyframe->time()); } } KisNodeSP child = node->firstChild(); while (child) { time = std::max(time, findLastKeyframeTimeRecursive(child)); child = child->nextSibling(); } return time; } int KisImageAnimationInterface::totalLength() { if (m_d->cachedLastFrameValue < 0) { m_d->cachedLastFrameValue = findLastKeyframeTimeRecursive(m_d->image->root()); } int lastKey = m_d->cachedLastFrameValue; lastKey = std::max(lastKey, m_d->fullClipRange.end()); lastKey = std::max(lastKey, m_d->currentUITime()); return lastKey + 1; } diff --git a/libs/image/kis_image_animation_interface.h b/libs/image/kis_image_animation_interface.h index 04a400d151..e38f53ba0f 100644 --- a/libs/image/kis_image_animation_interface.h +++ b/libs/image/kis_image_animation_interface.h @@ -1,208 +1,210 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_IMAGE_ANIMATION_INTERFACE_H #define __KIS_IMAGE_ANIMATION_INTERFACE_H #include #include #include "kis_types.h" #include "kritaimage_export.h" class KisUpdatesFacade; class KisTimeRange; class KoColor; namespace KisLayerUtils { struct SwitchFrameCommand; } class KRITAIMAGE_EXPORT KisImageAnimationInterface : public QObject { Q_OBJECT public: KisImageAnimationInterface(KisImage *image); KisImageAnimationInterface(const KisImageAnimationInterface &rhs, KisImage *newImage); ~KisImageAnimationInterface() override; /** * Returns true of the image has at least one animated layer */ bool hasAnimation() const; /** * Returns currently active frame of the underlying image. Some strokes * can override this value and it will report a different value. */ int currentTime() const; /** * Same as currentTime, except it isn't changed when background strokes * are running. */ int currentUITime() const; /** * While any non-current frame is being regenerated by the * strategy, the image is kept in a special state, named * 'externalFrameActive'. Is this state the following applies: * * 1) All the animated paint devices switch its state into * frameId() defined by global time. * * 2) All animation-not-capable devices switch to a temporary * content device, which *is in undefined state*. The stroke * should regenerate the image projection manually. */ bool externalFrameActive() const; void requestTimeSwitchWithUndo(int time); void requestTimeSwitchNonGUI(int time, bool useUndo = false); public Q_SLOTS: /** * Switches current frame (synchronously) and starts an * asynchronous regeneration of the entire image. */ void switchCurrentTimeAsync(int frameId, bool useUndo = false); public: /** * Start a backgroud thread that will recalculate some extra frame. * The result will be reported using two types of signals: * * 1) KisImage::sigImageUpdated() will be emitted for every chunk * of updated area. * * 2) sigFrameReady() will be emitted in the end of the operation. * IMPORTANT: to get the result you must connect to this signal * with Qt::DirectConnection and fetch the result from * frameProjection(). After the signal handler is exited, the * data will no longer be available. */ void requestFrameRegeneration(int frameId, const QRegion &dirtyRegion); + void notifyNodeChanged(const KisNode *node, const QRect &rect, bool recursive); + void notifyNodeChanged(const KisNode *node, const QVector &rects, bool recursive); void invalidateFrames(const KisTimeRange &range, const QRect &rect); /** * Changes the default color of the "external frame" projection of * the image's root layer. Please note that this command should be * executed from a context of an exclusive job! */ void setDefaultProjectionColor(const KoColor &color); /** * The current time range selected by user. * @return current time range */ const KisTimeRange& fullClipRange() const; void setFullClipRange(const KisTimeRange range); const KisTimeRange &playbackRange() const; void setPlaybackRange(const KisTimeRange range); int framerate() const; /** * @return **absolute** file name of the audio channel file */ QString audioChannelFileName() const; /** * Sets **absolute** file name of the audio channel file. Dont' try to pass * a relative path, it'll assert! */ void setAudioChannelFileName(const QString &fileName); /** * @return is the audio channel is currently muted */ bool isAudioMuted() const; /** * Mutes the audio channel */ void setAudioMuted(bool value); /** * Returns the preferred audio value in rangle [0, 1] */ qreal audioVolume() const; /** * Set the preferred volume for the audio channel in range [0, 1] */ void setAudioVolume(qreal value); public Q_SLOTS: void setFramerate(int fps); public: KisImageWSP image() const; int totalLength(); private: // interface for: friend class KisRegenerateFrameStrokeStrategy; friend class KisAnimationFrameCacheTest; friend struct KisLayerUtils::SwitchFrameCommand; friend class KisImageTest; void saveAndResetCurrentTime(int frameId, int *savedValue); void restoreCurrentTime(int *savedValue); void notifyFrameReady(); void notifyFrameCancelled(); KisUpdatesFacade* updatesFacade() const; void blockFrameInvalidation(bool value); friend class KisSwitchTimeStrokeStrategy; void explicitlySetCurrentTime(int frameId); Q_SIGNALS: void sigFrameReady(int time); void sigFrameCancelled(); void sigUiTimeChanged(int newTime); void sigFramesChanged(const KisTimeRange &range, const QRect &rect); void sigInternalRequestTimeSwitch(int frameId, bool useUndo); void sigFramerateChanged(); void sigFullClipRangeChanged(); void sigPlaybackRangeChanged(); /** * Emitted when the audio channel of the document is changed */ void sigAudioChannelChanged(); /** * Emitted when audion volume changes. Please note that it doesn't change * when you mute the channel! When muting, sigAudioChannelChanged() is used instead! */ void sigAudioVolumeChanged(); private: struct Private; const QScopedPointer m_d; }; #endif /* __KIS_IMAGE_ANIMATION_INTERFACE_H */ diff --git a/libs/image/kis_node.cpp b/libs/image/kis_node.cpp index b289fe26f4..1b9e22e6f2 100644 --- a/libs/image/kis_node.cpp +++ b/libs/image/kis_node.cpp @@ -1,650 +1,648 @@ /* * Copyright (c) 2007 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_node.h" #include #include #include #include #include #include #include #include #include "kis_global.h" #include "kis_node_graph_listener.h" #include "kis_node_visitor.h" #include "kis_processing_visitor.h" #include "kis_node_progress_proxy.h" #include "kis_busy_progress_indicator.h" #include "kis_clone_layer.h" #include "kis_safe_read_list.h" typedef KisSafeReadList KisSafeReadNodeList; #include "kis_abstract_projection_plane.h" #include "kis_projection_leaf.h" #include "kis_undo_adapter.h" #include "kis_keyframe_channel.h" /** *The link between KisProjection ans KisImageUpdater *uses queued signals with an argument of KisNodeSP type, *so we should register it beforehand */ struct KisNodeSPStaticRegistrar { KisNodeSPStaticRegistrar() { qRegisterMetaType("KisNodeSP"); } }; static KisNodeSPStaticRegistrar __registrar1; struct KisNodeListStaticRegistrar { KisNodeListStaticRegistrar() { qRegisterMetaType("KisNodeList"); } }; static KisNodeListStaticRegistrar __registrar2; /** * Note about "thread safety" of KisNode * * 1) One can *read* any information about node and node graph in any * number of threads concurrently. This operation is safe because * of the usage of KisSafeReadNodeList and will run concurrently * (lock-free). * * 2) One can *write* any information into the node or node graph in a * single thread only! Changing the graph concurrently is *not* * sane and therefore not supported. * * 3) One can *read and write* information about the node graph * concurrently, given that there is only *one* writer thread and * any number of reader threads. Please note that in this case the * node's code is just guaranteed *not to crash*, which is ensured * by nodeSubgraphLock. You need to ensure the sanity of the data * read by the reader threads yourself! */ struct Q_DECL_HIDDEN KisNode::Private { public: Private(KisNode *node) : graphListener(0) , nodeProgressProxy(0) , busyProgressIndicator(0) , projectionLeaf(new KisProjectionLeaf(node)) { } KisNodeWSP parent; KisNodeGraphListener *graphListener; KisSafeReadNodeList nodes; KisNodeProgressProxy *nodeProgressProxy; KisBusyProgressIndicator *busyProgressIndicator; QReadWriteLock nodeSubgraphLock; KisProjectionLeafSP projectionLeaf; const KisNode* findSymmetricClone(const KisNode *srcRoot, const KisNode *dstRoot, const KisNode *srcTarget); void processDuplicatedClones(const KisNode *srcDuplicationRoot, const KisNode *dstDuplicationRoot, KisNode *node); }; /** * Finds the layer in \p dstRoot subtree, which has the same path as * \p srcTarget has in \p srcRoot */ const KisNode* KisNode::Private::findSymmetricClone(const KisNode *srcRoot, const KisNode *dstRoot, const KisNode *srcTarget) { if (srcRoot == srcTarget) return dstRoot; KisSafeReadNodeList::const_iterator srcIter = srcRoot->m_d->nodes.constBegin(); KisSafeReadNodeList::const_iterator dstIter = dstRoot->m_d->nodes.constBegin(); for (; srcIter != srcRoot->m_d->nodes.constEnd(); srcIter++, dstIter++) { KIS_ASSERT_RECOVER_RETURN_VALUE((srcIter != srcRoot->m_d->nodes.constEnd()) == (dstIter != dstRoot->m_d->nodes.constEnd()), 0); const KisNode *node = findSymmetricClone(srcIter->data(), dstIter->data(), srcTarget); if (node) return node; } return 0; } /** * This function walks through a subtrees of old and new layers and * searches for clone layers. For each clone layer it checks whether * its copyFrom() lays inside the old subtree, and if it is so resets * it to the corresponding layer in the new subtree. * * That is needed when the user duplicates a group layer with all its * layer subtree. In such a case all the "internal" clones must stay * "internal" and not point to the layers of the older group. */ void KisNode::Private::processDuplicatedClones(const KisNode *srcDuplicationRoot, const KisNode *dstDuplicationRoot, KisNode *node) { if (KisCloneLayer *clone = dynamic_cast(node)) { KIS_ASSERT_RECOVER_RETURN(clone->copyFrom()); const KisNode *newCopyFrom = findSymmetricClone(srcDuplicationRoot, dstDuplicationRoot, clone->copyFrom()); if (newCopyFrom) { KisLayer *newCopyFromLayer = qobject_cast(const_cast(newCopyFrom)); KIS_ASSERT_RECOVER_RETURN(newCopyFromLayer); clone->setCopyFrom(newCopyFromLayer); } } KisSafeReadNodeList::const_iterator iter; FOREACH_SAFE(iter, node->m_d->nodes) { KisNode *child = const_cast((*iter).data()); processDuplicatedClones(srcDuplicationRoot, dstDuplicationRoot, child); } } KisNode::KisNode() : m_d(new Private(this)) { m_d->parent = 0; m_d->graphListener = 0; moveToThread(qApp->thread()); } KisNode::KisNode(const KisNode & rhs) : KisBaseNode(rhs) , m_d(new Private(this)) { m_d->parent = 0; m_d->graphListener = 0; moveToThread(qApp->thread()); // HACK ALERT: we create opacity channel in KisBaseNode, but we cannot // initialize its node from there! So workaround it here! QMap channels = rhs.keyframeChannels(); for (auto it = channels.begin(); it != channels.end(); ++it) { it.value()->setNode(this); } // NOTE: the nodes are not supposed to be added/removed while // creation of another node, so we do *no* locking here! KisSafeReadNodeList::const_iterator iter; FOREACH_SAFE(iter, rhs.m_d->nodes) { KisNodeSP child = (*iter)->clone(); child->createNodeProgressProxy(); m_d->nodes.append(child); child->setParent(this); } m_d->processDuplicatedClones(&rhs, this, this); } KisNode::~KisNode() { if (m_d->busyProgressIndicator) { m_d->busyProgressIndicator->prepareDestroying(); m_d->busyProgressIndicator->deleteLater(); } if (m_d->nodeProgressProxy) { m_d->nodeProgressProxy->prepareDestroying(); m_d->nodeProgressProxy->deleteLater(); } { QWriteLocker l(&m_d->nodeSubgraphLock); m_d->nodes.clear(); } delete m_d; } QRect KisNode::needRect(const QRect &rect, PositionToFilthy pos) const { Q_UNUSED(pos); return rect; } QRect KisNode::changeRect(const QRect &rect, PositionToFilthy pos) const { Q_UNUSED(pos); return rect; } QRect KisNode::accessRect(const QRect &rect, PositionToFilthy pos) const { Q_UNUSED(pos); return rect; } void KisNode::childNodeChanged(KisNodeSP changedChildNode) { } KisAbstractProjectionPlaneSP KisNode::projectionPlane() const { KIS_ASSERT_RECOVER_NOOP(0 && "KisNode::projectionPlane() is not defined!"); static KisAbstractProjectionPlaneSP plane = toQShared(new KisDumbProjectionPlane()); return plane; } KisProjectionLeafSP KisNode::projectionLeaf() const { return m_d->projectionLeaf; } bool KisNode::accept(KisNodeVisitor &v) { return v.visit(this); } void KisNode::accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) { visitor.visit(this, undoAdapter); } int KisNode::graphSequenceNumber() const { return m_d->graphListener ? m_d->graphListener->graphSequenceNumber() : -1; } KisNodeGraphListener *KisNode::graphListener() const { return m_d->graphListener; } void KisNode::setGraphListener(KisNodeGraphListener *graphListener) { m_d->graphListener = graphListener; QReadLocker l(&m_d->nodeSubgraphLock); KisSafeReadNodeList::const_iterator iter; FOREACH_SAFE(iter, m_d->nodes) { KisNodeSP child = (*iter); child->setGraphListener(graphListener); } } void KisNode::setParent(KisNodeWSP parent) { QWriteLocker l(&m_d->nodeSubgraphLock); m_d->parent = parent; } KisNodeSP KisNode::parent() const { QReadLocker l(&m_d->nodeSubgraphLock); return m_d->parent.isValid() ? KisNodeSP(m_d->parent) : KisNodeSP(); } KisBaseNodeSP KisNode::parentCallback() const { return parent(); } void KisNode::notifyParentVisibilityChanged(bool value) { QReadLocker l(&m_d->nodeSubgraphLock); KisSafeReadNodeList::const_iterator iter; FOREACH_SAFE(iter, m_d->nodes) { KisNodeSP child = (*iter); child->notifyParentVisibilityChanged(value); } } void KisNode::baseNodeChangedCallback() { if(m_d->graphListener) { m_d->graphListener->nodeChanged(this); } } void KisNode::baseNodeInvalidateAllFramesCallback() { if(m_d->graphListener) { m_d->graphListener->invalidateAllFrames(); } } void KisNode::addKeyframeChannel(KisKeyframeChannel *channel) { channel->setNode(this); KisBaseNode::addKeyframeChannel(channel); } KisNodeSP KisNode::firstChild() const { QReadLocker l(&m_d->nodeSubgraphLock); return !m_d->nodes.isEmpty() ? m_d->nodes.first() : 0; } KisNodeSP KisNode::lastChild() const { QReadLocker l(&m_d->nodeSubgraphLock); return !m_d->nodes.isEmpty() ? m_d->nodes.last() : 0; } KisNodeSP KisNode::prevChildImpl(KisNodeSP child) { /** * Warning: mind locking policy! * * The graph locks must be *always* taken in descending * order. That is if you want to (or it implicitly happens that * you) take a lock of a parent and a chil, you must first take * the lock of a parent, and only after that ask a child to do the * same. Otherwise you'll get a deadlock. */ QReadLocker l(&m_d->nodeSubgraphLock); int i = m_d->nodes.indexOf(child) - 1; return i >= 0 ? m_d->nodes.at(i) : 0; } KisNodeSP KisNode::nextChildImpl(KisNodeSP child) { /** * See a comment in KisNode::prevChildImpl() */ QReadLocker l(&m_d->nodeSubgraphLock); int i = m_d->nodes.indexOf(child) + 1; return i > 0 && i < m_d->nodes.size() ? m_d->nodes.at(i) : 0; } KisNodeSP KisNode::prevSibling() const { KisNodeSP parentNode = parent(); return parentNode ? parentNode->prevChildImpl(const_cast(this)) : 0; } KisNodeSP KisNode::nextSibling() const { KisNodeSP parentNode = parent(); return parentNode ? parentNode->nextChildImpl(const_cast(this)) : 0; } quint32 KisNode::childCount() const { QReadLocker l(&m_d->nodeSubgraphLock); return m_d->nodes.size(); } KisNodeSP KisNode::at(quint32 index) const { QReadLocker l(&m_d->nodeSubgraphLock); if (!m_d->nodes.isEmpty() && index < (quint32)m_d->nodes.size()) { return m_d->nodes.at(index); } return 0; } int KisNode::index(const KisNodeSP node) const { QReadLocker l(&m_d->nodeSubgraphLock); return m_d->nodes.indexOf(node); } QList KisNode::childNodes(const QStringList & nodeTypes, const KoProperties & properties) const { QReadLocker l(&m_d->nodeSubgraphLock); QList nodes; KisSafeReadNodeList::const_iterator iter; FOREACH_SAFE(iter, m_d->nodes) { if (*iter) { if (properties.isEmpty() || (*iter)->check(properties)) { bool rightType = true; if(!nodeTypes.isEmpty()) { rightType = false; Q_FOREACH (const QString &nodeType, nodeTypes) { if ((*iter)->inherits(nodeType.toLatin1())) { rightType = true; break; } } } if (rightType) { nodes.append(*iter); } } } } return nodes; } KisNodeSP KisNode::findChildByName(const QString &name) { KisNodeSP child = firstChild(); while (child) { if (child->name() == name) { return child; } if (child->childCount() > 0) { KisNodeSP grandChild = child->findChildByName(name); if (grandChild) { return grandChild; } } child = child->nextSibling(); } return 0; } bool KisNode::add(KisNodeSP newNode, KisNodeSP aboveThis) { Q_ASSERT(newNode); if (!newNode) return false; if (aboveThis && aboveThis->parent().data() != this) return false; if (!allowAsChild(newNode)) return false; if (newNode->parent()) return false; if (index(newNode) >= 0) return false; int idx = aboveThis ? this->index(aboveThis) + 1 : 0; // threoretical race condition may happen here ('idx' may become // deprecated until the write lock will be held). But we ignore // it, because it is not supported to add/remove nodes from two // concurrent threads simultaneously if (m_d->graphListener) { m_d->graphListener->aboutToAddANode(this, idx); } { QWriteLocker l(&m_d->nodeSubgraphLock); newNode->createNodeProgressProxy(); m_d->nodes.insert(idx, newNode); newNode->setParent(this); newNode->setGraphListener(m_d->graphListener); } childNodeChanged(newNode); if (m_d->graphListener) { m_d->graphListener->nodeHasBeenAdded(this, idx); } return true; } bool KisNode::remove(quint32 index) { if (index < childCount()) { KisNodeSP removedNode = at(index); if (m_d->graphListener) { m_d->graphListener->aboutToRemoveANode(this, index); } { QWriteLocker l(&m_d->nodeSubgraphLock); removedNode->setGraphListener(0); removedNode->setParent(0); // after calling aboutToRemoveANode or then the model get broken according to TT's modeltest m_d->nodes.removeAt(index); } childNodeChanged(removedNode); if (m_d->graphListener) { m_d->graphListener->nodeHasBeenRemoved(this, index); } return true; } return false; } bool KisNode::remove(KisNodeSP node) { return node->parent().data() == this ? remove(index(node)) : false; } KisNodeProgressProxy* KisNode::nodeProgressProxy() const { if (m_d->nodeProgressProxy) { return m_d->nodeProgressProxy; } else if (parent()) { return parent()->nodeProgressProxy(); } return 0; } KisBusyProgressIndicator* KisNode::busyProgressIndicator() const { if (m_d->busyProgressIndicator) { return m_d->busyProgressIndicator; } else if (parent()) { return parent()->busyProgressIndicator(); } return 0; } void KisNode::createNodeProgressProxy() { if (!m_d->nodeProgressProxy) { m_d->nodeProgressProxy = new KisNodeProgressProxy(this); m_d->busyProgressIndicator = new KisBusyProgressIndicator(m_d->nodeProgressProxy); m_d->nodeProgressProxy->moveToThread(this->thread()); m_d->busyProgressIndicator->moveToThread(this->thread()); } } void KisNode::setDirty() { setDirty(extent()); } void KisNode::setDirty(const QVector &rects) { - Q_FOREACH (const QRect &rc, rects) { - setDirty(rc); + if(m_d->graphListener) { + m_d->graphListener->requestProjectionUpdate(this, rects, true); } } void KisNode::setDirty(const QRegion ®ion) { setDirty(region.rects()); } void KisNode::setDirtyDontResetAnimationCache() { if(m_d->graphListener) { - m_d->graphListener->requestProjectionUpdate(this, extent(), false); + m_d->graphListener->requestProjectionUpdate(this, {extent()}, false); } } void KisNode::setDirty(const QRect & rect) { - if(m_d->graphListener) { - m_d->graphListener->requestProjectionUpdate(this, rect, true); - } + setDirty(QVector({rect})); } void KisNode::invalidateFrames(const KisTimeRange &range, const QRect &rect) { if(m_d->graphListener) { m_d->graphListener->invalidateFrames(range, rect); } } void KisNode::requestTimeSwitch(int time) { if(m_d->graphListener) { m_d->graphListener->requestTimeSwitch(time); } } void KisNode::syncLodCache() { // noop. everything is done by getLodCapableDevices() } KisPaintDeviceList KisNode::getLodCapableDevices() const { KisPaintDeviceList list; KisPaintDeviceSP device = paintDevice(); if (device) { list << device; } KisPaintDeviceSP originalDevice = original(); if (originalDevice && originalDevice != device) { list << originalDevice; } list << projectionPlane()->getLodCapableDevices(); return list; } diff --git a/libs/image/kis_node.h b/libs/image/kis_node.h index 86c3f76a1d..aaa8be8ce6 100644 --- a/libs/image/kis_node.h +++ b/libs/image/kis_node.h @@ -1,410 +1,410 @@ /* * Copyright (c) 2007 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_NODE_H #define _KIS_NODE_H #include "kis_types.h" #include "kis_base_node.h" #include "kritaimage_export.h" #include class QRect; class QStringList; class KoProperties; class KisNodeVisitor; class KisNodeGraphListener; class KisNodeProgressProxy; class KisBusyProgressIndicator; class KisAbstractProjectionPlane; class KisProjectionLeaf; class KisKeyframeChannel; class KisTimeRange; class KisUndoAdapter; /** * A KisNode is a KisBaseNode that knows about its direct peers, parent * and children and whether it can have children. * * THREAD-SAFETY: All const methods of this class and setDirty calls * are considered to be thread-safe(!). All the others * especially add(), remove() and setParent() must be * protected externally. * * NOTE: your subclasses must have the Q_OBJECT declaration, even if * you do not define new signals or slots. */ class KRITAIMAGE_EXPORT KisNode : public KisBaseNode { friend class KisFilterMaskTest; Q_OBJECT public: /** * The struct describing the position of the node * against the filthy node. * NOTE: please change KisBaseRectsWalker::getPositionToFilthy * when changing this struct */ enum PositionToFilthy { N_ABOVE_FILTHY = 0x08, N_FILTHY_PROJECTION = 0x20, N_FILTHY = 0x40, N_BELOW_FILTHY = 0x80 }; /** * Create an empty node without a parent. */ KisNode(); /** * Create a copy of this node. The copy will not have a parent * node. */ KisNode(const KisNode & rhs); /** * Delete this node */ ~KisNode() override; virtual KisNodeSP clone() const = 0; bool accept(KisNodeVisitor &v) override; void accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) override; /** * Re-implement this method to add constraints for the * subclasses that can be added as children to this node * * @return false if the given node is not allowed as a child to this node */ virtual bool allowAsChild(KisNodeSP) const = 0; /** * Set the entire node extent dirty; this percolates up to parent * nodes all the way to the root node. By default this is the * empty rect (through KisBaseNode::extent()) */ virtual void setDirty(); /** * Add the given rect to the set of dirty rects for this node; * this percolates up to parent nodes all the way to the root * node. */ - virtual void setDirty(const QRect & rect); + void setDirty(const QRect & rect); /** * Add the given rects to the set of dirty rects for this node; * this percolates up to parent nodes all the way to the root * node. */ virtual void setDirty(const QVector &rects); /** * Add the given region to the set of dirty rects for this node; * this percolates up to parent nodes all the way to the root * node, if propagate is true; */ - virtual void setDirty(const QRegion ®ion); + void setDirty(const QRegion ®ion); /** * @brief setDirtyDontResetAnimationCache does almost the same thing as usual * setDirty() call, but doesn't reset the animation cache (since onlion skins are * not used when rendering animation. */ void setDirtyDontResetAnimationCache(); /** * Informs that the frames in the given range are no longer valid * and need to be recached. * @param range frames to invalidate */ void invalidateFrames(const KisTimeRange &range, const QRect &rect); /** * Informs that the current world time should be changed. * Might be caused by e.g. undo operation */ void requestTimeSwitch(int time); /** * \return a pointer to a KisAbstractProjectionPlane interface of * the node. This interface is used by the image merging * framework to get information and to blending for the * layer. * * Please note the difference between need/change/accessRect and * the projectionPlane() interface. The former one gives * information about internal composition of the layer, and the * latter one about the total composition, including layer styles, * pass-through blending and etc. */ virtual KisAbstractProjectionPlaneSP projectionPlane() const; /** * Synchronizes LoD caches of the node with the current state of it. * The current level of detail is fetched from the image pointed by * default bounds object */ virtual void syncLodCache(); virtual KisPaintDeviceList getLodCapableDevices() const; /** * The rendering of the image may not always happen in the order * of the main graph. Pass-through nodes ake some subgraphs * linear, so it the order of rendering change. projectionLeaf() * is a special interface of KisNode that represents "a graph for * projection rendering". Therefore the nodes in projectionLeaf() * graph may have a different order the main one. */ virtual KisProjectionLeafSP projectionLeaf() const; protected: /** * \return internal changeRect() of the node. Do not mix with \see * projectionPlane() * * Some filters will cause a change of pixels those are outside * a requested rect. E.g. we change a rect of 2x2, then we want to * apply a convolution filter with kernel 4x4 (changeRect is * (2+2*3)x(2+2*3)=8x8) to that area. The rect that should be updated * on the layer will be exaclty 8x8. More than that the needRect for * that update will be 14x14. See \ref needeRect. */ virtual QRect changeRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const; /** * \return internal needRect() of the node. Do not mix with \see * projectionPlane() * * Some filters need pixels outside the current processing rect to * compute the new value (for instance, convolution filters) * See \ref changeRect * See \ref accessRect */ virtual QRect needRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const; /** * \return internal accessRect() of the node. Do not mix with \see * projectionPlane() * * Shows the area of image, that may be accessed during accessing * the node. * * Example. You have a layer that needs to prepare some rect on a * projection, say expectedRect. To perform this, the projection * of all the layers below of the size needRect(expectedRect) * should be calculeated by the merger beforehand and the layer * will access some other area of image inside the rect * accessRect(expectedRect) during updateProjection call. * * This knowledge about real access rect of a node is used by the * scheduler to avoid collisions between two multithreaded updaters * and so avoid flickering of the image. * * Currently, this method has nondefault value for shifted clone * layers only. */ virtual QRect accessRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const; /** * Called each time direct child nodes are added or removed under this * node as parent. This does not track changes inside the child nodes * or the child nodes' properties. */ virtual void childNodeChanged(KisNodeSP changedChildNode); public: // Graph methods /** * @return the graph sequence number calculated by the associated * graph listener. You can use it for checking for changes in the * graph. */ int graphSequenceNumber() const; /** * @return the graph listener this node belongs to. 0 if the node * does not belong to a grap listener. */ KisNodeGraphListener * graphListener() const; /** * Set the graph listener for this node. The graphlistener will be * informed before and after the list of child nodes has changed. */ void setGraphListener(KisNodeGraphListener * graphListener); /** * Returns the parent node of this node. This is 0 only for a root * node; otherwise this will be an actual Node */ KisNodeSP parent() const; /** * Returns the first child node of this node, or 0 if there are no * child nodes. */ KisNodeSP firstChild() const; /** * Returns the last child node of this node, or 0 if there are no * child nodes. */ KisNodeSP lastChild() const; /** * Returns the previous sibling of this node in the parent's list. * This is the node *above* this node in the composition stack. 0 * is returned if this child has no more previous siblings (== * firstChild()) */ KisNodeSP prevSibling() const; /** * Returns the next sibling of this node in the parent's list. * This is the node *below* this node in the composition stack. 0 * is returned if this child has no more next siblings (== * lastChild()) */ KisNodeSP nextSibling() const; /** * Returns how many direct child nodes this node has (not * recursive). */ quint32 childCount() const; /** * Retrieve the child node at the specified index. * * @return 0 if there is no node at this index. */ KisNodeSP at(quint32 index) const; /** * Retrieve the index of the specified child node. * * @return -1 if the specified node is not a child node of this * node. */ int index(const KisNodeSP node) const; /** * Return a list of child nodes of the current node that conform * to the specified constraints. There are no guarantees about the * order of the nodes in the list. The function is not recursive. * * @param nodeTypes. if not empty, only nodes that inherit the * classnames in this stringlist will be returned. * @param properties. if not empty, only nodes for which * KisNodeBase::check(properties) returns true will be returned. */ QList childNodes(const QStringList & nodeTypes, const KoProperties & properties) const; /** * @brief findChildByName finds the first child that has the given name * @param name the name to look for * @return the first child with the given name */ KisNodeSP findChildByName(const QString &name); public: /** * @return the node progress proxy used by this node, if this node has no progress * proxy, it will return the proxy of its parent, if the parent has no progress proxy * it will return 0 */ KisNodeProgressProxy* nodeProgressProxy() const; KisBusyProgressIndicator* busyProgressIndicator() const; private: /** * Create a node progress proxy for this node. You need to create a progress proxy only * if the node is going to appear in the layerbox, and it needs to be created before * the layer box is made aware of the proxy. */ void createNodeProgressProxy(); protected: KisBaseNodeSP parentCallback() const override; void notifyParentVisibilityChanged(bool value) override; void baseNodeChangedCallback() override; void baseNodeInvalidateAllFramesCallback() override; protected: void addKeyframeChannel(KisKeyframeChannel* channel) override; private: friend class KisNodeFacade; friend class KisNodeTest; friend class KisLayer; // Note: only for setting the preview mask! /** * Set the parent of this node. */ void setParent(KisNodeWSP parent); /** * Add the specified node above the specified node. If aboveThis * is 0, the node is added at the bottom. */ bool add(KisNodeSP newNode, KisNodeSP aboveThis); /** * Removes the node at the specified index from the child nodes. * * @return false if there is no node at this index */ bool remove(quint32 index); /** * Removes the node from the child nodes. * * @return false if there's no such node in this node. */ bool remove(KisNodeSP node); KisNodeSP prevChildImpl(KisNodeSP child); KisNodeSP nextChildImpl(KisNodeSP child); private: struct Private; Private * const m_d; }; Q_DECLARE_METATYPE(KisNodeSP) Q_DECLARE_METATYPE(KisNodeWSP) #endif diff --git a/libs/image/kis_node_graph_listener.cpp b/libs/image/kis_node_graph_listener.cpp index 0d04e22df3..6e9bcf2029 100644 --- a/libs/image/kis_node_graph_listener.cpp +++ b/libs/image/kis_node_graph_listener.cpp @@ -1,102 +1,102 @@ /* * Copyright (c) 2007 Boudewijn Rempt * Copyright (c) 2012 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_node_graph_listener.h" #include "kis_time_range.h" #include #include struct Q_DECL_HIDDEN KisNodeGraphListener::Private { Private() : sequenceNumber(0) {} int sequenceNumber; }; KisNodeGraphListener::KisNodeGraphListener() : m_d(new Private()) { } KisNodeGraphListener::~KisNodeGraphListener() { } void KisNodeGraphListener::aboutToAddANode(KisNode */*parent*/, int /*index*/) { m_d->sequenceNumber++; } void KisNodeGraphListener::nodeHasBeenAdded(KisNode */*parent*/, int /*index*/) { m_d->sequenceNumber++; } void KisNodeGraphListener::aboutToRemoveANode(KisNode */*parent*/, int /*index*/) { m_d->sequenceNumber++; } void KisNodeGraphListener::nodeHasBeenRemoved(KisNode */*parent*/, int /*index*/) { m_d->sequenceNumber++; } void KisNodeGraphListener::aboutToMoveNode(KisNode * /*node*/, int /*oldIndex*/, int /*newIndex*/) { m_d->sequenceNumber++; } void KisNodeGraphListener::nodeHasBeenMoved(KisNode * /*node*/, int /*oldIndex*/, int /*newIndex*/) { m_d->sequenceNumber++; } int KisNodeGraphListener::graphSequenceNumber() const { return m_d->sequenceNumber; } void KisNodeGraphListener::nodeChanged(KisNode * /*node*/) { } void KisNodeGraphListener::invalidateAllFrames() { } void KisNodeGraphListener::notifySelectionChanged() { } -void KisNodeGraphListener::requestProjectionUpdate(KisNode * /*node*/, const QRect& /*rect*/, bool /*resetAnimationCache*/) +void KisNodeGraphListener::requestProjectionUpdate(KisNode * /*node*/, const QVector &/*rects*/, bool /*resetAnimationCache*/) { } void KisNodeGraphListener::invalidateFrames(const KisTimeRange &range, const QRect &rect) { Q_UNUSED(range); Q_UNUSED(rect); } void KisNodeGraphListener::requestTimeSwitch(int time) { Q_UNUSED(time); } diff --git a/libs/image/kis_node_graph_listener.h b/libs/image/kis_node_graph_listener.h index 692359afcb..f0290f5856 100644 --- a/libs/image/kis_node_graph_listener.h +++ b/libs/image/kis_node_graph_listener.h @@ -1,123 +1,123 @@ /* * Copyright (c) 2007 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_NODE_GRAPH_LISTENER_H_ #define KIS_NODE_GRAPH_LISTENER_H_ #include "kritaimage_export.h" #include class KisTimeRange; class KisNode; class QRect; /** * Implementations of this class are called by nodes whenever the node * graph changes. These implementations can then emit the right * signals so Qt interview models can be updated before and after * changes. * * The reason for this go-between is that we don't want our nodes to * be QObjects, nor to have sig-slot connections between every node * and every mode. * * It also manages the sequence number of the graph. This is a number * which can be used as a checksum for whether the graph has chenged * from some period of time or not. \see graphSequenceNumber() */ class KRITAIMAGE_EXPORT KisNodeGraphListener { public: KisNodeGraphListener(); virtual ~KisNodeGraphListener(); /** * Inform the model that we're going to add a node. */ virtual void aboutToAddANode(KisNode *parent, int index); /** * Inform the model we're done adding a node. */ virtual void nodeHasBeenAdded(KisNode *parent, int index); /** * Inform the model we're going to remove a node. */ virtual void aboutToRemoveANode(KisNode *parent, int index); /** * Inform the model we're done removing a node. */ virtual void nodeHasBeenRemoved(KisNode *parent, int index); /** * Inform the model we're about to start moving a node (which * includes removing and adding the same node) */ virtual void aboutToMoveNode(KisNode * node, int oldIndex, int newIndex); /** * Inform the model we're done moving the node: it has been * removed and added successfully */ virtual void nodeHasBeenMoved(KisNode * node, int oldIndex, int newIndex); virtual void nodeChanged(KisNode * node); virtual void invalidateAllFrames(); /** * Inform the model that one of the selections in the graph is * changed. The sender is not passed to the function (at least for * now) because the UI should decide itself whether it needs to * fetch new selection of not. */ virtual void notifySelectionChanged(); /** * Inform the model that a node has been changed (setDirty) */ - virtual void requestProjectionUpdate(KisNode * node, const QRect& rect, bool resetAnimationCache); + virtual void requestProjectionUpdate(KisNode * node, const QVector &rects, bool resetAnimationCache); virtual void invalidateFrames(const KisTimeRange &range, const QRect &rect); virtual void requestTimeSwitch(int time); /** * Returns the sequence of the graph. * * Every time some operation performed, which might change the * hierarchy of the nodes, the sequence number grows by one. So * if you have any information about the graph which was acquired * when the sequence number was X and now it has become Y, it * means your information is outdated. * * It is used in the scheduler for checking whether queued walkers * should be regenerated. */ int graphSequenceNumber() const; private: struct Private; QScopedPointer m_d; }; #endif diff --git a/libs/image/kis_painter.cc b/libs/image/kis_painter.cc index 5ade111ff6..9cadbc02ae 100644 --- a/libs/image/kis_painter.cc +++ b/libs/image/kis_painter.cc @@ -1,2915 +1,2906 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2004 Boudewijn Rempt * Copyright (c) 2004 Clarence Dang * Copyright (c) 2004 Adrian Page * Copyright (c) 2004 Cyrille Berger * Copyright (c) 2008-2010 Lukáš Tvrdý * Copyright (c) 2010 José Luis Vergara Toloza * Copyright (c) 2011 Silvio Heinrich * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_painter.h" #include #include #include #include #include #ifndef Q_OS_WIN #include #endif #include #include #include #include #include #include #include -#include -#include -#include - #include "kis_image.h" #include "filter/kis_filter.h" #include "kis_layer.h" #include "kis_paint_device.h" #include "kis_fixed_paint_device.h" #include "kis_transaction.h" #include "kis_vec.h" #include "kis_iterator_ng.h" #include "kis_random_accessor_ng.h" -#include "kis_paintop.h" -#include "kis_selection.h" -#include "kis_fill_painter.h" + #include "filter/kis_filter_configuration.h" #include "kis_pixel_selection.h" #include #include "kis_paintop_registry.h" #include "kis_perspective_math.h" #include "tiles3/kis_random_accessor.h" #include #include #include "kis_lod_transform.h" #include "kis_algebra_2d.h" - +#include "krita_utils.h" // Maximum distance from a Bezier control point to the line through the start // and end points for the curve to be considered flat. #define BEZIER_FLATNESS_THRESHOLD 0.5 #define trunc(x) ((int)(x)) #ifndef Q_OS_WIN #endif -struct Q_DECL_HIDDEN KisPainter::Private { - Private(KisPainter *_q) : q(_q) {} - Private(KisPainter *_q, const KoColorSpace *cs) - : q(_q), paintColor(cs), backgroundColor(cs) {} - - KisPainter *q; - - KisPaintDeviceSP device; - KisSelectionSP selection; - KisTransaction* transaction; - KoUpdater* progressUpdater; - - QVector dirtyRects; - KisPaintOp* paintOp; - KoColor paintColor; - KoColor backgroundColor; - KoColor customColor; - KisFilterConfigurationSP generator; - KisPaintLayer* sourceLayer; - FillStyle fillStyle; - StrokeStyle strokeStyle; - bool antiAliasPolygonFill; - const KoPattern* pattern; - QPointF duplicateOffset; - quint32 pixelSize; - const KoColorSpace* colorSpace; - KoColorProfile* profile; - const KoCompositeOp* compositeOp; - const KoAbstractGradient* gradient; - KisPaintOpPresetSP paintOpPreset; - QImage polygonMaskImage; - QPainter* maskPainter; - KisFillPainter* fillPainter; - KisPaintDeviceSP polygon; - qint32 maskImageWidth; - qint32 maskImageHeight; - QPointF axesCenter; - bool mirrorHorizontally; - bool mirrorVertically; - bool isOpacityUnit; // TODO: move into ParameterInfo - KoCompositeOp::ParameterInfo paramInfo; - KoColorConversionTransformation::Intent renderingIntent; - KoColorConversionTransformation::ConversionFlags conversionFlags; - - bool tryReduceSourceRect(const KisPaintDevice *srcDev, - QRect *srcRect, - qint32 *srcX, - qint32 *srcY, - qint32 *srcWidth, - qint32 *srcHeight, - qint32 *dstX, - qint32 *dstY); - - void fillPainterPathImpl(const QPainterPath& path, const QRect &requestedRect); -}; +#include "kis_painter_p.h" KisPainter::KisPainter() : d(new Private(this)) { init(); } KisPainter::KisPainter(KisPaintDeviceSP device) : d(new Private(this, device->colorSpace())) { init(); Q_ASSERT(device); begin(device); } KisPainter::KisPainter(KisPaintDeviceSP device, KisSelectionSP selection) : d(new Private(this, device->colorSpace())) { init(); Q_ASSERT(device); begin(device); d->selection = selection; } void KisPainter::init() { d->selection = 0 ; d->transaction = 0; d->paintOp = 0; d->pattern = 0; d->sourceLayer = 0; d->fillStyle = FillStyleNone; d->strokeStyle = StrokeStyleBrush; d->antiAliasPolygonFill = true; d->progressUpdater = 0; d->gradient = 0; d->maskPainter = 0; d->fillPainter = 0; d->maskImageWidth = 255; d->maskImageHeight = 255; d->mirrorHorizontally = false; d->mirrorVertically = false; d->isOpacityUnit = true; d->paramInfo = KoCompositeOp::ParameterInfo(); d->renderingIntent = KoColorConversionTransformation::internalRenderingIntent(); d->conversionFlags = KoColorConversionTransformation::internalConversionFlags(); } KisPainter::~KisPainter() { // TODO: Maybe, don't be that strict? // deleteTransaction(); end(); delete d->paintOp; delete d->maskPainter; delete d->fillPainter; delete d; } template void copyAreaOptimizedImpl(const QPoint &dstPt, KisPaintDeviceSP src, KisPaintDeviceSP dst, const QRect &srcRect) { const QRect dstRect(dstPt, srcRect.size()); const bool srcEmpty = (src->extent() & srcRect).isEmpty(); const bool dstEmpty = (dst->extent() & dstRect).isEmpty(); if (!srcEmpty || !dstEmpty) { if (srcEmpty) { dst->clear(dstRect); } else { KisPainter gc(dst); gc.setCompositeOp(dst->colorSpace()->compositeOp(COMPOSITE_COPY)); if (useOldData) { gc.bitBltOldData(dstRect.topLeft(), src, srcRect); } else { gc.bitBlt(dstRect.topLeft(), src, srcRect); } } } } void KisPainter::copyAreaOptimized(const QPoint &dstPt, KisPaintDeviceSP src, KisPaintDeviceSP dst, const QRect &srcRect) { copyAreaOptimizedImpl(dstPt, src, dst, srcRect); } void KisPainter::copyAreaOptimizedOldData(const QPoint &dstPt, KisPaintDeviceSP src, KisPaintDeviceSP dst, const QRect &srcRect) { copyAreaOptimizedImpl(dstPt, src, dst, srcRect); } void KisPainter::copyAreaOptimized(const QPoint &dstPt, KisPaintDeviceSP src, KisPaintDeviceSP dst, const QRect &originalSrcRect, KisSelectionSP selection) { if (!selection) { copyAreaOptimized(dstPt, src, dst, originalSrcRect); return; } const QRect selectionRect = selection->selectedRect(); const QRect srcRect = originalSrcRect & selectionRect; const QPoint dstOffset = srcRect.topLeft() - originalSrcRect.topLeft(); const QRect dstRect = QRect(dstPt + dstOffset, srcRect.size()); const bool srcEmpty = (src->extent() & srcRect).isEmpty(); const bool dstEmpty = (dst->extent() & dstRect).isEmpty(); if (!srcEmpty || !dstEmpty) { //if (srcEmpty) { // doesn't support dstRect // dst->clearSelection(selection); // } else */ { KisPainter gc(dst); gc.setSelection(selection); gc.setCompositeOp(dst->colorSpace()->compositeOp(COMPOSITE_COPY)); gc.bitBlt(dstRect.topLeft(), src, srcRect); } } } KisPaintDeviceSP KisPainter::convertToAlphaAsAlpha(KisPaintDeviceSP src) { const KoColorSpace *srcCS = src->colorSpace(); const QRect processRect = src->extent(); KisPaintDeviceSP dst(new KisPaintDevice(KoColorSpaceRegistry::instance()->alpha8())); KisSequentialConstIterator srcIt(src, processRect); KisSequentialIterator dstIt(dst, processRect); do { const quint8 *srcPtr = srcIt.rawDataConst(); quint8 *alpha8Ptr = dstIt.rawData(); const quint8 white = srcCS->intensity8(srcPtr); const quint8 alpha = srcCS->opacityU8(srcPtr); *alpha8Ptr = KoColorSpaceMaths::multiply(alpha, KoColorSpaceMathsTraits::unitValue - white); } while (srcIt.nextPixel() && dstIt.nextPixel()); return dst; } KisPaintDeviceSP KisPainter::convertToAlphaAsGray(KisPaintDeviceSP src) { const KoColorSpace *srcCS = src->colorSpace(); const QRect processRect = src->extent(); KisPaintDeviceSP dst(new KisPaintDevice(KoColorSpaceRegistry::instance()->alpha8())); KisSequentialConstIterator srcIt(src, processRect); KisSequentialIterator dstIt(dst, processRect); do { const quint8 *srcPtr = srcIt.rawDataConst(); quint8 *alpha8Ptr = dstIt.rawData(); *alpha8Ptr = srcCS->intensity8(srcPtr); } while (srcIt.nextPixel() && dstIt.nextPixel()); return dst; } bool KisPainter::checkDeviceHasTransparency(KisPaintDeviceSP dev) { const QRect deviceBounds = dev->exactBounds(); const QRect imageBounds = dev->defaultBounds()->bounds(); if (deviceBounds.isEmpty() || (deviceBounds & imageBounds) != imageBounds) { return true; } const KoColorSpace *cs = dev->colorSpace(); KisSequentialConstIterator it(dev, deviceBounds); do { if (cs->opacityU8(it.rawDataConst()) != OPACITY_OPAQUE_U8) { return true; } } while(it.nextPixel()); return false; } void KisPainter::begin(KisPaintDeviceSP device) { begin(device, d->selection); } void KisPainter::begin(KisPaintDeviceSP device, KisSelectionSP selection) { if (!device) return; d->selection = selection; Q_ASSERT(device->colorSpace()); end(); d->device = device; d->colorSpace = device->colorSpace(); d->compositeOp = d->colorSpace->compositeOp(COMPOSITE_OVER); d->pixelSize = device->pixelSize(); } void KisPainter::end() { Q_ASSERT_X(!d->transaction, "KisPainter::end()", "end() was called for the painter having a transaction. " "Please use end/deleteTransaction() instead"); } void KisPainter::beginTransaction(const KUndo2MagicString& transactionName,int timedID) { Q_ASSERT_X(!d->transaction, "KisPainter::beginTransaction()", "You asked for a new transaction while still having " "another one. Please finish the first one with " "end/deleteTransaction() first"); d->transaction = new KisTransaction(transactionName, d->device); Q_CHECK_PTR(d->transaction); d->transaction->undoCommand()->setTimedID(timedID); } void KisPainter::revertTransaction() { Q_ASSERT_X(d->transaction, "KisPainter::revertTransaction()", "No transaction is in progress"); d->transaction->revert(); delete d->transaction; d->transaction = 0; } void KisPainter::endTransaction(KisUndoAdapter *undoAdapter) { Q_ASSERT_X(d->transaction, "KisPainter::endTransaction()", "No transaction is in progress"); d->transaction->commit(undoAdapter); delete d->transaction; d->transaction = 0; } void KisPainter::endTransaction(KisPostExecutionUndoAdapter *undoAdapter) { Q_ASSERT_X(d->transaction, "KisPainter::endTransaction()", "No transaction is in progress"); d->transaction->commit(undoAdapter); delete d->transaction; d->transaction = 0; } KUndo2Command* KisPainter::endAndTakeTransaction() { Q_ASSERT_X(d->transaction, "KisPainter::endTransaction()", "No transaction is in progress"); KUndo2Command *transactionData = d->transaction->endAndTake(); delete d->transaction; d->transaction = 0; return transactionData; } void KisPainter::deleteTransaction() { if (!d->transaction) return; delete d->transaction; d->transaction = 0; } void KisPainter::putTransaction(KisTransaction* transaction) { Q_ASSERT_X(!d->transaction, "KisPainter::putTransaction()", "You asked for a new transaction while still having " "another one. Please finish the first one with " "end/deleteTransaction() first"); d->transaction = transaction; } KisTransaction* KisPainter::takeTransaction() { Q_ASSERT_X(d->transaction, "KisPainter::takeTransaction()", "No transaction is in progress"); KisTransaction *temp = d->transaction; d->transaction = 0; return temp; } QVector KisPainter::takeDirtyRegion() { QVector vrect = d->dirtyRects; d->dirtyRects.clear(); return vrect; } void KisPainter::addDirtyRect(const QRect & rc) { QRect r = rc.normalized(); if (r.isValid()) { d->dirtyRects.append(rc); } } inline bool KisPainter::Private::tryReduceSourceRect(const KisPaintDevice *srcDev, QRect *srcRect, qint32 *srcX, qint32 *srcY, qint32 *srcWidth, qint32 *srcHeight, qint32 *dstX, qint32 *dstY) { /** * In case of COMPOSITE_COPY and Wrap Around Mode even the pixels * outside the device extent matter, because they will be either * directly copied (former case) or cloned from another area of * the image. */ if (compositeOp->id() != COMPOSITE_COPY && compositeOp->id() != COMPOSITE_DESTINATION_IN && compositeOp->id() != COMPOSITE_DESTINATION_ATOP && !srcDev->defaultBounds()->wrapAroundMode()) { /** * If srcDev->extent() (the area of the tiles containing * srcDev) is smaller than srcRect, then shrink srcRect to * that size. This is done as a speed optimization, useful for * stack recomposition in KisImage. srcRect won't grow if * srcDev->extent() is larger. */ *srcRect &= srcDev->extent(); if (srcRect->isEmpty()) return true; // Readjust the function paramenters to the new dimensions. *dstX += srcRect->x() - *srcX; // This will only add, not subtract *dstY += srcRect->y() - *srcY; // Idem srcRect->getRect(srcX, srcY, srcWidth, srcHeight); } return false; } void KisPainter::bitBltWithFixedSelection(qint32 dstX, qint32 dstY, const KisPaintDeviceSP srcDev, const KisFixedPaintDeviceSP selection, qint32 selX, qint32 selY, qint32 srcX, qint32 srcY, qint32 srcWidth, qint32 srcHeight) { // TODO: get selX and selY working as intended /* This check for nonsense ought to be a Q_ASSERT. However, when paintops are just initializing they perform some dummy passes with those parameters, and it must not crash */ if (srcWidth == 0 || srcHeight == 0) return; if (srcDev.isNull()) return; if (d->device.isNull()) return; // Check that selection has an alpha colorspace, crash if false Q_ASSERT(selection->colorSpace() == KoColorSpaceRegistry::instance()->alpha8()); QRect srcRect = QRect(srcX, srcY, srcWidth, srcHeight); QRect selRect = QRect(selX, selY, srcWidth, srcHeight); /* Trying to read outside a KisFixedPaintDevice is inherently wrong and shouldn't be done, so crash if someone attempts to do this. Don't resize YET as it would obfuscate the mistake. */ Q_ASSERT(selection->bounds().contains(selRect)); Q_UNUSED(selRect); // only used by the above Q_ASSERT /** * An optimization, which crops the source rect by the bounds of * the source device when it is possible */ if (d->tryReduceSourceRect(srcDev, &srcRect, &srcX, &srcY, &srcWidth, &srcHeight, &dstX, &dstY)) return; /* Create an intermediate byte array to hold information before it is written to the current paint device (d->device) */ quint8* dstBytes = 0; try { dstBytes = new quint8[srcWidth * srcHeight * d->device->pixelSize()]; } catch (std::bad_alloc) { warnKrita << "KisPainter::bitBltWithFixedSelection std::bad_alloc for " << srcWidth << " * " << srcHeight << " * " << d->device->pixelSize() << "dst bytes"; return; } d->device->readBytes(dstBytes, dstX, dstY, srcWidth, srcHeight); // Copy the relevant bytes of raw data from srcDev quint8* srcBytes = 0; try { srcBytes = new quint8[srcWidth * srcHeight * srcDev->pixelSize()]; } catch (std::bad_alloc) { warnKrita << "KisPainter::bitBltWithFixedSelection std::bad_alloc for " << srcWidth << " * " << srcHeight << " * " << d->device->pixelSize() << "src bytes"; return; } srcDev->readBytes(srcBytes, srcX, srcY, srcWidth, srcHeight); QRect selBounds = selection->bounds(); const quint8 *selRowStart = selection->data() + (selBounds.width() * (selY - selBounds.top()) + (selX - selBounds.left())) * selection->pixelSize(); /* * This checks whether there is nothing selected. */ if (!d->selection) { /* As there's nothing selected, blit to dstBytes (intermediary bit array), ignoring d->selection (the user selection)*/ d->paramInfo.dstRowStart = dstBytes; d->paramInfo.dstRowStride = srcWidth * d->device->pixelSize(); d->paramInfo.srcRowStart = srcBytes; d->paramInfo.srcRowStride = srcWidth * srcDev->pixelSize(); d->paramInfo.maskRowStart = selRowStart; d->paramInfo.maskRowStride = selBounds.width() * selection->pixelSize(); d->paramInfo.rows = srcHeight; d->paramInfo.cols = srcWidth; d->colorSpace->bitBlt(srcDev->colorSpace(), d->paramInfo, d->compositeOp, d->renderingIntent, d->conversionFlags); } else { /* Read the user selection (d->selection) bytes into an array, ready to merge in the next block*/ quint32 totalBytes = srcWidth * srcHeight * selection->pixelSize(); quint8* mergedSelectionBytes = 0; try { mergedSelectionBytes = new quint8[ totalBytes ]; } catch (std::bad_alloc) { warnKrita << "KisPainter::bitBltWithFixedSelection std::bad_alloc for " << srcWidth << " * " << srcHeight << " * " << d->device->pixelSize() << "total bytes"; return; } d->selection->projection()->readBytes(mergedSelectionBytes, dstX, dstY, srcWidth, srcHeight); // Merge selections here by multiplying them - compositeOP(COMPOSITE_MULT) d->paramInfo.dstRowStart = mergedSelectionBytes; d->paramInfo.dstRowStride = srcWidth * selection->pixelSize(); d->paramInfo.srcRowStart = selRowStart; d->paramInfo.srcRowStride = selBounds.width() * selection->pixelSize(); d->paramInfo.maskRowStart = 0; d->paramInfo.maskRowStride = 0; d->paramInfo.rows = srcHeight; d->paramInfo.cols = srcWidth; KoColorSpaceRegistry::instance()->alpha8()->compositeOp(COMPOSITE_MULT)->composite(d->paramInfo); // Blit to dstBytes (intermediary bit array) d->paramInfo.dstRowStart = dstBytes; d->paramInfo.dstRowStride = srcWidth * d->device->pixelSize(); d->paramInfo.srcRowStart = srcBytes; d->paramInfo.srcRowStride = srcWidth * srcDev->pixelSize(); d->paramInfo.maskRowStart = mergedSelectionBytes; d->paramInfo.maskRowStride = srcWidth * selection->pixelSize(); d->colorSpace->bitBlt(srcDev->colorSpace(), d->paramInfo, d->compositeOp, d->renderingIntent, d->conversionFlags); delete[] mergedSelectionBytes; } d->device->writeBytes(dstBytes, dstX, dstY, srcWidth, srcHeight); delete[] dstBytes; delete[] srcBytes; addDirtyRect(QRect(dstX, dstY, srcWidth, srcHeight)); } void KisPainter::bitBltWithFixedSelection(qint32 dstX, qint32 dstY, const KisPaintDeviceSP srcDev, const KisFixedPaintDeviceSP selection, qint32 srcWidth, qint32 srcHeight) { bitBltWithFixedSelection(dstX, dstY, srcDev, selection, 0, 0, 0, 0, srcWidth, srcHeight); } template void KisPainter::bitBltImpl(qint32 dstX, qint32 dstY, const KisPaintDeviceSP srcDev, qint32 srcX, qint32 srcY, qint32 srcWidth, qint32 srcHeight) { /* This check for nonsense ought to be a Q_ASSERT. However, when paintops are just initializing they perform some dummy passes with those parameters, and it must not crash */ if (srcWidth == 0 || srcHeight == 0) return; if (srcDev.isNull()) return; if (d->device.isNull()) return; QRect srcRect = QRect(srcX, srcY, srcWidth, srcHeight); if (d->compositeOp->id() == COMPOSITE_COPY) { if(!d->selection && d->isOpacityUnit && srcX == dstX && srcY == dstY && d->device->fastBitBltPossible(srcDev)) { if(useOldSrcData) { d->device->fastBitBltOldData(srcDev, srcRect); } else { d->device->fastBitBlt(srcDev, srcRect); } addDirtyRect(srcRect); return; } } else { /** * An optimization, which crops the source rect by the bounds of * the source device when it is possible */ if (d->tryReduceSourceRect(srcDev, &srcRect, &srcX, &srcY, &srcWidth, &srcHeight, &dstX, &dstY)) return; } qint32 dstY_ = dstY; qint32 srcY_ = srcY; qint32 rowsRemaining = srcHeight; // Read below KisRandomConstAccessorSP srcIt = srcDev->createRandomConstAccessorNG(srcX, srcY); KisRandomAccessorSP dstIt = d->device->createRandomAccessorNG(dstX, dstY); /* Here be a huge block of verbose code that does roughly the same than the other bit blit operations. This one is longer than the rest in an effort to optimize speed and memory use */ if (d->selection) { KisPaintDeviceSP selectionProjection(d->selection->projection()); KisRandomConstAccessorSP maskIt = selectionProjection->createRandomConstAccessorNG(dstX, dstY); while (rowsRemaining > 0) { qint32 dstX_ = dstX; qint32 srcX_ = srcX; qint32 columnsRemaining = srcWidth; qint32 numContiguousDstRows = dstIt->numContiguousRows(dstY_); qint32 numContiguousSrcRows = srcIt->numContiguousRows(srcY_); qint32 numContiguousSelRows = maskIt->numContiguousRows(dstY_); qint32 rows = qMin(numContiguousDstRows, numContiguousSrcRows); rows = qMin(rows, numContiguousSelRows); rows = qMin(rows, rowsRemaining); while (columnsRemaining > 0) { qint32 numContiguousDstColumns = dstIt->numContiguousColumns(dstX_); qint32 numContiguousSrcColumns = srcIt->numContiguousColumns(srcX_); qint32 numContiguousSelColumns = maskIt->numContiguousColumns(dstX_); qint32 columns = qMin(numContiguousDstColumns, numContiguousSrcColumns); columns = qMin(columns, numContiguousSelColumns); columns = qMin(columns, columnsRemaining); qint32 srcRowStride = srcIt->rowStride(srcX_, srcY_); srcIt->moveTo(srcX_, srcY_); qint32 dstRowStride = dstIt->rowStride(dstX_, dstY_); dstIt->moveTo(dstX_, dstY_); qint32 maskRowStride = maskIt->rowStride(dstX_, dstY_); maskIt->moveTo(dstX_, dstY_); d->paramInfo.dstRowStart = dstIt->rawData(); d->paramInfo.dstRowStride = dstRowStride; // if we don't use the oldRawData, we need to access the rawData of the source device. d->paramInfo.srcRowStart = useOldSrcData ? srcIt->oldRawData() : static_cast(srcIt.data())->rawData(); d->paramInfo.srcRowStride = srcRowStride; d->paramInfo.maskRowStart = static_cast(maskIt.data())->rawData(); d->paramInfo.maskRowStride = maskRowStride; d->paramInfo.rows = rows; d->paramInfo.cols = columns; d->colorSpace->bitBlt(srcDev->colorSpace(), d->paramInfo, d->compositeOp, d->renderingIntent, d->conversionFlags); srcX_ += columns; dstX_ += columns; columnsRemaining -= columns; } srcY_ += rows; dstY_ += rows; rowsRemaining -= rows; } } else { while (rowsRemaining > 0) { qint32 dstX_ = dstX; qint32 srcX_ = srcX; qint32 columnsRemaining = srcWidth; qint32 numContiguousDstRows = dstIt->numContiguousRows(dstY_); qint32 numContiguousSrcRows = srcIt->numContiguousRows(srcY_); qint32 rows = qMin(numContiguousDstRows, numContiguousSrcRows); rows = qMin(rows, rowsRemaining); while (columnsRemaining > 0) { qint32 numContiguousDstColumns = dstIt->numContiguousColumns(dstX_); qint32 numContiguousSrcColumns = srcIt->numContiguousColumns(srcX_); qint32 columns = qMin(numContiguousDstColumns, numContiguousSrcColumns); columns = qMin(columns, columnsRemaining); qint32 srcRowStride = srcIt->rowStride(srcX_, srcY_); srcIt->moveTo(srcX_, srcY_); qint32 dstRowStride = dstIt->rowStride(dstX_, dstY_); dstIt->moveTo(dstX_, dstY_); d->paramInfo.dstRowStart = dstIt->rawData(); d->paramInfo.dstRowStride = dstRowStride; // if we don't use the oldRawData, we need to access the rawData of the source device. d->paramInfo.srcRowStart = useOldSrcData ? srcIt->oldRawData() : static_cast(srcIt.data())->rawData(); d->paramInfo.srcRowStride = srcRowStride; d->paramInfo.maskRowStart = 0; d->paramInfo.maskRowStride = 0; d->paramInfo.rows = rows; d->paramInfo.cols = columns; d->colorSpace->bitBlt(srcDev->colorSpace(), d->paramInfo, d->compositeOp, d->renderingIntent, d->conversionFlags); srcX_ += columns; dstX_ += columns; columnsRemaining -= columns; } srcY_ += rows; dstY_ += rows; rowsRemaining -= rows; } } addDirtyRect(QRect(dstX, dstY, srcWidth, srcHeight)); } void KisPainter::bitBlt(qint32 dstX, qint32 dstY, const KisPaintDeviceSP srcDev, qint32 srcX, qint32 srcY, qint32 srcWidth, qint32 srcHeight) { bitBltImpl(dstX, dstY, srcDev, srcX, srcY, srcWidth, srcHeight); } void KisPainter::bitBlt(const QPoint & pos, const KisPaintDeviceSP srcDev, const QRect & srcRect) { bitBlt(pos.x(), pos.y(), srcDev, srcRect.x(), srcRect.y(), srcRect.width(), srcRect.height()); } void KisPainter::bitBltOldData(qint32 dstX, qint32 dstY, const KisPaintDeviceSP srcDev, qint32 srcX, qint32 srcY, qint32 srcWidth, qint32 srcHeight) { bitBltImpl(dstX, dstY, srcDev, srcX, srcY, srcWidth, srcHeight); } void KisPainter::bitBltOldData(const QPoint & pos, const KisPaintDeviceSP srcDev, const QRect & srcRect) { bitBltOldData(pos.x(), pos.y(), srcDev, srcRect.x(), srcRect.y(), srcRect.width(), srcRect.height()); } void KisPainter::fill(qint32 x, qint32 y, qint32 width, qint32 height, const KoColor& color) { /* This check for nonsense ought to be a Q_ASSERT. However, when paintops are just * initializing they perform some dummy passes with those parameters, and it must not crash */ if(width == 0 || height == 0 || d->device.isNull()) return; KoColor srcColor(color, d->device->compositionSourceColorSpace()); qint32 dstY = y; qint32 rowsRemaining = height; KisRandomAccessorSP dstIt = d->device->createRandomAccessorNG(x, y); if(d->selection) { KisPaintDeviceSP selectionProjection(d->selection->projection()); KisRandomConstAccessorSP maskIt = selectionProjection->createRandomConstAccessorNG(x, y); while(rowsRemaining > 0) { qint32 dstX = x; qint32 columnsRemaining = width; qint32 numContiguousDstRows = dstIt->numContiguousRows(dstY); qint32 numContiguousSelRows = maskIt->numContiguousRows(dstY); qint32 rows = qMin(numContiguousDstRows, numContiguousSelRows); rows = qMin(rows, rowsRemaining); while (columnsRemaining > 0) { qint32 numContiguousDstColumns = dstIt->numContiguousColumns(dstX); qint32 numContiguousSelColumns = maskIt->numContiguousColumns(dstX); qint32 columns = qMin(numContiguousDstColumns, numContiguousSelColumns); columns = qMin(columns, columnsRemaining); qint32 dstRowStride = dstIt->rowStride(dstX, dstY); dstIt->moveTo(dstX, dstY); qint32 maskRowStride = maskIt->rowStride(dstX, dstY); maskIt->moveTo(dstX, dstY); d->paramInfo.dstRowStart = dstIt->rawData(); d->paramInfo.dstRowStride = dstRowStride; d->paramInfo.srcRowStart = srcColor.data(); d->paramInfo.srcRowStride = 0; // srcRowStride is set to zero to use the compositeOp with only a single color pixel d->paramInfo.maskRowStart = maskIt->oldRawData(); d->paramInfo.maskRowStride = maskRowStride; d->paramInfo.rows = rows; d->paramInfo.cols = columns; d->colorSpace->bitBlt(srcColor.colorSpace(), d->paramInfo, d->compositeOp, d->renderingIntent, d->conversionFlags); dstX += columns; columnsRemaining -= columns; } dstY += rows; rowsRemaining -= rows; } } else { while(rowsRemaining > 0) { qint32 dstX = x; qint32 columnsRemaining = width; qint32 numContiguousDstRows = dstIt->numContiguousRows(dstY); qint32 rows = qMin(numContiguousDstRows, rowsRemaining); while(columnsRemaining > 0) { qint32 numContiguousDstColumns = dstIt->numContiguousColumns(dstX); qint32 columns = qMin(numContiguousDstColumns, columnsRemaining); qint32 dstRowStride = dstIt->rowStride(dstX, dstY); dstIt->moveTo(dstX, dstY); d->paramInfo.dstRowStart = dstIt->rawData(); d->paramInfo.dstRowStride = dstRowStride; d->paramInfo.srcRowStart = srcColor.data(); d->paramInfo.srcRowStride = 0; // srcRowStride is set to zero to use the compositeOp with only a single color pixel d->paramInfo.maskRowStart = 0; d->paramInfo.maskRowStride = 0; d->paramInfo.rows = rows; d->paramInfo.cols = columns; d->colorSpace->bitBlt(srcColor.colorSpace(), d->paramInfo, d->compositeOp, d->renderingIntent, d->conversionFlags); dstX += columns; columnsRemaining -= columns; } dstY += rows; rowsRemaining -= rows; } } addDirtyRect(QRect(x, y, width, height)); } void KisPainter::bltFixed(qint32 dstX, qint32 dstY, const KisFixedPaintDeviceSP srcDev, qint32 srcX, qint32 srcY, qint32 srcWidth, qint32 srcHeight) { /* This check for nonsense ought to be a Q_ASSERT. However, when paintops are just initializing they perform some dummy passes with those parameters, and it must not crash */ if (srcWidth == 0 || srcHeight == 0) return; if (srcDev.isNull()) return; if (d->device.isNull()) return; QRect srcRect = QRect(srcX, srcY, srcWidth, srcHeight); QRect srcBounds = srcDev->bounds(); /* Trying to read outside a KisFixedPaintDevice is inherently wrong and shouldn't be done, so crash if someone attempts to do this. Don't resize as it would obfuscate the mistake. */ Q_ASSERT(srcBounds.contains(srcRect)); Q_UNUSED(srcRect); // only used in above assertion /* Create an intermediate byte array to hold information before it is written to the current paint device (aka: d->device) */ quint8* dstBytes = 0; try { dstBytes = new quint8[srcWidth * srcHeight * d->device->pixelSize()]; } catch (std::bad_alloc) { warnKrita << "KisPainter::bltFixed std::bad_alloc for " << srcWidth << " * " << srcHeight << " * " << d->device->pixelSize() << "total bytes"; return; } d->device->readBytes(dstBytes, dstX, dstY, srcWidth, srcHeight); const quint8 *srcRowStart = srcDev->data() + (srcBounds.width() * (srcY - srcBounds.top()) + (srcX - srcBounds.left())) * srcDev->pixelSize(); d->paramInfo.dstRowStart = dstBytes; d->paramInfo.dstRowStride = srcWidth * d->device->pixelSize(); d->paramInfo.srcRowStart = srcRowStart; d->paramInfo.srcRowStride = srcBounds.width() * srcDev->pixelSize(); d->paramInfo.maskRowStart = 0; d->paramInfo.maskRowStride = 0; d->paramInfo.rows = srcHeight; d->paramInfo.cols = srcWidth; if (d->selection) { /* d->selection is a KisPaintDevice, so first a readBytes is performed to get the area of interest... */ KisPaintDeviceSP selectionProjection(d->selection->projection()); quint8* selBytes = 0; try { selBytes = new quint8[srcWidth * srcHeight * selectionProjection->pixelSize()]; } catch (std::bad_alloc) { delete[] dstBytes; return; } selectionProjection->readBytes(selBytes, dstX, dstY, srcWidth, srcHeight); d->paramInfo.maskRowStart = selBytes; d->paramInfo.maskRowStride = srcWidth * selectionProjection->pixelSize(); } // ...and then blit. d->colorSpace->bitBlt(srcDev->colorSpace(), d->paramInfo, d->compositeOp, d->renderingIntent, d->conversionFlags); d->device->writeBytes(dstBytes, dstX, dstY, srcWidth, srcHeight); delete[] d->paramInfo.maskRowStart; delete[] dstBytes; addDirtyRect(QRect(dstX, dstY, srcWidth, srcHeight)); } void KisPainter::bltFixed(const QPoint & pos, const KisFixedPaintDeviceSP srcDev, const QRect & srcRect) { bltFixed(pos.x(), pos.y(), srcDev, srcRect.x(), srcRect.y(), srcRect.width(), srcRect.height()); } void KisPainter::bltFixedWithFixedSelection(qint32 dstX, qint32 dstY, const KisFixedPaintDeviceSP srcDev, const KisFixedPaintDeviceSP selection, qint32 selX, qint32 selY, qint32 srcX, qint32 srcY, quint32 srcWidth, quint32 srcHeight) { // TODO: get selX and selY working as intended /* This check for nonsense ought to be a Q_ASSERT. However, when paintops are just initializing they perform some dummy passes with those parameters, and it must not crash */ if (srcWidth == 0 || srcHeight == 0) return; if (srcDev.isNull()) return; if (d->device.isNull()) return; // Check that selection has an alpha colorspace, crash if false Q_ASSERT(selection->colorSpace() == KoColorSpaceRegistry::instance()->alpha8()); QRect srcRect = QRect(srcX, srcY, srcWidth, srcHeight); QRect selRect = QRect(selX, selY, srcWidth, srcHeight); QRect srcBounds = srcDev->bounds(); QRect selBounds = selection->bounds(); /* Trying to read outside a KisFixedPaintDevice is inherently wrong and shouldn't be done, so crash if someone attempts to do this. Don't resize as it would obfuscate the mistake. */ Q_ASSERT(srcBounds.contains(srcRect)); Q_UNUSED(srcRect); // only used in above assertion Q_ASSERT(selBounds.contains(selRect)); Q_UNUSED(selRect); // only used in above assertion /* Create an intermediate byte array to hold information before it is written to the current paint device (aka: d->device) */ quint8* dstBytes = 0; try { dstBytes = new quint8[srcWidth * srcHeight * d->device->pixelSize()]; } catch (std::bad_alloc) { warnKrita << "KisPainter::bltFixedWithFixedSelection std::bad_alloc for " << srcWidth << " * " << srcHeight << " * " << d->device->pixelSize() << "total bytes"; return; } d->device->readBytes(dstBytes, dstX, dstY, srcWidth, srcHeight); const quint8 *srcRowStart = srcDev->data() + (srcBounds.width() * (srcY - srcBounds.top()) + (srcX - srcBounds.left())) * srcDev->pixelSize(); const quint8 *selRowStart = selection->data() + (selBounds.width() * (selY - selBounds.top()) + (selX - selBounds.left())) * selection->pixelSize(); if (!d->selection) { /* As there's nothing selected, blit to dstBytes (intermediary bit array), ignoring d->selection (the user selection)*/ d->paramInfo.dstRowStart = dstBytes; d->paramInfo.dstRowStride = srcWidth * d->device->pixelSize(); d->paramInfo.srcRowStart = srcRowStart; d->paramInfo.srcRowStride = srcBounds.width() * srcDev->pixelSize(); d->paramInfo.maskRowStart = selRowStart; d->paramInfo.maskRowStride = selBounds.width() * selection->pixelSize(); d->paramInfo.rows = srcHeight; d->paramInfo.cols = srcWidth; d->colorSpace->bitBlt(srcDev->colorSpace(), d->paramInfo, d->compositeOp, d->renderingIntent, d->conversionFlags); } else { /* Read the user selection (d->selection) bytes into an array, ready to merge in the next block*/ quint32 totalBytes = srcWidth * srcHeight * selection->pixelSize(); quint8 * mergedSelectionBytes = 0; try { mergedSelectionBytes = new quint8[ totalBytes ]; } catch (std::bad_alloc) { warnKrita << "KisPainter::bltFixedWithFixedSelection std::bad_alloc for " << totalBytes << "total bytes"; delete[] dstBytes; return; } d->selection->projection()->readBytes(mergedSelectionBytes, dstX, dstY, srcWidth, srcHeight); // Merge selections here by multiplying them - compositeOp(COMPOSITE_MULT) d->paramInfo.dstRowStart = mergedSelectionBytes; d->paramInfo.dstRowStride = srcWidth * selection->pixelSize(); d->paramInfo.srcRowStart = selRowStart; d->paramInfo.srcRowStride = selBounds.width() * selection->pixelSize(); d->paramInfo.maskRowStart = 0; d->paramInfo.maskRowStride = 0; d->paramInfo.rows = srcHeight; d->paramInfo.cols = srcWidth; KoColorSpaceRegistry::instance()->alpha8()->compositeOp(COMPOSITE_MULT)->composite(d->paramInfo); // Blit to dstBytes (intermediary bit array) d->paramInfo.dstRowStart = dstBytes; d->paramInfo.dstRowStride = srcWidth * d->device->pixelSize(); d->paramInfo.srcRowStart = srcRowStart; d->paramInfo.srcRowStride = srcBounds.width() * srcDev->pixelSize(); d->paramInfo.maskRowStart = mergedSelectionBytes; d->paramInfo.maskRowStride = srcWidth * selection->pixelSize(); d->colorSpace->bitBlt(srcDev->colorSpace(), d->paramInfo, d->compositeOp, d->renderingIntent, d->conversionFlags); delete[] mergedSelectionBytes; } d->device->writeBytes(dstBytes, dstX, dstY, srcWidth, srcHeight); delete[] dstBytes; addDirtyRect(QRect(dstX, dstY, srcWidth, srcHeight)); } void KisPainter::bltFixedWithFixedSelection(qint32 dstX, qint32 dstY, const KisFixedPaintDeviceSP srcDev, const KisFixedPaintDeviceSP selection, quint32 srcWidth, quint32 srcHeight) { bltFixedWithFixedSelection(dstX, dstY, srcDev, selection, 0, 0, 0, 0, srcWidth, srcHeight); } void KisPainter::paintLine(const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance) { if (d->device && d->paintOp && d->paintOp->canPaint()) { d->paintOp->paintLine(pi1, pi2, currentDistance); } } void KisPainter::paintPolyline(const vQPointF &points, int index, int numPoints) { if (index >= (int) points.count()) return; if (numPoints < 0) numPoints = points.count(); if (index + numPoints > (int) points.count()) numPoints = points.count() - index; if (numPoints > 1) { - KisDistanceInformation saveDist(points[0], 0.0, + KisDistanceInformation saveDist(points[0], KisAlgebra2D::directionBetweenPoints(points[0], points[1], 0.0)); for (int i = index; i < index + numPoints - 1; i++) { paintLine(points [i], points [i + 1], &saveDist); } } } static void getBezierCurvePoints(const KisVector2D &pos1, const KisVector2D &control1, const KisVector2D &control2, const KisVector2D &pos2, vQPointF& points) { LineEquation line = LineEquation::Through(pos1, pos2); qreal d1 = line.absDistance(control1); qreal d2 = line.absDistance(control2); if (d1 < BEZIER_FLATNESS_THRESHOLD && d2 < BEZIER_FLATNESS_THRESHOLD) { points.push_back(toQPointF(pos1)); } else { // Midpoint subdivision. See Foley & Van Dam Computer Graphics P.508 KisVector2D l2 = (pos1 + control1) / 2; KisVector2D h = (control1 + control2) / 2; KisVector2D l3 = (l2 + h) / 2; KisVector2D r3 = (control2 + pos2) / 2; KisVector2D r2 = (h + r3) / 2; KisVector2D l4 = (l3 + r2) / 2; getBezierCurvePoints(pos1, l2, l3, l4, points); getBezierCurvePoints(l4, r2, r3, pos2, points); } } void KisPainter::getBezierCurvePoints(const QPointF &pos1, const QPointF &control1, const QPointF &control2, const QPointF &pos2, vQPointF& points) const { ::getBezierCurvePoints(toKisVector2D(pos1), toKisVector2D(control1), toKisVector2D(control2), toKisVector2D(pos2), points); } void KisPainter::paintBezierCurve(const KisPaintInformation &pi1, const QPointF &control1, const QPointF &control2, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance) { if (d->paintOp && d->paintOp->canPaint()) { d->paintOp->paintBezierCurve(pi1, control1, control2, pi2, currentDistance); } } void KisPainter::paintRect(const QRectF &rect) { QRectF normalizedRect = rect.normalized(); vQPointF points; points.push_back(normalizedRect.topLeft()); points.push_back(normalizedRect.bottomLeft()); points.push_back(normalizedRect.bottomRight()); points.push_back(normalizedRect.topRight()); paintPolygon(points); } void KisPainter::paintRect(const qreal x, const qreal y, const qreal w, const qreal h) { paintRect(QRectF(x, y, w, h)); } void KisPainter::paintEllipse(const QRectF &rect) { QRectF r = rect.normalized(); // normalize before checking as negative width and height are empty too if (r.isEmpty()) return; // See http://www.whizkidtech.redprince.net/bezier/circle/ for explanation. // kappa = (4/3*(sqrt(2)-1)) const qreal kappa = 0.5522847498; const qreal lx = (r.width() / 2) * kappa; const qreal ly = (r.height() / 2) * kappa; QPointF center = r.center(); QPointF p0(r.left(), center.y()); QPointF p1(r.left(), center.y() - ly); QPointF p2(center.x() - lx, r.top()); QPointF p3(center.x(), r.top()); vQPointF points; getBezierCurvePoints(p0, p1, p2, p3, points); QPointF p4(center.x() + lx, r.top()); QPointF p5(r.right(), center.y() - ly); QPointF p6(r.right(), center.y()); getBezierCurvePoints(p3, p4, p5, p6, points); QPointF p7(r.right(), center.y() + ly); QPointF p8(center.x() + lx, r.bottom()); QPointF p9(center.x(), r.bottom()); getBezierCurvePoints(p6, p7, p8, p9, points); QPointF p10(center.x() - lx, r.bottom()); QPointF p11(r.left(), center.y() + ly); getBezierCurvePoints(p9, p10, p11, p0, points); paintPolygon(points); } void KisPainter::paintEllipse(const qreal x, const qreal y, const qreal w, const qreal h) { paintEllipse(QRectF(x, y, w, h)); } void KisPainter::paintAt(const KisPaintInformation& pi, KisDistanceInformation *savedDist) { if (d->paintOp && d->paintOp->canPaint()) { d->paintOp->paintAt(pi, savedDist); } } void KisPainter::fillPolygon(const vQPointF& points, FillStyle fillStyle) { if (points.count() < 3) { return; } if (fillStyle == FillStyleNone) { return; } QPainterPath polygonPath; polygonPath.moveTo(points.at(0)); for (int pointIndex = 1; pointIndex < points.count(); pointIndex++) { polygonPath.lineTo(points.at(pointIndex)); } polygonPath.closeSubpath(); d->fillStyle = fillStyle; fillPainterPath(polygonPath); } void KisPainter::paintPolygon(const vQPointF& points) { if (d->fillStyle != FillStyleNone) { fillPolygon(points, d->fillStyle); } if (d->strokeStyle != StrokeStyleNone) { if (points.count() > 1) { - KisDistanceInformation distance(points[0], 0.0, + KisDistanceInformation distance(points[0], KisAlgebra2D::directionBetweenPoints(points[0], points[1], 0.0)); for (int i = 0; i < points.count() - 1; i++) { paintLine(KisPaintInformation(points[i]), KisPaintInformation(points[i + 1]), &distance); } paintLine(points[points.count() - 1], points[0], &distance); } } } void KisPainter::paintPainterPath(const QPainterPath& path) { if (d->fillStyle != FillStyleNone) { fillPainterPath(path); } if (d->strokeStyle == StrokeStyleNone) return; QPointF lastPoint, nextPoint; int elementCount = path.elementCount(); KisDistanceInformation saveDist; for (int i = 0; i < elementCount; i++) { QPainterPath::Element element = path.elementAt(i); switch (element.type) { case QPainterPath::MoveToElement: lastPoint = QPointF(element.x, element.y); break; case QPainterPath::LineToElement: nextPoint = QPointF(element.x, element.y); paintLine(KisPaintInformation(lastPoint), KisPaintInformation(nextPoint), &saveDist); lastPoint = nextPoint; break; case QPainterPath::CurveToElement: nextPoint = QPointF(path.elementAt(i + 2).x, path.elementAt(i + 2).y); paintBezierCurve(KisPaintInformation(lastPoint), QPointF(path.elementAt(i).x, path.elementAt(i).y), QPointF(path.elementAt(i + 1).x, path.elementAt(i + 1).y), KisPaintInformation(nextPoint), &saveDist); lastPoint = nextPoint; break; default: continue; } } } void KisPainter::fillPainterPath(const QPainterPath& path) { fillPainterPath(path, QRect()); } void KisPainter::fillPainterPath(const QPainterPath& path, const QRect &requestedRect) { if (d->mirrorHorizontally || d->mirrorVertically) { KisLodTransform lod(d->device); QPointF effectiveAxesCenter = lod.map(d->axesCenter); QTransform C1 = QTransform::fromTranslate(-effectiveAxesCenter.x(), -effectiveAxesCenter.y()); QTransform C2 = QTransform::fromTranslate(effectiveAxesCenter.x(), effectiveAxesCenter.y()); QTransform t; QPainterPath newPath; QRect newRect; if (d->mirrorHorizontally) { t = C1 * QTransform::fromScale(-1,1) * C2; newPath = t.map(path); newRect = t.mapRect(requestedRect); d->fillPainterPathImpl(newPath, newRect); } if (d->mirrorVertically) { t = C1 * QTransform::fromScale(1,-1) * C2; newPath = t.map(path); newRect = t.mapRect(requestedRect); d->fillPainterPathImpl(newPath, newRect); } if (d->mirrorHorizontally && d->mirrorVertically) { t = C1 * QTransform::fromScale(-1,-1) * C2; newPath = t.map(path); newRect = t.mapRect(requestedRect); d->fillPainterPathImpl(newPath, newRect); } } d->fillPainterPathImpl(path, requestedRect); } void KisPainter::Private::fillPainterPathImpl(const QPainterPath& path, const QRect &requestedRect) { if (fillStyle == FillStyleNone) { return; } // Fill the polygon bounding rectangle with the required contents then we'll // create a mask for the actual polygon coverage. if (!fillPainter) { polygon = device->createCompositionSourceDevice(); fillPainter = new KisFillPainter(polygon); } else { polygon->clear(); } Q_CHECK_PTR(polygon); QRectF boundingRect = path.boundingRect(); QRect fillRect = boundingRect.toAlignedRect(); // Expand the rectangle to allow for anti-aliasing. fillRect.adjust(-1, -1, 1, 1); if (requestedRect.isValid()) { fillRect &= requestedRect; } switch (fillStyle) { default: // Fall through case FillStyleGradient: // Currently unsupported, fall through case FillStyleStrokes: // Currently unsupported, fall through warnImage << "Unknown or unsupported fill style in fillPolygon\n"; case FillStyleForegroundColor: fillPainter->fillRect(fillRect, q->paintColor(), OPACITY_OPAQUE_U8); break; case FillStyleBackgroundColor: fillPainter->fillRect(fillRect, q->backgroundColor(), OPACITY_OPAQUE_U8); break; case FillStylePattern: if (pattern) { // if the user hasn't got any patterns installed, we shouldn't crash... fillPainter->fillRect(fillRect, pattern); } break; case FillStyleGenerator: if (generator) { // if the user hasn't got any generators, we shouldn't crash... fillPainter->fillRect(fillRect.x(), fillRect.y(), fillRect.width(), fillRect.height(), q->generator()); } break; } if (polygonMaskImage.isNull() || (maskPainter == 0)) { polygonMaskImage = QImage(maskImageWidth, maskImageHeight, QImage::Format_ARGB32_Premultiplied); maskPainter = new QPainter(&polygonMaskImage); maskPainter->setRenderHint(QPainter::Antialiasing, q->antiAliasPolygonFill()); } // Break the mask up into chunks so we don't have to allocate a potentially very large QImage. const QColor black(Qt::black); const QBrush brush(Qt::white); for (qint32 x = fillRect.x(); x < fillRect.x() + fillRect.width(); x += maskImageWidth) { for (qint32 y = fillRect.y(); y < fillRect.y() + fillRect.height(); y += maskImageHeight) { polygonMaskImage.fill(black.rgb()); maskPainter->translate(-x, -y); maskPainter->fillPath(path, brush); maskPainter->translate(x, y); qint32 rectWidth = qMin(fillRect.x() + fillRect.width() - x, maskImageWidth); qint32 rectHeight = qMin(fillRect.y() + fillRect.height() - y, maskImageHeight); KisHLineIteratorSP lineIt = polygon->createHLineIteratorNG(x, y, rectWidth); quint8 tmp; for (int row = y; row < y + rectHeight; row++) { QRgb* line = reinterpret_cast(polygonMaskImage.scanLine(row - y)); do { tmp = qRed(line[lineIt->x() - x]); polygon->colorSpace()->applyAlphaU8Mask(lineIt->rawData(), &tmp, 1); } while (lineIt->nextPixel()); lineIt->nextRow(); } } } QRect bltRect = !requestedRect.isEmpty() ? requestedRect : fillRect; q->bitBlt(bltRect.x(), bltRect.y(), polygon, bltRect.x(), bltRect.y(), bltRect.width(), bltRect.height()); } void KisPainter::drawPainterPath(const QPainterPath& path, const QPen& pen) { drawPainterPath(path, pen, QRect()); } void KisPainter::drawPainterPath(const QPainterPath& path, const QPen& pen, const QRect &requestedRect) { // we are drawing mask, it has to be white // color of the path is given by paintColor() Q_ASSERT(pen.color() == Qt::white); if (!d->fillPainter) { d->polygon = d->device->createCompositionSourceDevice(); d->fillPainter = new KisFillPainter(d->polygon); } else { d->polygon->clear(); } Q_CHECK_PTR(d->polygon); QRectF boundingRect = path.boundingRect(); QRect fillRect = boundingRect.toAlignedRect(); // take width of the pen into account int penWidth = qRound(pen.widthF()); fillRect.adjust(-penWidth, -penWidth, penWidth, penWidth); // Expand the rectangle to allow for anti-aliasing. fillRect.adjust(-1, -1, 1, 1); if (!requestedRect.isNull()) { fillRect &= requestedRect; } d->fillPainter->fillRect(fillRect, paintColor(), OPACITY_OPAQUE_U8); if (d->polygonMaskImage.isNull() || (d->maskPainter == 0)) { d->polygonMaskImage = QImage(d->maskImageWidth, d->maskImageHeight, QImage::Format_ARGB32_Premultiplied); d->maskPainter = new QPainter(&d->polygonMaskImage); d->maskPainter->setRenderHint(QPainter::Antialiasing, antiAliasPolygonFill()); } // Break the mask up into chunks so we don't have to allocate a potentially very large QImage. const QColor black(Qt::black); QPen oldPen = d->maskPainter->pen(); d->maskPainter->setPen(pen); for (qint32 x = fillRect.x(); x < fillRect.x() + fillRect.width(); x += d->maskImageWidth) { for (qint32 y = fillRect.y(); y < fillRect.y() + fillRect.height(); y += d->maskImageHeight) { d->polygonMaskImage.fill(black.rgb()); d->maskPainter->translate(-x, -y); d->maskPainter->drawPath(path); d->maskPainter->translate(x, y); qint32 rectWidth = qMin(fillRect.x() + fillRect.width() - x, d->maskImageWidth); qint32 rectHeight = qMin(fillRect.y() + fillRect.height() - y, d->maskImageHeight); KisHLineIteratorSP lineIt = d->polygon->createHLineIteratorNG(x, y, rectWidth); quint8 tmp; for (int row = y; row < y + rectHeight; row++) { QRgb* line = reinterpret_cast(d->polygonMaskImage.scanLine(row - y)); do { tmp = qRed(line[lineIt->x() - x]); d->polygon->colorSpace()->applyAlphaU8Mask(lineIt->rawData(), &tmp, 1); } while (lineIt->nextPixel()); lineIt->nextRow(); } } } d->maskPainter->setPen(oldPen); QRect r = d->polygon->extent(); bitBlt(r.x(), r.y(), d->polygon, r.x(), r.y(), r.width(), r.height()); } inline void KisPainter::compositeOnePixel(quint8 *dst, const KoColor &color) { d->paramInfo.dstRowStart = dst; d->paramInfo.dstRowStride = 0; d->paramInfo.srcRowStart = color.data(); d->paramInfo.srcRowStride = 0; d->paramInfo.maskRowStart = 0; d->paramInfo.maskRowStride = 0; d->paramInfo.rows = 1; d->paramInfo.cols = 1; d->colorSpace->bitBlt(color.colorSpace(), d->paramInfo, d->compositeOp, d->renderingIntent, d->conversionFlags); } /**/ void KisPainter::drawLine(const QPointF& start, const QPointF& end, qreal width, bool antialias){ int x1 = start.x(); int y1 = start.y(); int x2 = end.x(); int y2 = end.y(); if ((x2 == x1 ) && (y2 == y1)) return; int dstX = x2-x1; int dstY = y2-y1; qreal uniC = dstX*y1 - dstY*x1; qreal projectionDenominator = 1.0 / (pow((double)dstX, 2) + pow((double)dstY, 2)); qreal subPixel; if (qAbs(dstX) > qAbs(dstY)){ subPixel = start.x() - x1; }else{ subPixel = start.y() - y1; } qreal halfWidth = width * 0.5 + subPixel; int W_ = qRound(halfWidth) + 1; // save the state int X1_ = x1; int Y1_ = y1; int X2_ = x2; int Y2_ = y2; if (x2device->createRandomAccessorNG(x1, y1); KisRandomConstAccessorSP selectionAccessor; if (d->selection) { selectionAccessor = d->selection->projection()->createRandomConstAccessorNG(x1, y1); } for (int y = y1-W_; y < y2+W_ ; y++){ for (int x = x1-W_; x < x2+W_; x++){ projection = ( (x-X1_)* dstX + (y-Y1_)*dstY ) * projectionDenominator; scanX = X1_ + projection * dstX; scanY = Y1_ + projection * dstY; if (((scanX < x1) || (scanX > x2)) || ((scanY < y1) || (scanY > y2))) { AA_ = qMin( sqrt( pow((double)x - X1_, 2) + pow((double)y - Y1_, 2) ), sqrt( pow((double)x - X2_, 2) + pow((double)y - Y2_, 2) )); }else{ AA_ = qAbs(dstY*x - dstX*y + uniC) * denominator; } if (AA_>halfWidth) { continue; } accessor->moveTo(x, y); if (selectionAccessor) selectionAccessor->moveTo(x,y); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { KoColor mycolor = d->paintColor; if (antialias && AA_ > halfWidth-1.0) { mycolor.colorSpace()->multiplyAlpha(mycolor.data(), 1.0 - (AA_-(halfWidth-1.0)), 1); } compositeOnePixel(accessor->rawData(), mycolor); } } } } /**/ void KisPainter::drawLine(const QPointF & start, const QPointF & end) { drawThickLine(start, end, 1, 1); } void KisPainter::drawDDALine(const QPointF & start, const QPointF & end) { int x = int(start.x()); int y = int(start.y()); int x2 = int(end.x()); int y2 = int(end.y()); // Width and height of the line int xd = x2 - x; int yd = y2 - y; float m = (float)yd / (float)xd; float fx = x; float fy = y; int inc; KisRandomAccessorSP accessor = d->device->createRandomAccessorNG(x, y); KisRandomConstAccessorSP selectionAccessor; if (d->selection) { selectionAccessor = d->selection->projection()->createRandomConstAccessorNG(x, y); } accessor->moveTo(x, y); if (selectionAccessor) selectionAccessor->moveTo(x,y); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { compositeOnePixel(accessor->rawData(), d->paintColor); } if (fabs(m) > 1.0f) { inc = (yd > 0) ? 1 : -1; m = 1.0f / m; m *= inc; while (y != y2) { y = y + inc; fx = fx + m; x = qRound(fx); accessor->moveTo(x, y); if (selectionAccessor) selectionAccessor->moveTo(x, y); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { compositeOnePixel(accessor->rawData(), d->paintColor); } } } else { inc = (xd > 0) ? 1 : -1; m *= inc; while (x != x2) { x = x + inc; fy = fy + m; y = qRound(fy); accessor->moveTo(x, y); if (selectionAccessor) selectionAccessor->moveTo(x, y); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { compositeOnePixel(accessor->rawData(), d->paintColor); } } } } void KisPainter::drawWobblyLine(const QPointF & start, const QPointF & end) { KisRandomAccessorSP accessor = d->device->createRandomAccessorNG(start.x(), start.y()); KisRandomConstAccessorSP selectionAccessor; if (d->selection) { selectionAccessor = d->selection->projection()->createRandomConstAccessorNG(start.x(), start.y()); } KoColor mycolor(d->paintColor); int x1 = start.x(); int y1 = start.y(); int x2 = end.x(); int y2 = end.y(); // Width and height of the line int xd = (x2 - x1); int yd = (y2 - y1); int x; int y; float fx = (x = x1); float fy = (y = y1); float m = (float)yd / (float)xd; int inc; if (fabs(m) > 1) { inc = (yd > 0) ? 1 : -1; m = 1.0f / m; m *= inc; while (y != y2) { fx = fx + m; y = y + inc; x = qRound(fx); float br1 = int(fx + 1) - fx; float br2 = fx - (int)fx; accessor->moveTo(x, y); if (selectionAccessor) selectionAccessor->moveTo(x, y); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { mycolor.setOpacity((quint8)(255*br1)); compositeOnePixel(accessor->rawData(), mycolor); } accessor->moveTo(x + 1, y); if (selectionAccessor) selectionAccessor->moveTo(x + 1, y); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { mycolor.setOpacity((quint8)(255*br2)); compositeOnePixel(accessor->rawData(), mycolor); } } } else { inc = (xd > 0) ? 1 : -1; m *= inc; while (x != x2) { fy = fy + m; x = x + inc; y = qRound(fy); float br1 = int(fy + 1) - fy; float br2 = fy - (int)fy; accessor->moveTo(x, y); if (selectionAccessor) selectionAccessor->moveTo(x, y); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { mycolor.setOpacity((quint8)(255*br1)); compositeOnePixel(accessor->rawData(), mycolor); } accessor->moveTo(x, y + 1); if (selectionAccessor) selectionAccessor->moveTo(x, y + 1); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { mycolor.setOpacity((quint8)(255*br2)); compositeOnePixel(accessor->rawData(), mycolor); } } } } void KisPainter::drawWuLine(const QPointF & start, const QPointF & end) { KisRandomAccessorSP accessor = d->device->createRandomAccessorNG(start.x(), start.y()); KisRandomConstAccessorSP selectionAccessor; if (d->selection) { selectionAccessor = d->selection->projection()->createRandomConstAccessorNG(start.x(), start.y()); } KoColor lineColor(d->paintColor); int x1 = start.x(); int y1 = start.y(); int x2 = end.x(); int y2 = end.y(); float grad, xd, yd; float xgap, ygap, xend, yend, yf, xf; float brightness1, brightness2; int ix1, ix2, iy1, iy2; quint8 c1, c2; // gradient of line xd = (x2 - x1); yd = (y2 - y1); if (yd == 0) { /* Horizontal line */ int incr = (x1 < x2) ? 1 : -1; ix1 = (int)x1; ix2 = (int)x2; iy1 = (int)y1; while (ix1 != ix2) { ix1 = ix1 + incr; accessor->moveTo(ix1, iy1); if (selectionAccessor) selectionAccessor->moveTo(ix1, iy1); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { compositeOnePixel(accessor->rawData(), lineColor); } } return; } if (xd == 0) { /* Vertical line */ int incr = (y1 < y2) ? 1 : -1; iy1 = (int)y1; iy2 = (int)y2; ix1 = (int)x1; while (iy1 != iy2) { iy1 = iy1 + incr; accessor->moveTo(ix1, iy1); if (selectionAccessor) selectionAccessor->moveTo(ix1, iy1); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { compositeOnePixel(accessor->rawData(), lineColor); } } return; } if (fabs(xd) > fabs(yd)) { // horizontal line // line have to be paint from left to right if (x1 > x2) { float tmp; tmp = x1; x1 = x2; x2 = tmp; tmp = y1; y1 = y2; y2 = tmp; xd = (x2 - x1); yd = (y2 - y1); } grad = yd / xd; // nearest X,Y interger coordinates xend = static_cast(x1 + 0.5f); yend = y1 + grad * (xend - x1); xgap = invertFrac(x1 + 0.5f); ix1 = static_cast(xend); iy1 = static_cast(yend); // calc the intensity of the other end point pixel pair. brightness1 = invertFrac(yend) * xgap; brightness2 = frac(yend) * xgap; c1 = (int)(brightness1 * OPACITY_OPAQUE_U8); c2 = (int)(brightness2 * OPACITY_OPAQUE_U8); accessor->moveTo(ix1, iy1); if (selectionAccessor) selectionAccessor->moveTo(ix1, iy1); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { lineColor.setOpacity(c1); compositeOnePixel(accessor->rawData(), lineColor); } accessor->moveTo(ix1, iy1 + 1); if (selectionAccessor) selectionAccessor->moveTo(ix1, iy1 + 1); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { lineColor.setOpacity(c2); compositeOnePixel(accessor->rawData(), lineColor); } // calc first Y-intersection for main loop yf = yend + grad; xend = trunc(x2 + 0.5f); yend = y2 + grad * (xend - x2); xgap = invertFrac(x2 - 0.5f); ix2 = static_cast(xend); iy2 = static_cast(yend); brightness1 = invertFrac(yend) * xgap; brightness2 = frac(yend) * xgap; c1 = (int)(brightness1 * OPACITY_OPAQUE_U8); c2 = (int)(brightness2 * OPACITY_OPAQUE_U8); accessor->moveTo(ix2, iy2); if (selectionAccessor) selectionAccessor->moveTo(ix2, iy2); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { lineColor.setOpacity(c1); compositeOnePixel(accessor->rawData(), lineColor); } accessor->moveTo(ix2, iy2 + 1); if (selectionAccessor) selectionAccessor->moveTo(ix2, iy2 + 1); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { lineColor.setOpacity(c2); compositeOnePixel(accessor->rawData(), lineColor); } // main loop for (int x = ix1 + 1; x <= ix2 - 1; x++) { brightness1 = invertFrac(yf); brightness2 = frac(yf); c1 = (int)(brightness1 * OPACITY_OPAQUE_U8); c2 = (int)(brightness2 * OPACITY_OPAQUE_U8); accessor->moveTo(x, int (yf)); if (selectionAccessor) selectionAccessor->moveTo(x, int (yf)); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { lineColor.setOpacity(c1); compositeOnePixel(accessor->rawData(), lineColor); } accessor->moveTo(x, int (yf) + 1); if (selectionAccessor) selectionAccessor->moveTo(x, int (yf) + 1); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { lineColor.setOpacity(c2); compositeOnePixel(accessor->rawData(), lineColor); } yf = yf + grad; } } else { //vertical // line have to be painted from left to right if (y1 > y2) { float tmp; tmp = x1; x1 = x2; x2 = tmp; tmp = y1; y1 = y2; y2 = tmp; xd = (x2 - x1); yd = (y2 - y1); } grad = xd / yd; // nearest X,Y interger coordinates yend = static_cast(y1 + 0.5f); xend = x1 + grad * (yend - y1); ygap = invertFrac(y1 + 0.5f); ix1 = static_cast(xend); iy1 = static_cast(yend); // calc the intensity of the other end point pixel pair. brightness1 = invertFrac(xend) * ygap; brightness2 = frac(xend) * ygap; c1 = (int)(brightness1 * OPACITY_OPAQUE_U8); c2 = (int)(brightness2 * OPACITY_OPAQUE_U8); accessor->moveTo(ix1, iy1); if (selectionAccessor) selectionAccessor->moveTo(ix1, iy1); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { lineColor.setOpacity(c1); compositeOnePixel(accessor->rawData(), lineColor); } accessor->moveTo(x1 + 1, y1); if (selectionAccessor) selectionAccessor->moveTo(x1 + 1, y1); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { lineColor.setOpacity(c2); compositeOnePixel(accessor->rawData(), lineColor); } // calc first Y-intersection for main loop xf = xend + grad; yend = trunc(y2 + 0.5f); xend = x2 + grad * (yend - y2); ygap = invertFrac(y2 - 0.5f); ix2 = static_cast(xend); iy2 = static_cast(yend); brightness1 = invertFrac(xend) * ygap; brightness2 = frac(xend) * ygap; c1 = (int)(brightness1 * OPACITY_OPAQUE_U8); c2 = (int)(brightness2 * OPACITY_OPAQUE_U8); accessor->moveTo(ix2, iy2); if (selectionAccessor) selectionAccessor->moveTo(ix2, iy2); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { lineColor.setOpacity(c1); compositeOnePixel(accessor->rawData(), lineColor); } accessor->moveTo(ix2 + 1, iy2); if (selectionAccessor) selectionAccessor->moveTo(ix2 + 1, iy2); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { lineColor.setOpacity(c2); compositeOnePixel(accessor->rawData(), lineColor); } // main loop for (int y = iy1 + 1; y <= iy2 - 1; y++) { brightness1 = invertFrac(xf); brightness2 = frac(xf); c1 = (int)(brightness1 * OPACITY_OPAQUE_U8); c2 = (int)(brightness2 * OPACITY_OPAQUE_U8); accessor->moveTo(int (xf), y); if (selectionAccessor) selectionAccessor->moveTo(int (xf), y); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { lineColor.setOpacity(c1); compositeOnePixel(accessor->rawData(), lineColor); } accessor->moveTo(int (xf) + 1, y); if (selectionAccessor) selectionAccessor->moveTo(int (xf) + 1, y); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { lineColor.setOpacity(c2); compositeOnePixel(accessor->rawData(), lineColor); } xf = xf + grad; } }//end-of-else } void KisPainter::drawThickLine(const QPointF & start, const QPointF & end, int startWidth, int endWidth) { KisRandomAccessorSP accessor = d->device->createRandomAccessorNG(start.x(), start.y()); KisRandomConstAccessorSP selectionAccessor; if (d->selection) { selectionAccessor = d->selection->projection()->createRandomConstAccessorNG(start.x(), start.y()); } const KoColorSpace *cs = d->device->colorSpace(); KoColor c1(d->paintColor); KoColor c2(d->paintColor); KoColor c3(d->paintColor); KoColor col1(c1); KoColor col2(c1); float grada, gradb, dxa, dxb, dya, dyb, fraca, fracb, xfa, yfa, xfb, yfb, b1a, b2a, b1b, b2b, dstX, dstY; int x, y, ix1, ix2, iy1, iy2; int x0a, y0a, x1a, y1a, x0b, y0b, x1b, y1b; int tp0, tn0, tp1, tn1; int horizontal = 0; float opacity = 1.0; tp0 = startWidth / 2; tn0 = startWidth / 2; if (startWidth % 2 == 0) // even width startWidth tn0--; tp1 = endWidth / 2; tn1 = endWidth / 2; if (endWidth % 2 == 0) // even width endWidth tn1--; int x0 = qRound(start.x()); int y0 = qRound(start.y()); int x1 = qRound(end.x()); int y1 = qRound(end.y()); dstX = x1 - x0; // run of general line dstY = y1 - y0; // rise of general line if (dstY < 0) dstY = -dstY; if (dstX < 0) dstX = -dstX; if (dstX > dstY) { // horizontalish horizontal = 1; x0a = x0; y0a = y0 - tn0; x0b = x0; y0b = y0 + tp0; x1a = x1; y1a = y1 - tn1; x1b = x1; y1b = y1 + tp1; } else { x0a = x0 - tn0; y0a = y0; x0b = x0 + tp0; y0b = y0; x1a = x1 - tn1; y1a = y1; x1b = x1 + tp1; y1b = y1; } if (horizontal) { // draw endpoints for (int i = y0a; i <= y0b; i++) { accessor->moveTo(x0, i); if (selectionAccessor) selectionAccessor->moveTo(x0, i); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { compositeOnePixel(accessor->rawData(), c1); } } for (int i = y1a; i <= y1b; i++) { accessor->moveTo(x1, i); if (selectionAccessor) selectionAccessor->moveTo(x1, i); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { compositeOnePixel(accessor->rawData(), c1); } } } else { for (int i = x0a; i <= x0b; i++) { accessor->moveTo(i, y0); if (selectionAccessor) selectionAccessor->moveTo(i, y0); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { compositeOnePixel(accessor->rawData(), c1); } } for (int i = x1a; i <= x1b; i++) { accessor->moveTo(i, y1); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { compositeOnePixel(accessor->rawData(), c1); } } } //antialias endpoints if (x1 != x0 && y1 != y0) { if (horizontal) { accessor->moveTo(x0a, y0a - 1); if (selectionAccessor) selectionAccessor->moveTo(x0a, y0a - 1); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { qreal alpha = cs->opacityF(accessor->rawData()); opacity = .25 * c1.opacityF() + (1 - .25) * alpha; col1.setOpacity(opacity); compositeOnePixel(accessor->rawData(), col1); } accessor->moveTo(x1b, y1b + 1); if (selectionAccessor) selectionAccessor->moveTo(x1b, y1b + 1); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { qreal alpha = cs->opacityF(accessor->rawData()); opacity = .25 * c2.opacityF() + (1 - .25) * alpha; col1.setOpacity(opacity); compositeOnePixel(accessor->rawData(), col1); } } else { accessor->moveTo(x0a - 1, y0a); if (selectionAccessor) selectionAccessor->moveTo(x0a - 1, y0a); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { qreal alpha = cs->opacityF(accessor->rawData()); opacity = .25 * c1.opacityF() + (1 - .25) * alpha; col1.setOpacity(opacity); compositeOnePixel(accessor->rawData(), col1); } accessor->moveTo(x1b + 1, y1b); if (selectionAccessor) selectionAccessor->moveTo(x1b + 1, y1b); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { qreal alpha = cs->opacityF(accessor->rawData()); opacity = .25 * c2.opacityF() + (1 - .25) * alpha; col1.setOpacity(opacity); compositeOnePixel(accessor->rawData(), col1); } } } dxa = x1a - x0a; // run of a dya = y1a - y0a; // rise of a dxb = x1b - x0b; // run of b dyb = y1b - y0b; // rise of b if (horizontal) { // horizontal-ish lines if (x1 < x0) { int xt, yt, wt; KoColor tmp; xt = x1a; x1a = x0a; x0a = xt; yt = y1a; y1a = y0a; y0a = yt; xt = x1b; x1b = x0b; x0b = xt; yt = y1b; y1b = y0b; y0b = yt; xt = x1; x1 = x0; x0 = xt; yt = y1; y1 = y0; y0 = yt; tmp = c1; c1 = c2; c2 = tmp; wt = startWidth; startWidth = endWidth; endWidth = wt; } grada = dya / dxa; gradb = dyb / dxb; ix1 = x0; iy1 = y0; ix2 = x1; iy2 = y1; yfa = y0a + grada; yfb = y0b + gradb; for (x = ix1 + 1; x <= ix2 - 1; x++) { fraca = yfa - int (yfa); b1a = 1 - fraca; b2a = fraca; fracb = yfb - int (yfb); b1b = 1 - fracb; b2b = fracb; // color first pixel of bottom line opacity = ((x - ix1) / dstX) * c2.opacityF() + (1 - (x - ix1) / dstX) * c1.opacityF(); c3.setOpacity(opacity); accessor->moveTo(x, (int)yfa); if (selectionAccessor) selectionAccessor->moveTo(x, (int)yfa); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { qreal alpha = cs->opacityF(accessor->rawData()); opacity = b1a * c3.opacityF() + (1 - b1a) * alpha; col1.setOpacity(opacity); compositeOnePixel(accessor->rawData(), col1); } // color first pixel of top line if (!(startWidth == 1 && endWidth == 1)) { accessor->moveTo(x, (int)yfb); if (selectionAccessor) selectionAccessor->moveTo(x, (int)yfb); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { qreal alpha = cs->opacityF(accessor->rawData()); opacity = b1b * c3.opacityF() + (1 - b1b) * alpha; col1.setOpacity(opacity); compositeOnePixel(accessor->rawData(), col1); } } // color second pixel of bottom line if (grada != 0 && grada != 1) { // if not flat or exact diagonal accessor->moveTo(x, int (yfa) + 1); if (selectionAccessor) selectionAccessor->moveTo(x, int (yfa) + 1); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { qreal alpha = cs->opacityF(accessor->rawData()); opacity = b2a * c3.opacityF() + (1 - b2a) * alpha; col2.setOpacity(opacity); compositeOnePixel(accessor->rawData(), col2); } } // color second pixel of top line if (gradb != 0 && gradb != 1 && !(startWidth == 1 && endWidth == 1)) { accessor->moveTo(x, int (yfb) + 1); if (selectionAccessor) selectionAccessor->moveTo(x, int (yfb) + 1); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { qreal alpha = cs->opacityF(accessor->rawData()); opacity = b2b * c3.opacityF() + (1 - b2b) * alpha; col2.setOpacity(opacity); compositeOnePixel(accessor->rawData(), col2); } } // fill remaining pixels if (!(startWidth == 1 && endWidth == 1)) { if (yfa < yfb) for (int i = yfa + 1; i <= yfb; i++) { accessor->moveTo(x, i); if (selectionAccessor) selectionAccessor->moveTo(x, i); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { compositeOnePixel(accessor->rawData(), c3); } } else for (int i = yfa + 1; i >= yfb; i--) { accessor->moveTo(x, i); if (selectionAccessor) selectionAccessor->moveTo(x, i); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { compositeOnePixel(accessor->rawData(), c3); } } } yfa += grada; yfb += gradb; } } else { // vertical-ish lines if (y1 < y0) { int xt, yt, wt; xt = x1a; x1a = x0a; x0a = xt; yt = y1a; y1a = y0a; y0a = yt; xt = x1b; x1b = x0b; x0b = xt; yt = y1b; y1b = y0b; y0b = yt; xt = x1; x1 = x0; x0 = xt; yt = y1; y1 = y0; y0 = yt; KoColor tmp; tmp = c1; c1 = c2; c2 = tmp; wt = startWidth; startWidth = endWidth; endWidth = wt; } grada = dxa / dya; gradb = dxb / dyb; ix1 = x0; iy1 = y0; ix2 = x1; iy2 = y1; xfa = x0a + grada; xfb = x0b + gradb; for (y = iy1 + 1; y <= iy2 - 1; y++) { fraca = xfa - int (xfa); b1a = 1 - fraca; b2a = fraca; fracb = xfb - int (xfb); b1b = 1 - fracb; b2b = fracb; // color first pixel of left line opacity = ((y - iy1) / dstY) * c2.opacityF() + (1 - (y - iy1) / dstY) * c1.opacityF(); c3.setOpacity(opacity); accessor->moveTo(int (xfa), y); if (selectionAccessor) selectionAccessor->moveTo(int (xfa), y); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { qreal alpha = cs->opacityF(accessor->rawData()); opacity = b1a * c3.opacityF() + (1 - b1a) * alpha; col1.setOpacity(opacity); compositeOnePixel(accessor->rawData(), col1); } // color first pixel of right line if (!(startWidth == 1 && endWidth == 1)) { accessor->moveTo(int(xfb), y); if (selectionAccessor) selectionAccessor->moveTo(int(xfb), y); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { qreal alpha = cs->opacityF(accessor->rawData()); opacity = b1b * c3.opacityF() + (1 - b1b) * alpha; col1.setOpacity(opacity); compositeOnePixel(accessor->rawData(), col1); } } // color second pixel of left line if (grada != 0 && grada != 1) { // if not flat or exact diagonal accessor->moveTo(int(xfa) + 1, y); if (selectionAccessor) selectionAccessor->moveTo(int(xfa) + 1, y); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { qreal alpha = cs->opacityF(accessor->rawData()); opacity = b2a * c3.opacityF() + (1 - b2a) * alpha; col2.setOpacity(opacity); compositeOnePixel(accessor->rawData(), col2); } } // color second pixel of right line if (gradb != 0 && gradb != 1 && !(startWidth == 1 && endWidth == 1)) { accessor->moveTo(int(xfb) + 1, y); if (selectionAccessor) selectionAccessor->moveTo(int(xfb) + 1, y); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { qreal alpha = cs->opacityF(accessor->rawData()); opacity = b2b * c3.opacityF() + (1 - b2b) * alpha; col2.setOpacity(opacity); compositeOnePixel(accessor->rawData(), col2); } } // fill remaining pixels between current xfa,xfb if (!(startWidth == 1 && endWidth == 1)) { if (xfa < xfb) for (int i = (int) xfa + 1; i <= (int) xfb; i++) { accessor->moveTo(i, y); if (selectionAccessor) selectionAccessor->moveTo(i, y); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { compositeOnePixel(accessor->rawData(), c3); } } else for (int i = (int) xfb; i <= (int) xfa + 1; i++) { accessor->moveTo(i, y); if (selectionAccessor) selectionAccessor->moveTo(i, y); if (!selectionAccessor || *selectionAccessor->oldRawData() > SELECTION_THRESHOLD) { compositeOnePixel(accessor->rawData(), c3); } } } xfa += grada; xfb += gradb; } } } void KisPainter::setProgress(KoUpdater * progressUpdater) { d->progressUpdater = progressUpdater; } const KisPaintDeviceSP KisPainter::device() const { return d->device; } KisPaintDeviceSP KisPainter::device() { return d->device; } void KisPainter::setChannelFlags(QBitArray channelFlags) { Q_ASSERT(channelFlags.isEmpty() || quint32(channelFlags.size()) == d->colorSpace->channelCount()); // Now, if all bits in the channelflags are true, pass an empty channel flags bitarray // because otherwise the compositeops cannot optimize. d->paramInfo.channelFlags = channelFlags; if (!channelFlags.isEmpty() && channelFlags == QBitArray(channelFlags.size(), true)) { d->paramInfo.channelFlags = QBitArray(); } } QBitArray KisPainter::channelFlags() { return d->paramInfo.channelFlags; } void KisPainter::setPattern(const KoPattern * pattern) { d->pattern = pattern; } const KoPattern * KisPainter::pattern() const { return d->pattern; } void KisPainter::setPaintColor(const KoColor& color) { d->paintColor = color; if (d->device) { d->paintColor.convertTo(d->device->compositionSourceColorSpace()); } } const KoColor &KisPainter::paintColor() const { return d->paintColor; } void KisPainter::setBackgroundColor(const KoColor& color) { d->backgroundColor = color; if (d->device) { d->backgroundColor.convertTo(d->device->compositionSourceColorSpace()); } } const KoColor &KisPainter::backgroundColor() const { return d->backgroundColor; } void KisPainter::setGenerator(KisFilterConfigurationSP generator) { d->generator = generator; } const KisFilterConfigurationSP KisPainter::generator() const { return d->generator; } void KisPainter::setFillStyle(FillStyle fillStyle) { d->fillStyle = fillStyle; } KisPainter::FillStyle KisPainter::fillStyle() const { return d->fillStyle; } void KisPainter::setAntiAliasPolygonFill(bool antiAliasPolygonFill) { d->antiAliasPolygonFill = antiAliasPolygonFill; } bool KisPainter::antiAliasPolygonFill() { return d->antiAliasPolygonFill; } void KisPainter::setStrokeStyle(KisPainter::StrokeStyle strokeStyle) { d->strokeStyle = strokeStyle; } KisPainter::StrokeStyle KisPainter::strokeStyle() const { return d->strokeStyle; } void KisPainter::setFlow(quint8 flow) { d->paramInfo.flow = float(flow) / 255.0f; } quint8 KisPainter::flow() const { return quint8(d->paramInfo.flow * 255.0f); } void KisPainter::setOpacityUpdateAverage(quint8 opacity) { d->isOpacityUnit = opacity == OPACITY_OPAQUE_U8; d->paramInfo.updateOpacityAndAverage(float(opacity) / 255.0f); } +void KisPainter::setAverageOpacity(qreal averageOpacity) +{ + d->paramInfo.setOpacityAndAverage(d->paramInfo.opacity, averageOpacity); +} + +qreal KisPainter::blendAverageOpacity(qreal opacity, qreal averageOpacity) +{ + const float exponent = 0.1; + + return averageOpacity < opacity ? + opacity : + exponent * opacity + (1.0 - exponent) * (averageOpacity); +} + void KisPainter::setOpacity(quint8 opacity) { d->isOpacityUnit = opacity == OPACITY_OPAQUE_U8; d->paramInfo.opacity = float(opacity) / 255.0f; } quint8 KisPainter::opacity() const { return quint8(d->paramInfo.opacity * 255.0f); } void KisPainter::setCompositeOp(const KoCompositeOp * op) { d->compositeOp = op; } const KoCompositeOp * KisPainter::compositeOp() { return d->compositeOp; } /** * TODO: Rename this setCompositeOpId(). See KoCompositeOpRegistry.h */ void KisPainter::setCompositeOp(const QString& op) { d->compositeOp = d->colorSpace->compositeOp(op); } void KisPainter::setSelection(KisSelectionSP selection) { d->selection = selection; } KisSelectionSP KisPainter::selection() { return d->selection; } KoUpdater * KisPainter::progressUpdater() { return d->progressUpdater; } void KisPainter::setGradient(const KoAbstractGradient* gradient) { d->gradient = gradient; } const KoAbstractGradient* KisPainter::gradient() const { return d->gradient; } void KisPainter::setPaintOpPreset(KisPaintOpPresetSP preset, KisNodeSP node, KisImageSP image) { d->paintOpPreset = preset; KisPaintOp *paintop = KisPaintOpRegistry::instance()->paintOp(preset, this, node, image); Q_ASSERT(paintop); if (paintop) { delete d->paintOp; d->paintOp = paintop; } else { warnKrita << "Could not create paintop for preset " << preset->name(); } } KisPaintOpPresetSP KisPainter::preset() const { return d->paintOpPreset; } KisPaintOp* KisPainter::paintOp() const { return d->paintOp; } void KisPainter::setMirrorInformation(const QPointF& axesCenter, bool mirrorHorizontally, bool mirrorVertically) { d->axesCenter = axesCenter; d->mirrorHorizontally = mirrorHorizontally; d->mirrorVertically = mirrorVertically; } -void KisPainter::copyMirrorInformation(KisPainter* painter) +bool KisPainter::hasMirroring() const { - painter->setMirrorInformation(d->axesCenter, d->mirrorHorizontally, d->mirrorVertically); + return d->mirrorHorizontally || d->mirrorVertically; } -bool KisPainter::hasMirroring() const +bool KisPainter::hasHorizontalMirroring() const { - return d->mirrorHorizontally || d->mirrorVertically; + return d->mirrorHorizontally; +} + +bool KisPainter::hasVerticalMirroring() const +{ + return d->mirrorVertically; } void KisPainter::setMaskImageSize(qint32 width, qint32 height) { d->maskImageWidth = qBound(1, width, 256); d->maskImageHeight = qBound(1, height, 256); d->fillPainter = 0; d->polygonMaskImage = QImage(); } //void KisPainter::setLockAlpha(bool protect) //{ // if(d->paramInfo.channelFlags.isEmpty()) { // d->paramInfo.channelFlags = d->colorSpace->channelFlags(true, true); // } // QBitArray switcher = // d->colorSpace->channelFlags(protect, !protect); // if(protect) { // d->paramInfo.channelFlags &= switcher; // } // else { // d->paramInfo.channelFlags |= switcher; // } // Q_ASSERT(quint32(d->paramInfo.channelFlags.size()) == d->colorSpace->channelCount()); //} //bool KisPainter::alphaLocked() const //{ // QBitArray switcher = d->colorSpace->channelFlags(false, true); // return !(d->paramInfo.channelFlags & switcher).count(true); //} void KisPainter::setRenderingIntent(KoColorConversionTransformation::Intent intent) { d->renderingIntent = intent; } void KisPainter::setColorConversionFlags(KoColorConversionTransformation::ConversionFlags conversionFlags) { d->conversionFlags = conversionFlags; } +void KisPainter::setRunnableStrokeJobsInterface(KisRunnableStrokeJobsInterface *interface) +{ + d->runnableStrokeJobsInterface = interface; +} + +KisRunnableStrokeJobsInterface *KisPainter::runnableStrokeJobsInterface() const +{ + if (!d->runnableStrokeJobsInterface) { + if (!d->fakeRunnableStrokeJobsInterface) { + d->fakeRunnableStrokeJobsInterface.reset(new KisFakeRunnableStrokeJobsExecutor()); + } + return d->fakeRunnableStrokeJobsInterface.data(); + } + + return d->runnableStrokeJobsInterface; +} + void KisPainter::renderMirrorMaskSafe(QRect rc, KisFixedPaintDeviceSP dab, bool preserveDab) { if (!d->mirrorHorizontally && !d->mirrorVertically) return; KisFixedPaintDeviceSP dabToProcess = dab; if (preserveDab) { dabToProcess = new KisFixedPaintDevice(*dab); } renderMirrorMask(rc, dabToProcess); } void KisPainter::renderMirrorMaskSafe(QRect rc, KisPaintDeviceSP dab, int sx, int sy, KisFixedPaintDeviceSP mask, bool preserveMask) { if (!d->mirrorHorizontally && !d->mirrorVertically) return; KisFixedPaintDeviceSP maskToProcess = mask; if (preserveMask) { maskToProcess = new KisFixedPaintDevice(*mask); } renderMirrorMask(rc, dab, sx, sy, maskToProcess); } void KisPainter::renderMirrorMask(QRect rc, KisFixedPaintDeviceSP dab) { int x = rc.topLeft().x(); int y = rc.topLeft().y(); KisLodTransform t(d->device); - QPointF effectiveAxesCenter = t.map(d->axesCenter); + QPoint effectiveAxesCenter = t.map(d->axesCenter).toPoint(); int mirrorX = -((x+rc.width()) - effectiveAxesCenter.x()) + effectiveAxesCenter.x(); int mirrorY = -((y+rc.height()) - effectiveAxesCenter.y()) + effectiveAxesCenter.y(); if (d->mirrorHorizontally && d->mirrorVertically){ dab->mirror(true, false); bltFixed(mirrorX, y, dab, 0,0,rc.width(),rc.height()); dab->mirror(false,true); bltFixed(mirrorX, mirrorY, dab, 0,0,rc.width(),rc.height()); dab->mirror(true, false); bltFixed(x, mirrorY, dab, 0,0,rc.width(),rc.height()); } else if (d->mirrorHorizontally){ dab->mirror(true, false); bltFixed(mirrorX, y, dab, 0,0,rc.width(),rc.height()); } else if (d->mirrorVertically){ dab->mirror(false, true); bltFixed(x, mirrorY, dab, 0,0,rc.width(),rc.height()); } } void KisPainter::renderMirrorMask(QRect rc, KisFixedPaintDeviceSP dab, KisFixedPaintDeviceSP mask) { int x = rc.topLeft().x(); int y = rc.topLeft().y(); KisLodTransform t(d->device); - QPointF effectiveAxesCenter = t.map(d->axesCenter); + QPoint effectiveAxesCenter = t.map(d->axesCenter).toPoint(); int mirrorX = -((x+rc.width()) - effectiveAxesCenter.x()) + effectiveAxesCenter.x(); int mirrorY = -((y+rc.height()) - effectiveAxesCenter.y()) + effectiveAxesCenter.y(); if (d->mirrorHorizontally && d->mirrorVertically){ dab->mirror(true, false); mask->mirror(true, false); bltFixedWithFixedSelection(mirrorX,y, dab, mask, rc.width() ,rc.height() ); dab->mirror(false,true); mask->mirror(false, true); bltFixedWithFixedSelection(mirrorX,mirrorY, dab, mask, rc.width() ,rc.height() ); dab->mirror(true, false); mask->mirror(true, false); bltFixedWithFixedSelection(x,mirrorY, dab, mask, rc.width() ,rc.height() ); }else if (d->mirrorHorizontally){ dab->mirror(true, false); mask->mirror(true, false); bltFixedWithFixedSelection(mirrorX,y, dab, mask, rc.width() ,rc.height() ); }else if (d->mirrorVertically){ dab->mirror(false, true); mask->mirror(false, true); bltFixedWithFixedSelection(x,mirrorY, dab, mask, rc.width() ,rc.height() ); } } void KisPainter::renderMirrorMask(QRect rc, KisPaintDeviceSP dab){ if (d->mirrorHorizontally || d->mirrorVertically){ KisFixedPaintDeviceSP mirrorDab(new KisFixedPaintDevice(dab->colorSpace())); QRect dabRc( QPoint(0,0), QSize(rc.width(),rc.height()) ); mirrorDab->setRect(dabRc); - mirrorDab->initialize(); + mirrorDab->lazyGrowBufferWithoutInitialization(); dab->readBytes(mirrorDab->data(),rc); renderMirrorMask( QRect(rc.topLeft(),dabRc.size()), mirrorDab); } } void KisPainter::renderMirrorMask(QRect rc, KisPaintDeviceSP dab, int sx, int sy, KisFixedPaintDeviceSP mask) { if (d->mirrorHorizontally || d->mirrorVertically){ KisFixedPaintDeviceSP mirrorDab(new KisFixedPaintDevice(dab->colorSpace())); QRect dabRc( QPoint(0,0), QSize(rc.width(),rc.height()) ); mirrorDab->setRect(dabRc); - mirrorDab->initialize(); + mirrorDab->lazyGrowBufferWithoutInitialization(); dab->readBytes(mirrorDab->data(),QRect(QPoint(sx,sy),rc.size())); renderMirrorMask(rc, mirrorDab, mask); } } void KisPainter::renderDabWithMirroringNonIncremental(QRect rc, KisPaintDeviceSP dab) { QVector rects; int x = rc.topLeft().x(); int y = rc.topLeft().y(); KisLodTransform t(d->device); - QPointF effectiveAxesCenter = t.map(d->axesCenter); + QPoint effectiveAxesCenter = t.map(d->axesCenter).toPoint(); int mirrorX = -((x+rc.width()) - effectiveAxesCenter.x()) + effectiveAxesCenter.x(); int mirrorY = -((y+rc.height()) - effectiveAxesCenter.y()) + effectiveAxesCenter.y(); rects << rc; if (d->mirrorHorizontally && d->mirrorVertically){ rects << QRect(mirrorX, y, rc.width(), rc.height()); rects << QRect(mirrorX, mirrorY, rc.width(), rc.height()); rects << QRect(x, mirrorY, rc.width(), rc.height()); } else if (d->mirrorHorizontally) { rects << QRect(mirrorX, y, rc.width(), rc.height()); } else if (d->mirrorVertically) { rects << QRect(x, mirrorY, rc.width(), rc.height()); } Q_FOREACH (const QRect &rc, rects) { d->device->clear(rc); } QRect resultRect = dab->extent() | rc; bool intersects = false; for (int i = 1; i < rects.size(); i++) { if (rects[i].intersects(resultRect)) { intersects = true; break; } } /** * If there are no cross-intersections, we can use a fast path * and do no cycling recompositioning */ if (!intersects) { rects.resize(1); } Q_FOREACH (const QRect &rc, rects) { bitBlt(rc.topLeft(), dab, rc); } Q_FOREACH (const QRect &rc, rects) { renderMirrorMask(rc, dab); } } +void KisPainter::mirrorRect(Qt::Orientation direction, QRect *rc) const +{ + KisLodTransform t(d->device); + QPoint effectiveAxesCenter = t.map(d->axesCenter).toPoint(); + + KritaUtils::mirrorRect(direction, effectiveAxesCenter, rc); +} + +void KisPainter::mirrorDab(Qt::Orientation direction, KisRenderedDab *dab) const +{ + KisLodTransform t(d->device); + QPoint effectiveAxesCenter = t.map(d->axesCenter).toPoint(); + + KritaUtils::mirrorDab(direction, effectiveAxesCenter, dab); +} diff --git a/libs/image/kis_painter.h b/libs/image/kis_painter.h index 353afb1a28..ef1c5e2a7e 100644 --- a/libs/image/kis_painter.h +++ b/libs/image/kis_painter.h @@ -1,809 +1,860 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2004 Clarence Dang * Copyright (c) 2008-2010 Lukáš Tvrdý * Copyright (c) 2010 José Luis Vergara Toloza * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_PAINTER_H_ #define KIS_PAINTER_H_ #include #include #include #include #include "kundo2magicstring.h" #include "kis_types.h" #include #include class QPen; class KUndo2Command; class QRect; class QRectF; class QBitArray; class QPainterPath; class KoAbstractGradient; class KoUpdater; class KoColor; class KoCompositeOp; class KisUndoAdapter; class KisPostExecutionUndoAdapter; class KisTransaction; class KoPattern; class KisPaintInformation; class KisPaintOp; class KisDistanceInformation; +class KisRenderedDab; +class KisRunnableStrokeJobsInterface; /** * KisPainter contains the graphics primitives necessary to draw on a * KisPaintDevice. This is the same kind of abstraction as used in Qt * itself, where you have QPainter and QPaintDevice. * * However, KisPainter works on a tiled image and supports different * color models, and that's a lot more complicated. * * KisPainter supports transactions that can group various paint operations * in one undoable step. * * For more complex operations, you might want to have a look at the subclasses * of KisPainter: KisConvolutionPainter, KisFillPainter and KisGradientPainter * * KisPainter sets a number of default values, like COMPOSITE_OVER for compositeop, * OPACITY_OPAQUE for opacity and no selection for selection. */ class KRITAIMAGE_EXPORT KisPainter { public: /// Construct painter without a device KisPainter(); /// Construct a painter, and begin painting on the device KisPainter(KisPaintDeviceSP device); /// Construct a painter, and begin painting on the device. All actions will be masked by the given selection. KisPainter(KisPaintDeviceSP device, KisSelectionSP selection); virtual ~KisPainter(); public: static void copyAreaOptimized(const QPoint &dstPt, KisPaintDeviceSP src, KisPaintDeviceSP dst, const QRect &originalSrcRect); static void copyAreaOptimizedOldData(const QPoint &dstPt, KisPaintDeviceSP src, KisPaintDeviceSP dst, const QRect &originalSrcRect); static void copyAreaOptimized(const QPoint &dstPt, KisPaintDeviceSP src, KisPaintDeviceSP dst, const QRect &originalSrcRect, KisSelectionSP selection); static KisPaintDeviceSP convertToAlphaAsAlpha(KisPaintDeviceSP src); static KisPaintDeviceSP convertToAlphaAsGray(KisPaintDeviceSP src); static bool checkDeviceHasTransparency(KisPaintDeviceSP dev); /** * Start painting on the specified device. Not undoable. */ void begin(KisPaintDeviceSP device); /** * Start painting on the specified paint device. All actions will be masked by the given selection. */ void begin(KisPaintDeviceSP device, KisSelectionSP selection); /** * Finish painting on the current device */ void end(); /** * If set, the painter action is cancelable, if the action supports that. */ void setProgress(KoUpdater * progressUpdater); /// Begin an undoable paint operation void beginTransaction(const KUndo2MagicString& transactionName = KUndo2MagicString(),int timedID = -1); /// Cancel all the changes made by the painter void revertTransaction(); /// Finish the undoable paint operation void endTransaction(KisUndoAdapter *undoAdapter); /** * Finish transaction and load it to a special adapter for strokes */ void endTransaction(KisPostExecutionUndoAdapter *undoAdapter); /** * Finishes a transaction and returns a pointer to its undo command */ KUndo2Command* endAndTakeTransaction(); /** * Finish the transaction and delete it's undo information. * NOTE: Be careful, because all the previous transactions * will become non-undoable after execution of this method. */ void deleteTransaction(); /// continue a transaction started somewhere else void putTransaction(KisTransaction* transaction); /// take transaction out of the reach of KisPainter KisTransaction* takeTransaction(); /// Returns the current paint device. const KisPaintDeviceSP device() const; KisPaintDeviceSP device(); /** * Blast a region of srcWidth @param srcWidth and srcHeight @param srcHeight from @param * srcDev onto the current paint device. @param srcX and @param srcY set the x and y * positions of the origin top-left corner, @param dstX and @param dstY those of * the destination. * Any pixel read outside the limits of @param srcDev will return the * default pixel, this is a property of \ref KisPaintDevice. * * @param dstX the destination x-coordinate * @param dstY the destination y-coordinate * @param srcDev the source device * @param srcX the source x-coordinate * @param srcY the source y-coordinate * @param srcWidth the width of the region to be manipulated * @param srcHeight the height of the region to be manipulated */ void bitBlt(qint32 dstX, qint32 dstY, const KisPaintDeviceSP srcDev, qint32 srcX, qint32 srcY, qint32 srcWidth, qint32 srcHeight); /** * Convenience method that uses QPoint and QRect. * * @param pos the destination coordinate, it replaces @param dstX and @param dstY. * @param srcDev the source device. * @param srcRect the rectangle describing the area to blast from @param srcDev into the current paint device. * @param srcRect replaces @param srcX, @param srcY, @param srcWidth and @param srcHeight. * */ void bitBlt(const QPoint & pos, const KisPaintDeviceSP srcDev, const QRect & srcRect); /** * The same as @ref bitBlt() but reads data from oldData() part of the device * * @param dstX the destination x-coordinate * @param dstY the destination y-coordinate * @param srcDev the source device * @param srcX the source x-coordinate * @param srcY the source y-coordinate * @param srcWidth the width of the region to be manipulated * @param srcHeight the height of the region to be manipulated */ void bitBltOldData(qint32 dstX, qint32 dstY, const KisPaintDeviceSP srcDev, qint32 srcX, qint32 srcY, qint32 srcWidth, qint32 srcHeight); /** * Convenience method that uses QPoint and QRect. * * @param pos the destination coordinate, it replaces @param dstX and @param dstY. * @param srcDev the source device. * @param srcRect the rectangle describing the area to blast from @param srcDev into the current paint device. * @param srcRect replaces @param srcX, @param srcY, @param srcWidth and @param srcHeight. * */ void bitBltOldData(const QPoint & pos, const KisPaintDeviceSP srcDev, const QRect & srcRect); /** * Blasts a @param selection of srcWidth @param srcWidth and srcHeight @param srcHeight * of @param srcDev on the current paint device. There is parameters * to control where the area begins in each distinct device, explained below. * @param selection can be used as a mask to shape @param srcDev to * something interesting in the same step it is rendered to the current * paint device. @param selection 's colorspace must be alpha8 (the * colorspace for selections/transparency), the rectangle formed by * @param selX, @param selY, @param srcWidth and @param srcHeight must not go * beyond its limits, and they must be different from zero. * @param selection and KisPainter's selection (the user selection) are * fused together through the composite operation COMPOSITE_MULT. * Any pixel read outside the limits of @param srcDev will return the * default pixel, this is a property of \ref KisPaintDevice. * * @param dstX the destination x-coordinate * @param dstY the destination y-coordinate * @param srcDev the source device * @param selection the custom selection to apply on the source device * @param selX the selection x-coordinate * @param selY the selection y-coordinate * @param srcX the source x-coordinate * @param srcY the source y-coordinate * @param srcWidth the width of the region to be manipulated * @param srcHeight the height of the region to be manipulated * */ void bitBltWithFixedSelection(qint32 dstX, qint32 dstY, const KisPaintDeviceSP srcDev, const KisFixedPaintDeviceSP selection, qint32 selX, qint32 selY, qint32 srcX, qint32 srcY, qint32 srcWidth, qint32 srcHeight); /** * Convenience method that assumes @param selX, @param selY, @param srcX and @param srcY are * equal to 0. Best used when @param selection and the desired area of @param srcDev have exactly * the same dimensions and are specially made for each other. * * @param dstX the destination x-coordinate * @param dstY the destination y-coordinate * @param srcDev the source device * @param selection the custom selection to apply on the source device * @param srcWidth the width of the region to be manipulated * @param srcHeight the height of the region to be manipulated */ void bitBltWithFixedSelection(qint32 dstX, qint32 dstY, const KisPaintDeviceSP srcDev, const KisFixedPaintDeviceSP selection, qint32 srcWidth, qint32 srcHeight); /** * Blast a region of srcWidth @param srcWidth and srcHeight @param srcHeight from @param srcDev onto the current * paint device. @param srcX and @param srcY set the x and y positions of the * origin top-left corner, @param dstX and @param dstY those of the destination. * @param srcDev is a \ref KisFixedPaintDevice: this means that @param srcDev must have the same * colorspace as the destination device. * * @param dstX the destination x-coordinate * @param dstY the destination y-coordinate * @param srcDev the source device * @param srcX the source x-coordinate * @param srcY the source y-coordinate * @param srcWidth the width of the region to be manipulated * @param srcHeight the height of the region to be manipulated */ void bltFixed(qint32 dstX, qint32 dstY, const KisFixedPaintDeviceSP srcDev, qint32 srcX, qint32 srcY, qint32 srcWidth, qint32 srcHeight); + + /** + * Render the area \p rc from \p srcDevices on the destination device. + * If \p rc doesn't cross the device's rect, then the device is not + * rendered at all. + */ + void bltFixed(const QRect &rc, const QList allSrcDevices); + /** * Convenience method that uses QPoint and QRect. * * @param pos the destination coordinate, it replaces @param dstX and @param dstY. * @param srcDev the source device. * @param srcRect the rectangle describing the area to blast from @param srcDev into the current paint device. * @param srcRect replaces @param srcX, @param srcY, @param srcWidth and @param srcHeight. * */ void bltFixed(const QPoint & pos, const KisFixedPaintDeviceSP srcDev, const QRect & srcRect); /** * Blasts a @param selection of srcWidth @param srcWidth and srcHeight @param srcHeight * of @param srcDev on the current paint device. There is parameters to control * the top-left corner of the area in each respective paint device (@param dstX, * @param dstY, @param srcX, @param srcY). * @param selection can be used as a mask to shape @param srcDev to something * interesting in the same step it is rendered to the current paint device. * @param srcDev is a \ref KisFixedPaintDevice: this means that @param srcDev * must have the same colorspace as the destination device. * @param selection 's colorspace must be alpha8 (the colorspace for * selections/transparency). * The rectangle formed by the respective top-left coordinates of each device * and @param srcWidth and @param srcHeight must not go beyond their limits, and * they must be different from zero. * @param selection and KisPainter's selection (the user selection) are * fused together through the composite operation COMPOSITE_MULT. * * @param dstX the destination x-coordinate * @param dstY the destination y-coordinate * @param srcDev the source device * @param selection the selection stored in fixed device * @param selX the selection x-coordinate * @param selY the selection y-coordinate * @param srcX the source x-coordinate * @param srcY the source y-coordinate * @param srcWidth the width of the region to be manipulated * @param srcHeight the height of the region to be manipulated */ void bltFixedWithFixedSelection(qint32 dstX, qint32 dstY, const KisFixedPaintDeviceSP srcDev, const KisFixedPaintDeviceSP selection, qint32 selX, qint32 selY, qint32 srcX, qint32 srcY, quint32 srcWidth, quint32 srcHeight); /** * Convenience method that assumes @param selX, @param selY, @param srcX and @param srcY are * equal to 0. Best used when @param selection and @param srcDev have exactly the same * dimensions and are specially made for each other. * * @param dstX the destination x-coordinate * @param dstY the destination y-coordinate * @param srcDev the source device * @param selection the custom selection to apply on the source device * @param srcWidth the width of the region to be manipulated * @param srcHeight the height of the region to be manipulated */ void bltFixedWithFixedSelection(qint32 dstX, qint32 dstY, const KisFixedPaintDeviceSP srcDev, const KisFixedPaintDeviceSP selection, quint32 srcWidth, quint32 srcHeight); /** * fills a region of width @param width and height @param height of the current * paint device with the color @param color. @param x and @param y set the x and y positions of the * origin top-left corner. * * @param x the destination x-coordinate * @param y the destination y-coordinate * @param width the width of the region to be manipulated * @param height the height of the region to be manipulated * @param color the color the area is filled with */ void fill(qint32 x, qint32 y, qint32 width, qint32 height, const KoColor& color); /** * First you need to setup the painter with setMirrorInformation, * then these set of methods provide way to render the devices mirrored * according the axesCenter vertically or horizontally or both. * * @param rc rectangle area covered by dab * @param dab this device will be mirrored in-place, it means that it will be changed */ void renderMirrorMask(QRect rc, KisFixedPaintDeviceSP dab); void renderMirrorMask(QRect rc, KisFixedPaintDeviceSP dab, KisFixedPaintDeviceSP mask); void renderMirrorMask(QRect rc, KisPaintDeviceSP dab); void renderMirrorMask(QRect rc, KisPaintDeviceSP dab, int sx, int sy, KisFixedPaintDeviceSP mask); /** * Convenience method for renderMirrorMask(), allows to choose whether * we need to preserve out dab or do the transformations in-place. * * @param rc rectangle area covered by dab * @param dab the device to render * @param preserveDab states whether a temporary device should be * created to do the transformations */ void renderMirrorMaskSafe(QRect rc, KisFixedPaintDeviceSP dab, bool preserveDab); /** * Convenience method for renderMirrorMask(), allows to choose whether * we need to preserve our fixed mask or do the transformations in-place. * * @param rc rectangle area covered by dab * @param dab the device to render * @param mask mask to use for rendering * @param preserveMask states whether a temporary device should be * created to do the transformations */ void renderMirrorMaskSafe(QRect rc, KisPaintDeviceSP dab, int sx, int sy, KisFixedPaintDeviceSP mask, bool preserveMask); /** * A complex method that re-renders a dab on an \p rc area. * The \p rc area and all the dedicated mirroring areas are cleared * before the painting, so this method should be used by paintops * which do not update the canvas incrementally, but instead * regenerate some internal cache \p dab with the COMPOSITE_COPY op. * * \see KisExperimentPaintOp */ void renderDabWithMirroringNonIncremental(QRect rc, KisPaintDeviceSP dab); /** * The methods in this class do not tell the paintdevice to update, but they calculate the * dirty area. This method returns this dirty area and resets it. */ QVector takeDirtyRegion(); /** * Paint a line that connects the dots in points */ void paintPolyline(const QVector &points, int index = 0, int numPoints = -1); /** * Draw a line between pos1 and pos2 using the currently set brush and color. * If savedDist is less than zero, the brush is painted at pos1 before being * painted along the line using the spacing setting. * @return the drag distance, that is the remains of the distance between p1 and p2 not covered * because the currenlty set brush has a spacing greater than that distance. */ void paintLine(const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *curentDistance); /** * Draw a Bezier curve between pos1 and pos2 using control points 1 and 2. * If savedDist is less than zero, the brush is painted at pos1 before being * painted along the curve using the spacing setting. * @return the drag distance, that is the remains of the distance between p1 and p2 not covered * because the currenlty set brush has a spacing greater than that distance. */ void paintBezierCurve(const KisPaintInformation &pi1, const QPointF &control1, const QPointF &control2, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance); /** * Fill the given vector points with the points needed to draw the Bezier curve between * pos1 and pos2 using control points 1 and 2, excluding the final pos2. */ void getBezierCurvePoints(const QPointF &pos1, const QPointF &control1, const QPointF &control2, const QPointF &pos2, vQPointF& points) const; /** * Paint a rectangle. * @param rect the rectangle to paint. */ void paintRect(const QRectF &rect); /** * Paint a rectangle. * * @param x x coordinate of the top-left corner * @param y y coordinate of the top-left corner * @param w the rectangle width * @param h the rectangle height */ void paintRect(const qreal x, const qreal y, const qreal w, const qreal h); /** * Paint the ellipse that fills the given rectangle. * * @param rect the rectangle containing the ellipse to paint. */ void paintEllipse(const QRectF &rect); /** * Paint the ellipse that fills the given rectangle. * * @param x x coordinate of the top-left corner * @param y y coordinate of the top-left corner * @param w the rectangle width * @param h the rectangle height */ void paintEllipse(const qreal x, const qreal y, const qreal w, const qreal h); /** * Paint the polygon with the points given in points. It automatically closes the polygon * by drawing the line from the last point to the first. */ void paintPolygon(const vQPointF& points); /** Draw a spot at pos using the currently set paint op, brush and color */ void paintAt(const KisPaintInformation &pos, KisDistanceInformation *savedDist); /** * Stroke the given QPainterPath. */ void paintPainterPath(const QPainterPath& path); /** * Fills the area enclosed by the given QPainterPath * Convenience method for fillPainterPath(path, rect) */ void fillPainterPath(const QPainterPath& path); /** * Fills the portion of an area enclosed by the given QPainterPath * * \param rect the portion of the path to fill */ void fillPainterPath(const QPainterPath& path, const QRect &requestedRect); /** * Draw the path using the Pen * * if \p requestedRect is null, the entire path is painted */ void drawPainterPath(const QPainterPath& path, const QPen& pen, const QRect &requestedRect); // convenience overload void drawPainterPath(const QPainterPath& path, const QPen& pen); /** * paint an unstroked one-pixel wide line from specified start position to the * specified end position. * */ void drawLine(const QPointF & start, const QPointF & end); /** * paint an unstroked line with thickness from specified start position to the * specified end position. Scanline algorithm is used. */ void drawLine(const QPointF &start, const QPointF &end, qreal width, bool antialias); /** * paints an unstroked, aliased one-pixel line using the DDA algorithm from specified start position to the * specified end position. * */ void drawDDALine(const QPointF & start, const QPointF & end); /** * Paint an unstroked, wobbly one-pixel wide line from the specified start to the specified * end position. * */ void drawWobblyLine(const QPointF & start, const QPointF & end); /** * Paint an unstroked, anti-aliased one-pixel wide line from the specified start to the specified * end position using the Wu algorithm */ void drawWuLine(const QPointF & start, const QPointF & end); /** * Paint an unstroked wide line from the specified start to the specified * end position with width varying from @param w1 at the start to @param w2 at * the end. * * XXX: the width should be set in doubles, not integers. */ void drawThickLine(const QPointF & start, const QPointF & end, int startWidth, int endWidth); /** * Set the channelflags: a bit array where true means that the * channel corresponding in position with the bit will be read * by the operation, and false means that it will not be affected. * * An empty channelFlags parameter means that all channels are * affected. * * @param the bit array that masks the source channels; only * the channels where the corresponding bit is true will will be * composited onto the destination device. */ void setChannelFlags(QBitArray channelFlags); /// @return the channel flags QBitArray channelFlags(); /** * Set the paintop preset to use. If @param image is given, * the paintop will be created using this image as parameter. * Some paintops really want to know about the image they work * for, e.g. the clone paintop. */ void setPaintOpPreset(KisPaintOpPresetSP preset, KisNodeSP node, KisImageSP image); /// Return the paintop preset KisPaintOpPresetSP preset() const; /** * Return the active paintop (which is created based on the specified preset and * will be deleted as soon as the KisPainter instance dies). */ KisPaintOp* paintOp() const; void setMirrorInformation(const QPointF &axesCenter, bool mirrorHorizontally, bool mirrorVertically); - /** - * copy the mirror information to other painter - */ - void copyMirrorInformation(KisPainter * painter); - /** * Returns whether the mirroring methods will do any * work when called */ bool hasMirroring() const; + /** + * Indicates if horizontal mirroring mode is activated + */ + bool hasHorizontalMirroring() const; + + /** + * Indicates if vertical mirroring mode is activated + */ + bool hasVerticalMirroring() const; + + /** + * Mirror \p rc in the requested \p direction around the center point defined + * in the painter. + */ + void mirrorRect(Qt::Orientation direction, QRect *rc) const; + + /** + * Mirror \p dab in the requested direction around the center point defined + * in the painter. The dab's offset is adjusted automatically. + */ + void mirrorDab(Qt::Orientation direction, KisRenderedDab *dab) const; + /// Set the current pattern void setPattern(const KoPattern * pattern); /// Returns the currently set pattern const KoPattern * pattern() const; /** * Set the color that will be used to paint with, and convert it * to the color space of the current paint device. */ void setPaintColor(const KoColor& color); /// Returns the color that will be used to paint with const KoColor &paintColor() const; /** * Set the current background color, and convert it * to the color space of the current paint device. */ void setBackgroundColor(const KoColor& color); /// Returns the current background color const KoColor &backgroundColor() const; /// Set the current generator (a generator can be used to fill an area void setGenerator(KisFilterConfigurationSP generator); /// @return the current generator configuration const KisFilterConfigurationSP generator() const; /// This enum contains the styles with which we can fill things like polygons and ellipses enum FillStyle { FillStyleNone, FillStyleForegroundColor, FillStyleBackgroundColor, FillStylePattern, FillStyleGradient, FillStyleStrokes, FillStyleGenerator, }; /// Set the current style with which to fill void setFillStyle(FillStyle fillStyle); /// Returns the current fill style FillStyle fillStyle() const; /// Set whether a polygon's filled area should be anti-aliased or not. The default is true. void setAntiAliasPolygonFill(bool antiAliasPolygonFill); /// Return whether a polygon's filled area should be anti-aliased or not bool antiAliasPolygonFill(); /// The style of the brush stroke around polygons and so enum StrokeStyle { StrokeStyleNone, StrokeStyleBrush }; /// Set the current brush stroke style void setStrokeStyle(StrokeStyle strokeStyle); /// Returns the current brush stroke style StrokeStyle strokeStyle() const; void setFlow(quint8 flow); quint8 flow() const; /** * Sets the opacity of the painting and recalculates the * mean opacity of the stroke. This mean value is used to * make ALPHA_DARKEN painting look correct */ void setOpacityUpdateAverage(quint8 opacity); + /** + * Sets average opacity, that is used to make ALPHA_DARKEN painting look correct + */ + void setAverageOpacity(qreal averageOpacity); + + /** + * Calculate average opacity value after painting a single dab with \p opacity + */ + static qreal blendAverageOpacity(qreal opacity, qreal averageOpacity); + /// Set the opacity which is used in painting (like filling polygons) void setOpacity(quint8 opacity); /// Returns the opacity that is used in painting quint8 opacity() const; /// Set the composite op for this painter void setCompositeOp(const KoCompositeOp * op); const KoCompositeOp * compositeOp(); /// Set the composite op for this painter by string. /// Note: the colorspace must be set previously! void setCompositeOp(const QString& op); /** * Add the r to the current dirty rect. */ void addDirtyRect(const QRect & r); /** * Reset the selection to the given selection. All painter actions will be * masked by the specified selection. */ void setSelection(KisSelectionSP selection); /** * @return the selection set on this painter. */ KisSelectionSP selection(); void setGradient(const KoAbstractGradient* gradient); const KoAbstractGradient* gradient() const; /** * Set the size of the tile in fillPainterPath, useful when optimizing the use of fillPainterPath * e.g. Spray paintop uses more small tiles, although selections uses bigger tiles. QImage::fill * is quite expensive so with smaller images you can save instructions * Default and maximum size is 256x256 image */ void setMaskImageSize(qint32 width, qint32 height); // /** // * If the alpha channel is locked, the alpha values of the paint device we are painting on // * will not change. // */ // void setLockAlpha(bool protect); // bool alphaLocked() const; /** * set the rendering intent in case pixels need to be converted before painting */ void setRenderingIntent(KoColorConversionTransformation::Intent intent); /** * set the conversion flags in case pixels need to be converted before painting */ void setColorConversionFlags(KoColorConversionTransformation::ConversionFlags conversionFlags); + /** + * Set interface for running asynchronous jobs by paintops. + * + * NOTE: the painter does *not* own the interface device. It is the responsibility + * of the caller to ensure that the interface object is alive during the lifetime + * of the painter. + */ + void setRunnableStrokeJobsInterface(KisRunnableStrokeJobsInterface *interface); + + /** + * Get the interface for running asynchronous jobs. It is used by paintops mostly. + */ + KisRunnableStrokeJobsInterface* runnableStrokeJobsInterface() const; + protected: /// Initialize, set everything to '0' or defaults void init(); /// Fill the polygon defined by points with the fillStyle void fillPolygon(const vQPointF& points, FillStyle fillStyle); private: KisPainter(const KisPainter&); KisPainter& operator=(const KisPainter&); float frac(float value) { float tmp = 0; return modff(value , &tmp); } float invertFrac(float value) { float tmp = 0; return 1.0f - modff(value , &tmp); } protected: KoUpdater * progressUpdater(); private: template void bitBltImpl(qint32 dstX, qint32 dstY, const KisPaintDeviceSP srcDev, qint32 srcX, qint32 srcY, qint32 srcWidth, qint32 srcHeight); inline void compositeOnePixel(quint8 *dst, const KoColor &color); private: struct Private; Private* const d; }; #endif // KIS_PAINTER_H_ diff --git a/libs/image/kis_painter_blt_multi_fixed.cpp b/libs/image/kis_painter_blt_multi_fixed.cpp new file mode 100644 index 0000000000..5db6f4d43a --- /dev/null +++ b/libs/image/kis_painter_blt_multi_fixed.cpp @@ -0,0 +1,206 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kis_painter.h" +#include "kis_painter_p.h" + +#include "kis_paint_device.h" +#include "kis_fixed_paint_device.h" +#include "kis_random_accessor_ng.h" +#include "KisRenderedDab.h" + +void KisPainter::Private::applyDevice(const QRect &applyRect, + const KisRenderedDab &dab, + KisRandomAccessorSP dstIt, + const KoColorSpace *srcColorSpace, + KoCompositeOp::ParameterInfo &localParamInfo) +{ + const QRect dabRect = dab.realBounds(); + const QRect rc = applyRect & dabRect; + + const int srcPixelSize = srcColorSpace->pixelSize(); + const int dabRowStride = srcPixelSize * dabRect.width(); + + + qint32 dstY = rc.y(); + qint32 rowsRemaining = rc.height(); + + while (rowsRemaining > 0) { + qint32 dstX = rc.x(); + + qint32 numContiguousDstRows = dstIt->numContiguousRows(dstY); + qint32 rows = qMin(rowsRemaining, numContiguousDstRows); + + qint32 columnsRemaining = rc.width(); + + while (columnsRemaining > 0) { + + qint32 numContiguousDstColumns = dstIt->numContiguousColumns(dstX); + qint32 columns = qMin(numContiguousDstColumns, columnsRemaining); + + qint32 dstRowStride = dstIt->rowStride(dstX, dstY); + dstIt->moveTo(dstX, dstY); + + localParamInfo.dstRowStart = dstIt->rawData(); + localParamInfo.dstRowStride = dstRowStride; + localParamInfo.maskRowStart = 0; + localParamInfo.maskRowStride = 0; + localParamInfo.rows = rows; + localParamInfo.cols = columns; + + + const int dabX = dstX - dabRect.x(); + const int dabY = dstY - dabRect.y(); + + localParamInfo.srcRowStart = dab.device->constData() + dabX * pixelSize + dabY * dabRowStride; + localParamInfo.srcRowStride = dabRowStride; + localParamInfo.setOpacityAndAverage(dab.opacity, dab.averageOpacity); + localParamInfo.flow = dab.flow; + colorSpace->bitBlt(srcColorSpace, localParamInfo, compositeOp, renderingIntent, conversionFlags); + + dstX += columns; + columnsRemaining -= columns; + } + + dstY += rows; + rowsRemaining -= rows; + } + +} + +void KisPainter::Private::applyDeviceWithSelection(const QRect &applyRect, + const KisRenderedDab &dab, + KisRandomAccessorSP dstIt, + KisRandomConstAccessorSP maskIt, + const KoColorSpace *srcColorSpace, + KoCompositeOp::ParameterInfo &localParamInfo) +{ + const QRect dabRect = dab.realBounds(); + const QRect rc = applyRect & dabRect; + + const int srcPixelSize = srcColorSpace->pixelSize(); + const int dabRowStride = srcPixelSize * dabRect.width(); + + + qint32 dstY = rc.y(); + qint32 rowsRemaining = rc.height(); + + while (rowsRemaining > 0) { + qint32 dstX = rc.x(); + + qint32 numContiguousDstRows = dstIt->numContiguousRows(dstY); + qint32 numContiguousMaskRows = maskIt->numContiguousRows(dstY); + qint32 rows = qMin(rowsRemaining, qMin(numContiguousDstRows, numContiguousMaskRows)); + + qint32 columnsRemaining = rc.width(); + + while (columnsRemaining > 0) { + + qint32 numContiguousDstColumns = dstIt->numContiguousColumns(dstX); + qint32 numContiguousMaskColumns = maskIt->numContiguousColumns(dstX); + qint32 columns = qMin(columnsRemaining, qMin(numContiguousDstColumns, numContiguousMaskColumns)); + + qint32 dstRowStride = dstIt->rowStride(dstX, dstY); + qint32 maskRowStride = maskIt->rowStride(dstX, dstY); + dstIt->moveTo(dstX, dstY); + maskIt->moveTo(dstX, dstY); + + localParamInfo.dstRowStart = dstIt->rawData(); + localParamInfo.dstRowStride = dstRowStride; + localParamInfo.maskRowStart = maskIt->rawDataConst(); + localParamInfo.maskRowStride = maskRowStride; + localParamInfo.rows = rows; + localParamInfo.cols = columns; + + + const int dabX = dstX - dabRect.x(); + const int dabY = dstY - dabRect.y(); + + localParamInfo.srcRowStart = dab.device->constData() + dabX * pixelSize + dabY * dabRowStride; + localParamInfo.srcRowStride = dabRowStride; + localParamInfo.setOpacityAndAverage(dab.opacity, dab.averageOpacity); + localParamInfo.flow = dab.flow; + colorSpace->bitBlt(srcColorSpace, localParamInfo, compositeOp, renderingIntent, conversionFlags); + + dstX += columns; + columnsRemaining -= columns; + } + + dstY += rows; + rowsRemaining -= rows; + } + +} + +void KisPainter::bltFixed(const QRect &applyRect, const QList allSrcDevices) +{ + const KoColorSpace *srcColorSpace = 0; + QList devices; + QRect rc = applyRect; + + if (d->selection) { + rc &= d->selection->selectedRect(); + } + + QRect totalDevicesRect; + + Q_FOREACH (const KisRenderedDab &dab, allSrcDevices) { + if (rc.intersects(dab.realBounds())) { + devices.append(dab); + totalDevicesRect |= dab.realBounds(); + } + + if (!srcColorSpace) { + srcColorSpace = dab.device->colorSpace(); + } else { + KIS_SAFE_ASSERT_RECOVER_RETURN(*srcColorSpace == *dab.device->colorSpace()); + } + } + + rc &= totalDevicesRect; + + if (devices.isEmpty() || rc.isEmpty()) return; + + KoCompositeOp::ParameterInfo localParamInfo = d->paramInfo; + KisRandomAccessorSP dstIt = d->device->createRandomAccessorNG(rc.left(), rc.top()); + KisRandomConstAccessorSP maskIt = d->selection ? d->selection->projection()->createRandomConstAccessorNG(rc.left(), rc.top()) : 0; + + if (maskIt) { + Q_FOREACH (const KisRenderedDab &dab, devices) { + d->applyDeviceWithSelection(rc, dab, dstIt, maskIt, srcColorSpace, localParamInfo); + } + } else { + Q_FOREACH (const KisRenderedDab &dab, devices) { + d->applyDevice(rc, dab, dstIt, srcColorSpace, localParamInfo); + } + } + + +#if 0 + // the code above does basically the same thing as this one, + // but more efficiently :) + + Q_FOREACH (KisFixedPaintDeviceSP dev, devices) { + const QRect copyRect = dev->bounds() & rc; + if (copyRect.isEmpty()) continue; + + bltFixed(copyRect.topLeft(), dev, copyRect); + } +#endif +} + diff --git a/libs/image/kis_painter_p.h b/libs/image/kis_painter_p.h new file mode 100644 index 0000000000..fd150fdb5e --- /dev/null +++ b/libs/image/kis_painter_p.h @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KISPAINTERPRIVATE_H +#define KISPAINTERPRIVATE_H + +#include +#include +#include +#include +#include "kis_paintop.h" +#include "kis_selection.h" +#include "kis_fill_painter.h" +#include "kis_painter.h" +#include "kis_paintop_preset.h" +#include + +struct Q_DECL_HIDDEN KisPainter::Private { + Private(KisPainter *_q) : q(_q) {} + Private(KisPainter *_q, const KoColorSpace *cs) + : q(_q), paintColor(cs), backgroundColor(cs) {} + + KisPainter *q; + + KisPaintDeviceSP device; + KisSelectionSP selection; + KisTransaction* transaction; + KoUpdater* progressUpdater; + + QVector dirtyRects; + KisPaintOp* paintOp; + KoColor paintColor; + KoColor backgroundColor; + KoColor customColor; + KisFilterConfigurationSP generator; + KisPaintLayer* sourceLayer; + FillStyle fillStyle; + StrokeStyle strokeStyle; + bool antiAliasPolygonFill; + const KoPattern* pattern; + QPointF duplicateOffset; + quint32 pixelSize; + const KoColorSpace* colorSpace; + KoColorProfile* profile; + const KoCompositeOp* compositeOp; + const KoAbstractGradient* gradient; + KisPaintOpPresetSP paintOpPreset; + QImage polygonMaskImage; + QPainter* maskPainter; + KisFillPainter* fillPainter; + KisPaintDeviceSP polygon; + qint32 maskImageWidth; + qint32 maskImageHeight; + QPointF axesCenter; + bool mirrorHorizontally; + bool mirrorVertically; + bool isOpacityUnit; // TODO: move into ParameterInfo + KoCompositeOp::ParameterInfo paramInfo; + KoColorConversionTransformation::Intent renderingIntent; + KoColorConversionTransformation::ConversionFlags conversionFlags; + KisRunnableStrokeJobsInterface *runnableStrokeJobsInterface = 0; + QScopedPointer fakeRunnableStrokeJobsInterface; + + bool tryReduceSourceRect(const KisPaintDevice *srcDev, + QRect *srcRect, + qint32 *srcX, + qint32 *srcY, + qint32 *srcWidth, + qint32 *srcHeight, + qint32 *dstX, + qint32 *dstY); + + void fillPainterPathImpl(const QPainterPath& path, const QRect &requestedRect); + + void applyDevice(const QRect &applyRect, + const KisRenderedDab &dab, + KisRandomAccessorSP dstIt, + const KoColorSpace *srcColorSpace, + KoCompositeOp::ParameterInfo &localParamInfo); + + void applyDeviceWithSelection(const QRect &applyRect, + const KisRenderedDab &dab, + KisRandomAccessorSP dstIt, + KisRandomConstAccessorSP maskIt, + const KoColorSpace *srcColorSpace, + KoCompositeOp::ParameterInfo &localParamInfo); + +}; + +#endif // KISPAINTERPRIVATE_H diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/image/kis_projection_updates_filter.cpp index e1493ec367..3e3fc6be59 100644 --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/image/kis_projection_updates_filter.cpp @@ -1,36 +1,36 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_projection_updates_filter.h" #include #include KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() { } -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) +bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QVector &rects, bool resetAnimationCache) { Q_UNUSED(image); Q_UNUSED(node); - Q_UNUSED(rect); + Q_UNUSED(rects); Q_UNUSED(resetAnimationCache); return true; } diff --git a/libs/image/kis_projection_updates_filter.h b/libs/image/kis_projection_updates_filter.h index 9910cc0355..13054670b9 100644 --- a/libs/image/kis_projection_updates_filter.h +++ b/libs/image/kis_projection_updates_filter.h @@ -1,50 +1,50 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_PROJECTION_UPDATES_FILTER_H #define __KIS_PROJECTION_UPDATES_FILTER_H #include class KisImage; class KisNode; class QRect; class KisProjectionUpdatesFilter { public: virtual ~KisProjectionUpdatesFilter(); /** * \return true if an update should be dropped by the image */ - virtual bool filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) = 0; + virtual bool filter(KisImage *image, KisNode *node, const QVector &rects, bool resetAnimationCache) = 0; }; /** * A dummy filter implementation that eats all the updates */ class KisDropAllProjectionUpdatesFilter : public KisProjectionUpdatesFilter { public: - bool filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) override; + bool filter(KisImage *image, KisNode *node, const QVector &rects, bool resetAnimationCache) override; }; #endif /* __KIS_PROJECTION_UPDATES_FILTER_H */ diff --git a/libs/image/kis_simple_update_queue.cpp b/libs/image/kis_simple_update_queue.cpp index 7316a8d48d..81c5509814 100644 --- a/libs/image/kis_simple_update_queue.cpp +++ b/libs/image/kis_simple_update_queue.cpp @@ -1,375 +1,397 @@ /* * Copyright (c) 2010 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_simple_update_queue.h" #include +#include #include "kis_image_config.h" #include "kis_full_refresh_walker.h" #include "kis_spontaneous_job.h" //#define ENABLE_DEBUG_JOIN //#define ENABLE_ACCUMULATOR #ifdef ENABLE_DEBUG_JOIN #define DEBUG_JOIN(baseRect, newRect, alpha) \ dbgKrita << "Two rects were joined:\t" \ << (baseRect) << "+" << (newRect) << "->" \ << ((baseRect) | (newRect)) << "(" << alpha << ")" #else #define DEBUG_JOIN(baseRect, newRect, alpha) #endif /* ENABLE_DEBUG_JOIN */ #ifdef ENABLE_ACCUMULATOR #define DECLARE_ACCUMULATOR() static qreal _baseAmount=0, _newAmount=0 #define ACCUMULATOR_ADD(baseAmount, newAmount) \ do {_baseAmount += baseAmount; _newAmount += newAmount;} while (0) #define ACCUMULATOR_DEBUG() \ dbgKrita << "Accumulated alpha:" << _newAmount / _baseAmount #else #define DECLARE_ACCUMULATOR() #define ACCUMULATOR_ADD(baseAmount, newAmount) #define ACCUMULATOR_DEBUG() #endif /* ENABLE_ACCUMULATOR */ KisSimpleUpdateQueue::KisSimpleUpdateQueue() : m_overrideLevelOfDetail(-1) { updateSettings(); } KisSimpleUpdateQueue::~KisSimpleUpdateQueue() { QMutexLocker locker(&m_lock); while (!m_spontaneousJobsList.isEmpty()) { delete m_spontaneousJobsList.takeLast(); } } void KisSimpleUpdateQueue::updateSettings() { QMutexLocker locker(&m_lock); KisImageConfig config; m_patchWidth = config.updatePatchWidth(); m_patchHeight = config.updatePatchHeight(); m_maxCollectAlpha = config.maxCollectAlpha(); m_maxMergeAlpha = config.maxMergeAlpha(); m_maxMergeCollectAlpha = config.maxMergeCollectAlpha(); } int KisSimpleUpdateQueue::overrideLevelOfDetail() const { return m_overrideLevelOfDetail; } void KisSimpleUpdateQueue::processQueue(KisUpdaterContext &updaterContext) { updaterContext.lock(); while(updaterContext.hasSpareThread() && processOneJob(updaterContext)); updaterContext.unlock(); } bool KisSimpleUpdateQueue::processOneJob(KisUpdaterContext &updaterContext) { QMutexLocker locker(&m_lock); KisBaseRectsWalkerSP item; KisMutableWalkersListIterator iter(m_updatesList); bool jobAdded = false; int currentLevelOfDetail = updaterContext.currentLevelOfDetail(); while(iter.hasNext()) { item = iter.next(); if ((currentLevelOfDetail < 0 || currentLevelOfDetail == item->levelOfDetail()) && !item->checksumValid()) { m_overrideLevelOfDetail = item->levelOfDetail(); item->recalculate(item->requestedRect()); m_overrideLevelOfDetail = -1; } if ((currentLevelOfDetail < 0 || currentLevelOfDetail == item->levelOfDetail()) && updaterContext.isJobAllowed(item)) { updaterContext.addMergeJob(item); iter.remove(); jobAdded = true; break; } } if (jobAdded) return true; if (!m_spontaneousJobsList.isEmpty()) { /** * WARNING: Please note that this still doesn't guarantee that * the spontaneous jobs are exclusive, since updates and/or * strokes can be added after them. The only thing it * guarantees that two spontaneous jobs will not be executed * in parallel. * * Right now it works as it is. Probably will need to be fixed * in the future. */ qint32 numMergeJobs; qint32 numStrokeJobs; updaterContext.getJobsSnapshot(numMergeJobs, numStrokeJobs); if (!numMergeJobs && !numStrokeJobs) { KisSpontaneousJob *job = m_spontaneousJobsList.takeFirst(); updaterContext.addSpontaneousJob(job); jobAdded = true; } } return jobAdded; } -void KisSimpleUpdateQueue::addUpdateJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail) +void KisSimpleUpdateQueue::addUpdateJob(KisNodeSP node, const QVector &rects, const QRect& cropRect, int levelOfDetail) { - addJob(node, rc, cropRect, levelOfDetail, KisBaseRectsWalker::UPDATE); + addJob(node, rects, cropRect, levelOfDetail, KisBaseRectsWalker::UPDATE); } +void KisSimpleUpdateQueue::addUpdateJob(KisNodeSP node, const QRect &rc, const QRect& cropRect, int levelOfDetail) +{ + addJob(node, {rc}, cropRect, levelOfDetail, KisBaseRectsWalker::UPDATE); +} + + void KisSimpleUpdateQueue::addUpdateNoFilthyJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail) { - addJob(node, rc, cropRect, levelOfDetail, KisBaseRectsWalker::UPDATE_NO_FILTHY); + addJob(node, {rc}, cropRect, levelOfDetail, KisBaseRectsWalker::UPDATE_NO_FILTHY); } void KisSimpleUpdateQueue::addFullRefreshJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail) { - addJob(node, rc, cropRect, levelOfDetail, KisBaseRectsWalker::FULL_REFRESH); + addJob(node, {rc}, cropRect, levelOfDetail, KisBaseRectsWalker::FULL_REFRESH); } -void KisSimpleUpdateQueue::addJob(KisNodeSP node, const QRect& rc, +void KisSimpleUpdateQueue::addJob(KisNodeSP node, const QVector &rects, const QRect& cropRect, int levelOfDetail, KisBaseRectsWalker::UpdateType type) { - if(trySplitJob(node, rc, cropRect, levelOfDetail, type)) return; - if(tryMergeJob(node, rc, cropRect, levelOfDetail, type)) return; + QList walkers; - KisBaseRectsWalkerSP walker; + Q_FOREACH (const QRect &rc, rects) { + if (rc.isEmpty()) continue; - if (type == KisBaseRectsWalker::UPDATE) { - walker = new KisMergeWalker(cropRect, KisMergeWalker::DEFAULT); - } - else if (type == KisBaseRectsWalker::FULL_REFRESH) { - walker = new KisFullRefreshWalker(cropRect); - } - else if (type == KisBaseRectsWalker::UPDATE_NO_FILTHY) { - walker = new KisMergeWalker(cropRect, KisMergeWalker::NO_FILTHY); - } - /* else if(type == KisBaseRectsWalker::UNSUPPORTED) fatalKrita; */ + KisBaseRectsWalkerSP walker; - walker->collectRects(node, rc); + if(trySplitJob(node, rc, cropRect, levelOfDetail, type)) continue; + if(tryMergeJob(node, rc, cropRect, levelOfDetail, type)) continue; - m_lock.lock(); - m_updatesList.append(walker); - m_lock.unlock(); + if (type == KisBaseRectsWalker::UPDATE) { + walker = new KisMergeWalker(cropRect, KisMergeWalker::DEFAULT); + } + else if (type == KisBaseRectsWalker::FULL_REFRESH) { + walker = new KisFullRefreshWalker(cropRect); + } + else if (type == KisBaseRectsWalker::UPDATE_NO_FILTHY) { + walker = new KisMergeWalker(cropRect, KisMergeWalker::NO_FILTHY); + } + /* else if(type == KisBaseRectsWalker::UNSUPPORTED) fatalKrita; */ + + walker->collectRects(node, rc); + walkers.append(walker); + } + + if (!walkers.isEmpty()) { + m_lock.lock(); + m_updatesList.append(walkers); + m_lock.unlock(); + } } void KisSimpleUpdateQueue::addSpontaneousJob(KisSpontaneousJob *spontaneousJob) { QMutexLocker locker(&m_lock); KisSpontaneousJob *item; KisMutableSpontaneousJobsListIterator iter(m_spontaneousJobsList); iter.toBack(); while(iter.hasPrevious()) { item = iter.previous(); if (spontaneousJob->overrides(item)) { iter.remove(); delete item; } } m_spontaneousJobsList.append(spontaneousJob); } bool KisSimpleUpdateQueue::isEmpty() const { QMutexLocker locker(&m_lock); return m_updatesList.isEmpty() && m_spontaneousJobsList.isEmpty(); } qint32 KisSimpleUpdateQueue::sizeMetric() const { QMutexLocker locker(&m_lock); return m_updatesList.size() + m_spontaneousJobsList.size(); } bool KisSimpleUpdateQueue::trySplitJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail, KisBaseRectsWalker::UpdateType type) { if(rc.width() <= m_patchWidth || rc.height() <= m_patchHeight) return false; // a bit of recursive splitting... qint32 firstCol = rc.x() / m_patchWidth; qint32 firstRow = rc.y() / m_patchHeight; qint32 lastCol = (rc.x() + rc.width()) / m_patchWidth; qint32 lastRow = (rc.y() + rc.height()) / m_patchHeight; + QVector splitRects; + for(qint32 i = firstRow; i <= lastRow; i++) { for(qint32 j = firstCol; j <= lastCol; j++) { QRect maxPatchRect(j * m_patchWidth, i * m_patchHeight, m_patchWidth, m_patchHeight); QRect patchRect = rc & maxPatchRect; - addJob(node, patchRect, cropRect, levelOfDetail, type); + splitRects.append(patchRect); } } + + KIS_SAFE_ASSERT_RECOVER_NOOP(!splitRects.isEmpty()); + addJob(node, splitRects, cropRect, levelOfDetail, type); + return true; } bool KisSimpleUpdateQueue::tryMergeJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail, KisBaseRectsWalker::UpdateType type) { QMutexLocker locker(&m_lock); QRect baseRect = rc; KisBaseRectsWalkerSP goodCandidate; KisBaseRectsWalkerSP item; KisWalkersListIterator iter(m_updatesList); /** * We add new jobs to the tail of the list, * so it's more probable to find a good candidate here. */ iter.toBack(); while(iter.hasPrevious()) { item = iter.previous(); if(item->startNode() != node) continue; if(item->type() != type) continue; if(item->cropRect() != cropRect) continue; if(item->levelOfDetail() != levelOfDetail) continue; if(joinRects(baseRect, item->requestedRect(), m_maxMergeAlpha)) { goodCandidate = item; break; } } if(goodCandidate) collectJobs(goodCandidate, baseRect, m_maxMergeCollectAlpha); return (bool)goodCandidate; } void KisSimpleUpdateQueue::optimize() { QMutexLocker locker(&m_lock); if(m_updatesList.size() <= 1) return; KisBaseRectsWalkerSP baseWalker = m_updatesList.first(); QRect baseRect = baseWalker->requestedRect(); collectJobs(baseWalker, baseRect, m_maxCollectAlpha); } void KisSimpleUpdateQueue::collectJobs(KisBaseRectsWalkerSP &baseWalker, QRect baseRect, const qreal maxAlpha) { KisBaseRectsWalkerSP item; KisMutableWalkersListIterator iter(m_updatesList); while(iter.hasNext()) { item = iter.next(); if(item == baseWalker) continue; if(item->type() != baseWalker->type()) continue; if(item->startNode() != baseWalker->startNode()) continue; if(item->cropRect() != baseWalker->cropRect()) continue; if(item->levelOfDetail() != baseWalker->levelOfDetail()) continue; if(joinRects(baseRect, item->requestedRect(), maxAlpha)) { iter.remove(); } } if(baseWalker->requestedRect() != baseRect) { baseWalker->collectRects(baseWalker->startNode(), baseRect); } } bool KisSimpleUpdateQueue::joinRects(QRect& baseRect, const QRect& newRect, qreal maxAlpha) { QRect unitedRect = baseRect | newRect; if(unitedRect.width() > m_patchWidth || unitedRect.height() > m_patchHeight) return false; bool result = false; qint64 baseWork = baseRect.width() * baseRect.height() + newRect.width() * newRect.height(); qint64 newWork = unitedRect.width() * unitedRect.height(); qreal alpha = qreal(newWork) / baseWork; if(alpha < maxAlpha) { DEBUG_JOIN(baseRect, newRect, alpha); DECLARE_ACCUMULATOR(); ACCUMULATOR_ADD(baseWork, newWork); ACCUMULATOR_DEBUG(); baseRect = unitedRect; result = true; } return result; } KisWalkersList& KisTestableSimpleUpdateQueue::getWalkersList() { return m_updatesList; } KisSpontaneousJobsList& KisTestableSimpleUpdateQueue::getSpontaneousJobsList() { return m_spontaneousJobsList; } diff --git a/libs/image/kis_simple_update_queue.h b/libs/image/kis_simple_update_queue.h index 9f9fb77df7..e51065c53d 100644 --- a/libs/image/kis_simple_update_queue.h +++ b/libs/image/kis_simple_update_queue.h @@ -1,115 +1,116 @@ /* * Copyright (c) 2010 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_SIMPLE_UPDATE_QUEUE_H #define __KIS_SIMPLE_UPDATE_QUEUE_H #include #include "kis_updater_context.h" typedef QList KisWalkersList; typedef QListIterator KisWalkersListIterator; typedef QMutableListIterator KisMutableWalkersListIterator; typedef QList KisSpontaneousJobsList; typedef QListIterator KisSpontaneousJobsListIterator; typedef QMutableListIterator KisMutableSpontaneousJobsListIterator; class KRITAIMAGE_EXPORT KisSimpleUpdateQueue { public: KisSimpleUpdateQueue(); virtual ~KisSimpleUpdateQueue(); void processQueue(KisUpdaterContext &updaterContext); - void addUpdateJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail); + void addUpdateJob(KisNodeSP node, const QVector &rects, const QRect& cropRect, int levelOfDetail); + void addUpdateJob(KisNodeSP node, const QRect &rc, const QRect& cropRect, int levelOfDetail); void addUpdateNoFilthyJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail); void addFullRefreshJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail); void addSpontaneousJob(KisSpontaneousJob *spontaneousJob); void optimize(); bool isEmpty() const; qint32 sizeMetric() const; void updateSettings(); int overrideLevelOfDetail() const; protected: - void addJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail, KisBaseRectsWalker::UpdateType type); + void addJob(KisNodeSP node, const QVector &rects, const QRect& cropRect, int levelOfDetail, KisBaseRectsWalker::UpdateType type); bool processOneJob(KisUpdaterContext &updaterContext); bool trySplitJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail, KisBaseRectsWalker::UpdateType type); bool tryMergeJob(KisNodeSP node, const QRect& rc, const QRect& cropRect, int levelOfDetail, KisBaseRectsWalker::UpdateType type); void collectJobs(KisBaseRectsWalkerSP &baseWalker, QRect baseRect, const qreal maxAlpha); bool joinRects(QRect& baseRect, const QRect& newRect, qreal maxAlpha); protected: mutable QMutex m_lock; KisWalkersList m_updatesList; KisSpontaneousJobsList m_spontaneousJobsList; /** * Parameters of optimization * (loaded from a configuration file) */ /** * Big update areas are split into a set of smaller * ones, m_patchWidth and m_patchHeight represent the * size of these areas. */ qint32 m_patchWidth; qint32 m_patchHeight; /** * Maximum coefficient of work while regular optimization() */ qreal m_maxCollectAlpha; /** * Maximum coefficient of work when to rects are considered * similar and are merged in tryMergeJob() */ qreal m_maxMergeAlpha; /** * The coefficient of work used while collecting phase of tryToMerge() */ qreal m_maxMergeCollectAlpha; int m_overrideLevelOfDetail; }; class KRITAIMAGE_EXPORT KisTestableSimpleUpdateQueue : public KisSimpleUpdateQueue { public: KisWalkersList& getWalkersList(); KisSpontaneousJobsList& getSpontaneousJobsList(); }; #endif /* __KIS_SIMPLE_UPDATE_QUEUE_H */ diff --git a/libs/image/kis_stroke.cpp b/libs/image/kis_stroke.cpp index d940602317..4d7d67b572 100644 --- a/libs/image/kis_stroke.cpp +++ b/libs/image/kis_stroke.cpp @@ -1,315 +1,336 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_stroke.h" #include "kis_stroke_strategy.h" KisStroke::KisStroke(KisStrokeStrategy *strokeStrategy, Type type, int levelOfDetail) : m_strokeStrategy(strokeStrategy), m_strokeInitialized(false), m_strokeEnded(false), m_strokeSuspended(false), m_isCancelled(false), - m_prevJobSequential(false), m_worksOnLevelOfDetail(levelOfDetail), m_type(type) { m_initStrategy.reset(m_strokeStrategy->createInitStrategy()); m_dabStrategy.reset(m_strokeStrategy->createDabStrategy()); m_cancelStrategy.reset(m_strokeStrategy->createCancelStrategy()); m_finishStrategy.reset(m_strokeStrategy->createFinishStrategy()); m_suspendStrategy.reset(m_strokeStrategy->createSuspendStrategy()); m_resumeStrategy.reset(m_strokeStrategy->createResumeStrategy()); + m_strokeStrategy->notifyUserStartedStroke(); + if(!m_initStrategy) { m_strokeInitialized = true; } else { enqueue(m_initStrategy.data(), m_strokeStrategy->createInitData()); } } KisStroke::~KisStroke() { Q_ASSERT(m_strokeEnded); Q_ASSERT(m_jobsQueue.isEmpty()); } bool KisStroke::supportsSuspension() { return !m_strokeInitialized || (m_suspendStrategy && m_resumeStrategy); } void KisStroke::suspendStroke(KisStrokeSP recipient) { if (!m_strokeInitialized || m_strokeSuspended || (m_strokeEnded && !hasJobs())) { return; } KIS_ASSERT_RECOVER_NOOP(m_suspendStrategy && m_resumeStrategy); prepend(m_resumeStrategy.data(), m_strokeStrategy->createResumeData(), worksOnLevelOfDetail(), false); recipient->prepend(m_suspendStrategy.data(), m_strokeStrategy->createSuspendData(), worksOnLevelOfDetail(), false); m_strokeSuspended = true; } void KisStroke::addJob(KisStrokeJobData *data) { Q_ASSERT(!m_strokeEnded || m_isCancelled); enqueue(m_dabStrategy.data(), data); } +void KisStroke::addMutatedJobs(const QVector list) +{ + // factory methods can return null, if no action is needed + if (!m_dabStrategy) { + qDeleteAll(list); + return; + } + + // Find first non-alien (non-suspend/non-resume) job + // + // Please note that this algorithm will stop working at the day we start + // adding alien jobs not to the beginning of the stroke, but to other places. + // Right now both suspend and resume jobs are added to the beginning of + // the stroke. + + auto it = std::find_if(m_jobsQueue.begin(), m_jobsQueue.end(), + [] (KisStrokeJob *job) { + return job->isOwnJob(); + }); + + + Q_FOREACH (KisStrokeJobData *data, list) { + it = m_jobsQueue.insert(it, new KisStrokeJob(m_dabStrategy.data(), data, worksOnLevelOfDetail(), true)); + ++it; + } +} + KisStrokeJob* KisStroke::popOneJob() { KisStrokeJob *job = dequeue(); if(job) { - m_prevJobSequential = job->isSequential() || job->isBarrier(); - m_strokeInitialized = true; m_strokeSuspended = false; } return job; } KUndo2MagicString KisStroke::name() const { return m_strokeStrategy->name(); } bool KisStroke::hasJobs() const { return !m_jobsQueue.isEmpty(); } qint32 KisStroke::numJobs() const { return m_jobsQueue.size(); } void KisStroke::endStroke() { Q_ASSERT(!m_strokeEnded); m_strokeEnded = true; enqueue(m_finishStrategy.data(), m_strokeStrategy->createFinishData()); + m_strokeStrategy->notifyUserEndedStroke(); } /** * About cancelling the stroke * There may be four different states of the stroke, when cancel * is requested: * 1) Not initialized, has jobs -- just clear the queue * 2) Initialized, has jobs, not finished -- clear the queue, * enqueue the cancel job * 5) Initialized, no jobs, not finished -- enqueue the cancel job * 3) Initialized, has jobs, finished -- clear the queue, enqueue * the cancel job * 4) Initialized, no jobs, finished -- it's too late to cancel * anything * 6) Initialized, has jobs, cancelled -- cancelling twice is a permitted * operation, though it does nothing */ void KisStroke::cancelStroke() { // case 6 if (m_isCancelled) return; const bool effectivelyInitialized = m_strokeInitialized || m_strokeStrategy->needsExplicitCancel(); if(!effectivelyInitialized) { /** * Lod0 stroke cannot be suspended and !initialized at the * same time, because the suspend job is created iff the * stroke has already done some meaningful work. * * At the same time, LodN stroke can be prepended with a * 'suspend' job even when it has not been started yet. That * is obvious: we should suspend the other stroke before doing * anything else. */ KIS_ASSERT_RECOVER_NOOP(type() == LODN || sanityCheckAllJobsAreCancellable()); clearQueueOnCancel(); } else if(effectivelyInitialized && (!m_jobsQueue.isEmpty() || !m_strokeEnded)) { clearQueueOnCancel(); enqueue(m_cancelStrategy.data(), m_strokeStrategy->createCancelData()); } // else { // too late ... // } m_isCancelled = true; m_strokeEnded = true; } bool KisStroke::canCancel() const { return m_isCancelled || !m_strokeInitialized || !m_jobsQueue.isEmpty() || !m_strokeEnded; } bool KisStroke::sanityCheckAllJobsAreCancellable() const { Q_FOREACH (KisStrokeJob *item, m_jobsQueue) { if (!item->isCancellable()) { return false; } } return true; } void KisStroke::clearQueueOnCancel() { QQueue::iterator it = m_jobsQueue.begin(); while (it != m_jobsQueue.end()) { if ((*it)->isCancellable()) { delete (*it); it = m_jobsQueue.erase(it); } else { ++it; } } } bool KisStroke::isInitialized() const { return m_strokeInitialized; } bool KisStroke::isEnded() const { return m_strokeEnded; } bool KisStroke::isCancelled() const { return m_isCancelled; } bool KisStroke::isExclusive() const { return m_strokeStrategy->isExclusive(); } bool KisStroke::supportsWrapAroundMode() const { return m_strokeStrategy->supportsWrapAroundMode(); } int KisStroke::worksOnLevelOfDetail() const { return m_worksOnLevelOfDetail; } bool KisStroke::canForgetAboutMe() const { return m_strokeStrategy->canForgetAboutMe(); } -bool KisStroke::prevJobSequential() const +qreal KisStroke::balancingRatioOverride() const { - return m_prevJobSequential; -} - -bool KisStroke::nextJobSequential() const -{ - return !m_jobsQueue.isEmpty() ? - m_jobsQueue.head()->isSequential() : false; + return m_strokeStrategy->balancingRatioOverride(); } -bool KisStroke::nextJobBarrier() const +KisStrokeJobData::Sequentiality KisStroke::nextJobSequentiality() const { return !m_jobsQueue.isEmpty() ? - m_jobsQueue.head()->isBarrier() : false; + m_jobsQueue.head()->sequentiality() : KisStrokeJobData::SEQUENTIAL; } void KisStroke::enqueue(KisStrokeJobStrategy *strategy, KisStrokeJobData *data) { // factory methods can return null, if no action is needed if(!strategy) { delete data; return; } m_jobsQueue.enqueue(new KisStrokeJob(strategy, data, worksOnLevelOfDetail(), true)); } void KisStroke::prepend(KisStrokeJobStrategy *strategy, KisStrokeJobData *data, int levelOfDetail, - bool isCancellable) + bool isOwnJob) { // factory methods can return null, if no action is needed if(!strategy) { delete data; return; } // LOG_MERGE_FIXME: Q_UNUSED(levelOfDetail); - m_jobsQueue.prepend(new KisStrokeJob(strategy, data, worksOnLevelOfDetail(), isCancellable)); + m_jobsQueue.prepend(new KisStrokeJob(strategy, data, worksOnLevelOfDetail(), isOwnJob)); } KisStrokeJob* KisStroke::dequeue() { return !m_jobsQueue.isEmpty() ? m_jobsQueue.dequeue() : 0; } void KisStroke::setLodBuddy(KisStrokeSP buddy) { m_lodBuddy = buddy; } KisStrokeSP KisStroke::lodBuddy() const { return m_lodBuddy; } KisStroke::Type KisStroke::type() const { if (m_type == LOD0) { KIS_ASSERT_RECOVER_NOOP(m_lodBuddy && "LOD0 strokes must always have a buddy"); } else if (m_type == LODN) { KIS_ASSERT_RECOVER_NOOP(m_worksOnLevelOfDetail > 0 && "LODN strokes must work on LOD > 0!"); } else if (m_type == LEGACY) { KIS_ASSERT_RECOVER_NOOP(m_worksOnLevelOfDetail == 0 && "LEGACY strokes must work on LOD == 0!"); } return m_type; } diff --git a/libs/image/kis_stroke.h b/libs/image/kis_stroke.h index 196c9a6bda..9d740e1813 100644 --- a/libs/image/kis_stroke.h +++ b/libs/image/kis_stroke.h @@ -1,127 +1,125 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_STROKE_H #define __KIS_STROKE_H #include #include #include #include "kritaimage_export.h" #include "kis_stroke_job.h" class KisStrokeStrategy; class KUndo2MagicString; class KRITAIMAGE_EXPORT KisStroke { public: enum Type { LEGACY, LOD0, LODN, SUSPEND, RESUME }; public: KisStroke(KisStrokeStrategy *strokeStrategy, Type type = LEGACY, int levelOfDetail = 0); ~KisStroke(); void addJob(KisStrokeJobData *data); + void addMutatedJobs(const QVector list); KUndo2MagicString name() const; bool hasJobs() const; qint32 numJobs() const; KisStrokeJob* popOneJob(); void endStroke(); void cancelStroke(); bool canCancel() const; bool supportsSuspension(); void suspendStroke(KisStrokeSP recipient); bool isInitialized() const; bool isEnded() const; bool isCancelled() const; bool isExclusive() const; bool supportsWrapAroundMode() const; int worksOnLevelOfDetail() const; bool canForgetAboutMe() const; + qreal balancingRatioOverride() const; - bool prevJobSequential() const; - bool nextJobSequential() const; - - bool nextJobBarrier() const; + KisStrokeJobData::Sequentiality nextJobSequentiality() const; void setLodBuddy(KisStrokeSP buddy); KisStrokeSP lodBuddy() const; Type type() const; private: void enqueue(KisStrokeJobStrategy *strategy, KisStrokeJobData *data); // for suspend/resume jobs void prepend(KisStrokeJobStrategy *strategy, KisStrokeJobData *data, int levelOfDetail, - bool isCancellable); + bool isOwnJob); KisStrokeJob* dequeue(); void clearQueueOnCancel(); bool sanityCheckAllJobsAreCancellable() const; private: // for testing use only, do not use in real code friend class KisStrokeTest; friend class KisStrokeStrategyUndoCommandBasedTest; QQueue& testingGetQueue() { return m_jobsQueue; } private: // the strategies are owned by the stroke QScopedPointer m_strokeStrategy; QScopedPointer m_initStrategy; QScopedPointer m_dabStrategy; QScopedPointer m_cancelStrategy; QScopedPointer m_finishStrategy; QScopedPointer m_suspendStrategy; QScopedPointer m_resumeStrategy; QQueue m_jobsQueue; bool m_strokeInitialized; bool m_strokeEnded; bool m_strokeSuspended; bool m_isCancelled; // cancelled strokes are always 'ended' as well - bool m_prevJobSequential; int m_worksOnLevelOfDetail; Type m_type; KisStrokeSP m_lodBuddy; }; #endif /* __KIS_STROKE_H */ diff --git a/libs/image/kis_stroke_job.h b/libs/image/kis_stroke_job.h index 35f28ea195..2a2b2adaa1 100644 --- a/libs/image/kis_stroke_job.h +++ b/libs/image/kis_stroke_job.h @@ -1,95 +1,103 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_STROKE_JOB_H #define __KIS_STROKE_JOB_H #include "kis_runnable.h" #include "kis_stroke_job_strategy.h" class KisStrokeJob : public KisRunnable { public: KisStrokeJob(KisStrokeJobStrategy *strategy, KisStrokeJobData *data, int levelOfDetail, - bool isCancellable) + bool isOwnJob) : m_dabStrategy(strategy), m_dabData(data), m_levelOfDetail(levelOfDetail), - m_isCancellable(isCancellable) + m_isOwnJob(isOwnJob) { } ~KisStrokeJob() override { delete m_dabData; } void run() override { m_dabStrategy->run(m_dabData); } + KisStrokeJobData::Sequentiality sequentiality() const { + return m_dabData ? m_dabData->sequentiality() : KisStrokeJobData::SEQUENTIAL; + } + bool isSequential() const { // Default value is 'SEQUENTIAL' return m_dabData ? m_dabData->isSequential() : true; } bool isBarrier() const { // Default value is simply 'SEQUENTIAL', *not* 'BARRIER' return m_dabData ? m_dabData->isBarrier() : false; } bool isExclusive() const { // Default value is 'NORMAL' return m_dabData ? m_dabData->isExclusive() : false; } int levelOfDetail() const { return m_levelOfDetail; } bool isCancellable() const { - return m_isCancellable; + return m_isOwnJob; + } + + bool isOwnJob() const { + return m_isOwnJob; } private: // for testing use only, do not use in real code friend QString getJobName(KisStrokeJob *job); friend QString getCommandName(KisStrokeJob *job); friend int cancelSeqNo(KisStrokeJob *job); KisStrokeJobStrategy* testingGetDabStrategy() { return m_dabStrategy; } KisStrokeJobData* testingGetDabData() { return m_dabData; } private: // Shared between different jobs KisStrokeJobStrategy *m_dabStrategy; // Owned by the job KisStrokeJobData *m_dabData; int m_levelOfDetail; - bool m_isCancellable; + bool m_isOwnJob; }; #endif /* __KIS_STROKE_JOB_H */ diff --git a/libs/image/kis_stroke_job_strategy.h b/libs/image/kis_stroke_job_strategy.h index ddfcf5b34d..45bb6eecc5 100644 --- a/libs/image/kis_stroke_job_strategy.h +++ b/libs/image/kis_stroke_job_strategy.h @@ -1,74 +1,75 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_STROKE_JOB_STRATEGY_H #define __KIS_STROKE_JOB_STRATEGY_H #include "kritaimage_export.h" class KRITAIMAGE_EXPORT KisStrokeJobData { public: enum Sequentiality { CONCURRENT, SEQUENTIAL, - BARRIER + BARRIER, + UNIQUELY_CONCURRENT }; enum Exclusivity { NORMAL, EXCLUSIVE }; public: KisStrokeJobData(Sequentiality sequentiality = SEQUENTIAL, Exclusivity exclusivity = NORMAL); virtual ~KisStrokeJobData(); bool isBarrier() const; bool isSequential() const; bool isExclusive() const; - Sequentiality sequentiality() { return m_sequentiality; }; - Exclusivity exclusivity() { return m_exclusivity; }; + Sequentiality sequentiality() { return m_sequentiality; } + Exclusivity exclusivity() { return m_exclusivity; } virtual KisStrokeJobData* createLodClone(int levelOfDetail); protected: KisStrokeJobData(const KisStrokeJobData &rhs); private: Sequentiality m_sequentiality; Exclusivity m_exclusivity; }; class KRITAIMAGE_EXPORT KisStrokeJobStrategy { public: KisStrokeJobStrategy(); virtual ~KisStrokeJobStrategy(); virtual void run(KisStrokeJobData *data) = 0; private: }; #endif /* __KIS_STROKE_JOB_STRATEGY_H */ diff --git a/libs/image/kis_stroke_strategy.cpp b/libs/image/kis_stroke_strategy.cpp index cbb6faad9c..f79b45d31b 100644 --- a/libs/image/kis_stroke_strategy.cpp +++ b/libs/image/kis_stroke_strategy.cpp @@ -1,208 +1,250 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_stroke_strategy.h" #include #include "kis_stroke_job_strategy.h" +#include "KisStrokesQueueMutatedJobInterface.h" KisStrokeStrategy::KisStrokeStrategy(QString id, const KUndo2MagicString &name) : m_exclusive(false), m_supportsWrapAroundMode(false), m_needsIndirectPainting(false), m_indirectPaintingCompositeOp(COMPOSITE_ALPHA_DARKEN), m_clearsRedoOnStart(true), m_requestsOtherStrokesToEnd(true), m_canForgetAboutMe(false), m_needsExplicitCancel(false), + m_balancingRatioOverride(-1.0), m_id(id), - m_name(name) + m_name(name), + m_mutatedJobsInterface(0) { } KisStrokeStrategy::KisStrokeStrategy(const KisStrokeStrategy &rhs) : m_exclusive(rhs.m_exclusive), m_supportsWrapAroundMode(rhs.m_supportsWrapAroundMode), m_needsIndirectPainting(rhs.m_needsIndirectPainting), m_indirectPaintingCompositeOp(rhs.m_indirectPaintingCompositeOp), m_clearsRedoOnStart(rhs.m_clearsRedoOnStart), m_requestsOtherStrokesToEnd(rhs.m_requestsOtherStrokesToEnd), m_canForgetAboutMe(rhs.m_canForgetAboutMe), m_needsExplicitCancel(rhs.m_needsExplicitCancel), + m_balancingRatioOverride(rhs.m_balancingRatioOverride), m_id(rhs.m_id), - m_name(rhs.m_name) + m_name(rhs.m_name), + m_mutatedJobsInterface(0) { - KIS_ASSERT_RECOVER_NOOP(!rhs.m_cancelStrokeId && + KIS_ASSERT_RECOVER_NOOP(!rhs.m_cancelStrokeId && !m_mutatedJobsInterface && "After the stroke has been started, no copying must happen"); } KisStrokeStrategy::~KisStrokeStrategy() { } +void KisStrokeStrategy::notifyUserStartedStroke() +{ +} + +void KisStrokeStrategy::notifyUserEndedStroke() +{ +} KisStrokeJobStrategy* KisStrokeStrategy::createInitStrategy() { return 0; } KisStrokeJobStrategy* KisStrokeStrategy::createFinishStrategy() { return 0; } KisStrokeJobStrategy* KisStrokeStrategy::createCancelStrategy() { return 0; } KisStrokeJobStrategy* KisStrokeStrategy::createDabStrategy() { return 0; } KisStrokeJobStrategy* KisStrokeStrategy::createSuspendStrategy() { return 0; } KisStrokeJobStrategy* KisStrokeStrategy::createResumeStrategy() { return 0; } KisStrokeJobData* KisStrokeStrategy::createInitData() { return 0; } KisStrokeJobData* KisStrokeStrategy::createFinishData() { return 0; } KisStrokeJobData* KisStrokeStrategy::createCancelData() { return 0; } KisStrokeJobData* KisStrokeStrategy::createSuspendData() { return 0; } KisStrokeJobData* KisStrokeStrategy::createResumeData() { return 0; } KisStrokeStrategy* KisStrokeStrategy::createLodClone(int levelOfDetail) { Q_UNUSED(levelOfDetail); return 0; } bool KisStrokeStrategy::isExclusive() const { return m_exclusive; } bool KisStrokeStrategy::supportsWrapAroundMode() const { return m_supportsWrapAroundMode; } bool KisStrokeStrategy::needsIndirectPainting() const { return m_needsIndirectPainting; } QString KisStrokeStrategy::indirectPaintingCompositeOp() const { return m_indirectPaintingCompositeOp; } QString KisStrokeStrategy::id() const { return m_id; } KUndo2MagicString KisStrokeStrategy::name() const { return m_name; } +void KisStrokeStrategy::setMutatedJobsInterface(KisStrokesQueueMutatedJobInterface *mutatedJobsInterface) +{ + m_mutatedJobsInterface = mutatedJobsInterface; +} + +void KisStrokeStrategy::addMutatedJobs(const QVector list) +{ + KIS_SAFE_ASSERT_RECOVER(m_mutatedJobsInterface && m_cancelStrokeId) { + qDeleteAll(list); + return; + } + + m_mutatedJobsInterface->addMutatedJobs(m_cancelStrokeId, list); +} + +void KisStrokeStrategy::addMutatedJob(KisStrokeJobData *data) +{ + addMutatedJobs({data}); +} + void KisStrokeStrategy::setExclusive(bool value) { m_exclusive = value; } void KisStrokeStrategy::setSupportsWrapAroundMode(bool value) { m_supportsWrapAroundMode = value; } void KisStrokeStrategy::setNeedsIndirectPainting(bool value) { m_needsIndirectPainting = value; } void KisStrokeStrategy::setIndirectPaintingCompositeOp(const QString &id) { m_indirectPaintingCompositeOp = id; } bool KisStrokeStrategy::clearsRedoOnStart() const { return m_clearsRedoOnStart; } void KisStrokeStrategy::setClearsRedoOnStart(bool value) { m_clearsRedoOnStart = value; } bool KisStrokeStrategy::requestsOtherStrokesToEnd() const { return m_requestsOtherStrokesToEnd; } void KisStrokeStrategy::setRequestsOtherStrokesToEnd(bool value) { m_requestsOtherStrokesToEnd = value; } bool KisStrokeStrategy::canForgetAboutMe() const { return m_canForgetAboutMe; } void KisStrokeStrategy::setCanForgetAboutMe(bool value) { m_canForgetAboutMe = value; } bool KisStrokeStrategy::needsExplicitCancel() const { return m_needsExplicitCancel; } void KisStrokeStrategy::setNeedsExplicitCancel(bool value) { m_needsExplicitCancel = value; } + +qreal KisStrokeStrategy::balancingRatioOverride() const +{ + return m_balancingRatioOverride; +} + +void KisStrokeStrategy::setBalancingRatioOverride(qreal value) +{ + m_balancingRatioOverride = value; +} diff --git a/libs/image/kis_stroke_strategy.h b/libs/image/kis_stroke_strategy.h index d38c94ed3b..34e48086e5 100644 --- a/libs/image/kis_stroke_strategy.h +++ b/libs/image/kis_stroke_strategy.h @@ -1,139 +1,213 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_STROKE_STRATEGY_H #define __KIS_STROKE_STRATEGY_H #include #include "kis_types.h" #include "kundo2magicstring.h" #include "kritaimage_export.h" + class KisStrokeJobStrategy; class KisStrokeJobData; +class KisStrokesQueueMutatedJobInterface; class KRITAIMAGE_EXPORT KisStrokeStrategy { public: KisStrokeStrategy(QString id = QString(), const KUndo2MagicString &name = KUndo2MagicString()); virtual ~KisStrokeStrategy(); + /** + * notifyUserStartedStroke() is a callback used by the strokes system to notify + * when the user adds the stroke to the strokes queue. That moment corresponds + * to the user calling strokesFacade->startStroke(strategy) and might happen much + * earlier than the first job being executed. + * + * NOTE: this method will be executed in the context of the GUI thread! + */ + virtual void notifyUserStartedStroke(); + + /** + * notifyUserEndedStroke() is a callback used by the strokes system to notify + * when the user ends the stroke. That moment corresponds to the user calling + * strokesFacade->endStroke(id) and might happen much earlier when the stroke + * even started its execution. + * + * NOTE: this method will be executed in the context of the GUI thread! + */ + virtual void notifyUserEndedStroke(); + virtual KisStrokeJobStrategy* createInitStrategy(); virtual KisStrokeJobStrategy* createFinishStrategy(); virtual KisStrokeJobStrategy* createCancelStrategy(); virtual KisStrokeJobStrategy* createDabStrategy(); virtual KisStrokeJobStrategy* createSuspendStrategy(); virtual KisStrokeJobStrategy* createResumeStrategy(); virtual KisStrokeJobData* createInitData(); virtual KisStrokeJobData* createFinishData(); virtual KisStrokeJobData* createCancelData(); virtual KisStrokeJobData* createSuspendData(); virtual KisStrokeJobData* createResumeData(); virtual KisStrokeStrategy* createLodClone(int levelOfDetail); bool isExclusive() const; bool supportsWrapAroundMode() const; bool needsIndirectPainting() const; QString indirectPaintingCompositeOp() const; /** * Returns true if mere start of the stroke should cancel all the * pending redo tasks. * * This method should return true in almost all circumstances * except if we are running an undo or redo stroke. */ bool clearsRedoOnStart() const; /** * Returns true if the other currently running strokes should be * politely asked to exit. The default value is 'true'. * * The only known exception right now is * KisRegenerateFrameStrokeStrategy which does not requests ending * of any actions, since it performs purely background action. */ bool requestsOtherStrokesToEnd() const; /** * Returns true if the update scheduler can cancel this stroke * when some other stroke is going to be started. This makes the * "forgettable" stroke very low priority. * * Default is 'false'. */ bool canForgetAboutMe() const; bool needsExplicitCancel() const; + /** + * \see setBalancingRatioOverride() for details + */ + qreal balancingRatioOverride() const; + QString id() const; KUndo2MagicString name() const; /** * Set up by the strokes queue during the stroke initialization */ void setCancelStrokeId(KisStrokeId id) { m_cancelStrokeId = id; } + void setMutatedJobsInterface(KisStrokesQueueMutatedJobInterface *mutatedJobsInterface); + protected: + // testing surrogate class + friend class KisMutatableDabStrategy; + /** * The cancel job may populate the stroke with some new jobs * for cancelling. To achieve this it needs the stroke id. * * WARNING: you can't add new jobs in any places other than * cancel job, because the stroke may be ended in any moment * by the user and the sequence of jobs will be broken */ KisStrokeId cancelStrokeId() { return m_cancelStrokeId; } + /** + * This function is supposed to be called by internal asynchronous + * jobs. It allows adding subtasks that may be executed concurrently. + * + * Requirements: + * * must be called *only* from within the context of the strokes + * worker thread during exectution one of its jobs + * + * Guarantees: + * * the added job is guaranteed to be executed in some time after + * the currently executed job, *before* the next SEQUENTIAL or + * BARRIER job + * * if the currently executed job is CUNCURRENTthe mutated job *may* + * start execution right after adding to the queue without waiting for + * its parent to complete. Though this behavior is *not* guaranteed, + * because addMutatedJob does not initiate processQueues(), because + * it may lead to a deadlock. + */ + void addMutatedJobs(const QVector list); + + /** + * Convenience override for addMutatedJobs() + */ + void addMutatedJob(KisStrokeJobData *data); + + // you are not supposed to change these parameters // after the KisStroke object has been created void setExclusive(bool value); void setSupportsWrapAroundMode(bool value); void setNeedsIndirectPainting(bool value); void setIndirectPaintingCompositeOp(const QString &id); void setClearsRedoOnStart(bool value); void setRequestsOtherStrokesToEnd(bool value); void setCanForgetAboutMe(bool value); void setNeedsExplicitCancel(bool value); + /** + * Set override for the desired scheduler balancing ratio: + * + * ratio = stroke jobs / update jobs + * + * If \p value < 1.0, then the priority is given to updates, if + * the value is higher than 1.0, then the priority is given + * to stroke jobs. + * + * Special value -1.0, suggests the scheduler to use the default value + * set by the user's config file (which is 100.0 by default). + */ + void setBalancingRatioOverride(qreal value); + protected: /** * Protected c-tor, used for cloning of hi-level strategies */ KisStrokeStrategy(const KisStrokeStrategy &rhs); private: bool m_exclusive; bool m_supportsWrapAroundMode; bool m_needsIndirectPainting; QString m_indirectPaintingCompositeOp; bool m_clearsRedoOnStart; bool m_requestsOtherStrokesToEnd; bool m_canForgetAboutMe; bool m_needsExplicitCancel; + qreal m_balancingRatioOverride; QString m_id; KUndo2MagicString m_name; KisStrokeId m_cancelStrokeId; + KisStrokesQueueMutatedJobInterface *m_mutatedJobsInterface; }; #endif /* __KIS_STROKE_STRATEGY_H */ diff --git a/libs/image/kis_strokes_queue.cpp b/libs/image/kis_strokes_queue.cpp index f3e4f629a3..a916b78472 100644 --- a/libs/image/kis_strokes_queue.cpp +++ b/libs/image/kis_strokes_queue.cpp @@ -1,795 +1,834 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_strokes_queue.h" #include #include #include #include "kis_stroke.h" #include "kis_updater_context.h" #include "kis_stroke_job_strategy.h" #include "kis_stroke_strategy.h" #include "kis_undo_stores.h" #include "kis_post_execution_undo_adapter.h" typedef QQueue StrokesQueue; typedef QQueue::iterator StrokesQueueIterator; #include "kis_image_interfaces.h" class KisStrokesQueue::LodNUndoStrokesFacade : public KisStrokesFacade { public: LodNUndoStrokesFacade(KisStrokesQueue *_q) : q(_q) {} KisStrokeId startStroke(KisStrokeStrategy *strokeStrategy) override { return q->startLodNUndoStroke(strokeStrategy); } void addJob(KisStrokeId id, KisStrokeJobData *data) override { KisStrokeSP stroke = id.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_NOOP(stroke); KIS_SAFE_ASSERT_RECOVER_NOOP(!stroke->lodBuddy()); KIS_SAFE_ASSERT_RECOVER_NOOP(stroke->type() == KisStroke::LODN); q->addJob(id, data); } void endStroke(KisStrokeId id) override { KisStrokeSP stroke = id.toStrongRef(); KIS_SAFE_ASSERT_RECOVER_NOOP(stroke); KIS_SAFE_ASSERT_RECOVER_NOOP(!stroke->lodBuddy()); KIS_SAFE_ASSERT_RECOVER_NOOP(stroke->type() == KisStroke::LODN); q->endStroke(id); } bool cancelStroke(KisStrokeId id) override { Q_UNUSED(id); qFatal("Not implemented"); return false; } private: KisStrokesQueue *q; }; struct Q_DECL_HIDDEN KisStrokesQueue::Private { Private(KisStrokesQueue *_q) : q(_q), openedStrokesCounter(0), needsExclusiveAccess(false), wrapAroundModeSupported(false), + balancingRatioOverride(-1.0), currentStrokeLoaded(false), lodNNeedsSynchronization(true), desiredLevelOfDetail(0), nextDesiredLevelOfDetail(0), lodNStrokesFacade(_q), lodNPostExecutionUndoAdapter(&lodNUndoStore, &lodNStrokesFacade) {} KisStrokesQueue *q; StrokesQueue strokesQueue; int openedStrokesCounter; bool needsExclusiveAccess; bool wrapAroundModeSupported; + qreal balancingRatioOverride; bool currentStrokeLoaded; bool lodNNeedsSynchronization; int desiredLevelOfDetail; int nextDesiredLevelOfDetail; QMutex mutex; KisLodSyncStrokeStrategyFactory lod0ToNStrokeStrategyFactory; KisSuspendResumeStrategyFactory suspendUpdatesStrokeStrategyFactory; KisSuspendResumeStrategyFactory resumeUpdatesStrokeStrategyFactory; KisSurrogateUndoStore lodNUndoStore; LodNUndoStrokesFacade lodNStrokesFacade; KisPostExecutionUndoAdapter lodNPostExecutionUndoAdapter; void cancelForgettableStrokes(); void startLod0ToNStroke(int levelOfDetail, bool forgettable); bool canUseLodN() const; StrokesQueueIterator findNewLod0Pos(); StrokesQueueIterator findNewLodNPos(KisStrokeSP lodN); bool shouldWrapInSuspendUpdatesStroke() const; void switchDesiredLevelOfDetail(bool forced); bool hasUnfinishedStrokes() const; void tryClearUndoOnStrokeCompletion(KisStrokeSP finishingStroke); }; KisStrokesQueue::KisStrokesQueue() : m_d(new Private(this)) { } KisStrokesQueue::~KisStrokesQueue() { Q_FOREACH (KisStrokeSP stroke, m_d->strokesQueue) { stroke->cancelStroke(); } delete m_d; } template typename StrokesQueue::iterator -executeStrokePair(const StrokePair &pair, StrokesQueue &queue, typename StrokesQueue::iterator it, KisStroke::Type type, int levelOfDetail) { +executeStrokePair(const StrokePair &pair, StrokesQueue &queue, typename StrokesQueue::iterator it, KisStroke::Type type, int levelOfDetail, KisStrokesQueueMutatedJobInterface *mutatedJobsInterface) { KisStrokeStrategy *strategy = pair.first; QList jobsData = pair.second; KisStrokeSP stroke(new KisStroke(strategy, type, levelOfDetail)); strategy->setCancelStrokeId(stroke); + strategy->setMutatedJobsInterface(mutatedJobsInterface); it = queue.insert(it, stroke); Q_FOREACH (KisStrokeJobData *jobData, jobsData) { stroke->addJob(jobData); } stroke->endStroke(); return it; } void KisStrokesQueue::Private::startLod0ToNStroke(int levelOfDetail, bool forgettable) { // precondition: lock held! // precondition: lod > 0 KIS_ASSERT_RECOVER_RETURN(levelOfDetail); if (!this->lod0ToNStrokeStrategyFactory) return; KisLodSyncPair syncPair = this->lod0ToNStrokeStrategyFactory(forgettable); - executeStrokePair(syncPair, this->strokesQueue, this->strokesQueue.end(), KisStroke::LODN, levelOfDetail); + executeStrokePair(syncPair, this->strokesQueue, this->strokesQueue.end(), KisStroke::LODN, levelOfDetail, q); this->lodNNeedsSynchronization = false; } void KisStrokesQueue::Private::cancelForgettableStrokes() { if (!strokesQueue.isEmpty() && !hasUnfinishedStrokes()) { Q_FOREACH (KisStrokeSP stroke, strokesQueue) { KIS_ASSERT_RECOVER_NOOP(stroke->isEnded()); if (stroke->canForgetAboutMe()) { stroke->cancelStroke(); } } } } bool KisStrokesQueue::Private::canUseLodN() const { Q_FOREACH (KisStrokeSP stroke, strokesQueue) { if (stroke->type() == KisStroke::LEGACY) { return false; } } return true; } bool KisStrokesQueue::Private::shouldWrapInSuspendUpdatesStroke() const { Q_FOREACH (KisStrokeSP stroke, strokesQueue) { if (stroke->isCancelled()) continue; if (stroke->type() == KisStroke::RESUME) { /** * Resuming process is long-running and consists of * multiple actions, therefore, if it has already started, * we cannot use it to guard our new stroke, so just skip it. * see https://phabricator.kde.org/T2542 */ if (stroke->isInitialized()) continue; return false; } } return true; } StrokesQueueIterator KisStrokesQueue::Private::findNewLod0Pos() { StrokesQueueIterator it = strokesQueue.begin(); StrokesQueueIterator end = strokesQueue.end(); for (; it != end; ++it) { if ((*it)->isCancelled()) continue; if ((*it)->type() == KisStroke::RESUME) { // \see a comment in shouldWrapInSuspendUpdatesStroke() if ((*it)->isInitialized()) continue; return it; } } return it; } StrokesQueueIterator KisStrokesQueue::Private::findNewLodNPos(KisStrokeSP lodN) { StrokesQueueIterator it = strokesQueue.begin(); StrokesQueueIterator end = strokesQueue.end(); for (; it != end; ++it) { if ((*it)->isCancelled()) continue; if (((*it)->type() == KisStroke::SUSPEND || (*it)->type() == KisStroke::RESUME) && (*it)->isInitialized()) { // \see a comment in shouldWrapInSuspendUpdatesStroke() continue; } if ((*it)->type() == KisStroke::LOD0 || (*it)->type() == KisStroke::SUSPEND || (*it)->type() == KisStroke::RESUME) { if (it != end && it == strokesQueue.begin()) { KisStrokeSP head = *it; if (head->supportsSuspension()) { head->suspendStroke(lodN); } } return it; } } return it; } KisStrokeId KisStrokesQueue::startLodNUndoStroke(KisStrokeStrategy *strokeStrategy) { QMutexLocker locker(&m_d->mutex); KIS_SAFE_ASSERT_RECOVER_NOOP(!m_d->lodNNeedsSynchronization); KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->desiredLevelOfDetail > 0); KisStrokeSP buddy(new KisStroke(strokeStrategy, KisStroke::LODN, m_d->desiredLevelOfDetail)); strokeStrategy->setCancelStrokeId(buddy); + strokeStrategy->setMutatedJobsInterface(this); m_d->strokesQueue.insert(m_d->findNewLodNPos(buddy), buddy); KisStrokeId id(buddy); m_d->openedStrokesCounter++; return id; } KisStrokeId KisStrokesQueue::startStroke(KisStrokeStrategy *strokeStrategy) { QMutexLocker locker(&m_d->mutex); KisStrokeSP stroke; KisStrokeStrategy* lodBuddyStrategy; m_d->cancelForgettableStrokes(); if (m_d->desiredLevelOfDetail && m_d->canUseLodN() && (lodBuddyStrategy = strokeStrategy->createLodClone(m_d->desiredLevelOfDetail))) { if (m_d->lodNNeedsSynchronization) { m_d->startLod0ToNStroke(m_d->desiredLevelOfDetail, false); } stroke = KisStrokeSP(new KisStroke(strokeStrategy, KisStroke::LOD0, 0)); KisStrokeSP buddy(new KisStroke(lodBuddyStrategy, KisStroke::LODN, m_d->desiredLevelOfDetail)); lodBuddyStrategy->setCancelStrokeId(buddy); + lodBuddyStrategy->setMutatedJobsInterface(this); stroke->setLodBuddy(buddy); m_d->strokesQueue.insert(m_d->findNewLodNPos(buddy), buddy); if (m_d->shouldWrapInSuspendUpdatesStroke()) { KisSuspendResumePair suspendPair = m_d->suspendUpdatesStrokeStrategyFactory(); KisSuspendResumePair resumePair = m_d->resumeUpdatesStrokeStrategyFactory(); StrokesQueueIterator it = m_d->findNewLod0Pos(); - it = executeStrokePair(resumePair, m_d->strokesQueue, it, KisStroke::RESUME, 0); + it = executeStrokePair(resumePair, m_d->strokesQueue, it, KisStroke::RESUME, 0, this); it = m_d->strokesQueue.insert(it, stroke); - it = executeStrokePair(suspendPair, m_d->strokesQueue, it, KisStroke::SUSPEND, 0); + it = executeStrokePair(suspendPair, m_d->strokesQueue, it, KisStroke::SUSPEND, 0, this); } else { m_d->strokesQueue.insert(m_d->findNewLod0Pos(), stroke); } } else { stroke = KisStrokeSP(new KisStroke(strokeStrategy, KisStroke::LEGACY, 0)); m_d->strokesQueue.enqueue(stroke); } KisStrokeId id(stroke); strokeStrategy->setCancelStrokeId(id); + strokeStrategy->setMutatedJobsInterface(this); m_d->openedStrokesCounter++; if (stroke->type() == KisStroke::LEGACY) { m_d->lodNNeedsSynchronization = true; } return id; } void KisStrokesQueue::addJob(KisStrokeId id, KisStrokeJobData *data) { QMutexLocker locker(&m_d->mutex); KisStrokeSP stroke = id.toStrongRef(); Q_ASSERT(stroke); KisStrokeSP buddy = stroke->lodBuddy(); if (buddy) { KisStrokeJobData *clonedData = data->createLodClone(buddy->worksOnLevelOfDetail()); KIS_ASSERT_RECOVER_RETURN(clonedData); buddy->addJob(clonedData); } stroke->addJob(data); } +void KisStrokesQueue::addMutatedJobs(KisStrokeId id, const QVector list) +{ + QMutexLocker locker(&m_d->mutex); + + KisStrokeSP stroke = id.toStrongRef(); + Q_ASSERT(stroke); + + stroke->addMutatedJobs(list); +} + void KisStrokesQueue::endStroke(KisStrokeId id) { QMutexLocker locker(&m_d->mutex); KisStrokeSP stroke = id.toStrongRef(); Q_ASSERT(stroke); stroke->endStroke(); m_d->openedStrokesCounter--; KisStrokeSP buddy = stroke->lodBuddy(); if (buddy) { buddy->endStroke(); } } bool KisStrokesQueue::cancelStroke(KisStrokeId id) { QMutexLocker locker(&m_d->mutex); KisStrokeSP stroke = id.toStrongRef(); if(stroke) { stroke->cancelStroke(); m_d->openedStrokesCounter--; KisStrokeSP buddy = stroke->lodBuddy(); if (buddy) { buddy->cancelStroke(); } } return stroke; } bool KisStrokesQueue::Private::hasUnfinishedStrokes() const { Q_FOREACH (KisStrokeSP stroke, strokesQueue) { if (!stroke->isEnded()) { return true; } } return false; } bool KisStrokesQueue::tryCancelCurrentStrokeAsync() { bool anythingCanceled = false; QMutexLocker locker(&m_d->mutex); /** * We cancel only ended strokes. This is done to avoid * handling dangling pointers problem (KisStrokeId). The owner * of a stroke will cancel the stroke itself if needed. */ if (!m_d->strokesQueue.isEmpty() && !m_d->hasUnfinishedStrokes()) { anythingCanceled = true; Q_FOREACH (KisStrokeSP currentStroke, m_d->strokesQueue) { KIS_ASSERT_RECOVER_NOOP(currentStroke->isEnded()); currentStroke->cancelStroke(); // we shouldn't cancel buddies... if (currentStroke->type() == KisStroke::LOD0) { /** * If the buddy has already finished, we cannot undo it because * it doesn't store any undo data. Therefore we just regenerate * the LOD caches. */ m_d->lodNNeedsSynchronization = true; } } } /** * NOTE: We do not touch the openedStrokesCounter here since * we work with closed id's only here */ return anythingCanceled; } UndoResult KisStrokesQueue::tryUndoLastStrokeAsync() { UndoResult result = UNDO_FAIL; QMutexLocker locker(&m_d->mutex); std::reverse_iterator it(m_d->strokesQueue.constEnd()); std::reverse_iterator end(m_d->strokesQueue.constBegin()); KisStrokeSP lastStroke; KisStrokeSP lastBuddy; bool buddyFound = false; for (; it != end; ++it) { if ((*it)->type() == KisStroke::LEGACY) { break; } if (!lastStroke && (*it)->type() == KisStroke::LOD0 && !(*it)->isCancelled()) { lastStroke = *it; lastBuddy = lastStroke->lodBuddy(); KIS_SAFE_ASSERT_RECOVER(lastBuddy) { lastStroke.clear(); lastBuddy.clear(); break; } } KIS_SAFE_ASSERT_RECOVER(!lastStroke || *it == lastBuddy || (*it)->type() != KisStroke::LODN) { lastStroke.clear(); lastBuddy.clear(); break; } if (lastStroke && *it == lastBuddy) { KIS_SAFE_ASSERT_RECOVER(lastBuddy->type() == KisStroke::LODN) { lastStroke.clear(); lastBuddy.clear(); break; } buddyFound = true; break; } } if (!lastStroke) return UNDO_FAIL; if (!lastStroke->isEnded()) return UNDO_FAIL; if (lastStroke->isCancelled()) return UNDO_FAIL; KIS_SAFE_ASSERT_RECOVER_NOOP(!buddyFound || lastStroke->isCancelled() == lastBuddy->isCancelled()); KIS_SAFE_ASSERT_RECOVER_NOOP(lastBuddy->isEnded()); if (!lastStroke->canCancel()) { return UNDO_WAIT; } lastStroke->cancelStroke(); if (buddyFound && lastBuddy->canCancel()) { lastBuddy->cancelStroke(); } else { // TODO: assert that checks that there is no other lodn strokes locker.unlock(); m_d->lodNUndoStore.undo(); m_d->lodNUndoStore.purgeRedoState(); locker.relock(); } result = UNDO_OK; return result; } void KisStrokesQueue::Private::tryClearUndoOnStrokeCompletion(KisStrokeSP finishingStroke) { if (finishingStroke->type() != KisStroke::RESUME) return; bool hasResumeStrokes = false; bool hasLod0Strokes = false; Q_FOREACH (KisStrokeSP stroke, strokesQueue) { if (stroke == finishingStroke) continue; hasLod0Strokes |= stroke->type() == KisStroke::LOD0; hasResumeStrokes |= stroke->type() == KisStroke::RESUME; } KIS_SAFE_ASSERT_RECOVER_NOOP(!hasLod0Strokes || hasResumeStrokes); if (!hasResumeStrokes && !hasLod0Strokes) { lodNUndoStore.clear(); } } void KisStrokesQueue::processQueue(KisUpdaterContext &updaterContext, bool externalJobsPending) { updaterContext.lock(); m_d->mutex.lock(); while(updaterContext.hasSpareThread() && processOneJob(updaterContext, externalJobsPending)); m_d->mutex.unlock(); updaterContext.unlock(); } bool KisStrokesQueue::needsExclusiveAccess() const { return m_d->needsExclusiveAccess; } bool KisStrokesQueue::wrapAroundModeSupported() const { return m_d->wrapAroundModeSupported; } +qreal KisStrokesQueue::balancingRatioOverride() const +{ + return m_d->balancingRatioOverride; +} + bool KisStrokesQueue::isEmpty() const { QMutexLocker locker(&m_d->mutex); return m_d->strokesQueue.isEmpty(); } qint32 KisStrokesQueue::sizeMetric() const { QMutexLocker locker(&m_d->mutex); if(m_d->strokesQueue.isEmpty()) return 0; // just a rough approximation return qMax(1, m_d->strokesQueue.head()->numJobs()) * m_d->strokesQueue.size(); } void KisStrokesQueue::Private::switchDesiredLevelOfDetail(bool forced) { if (forced || nextDesiredLevelOfDetail != desiredLevelOfDetail) { Q_FOREACH (KisStrokeSP stroke, strokesQueue) { if (stroke->type() != KisStroke::LEGACY) return; } const bool forgettable = forced && !lodNNeedsSynchronization && desiredLevelOfDetail == nextDesiredLevelOfDetail; desiredLevelOfDetail = nextDesiredLevelOfDetail; lodNNeedsSynchronization |= !forgettable; if (desiredLevelOfDetail) { startLod0ToNStroke(desiredLevelOfDetail, forgettable); } } } void KisStrokesQueue::explicitRegenerateLevelOfDetail() { QMutexLocker locker(&m_d->mutex); m_d->switchDesiredLevelOfDetail(true); } void KisStrokesQueue::setDesiredLevelOfDetail(int lod) { QMutexLocker locker(&m_d->mutex); if (lod == m_d->nextDesiredLevelOfDetail) return; m_d->nextDesiredLevelOfDetail = lod; m_d->switchDesiredLevelOfDetail(false); } void KisStrokesQueue::notifyUFOChangedImage() { QMutexLocker locker(&m_d->mutex); m_d->lodNNeedsSynchronization = true; } void KisStrokesQueue::debugDumpAllStrokes() { QMutexLocker locker(&m_d->mutex); qDebug() <<"==="; Q_FOREACH (KisStrokeSP stroke, m_d->strokesQueue) { qDebug() << ppVar(stroke->name()) << ppVar(stroke->type()) << ppVar(stroke->numJobs()) << ppVar(stroke->isInitialized()) << ppVar(stroke->isCancelled()); } qDebug() <<"==="; } void KisStrokesQueue::setLod0ToNStrokeStrategyFactory(const KisLodSyncStrokeStrategyFactory &factory) { m_d->lod0ToNStrokeStrategyFactory = factory; } void KisStrokesQueue::setSuspendUpdatesStrokeStrategyFactory(const KisSuspendResumeStrategyFactory &factory) { m_d->suspendUpdatesStrokeStrategyFactory = factory; } void KisStrokesQueue::setResumeUpdatesStrokeStrategyFactory(const KisSuspendResumeStrategyFactory &factory) { m_d->resumeUpdatesStrokeStrategyFactory = factory; } KisPostExecutionUndoAdapter *KisStrokesQueue::lodNPostExecutionUndoAdapter() const { return &m_d->lodNPostExecutionUndoAdapter; } KUndo2MagicString KisStrokesQueue::currentStrokeName() const { QMutexLocker locker(&m_d->mutex); if(m_d->strokesQueue.isEmpty()) return KUndo2MagicString(); return m_d->strokesQueue.head()->name(); } bool KisStrokesQueue::hasOpenedStrokes() const { QMutexLocker locker(&m_d->mutex); return m_d->openedStrokesCounter; } bool KisStrokesQueue::processOneJob(KisUpdaterContext &updaterContext, bool externalJobsPending) { if(m_d->strokesQueue.isEmpty()) return false; bool result = false; - qint32 numMergeJobs; - qint32 numStrokeJobs; - updaterContext.getJobsSnapshot(numMergeJobs, numStrokeJobs); + const int levelOfDetail = updaterContext.currentLevelOfDetail(); + + const KisUpdaterContextSnapshotEx snapshot = updaterContext.getContextSnapshotEx(); - int levelOfDetail = updaterContext.currentLevelOfDetail(); + const bool hasStrokeJobs = !(snapshot == ContextEmpty || + snapshot == HasMergeJob); + const bool hasMergeJobs = snapshot & HasMergeJob; - if(checkStrokeState(numStrokeJobs, levelOfDetail) && - checkExclusiveProperty(numMergeJobs, numStrokeJobs) && - checkSequentialProperty(numMergeJobs, numStrokeJobs) && - checkBarrierProperty(numMergeJobs, numStrokeJobs, - externalJobsPending)) { + if(checkStrokeState(hasStrokeJobs, levelOfDetail) && + checkExclusiveProperty(hasMergeJobs, hasStrokeJobs) && + checkSequentialProperty(snapshot, externalJobsPending)) { KisStrokeSP stroke = m_d->strokesQueue.head(); updaterContext.addStrokeJob(stroke->popOneJob()); result = true; } return result; } bool KisStrokesQueue::checkStrokeState(bool hasStrokeJobsRunning, int runningLevelOfDetail) { KisStrokeSP stroke = m_d->strokesQueue.head(); bool result = false; /** * We cannot start/continue a stroke if its LOD differs from * the one that is running on CPU */ bool hasLodCompatibility = checkLevelOfDetailProperty(runningLevelOfDetail); bool hasJobs = stroke->hasJobs(); /** * The stroke may be cancelled very fast. In this case it will * end up in the state: * * !stroke->isInitialized() && stroke->isEnded() && !stroke->hasJobs() * * This means that !isInitialised() doesn't imply there are any * jobs present. */ if(!stroke->isInitialized() && hasJobs && hasLodCompatibility) { /** * It might happen that the stroke got initialized, but its job was not * started due to some other reasons like exclusivity. Therefore the * stroke might end up in loaded, but uninitialized state. */ if (!m_d->currentStrokeLoaded) { m_d->needsExclusiveAccess = stroke->isExclusive(); m_d->wrapAroundModeSupported = stroke->supportsWrapAroundMode(); + m_d->balancingRatioOverride = stroke->balancingRatioOverride(); m_d->currentStrokeLoaded = true; } result = true; } else if(hasJobs && hasLodCompatibility) { /** * If the stroke has no initialization phase, then it can * arrive here unloaded. */ if (!m_d->currentStrokeLoaded) { m_d->needsExclusiveAccess = stroke->isExclusive(); m_d->wrapAroundModeSupported = stroke->supportsWrapAroundMode(); + m_d->balancingRatioOverride = stroke->balancingRatioOverride(); m_d->currentStrokeLoaded = true; } result = true; } else if(stroke->isEnded() && !hasJobs && !hasStrokeJobsRunning) { m_d->tryClearUndoOnStrokeCompletion(stroke); m_d->strokesQueue.dequeue(); // deleted by shared pointer m_d->needsExclusiveAccess = false; m_d->wrapAroundModeSupported = false; + m_d->balancingRatioOverride = -1.0; m_d->currentStrokeLoaded = false; m_d->switchDesiredLevelOfDetail(false); if(!m_d->strokesQueue.isEmpty()) { result = checkStrokeState(false, runningLevelOfDetail); } } return result; } -bool KisStrokesQueue::checkExclusiveProperty(qint32 numMergeJobs, - qint32 numStrokeJobs) +bool KisStrokesQueue::checkExclusiveProperty(bool hasMergeJobs, + bool hasStrokeJobs) { + Q_UNUSED(hasStrokeJobs); + if(!m_d->strokesQueue.head()->isExclusive()) return true; - Q_UNUSED(numMergeJobs); - Q_UNUSED(numStrokeJobs); - Q_ASSERT(!(numMergeJobs && numStrokeJobs)); - return numMergeJobs == 0; + return hasMergeJobs == 0; } -bool KisStrokesQueue::checkSequentialProperty(qint32 numMergeJobs, - qint32 numStrokeJobs) +bool KisStrokesQueue::checkSequentialProperty(KisUpdaterContextSnapshotEx snapshot, + bool externalJobsPending) { - Q_UNUSED(numMergeJobs); - KisStrokeSP stroke = m_d->strokesQueue.head(); - if(!stroke->prevJobSequential() && !stroke->nextJobSequential()) return true; - Q_ASSERT(!stroke->prevJobSequential() || numStrokeJobs <= 1); - return numStrokeJobs == 0; -} + if (snapshot & HasSequentialJob || + snapshot & HasBarrierJob) { + return false; + } -bool KisStrokesQueue::checkBarrierProperty(qint32 numMergeJobs, - qint32 numStrokeJobs, - bool externalJobsPending) -{ - KisStrokeSP stroke = m_d->strokesQueue.head(); - if(!stroke->nextJobBarrier()) return true; + KisStrokeJobData::Sequentiality nextSequentiality = + stroke->nextJobSequentiality(); + + if (nextSequentiality == KisStrokeJobData::UNIQUELY_CONCURRENT && + snapshot & HasUniquelyConcurrentJob) { + + return false; + } + + if (nextSequentiality == KisStrokeJobData::SEQUENTIAL && + (snapshot & HasUniquelyConcurrentJob || + snapshot & HasConcurrentJob)) { - return !numMergeJobs && !numStrokeJobs && !externalJobsPending; + return false; + } + + if (nextSequentiality == KisStrokeJobData::BARRIER && + (snapshot & HasUniquelyConcurrentJob || + snapshot & HasConcurrentJob || + snapshot & HasMergeJob || + externalJobsPending)) { + + return false; + } + + return true; } bool KisStrokesQueue::checkLevelOfDetailProperty(int runningLevelOfDetail) { KisStrokeSP stroke = m_d->strokesQueue.head(); return runningLevelOfDetail < 0 || stroke->worksOnLevelOfDetail() == runningLevelOfDetail; } diff --git a/libs/image/kis_strokes_queue.h b/libs/image/kis_strokes_queue.h index 08e8639a56..b7fda7dd85 100644 --- a/libs/image/kis_strokes_queue.h +++ b/libs/image/kis_strokes_queue.h @@ -1,101 +1,106 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_STROKES_QUEUE_H #define __KIS_STROKES_QUEUE_H #include "kritaimage_export.h" #include "kundo2magicstring.h" #include "kis_types.h" #include "kis_stroke_job_strategy.h" #include "kis_stroke_strategy.h" #include "kis_stroke_strategy_factory.h" #include "kis_strokes_queue_undo_result.h" - +#include "KisStrokesQueueMutatedJobInterface.h" +#include "KisUpdaterContextSnapshotEx.h" class KisUpdaterContext; class KisStroke; class KisStrokeStrategy; class KisStrokeJobData; class KisPostExecutionUndoAdapter; -class KRITAIMAGE_EXPORT KisStrokesQueue +class KRITAIMAGE_EXPORT KisStrokesQueue : public KisStrokesQueueMutatedJobInterface { public: KisStrokesQueue(); ~KisStrokesQueue(); KisStrokeId startStroke(KisStrokeStrategy *strokeStrategy); void addJob(KisStrokeId id, KisStrokeJobData *data); void endStroke(KisStrokeId id); bool cancelStroke(KisStrokeId id); bool tryCancelCurrentStrokeAsync(); UndoResult tryUndoLastStrokeAsync(); void processQueue(KisUpdaterContext &updaterContext, bool externalJobsPending); bool needsExclusiveAccess() const; bool isEmpty() const; qint32 sizeMetric() const; KUndo2MagicString currentStrokeName() const; bool hasOpenedStrokes() const; bool wrapAroundModeSupported() const; + qreal balancingRatioOverride() const; void setDesiredLevelOfDetail(int lod); void explicitRegenerateLevelOfDetail(); void setLod0ToNStrokeStrategyFactory(const KisLodSyncStrokeStrategyFactory &factory); void setSuspendUpdatesStrokeStrategyFactory(const KisSuspendResumeStrategyFactory &factory); void setResumeUpdatesStrokeStrategyFactory(const KisSuspendResumeStrategyFactory &factory); KisPostExecutionUndoAdapter* lodNPostExecutionUndoAdapter() const; /** * Notifies the queue, that someone else (neither strokes nor the * queue itself have changed the image. It means that the caches * should be regenerated */ void notifyUFOChangedImage(); void debugDumpAllStrokes(); + // interface for KisStrokeStrategy only! + void addMutatedJobs(KisStrokeId id, const QVector list) final; + private: bool processOneJob(KisUpdaterContext &updaterContext, bool externalJobsPending); bool checkStrokeState(bool hasStrokeJobsRunning, int runningLevelOfDetail); - bool checkExclusiveProperty(qint32 numMergeJobs, qint32 numStrokeJobs); - bool checkSequentialProperty(qint32 numMergeJobs, qint32 numStrokeJobs); - bool checkBarrierProperty(qint32 numMergeJobs, qint32 numStrokeJobs, + bool checkExclusiveProperty(bool hasMergeJobs, bool hasStrokeJobs); + bool checkSequentialProperty(KisUpdaterContextSnapshotEx snapshot, bool externalJobsPending); + bool checkBarrierProperty(bool hasMergeJobs, bool hasStrokeJobs, bool externalJobsPending); bool checkLevelOfDetailProperty(int runningLevelOfDetail); class LodNUndoStrokesFacade; KisStrokeId startLodNUndoStroke(KisStrokeStrategy *strokeStrategy); private: struct Private; Private * const m_d; }; #endif /* __KIS_STROKES_QUEUE_H */ diff --git a/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp b/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp index 3f890c5dc5..638b43bd25 100644 --- a/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp +++ b/libs/image/kis_suspend_projection_updates_stroke_strategy.cpp @@ -1,303 +1,305 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_suspend_projection_updates_stroke_strategy.h" #include #include #include inline uint qHash(const QRect &rc) { return rc.x() + (rc.y() << 16) + (rc.width() << 8) + (rc.height() << 24); } struct KisSuspendProjectionUpdatesStrokeStrategy::Private { KisImageWSP image; bool suspend; class SuspendLod0Updates : public KisProjectionUpdatesFilter { struct Request { Request() : resetAnimationCache(false) {} Request(const QRect &_rect, bool _resetAnimationCache) : rect(_rect), resetAnimationCache(_resetAnimationCache) { } QRect rect; bool resetAnimationCache; }; typedef QHash > RectsHash; public: SuspendLod0Updates() { } - bool filter(KisImage *image, KisNode *node, const QRect &rect, bool resetAnimationCache) override { + bool filter(KisImage *image, KisNode *node, const QVector &rects, bool resetAnimationCache) override { if (image->currentLevelOfDetail() > 0) return false; QMutexLocker l(&m_mutex); - m_requestsHash[KisNodeSP(node)].append(Request(rect, resetAnimationCache)); + + Q_FOREACH(const QRect &rc, rects) { + m_requestsHash[KisNodeSP(node)].append(Request(rc, resetAnimationCache)); + } + return true; } static inline QRect alignRect(const QRect &rc, const int step) { static const int decstep = step - 1; static const int invstep = ~decstep; int x0, y0, x1, y1; rc.getCoords(&x0, &y0, &x1, &y1); x0 &= invstep; y0 &= invstep; x1 |= decstep; y1 |= decstep; QRect result; result.setCoords(x0, y0, x1, y1); return result; } void notifyUpdates(KisNodeGraphListener *listener) { RectsHash::const_iterator it = m_requestsHash.constBegin(); RectsHash::const_iterator end = m_requestsHash.constEnd(); const int step = 64; for (; it != end; ++it) { KisNodeSP node = it.key(); QRegion region; bool resetAnimationCache = false; Q_FOREACH (const Request &req, it.value()) { region += alignRect(req.rect, step); resetAnimationCache |= req.resetAnimationCache; } - Q_FOREACH (const QRect &rc, region.rects()) { - // FIXME: constness: port rPU to SP - listener->requestProjectionUpdate(const_cast(node.data()), rc, resetAnimationCache); - } + // FIXME: constness: port rPU to SP + listener->requestProjectionUpdate(const_cast(node.data()), region.rects(), resetAnimationCache); } } private: RectsHash m_requestsHash; QMutex m_mutex; }; class SuspendData : public KisStrokeJobData { public: SuspendData() : KisStrokeJobData(SEQUENTIAL) {} }; class ResumeAndIssueGraphUpdatesData : public KisStrokeJobData { public: ResumeAndIssueGraphUpdatesData() : KisStrokeJobData(SEQUENTIAL) {} }; class UpdatesBarrierData : public KisStrokeJobData { public: UpdatesBarrierData() : KisStrokeJobData(BARRIER) {} }; class IssueCanvasUpdatesData : public KisStrokeJobData { public: IssueCanvasUpdatesData(QRect _updateRect) : KisStrokeJobData(CONCURRENT), updateRect(_updateRect) {} QRect updateRect; }; }; KisSuspendProjectionUpdatesStrokeStrategy::KisSuspendProjectionUpdatesStrokeStrategy(KisImageWSP image, bool suspend) : KisSimpleStrokeStrategy(suspend ? "suspend_stroke_strategy" : "resume_stroke_strategy"), m_d(new Private) { m_d->image = image; m_d->suspend = suspend; /** * Here we add a dumb INIT job so that KisStrokesQueue would know that the * stroke has already started or not. When the queue reaches the resume * stroke ans starts its execution, no Lod0 can execute anymore. So all the * new Lod0 strokes should go to the end of the queue and wrapped into * their own Suspend/Resume pair. */ enableJob(JOB_INIT, true); enableJob(JOB_DOSTROKE, true); enableJob(JOB_CANCEL, true); setNeedsExplicitCancel(true); } KisSuspendProjectionUpdatesStrokeStrategy::~KisSuspendProjectionUpdatesStrokeStrategy() { } /** * When the Lod0 stroke is being recalculated in the background we * should block all the updates it issues to avoid user distraction. * The result of the final stroke should be shown to the user in the * very end when everything is fully ready. Ideally the use should not * notice that the image has changed :) * * (Don't mix this process with suspend/resume capabilities of a * single stroke. That is a different system!) * * The process of the Lod0 regeneration consists of the following: * * 1) Suspend stroke executes. It sets a special updates filter on the * image. The filter blocks all the updates and saves them in an * internal structure to be emitted in the future. * * 2) Lod0 strokes are being recalculated. All their updates are * blocked and saved in the filter. * * 3) Resume stroke starts: * * 3.1) First it disables emitting of sigImageUpdated() so the gui * will not get any update notifications. * * 3.2) Then it enables updates themselves. * * 3.3) Initiates all the updates that were requested by the Lod0 * stroke. The node graph is regenerated, but the GUI does * not get this change. * * 3.4) Special barrier job waits for all the updates to finish * and, when they are done, enables GUI notifications again. * * 3.5) In a multithreaded way emits the GUI notifications for the * entire image. Multithreaded way is used to conform the * double-stage update principle of KisCanvas2. */ void KisSuspendProjectionUpdatesStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) { Private::SuspendData *suspendData = dynamic_cast(data); Private::ResumeAndIssueGraphUpdatesData *resumeData = dynamic_cast(data); Private::UpdatesBarrierData *barrierData = dynamic_cast(data); Private::IssueCanvasUpdatesData *canvasUpdates = dynamic_cast(data); KisImageSP image = m_d->image.toStrongRef(); if (!image) { return; } if (suspendData) { image->setProjectionUpdatesFilter( KisProjectionUpdatesFilterSP(new Private::SuspendLod0Updates())); } else if (resumeData) { image->disableUIUpdates(); resumeAndIssueUpdates(false); } else if (barrierData) { image->enableUIUpdates(); } else if (canvasUpdates) { image->notifyProjectionUpdated(canvasUpdates->updateRect); } } QList KisSuspendProjectionUpdatesStrokeStrategy::createSuspendJobsData(KisImageWSP /*image*/) { QList jobsData; jobsData << new Private::SuspendData(); return jobsData; } QList KisSuspendProjectionUpdatesStrokeStrategy::createResumeJobsData(KisImageWSP _image) { QList jobsData; jobsData << new Private::ResumeAndIssueGraphUpdatesData(); jobsData << new Private::UpdatesBarrierData(); using KritaUtils::splitRectIntoPatches; using KritaUtils::optimalPatchSize; KisImageSP image = _image; QVector rects = splitRectIntoPatches(image->bounds(), optimalPatchSize()); Q_FOREACH (const QRect &rc, rects) { jobsData << new Private::IssueCanvasUpdatesData(rc); } return jobsData; } void KisSuspendProjectionUpdatesStrokeStrategy::resumeAndIssueUpdates(bool dropUpdates) { KisImageSP image = m_d->image.toStrongRef(); if (!image) { return; } KisProjectionUpdatesFilterSP filter = image->projectionUpdatesFilter(); if (!filter) return; Private::SuspendLod0Updates *localFilter = dynamic_cast(filter.data()); if (localFilter) { image->setProjectionUpdatesFilter(KisProjectionUpdatesFilterSP()); if (!dropUpdates) { localFilter->notifyUpdates(image.data()); } } } void KisSuspendProjectionUpdatesStrokeStrategy::cancelStrokeCallback() { KisImageSP image = m_d->image.toStrongRef(); if (!image) { return; } /** * We shouldn't emit any ad-hoc updates when cancelling the * stroke. It generates weird temporary holes on the canvas, * making the user feel awful, thinking his image got * corrupted. We will just emit a common refreshGraphAsync() that * will do all the work in a beautiful way */ resumeAndIssueUpdates(true); if (!m_d->suspend) { // FIXME: optimize image->refreshGraphAsync(); } } diff --git a/libs/image/kis_update_job_item.h b/libs/image/kis_update_job_item.h index debaf3d6be..3ac09e3436 100644 --- a/libs/image/kis_update_job_item.h +++ b/libs/image/kis_update_job_item.h @@ -1,232 +1,257 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_UPDATE_JOB_ITEM_H #define __KIS_UPDATE_JOB_ITEM_H +#include + #include #include #include "kis_stroke_job.h" #include "kis_spontaneous_job.h" #include "kis_base_rects_walker.h" #include "kis_async_merger.h" class KisUpdateJobItem : public QObject, public QRunnable { Q_OBJECT public: - enum Type { - EMPTY, + enum class Type : int { + EMPTY = 0, + WAITING, MERGE, STROKE, SPONTANEOUS }; public: KisUpdateJobItem(QReadWriteLock *exclusiveJobLock) : m_exclusiveJobLock(exclusiveJobLock), - m_type(EMPTY), + m_atomicType(Type::EMPTY), m_runnableJob(0) { setAutoDelete(false); + KIS_SAFE_ASSERT_RECOVER_NOOP(m_atomicType.is_lock_free()); } ~KisUpdateJobItem() override { delete m_runnableJob; } void run() override { if (!isRunning()) return; /** * Here we break the idea of QThreadPool a bit. Ideally, we should split the * jobs into distinct QRunnable objects and pass all of them to QThreadPool. * That is a nice idea, but it doesn't work well when the jobs are small enough * and the number of available cores is high (>4 cores). It this case the * threads just tend to execute the job very quickly and go to sleep, which is - * an expencive operation. + * an expensive operation. * * To overcome this problem we try to bulk-process the jobs. In sigJobFinished() * signal (which is DirectConnection), the context may add the job to ourselves(!!!), * so we switch from "done" state into "running" again. */ - while (isRunning()) { - m_isExecuting.ref(); + while (1) { + KIS_SAFE_ASSERT_RECOVER_RETURN(isRunning()); if(m_exclusive) { m_exclusiveJobLock->lockForWrite(); } else { m_exclusiveJobLock->lockForRead(); } - if(m_type == MERGE) { + if(m_atomicType == Type::MERGE) { runMergeJob(); } else { - Q_ASSERT(m_type == STROKE || m_type == SPONTANEOUS); + KIS_ASSERT(m_atomicType == Type::STROKE || + m_atomicType == Type::SPONTANEOUS); + m_runnableJob->run(); - delete m_runnableJob; - m_runnableJob = 0; } setDone(); emit sigDoSomeUsefulWork(); + + // may flip the current state from Waiting -> Running again emit sigJobFinished(); m_exclusiveJobLock->unlock(); - m_isExecuting.deref(); + // try to exit the loop. Please note, that noone can flip the state from + // WAITING to EMPTY except ourselves! + Type expectedValue = Type::WAITING; + if (m_atomicType.compare_exchange_strong(expectedValue, Type::EMPTY)) { + break; + } } } inline void runMergeJob() { - Q_ASSERT(m_type == MERGE); + Q_ASSERT(m_atomicType == Type::MERGE); // dbgKrita << "Executing merge job" << m_walker->changeRect() // << "on thread" << QThread::currentThreadId(); m_merger.startMerge(*m_walker); QRect changeRect = m_walker->changeRect(); emit sigContinueUpdate(changeRect); } - inline void setWalker(KisBaseRectsWalkerSP walker) { - m_type = MERGE; + // return true if the thread should actually be started + inline bool setWalker(KisBaseRectsWalkerSP walker) { + KIS_ASSERT(m_atomicType <= Type::WAITING); + m_accessRect = walker->accessRect(); m_changeRect = walker->changeRect(); m_walker = walker; m_exclusive = false; m_runnableJob = 0; + + const Type oldState = m_atomicType.exchange(Type::MERGE); + return oldState == Type::EMPTY; } - inline void setStrokeJob(KisStrokeJob *strokeJob) { - m_type = STROKE; + // return true if the thread should actually be started + inline bool setStrokeJob(KisStrokeJob *strokeJob) { + KIS_ASSERT(m_atomicType <= Type::WAITING); + m_runnableJob = strokeJob; + m_strokeJobSequentiality = strokeJob->sequentiality(); m_exclusive = strokeJob->isExclusive(); m_walker = 0; m_accessRect = m_changeRect = QRect(); + + const Type oldState = m_atomicType.exchange(Type::STROKE); + return oldState == Type::EMPTY; } - inline void setSpontaneousJob(KisSpontaneousJob *spontaneousJob) { - m_type = SPONTANEOUS; + // return true if the thread should actually be started + inline bool setSpontaneousJob(KisSpontaneousJob *spontaneousJob) { + KIS_ASSERT(m_atomicType <= Type::WAITING); + m_runnableJob = spontaneousJob; m_exclusive = false; m_walker = 0; m_accessRect = m_changeRect = QRect(); + + const Type oldState = m_atomicType.exchange(Type::SPONTANEOUS); + return oldState == Type::EMPTY; } inline void setDone() { m_walker = 0; + delete m_runnableJob; m_runnableJob = 0; - m_type = EMPTY; + m_atomicType = Type::WAITING; } inline bool isRunning() const { - return m_type != EMPTY; + return m_atomicType >= Type::MERGE; } inline Type type() const { - return m_type; + return m_atomicType; } inline const QRect& accessRect() const { return m_accessRect; } inline const QRect& changeRect() const { return m_changeRect; } - inline bool hasThreadAttached() const { - return m_isExecuting; + inline KisStrokeJobData::Sequentiality strokeJobSequentiality() const { + return m_strokeJobSequentiality; } Q_SIGNALS: void sigContinueUpdate(const QRect& rc); void sigDoSomeUsefulWork(); void sigJobFinished(); private: /** * Open walker and stroke job for the testing suite. * Please, do not use it in production code. */ friend class KisSimpleUpdateQueueTest; friend class KisStrokesQueueTest; friend class KisUpdateSchedulerTest; friend class KisTestableUpdaterContext; inline KisBaseRectsWalkerSP walker() const { return m_walker; } inline KisStrokeJob* strokeJob() const { KisStrokeJob *job = dynamic_cast(m_runnableJob); Q_ASSERT(job); return job; } inline void testingSetDone() { - if(m_type == STROKE) { - delete m_runnableJob; - } setDone(); } private: /** * \see KisUpdaterContext::m_exclusiveJobLock */ QReadWriteLock *m_exclusiveJobLock; bool m_exclusive; - volatile Type m_type; + std::atomic m_atomicType; + + volatile KisStrokeJobData::Sequentiality m_strokeJobSequentiality; /** * Runnable jobs part * The job is owned by the context and deleted after completion */ KisRunnable *m_runnableJob; /** * Merge jobs part */ KisBaseRectsWalkerSP m_walker; KisAsyncMerger m_merger; /** * These rects cache actual values from the walker * to eliminate concurrent access to a walker structure */ QRect m_accessRect; QRect m_changeRect; - - QAtomicInt m_isExecuting; }; #endif /* __KIS_UPDATE_JOB_ITEM_H */ diff --git a/libs/image/kis_update_scheduler.cpp b/libs/image/kis_update_scheduler.cpp index dd198505e4..1fc6e373e1 100644 --- a/libs/image/kis_update_scheduler.cpp +++ b/libs/image/kis_update_scheduler.cpp @@ -1,493 +1,504 @@ /* * Copyright (c) 2010 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_update_scheduler.h" #include "klocalizedstring.h" #include "kis_image_config.h" #include "kis_merge_walker.h" #include "kis_full_refresh_walker.h" #include "kis_updater_context.h" #include "kis_simple_update_queue.h" #include "kis_strokes_queue.h" #include "kis_queues_progress_updater.h" #include "KisUpdateSchedulerConfigNotifier.h" #include #include "kis_lazy_wait_condition.h" #include //#define DEBUG_BALANCING #ifdef DEBUG_BALANCING #define DEBUG_BALANCING_METRICS(decidedFirst, excl) \ dbgKrita << "Balance decision:" << decidedFirst \ << "(" << excl << ")" \ << "updates:" << m_d->updatesQueue.sizeMetric() \ << "strokes:" << m_d->strokesQueue.sizeMetric() #else #define DEBUG_BALANCING_METRICS(decidedFirst, excl) #endif struct Q_DECL_HIDDEN KisUpdateScheduler::Private { Private(KisUpdateScheduler *_q, KisProjectionUpdateListener *p) : q(_q) , updaterContext(KisImageConfig().maxNumberOfThreads(), q) , projectionUpdateListener(p) {} KisUpdateScheduler *q; KisSimpleUpdateQueue updatesQueue; KisStrokesQueue strokesQueue; KisUpdaterContext updaterContext; bool processingBlocked = false; - qreal balancingRatio = 1.0; // updates-queue-size/strokes-queue-size + qreal defaultBalancingRatio = 1.0; // desired strokes-queue-size / updates-queue-size KisProjectionUpdateListener *projectionUpdateListener; KisQueuesProgressUpdater *progressUpdater = 0; QAtomicInt updatesLockCounter; QReadWriteLock updatesStartLock; KisLazyWaitCondition updatesFinishedCondition; + + qreal balancingRatio() const { + const qreal strokeRatioOverride = strokesQueue.balancingRatioOverride(); + return strokeRatioOverride > 0 ? strokeRatioOverride : defaultBalancingRatio; + } }; KisUpdateScheduler::KisUpdateScheduler(KisProjectionUpdateListener *projectionUpdateListener, QObject *parent) : QObject(parent), m_d(new Private(this, projectionUpdateListener)) { updateSettings(); connectSignals(); } KisUpdateScheduler::KisUpdateScheduler() : m_d(new Private(this, 0)) { } KisUpdateScheduler::~KisUpdateScheduler() { delete m_d->progressUpdater; delete m_d; } void KisUpdateScheduler::setThreadsLimit(int value) { KIS_SAFE_ASSERT_RECOVER_RETURN(!m_d->processingBlocked); barrierLock(); m_d->updaterContext.lock(); m_d->updaterContext.setThreadsLimit(value); m_d->updaterContext.unlock(); unlock(false); } int KisUpdateScheduler::threadsLimit() const { std::lock_guard l(m_d->updaterContext); return m_d->updaterContext.threadsLimit(); } void KisUpdateScheduler::connectSignals() { connect(&m_d->updaterContext, SIGNAL(sigContinueUpdate(const QRect&)), SLOT(continueUpdate(const QRect&)), Qt::DirectConnection); connect(&m_d->updaterContext, SIGNAL(sigDoSomeUsefulWork()), SLOT(doSomeUsefulWork()), Qt::DirectConnection); connect(&m_d->updaterContext, SIGNAL(sigSpareThreadAppeared()), SLOT(spareThreadAppeared()), Qt::DirectConnection); connect(KisUpdateSchedulerConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(updateSettings())); } void KisUpdateScheduler::setProgressProxy(KoProgressProxy *progressProxy) { delete m_d->progressUpdater; m_d->progressUpdater = progressProxy ? new KisQueuesProgressUpdater(progressProxy, this) : 0; } void KisUpdateScheduler::progressUpdate() { if (!m_d->progressUpdater) return; if(!m_d->strokesQueue.hasOpenedStrokes()) { QString jobName = m_d->strokesQueue.currentStrokeName().toString(); if(jobName.isEmpty()) { jobName = i18n("Updating..."); } int sizeMetric = m_d->strokesQueue.sizeMetric(); if (!sizeMetric) { sizeMetric = m_d->updatesQueue.sizeMetric(); } m_d->progressUpdater->updateProgress(sizeMetric, jobName); } else { m_d->progressUpdater->hide(); } } -void KisUpdateScheduler::updateProjection(KisNodeSP node, const QRect& rc, const QRect &cropRect) +void KisUpdateScheduler::updateProjection(KisNodeSP node, const QVector &rects, const QRect &cropRect) +{ + m_d->updatesQueue.addUpdateJob(node, rects, cropRect, currentLevelOfDetail()); + processQueues(); +} + +void KisUpdateScheduler::updateProjection(KisNodeSP node, const QRect &rc, const QRect &cropRect) { m_d->updatesQueue.addUpdateJob(node, rc, cropRect, currentLevelOfDetail()); processQueues(); } void KisUpdateScheduler::updateProjectionNoFilthy(KisNodeSP node, const QRect& rc, const QRect &cropRect) { m_d->updatesQueue.addUpdateNoFilthyJob(node, rc, cropRect, currentLevelOfDetail()); processQueues(); } void KisUpdateScheduler::fullRefreshAsync(KisNodeSP root, const QRect& rc, const QRect &cropRect) { m_d->updatesQueue.addFullRefreshJob(root, rc, cropRect, currentLevelOfDetail()); processQueues(); } void KisUpdateScheduler::fullRefresh(KisNodeSP root, const QRect& rc, const QRect &cropRect) { KisBaseRectsWalkerSP walker = new KisFullRefreshWalker(cropRect); walker->collectRects(root, rc); bool needLock = true; if(m_d->processingBlocked) { warnImage << "WARNING: Calling synchronous fullRefresh under a scheduler lock held"; warnImage << "We will not assert for now, but please port caller's to strokes"; warnImage << "to avoid this warning"; needLock = false; } if(needLock) lock(); m_d->updaterContext.lock(); Q_ASSERT(m_d->updaterContext.isJobAllowed(walker)); m_d->updaterContext.addMergeJob(walker); m_d->updaterContext.waitForDone(); m_d->updaterContext.unlock(); if(needLock) unlock(true); } void KisUpdateScheduler::addSpontaneousJob(KisSpontaneousJob *spontaneousJob) { m_d->updatesQueue.addSpontaneousJob(spontaneousJob); processQueues(); } KisStrokeId KisUpdateScheduler::startStroke(KisStrokeStrategy *strokeStrategy) { KisStrokeId id = m_d->strokesQueue.startStroke(strokeStrategy); processQueues(); return id; } void KisUpdateScheduler::addJob(KisStrokeId id, KisStrokeJobData *data) { m_d->strokesQueue.addJob(id, data); processQueues(); } void KisUpdateScheduler::endStroke(KisStrokeId id) { m_d->strokesQueue.endStroke(id); processQueues(); } bool KisUpdateScheduler::cancelStroke(KisStrokeId id) { bool result = m_d->strokesQueue.cancelStroke(id); processQueues(); return result; } bool KisUpdateScheduler::tryCancelCurrentStrokeAsync() { return m_d->strokesQueue.tryCancelCurrentStrokeAsync(); } UndoResult KisUpdateScheduler::tryUndoLastStrokeAsync() { return m_d->strokesQueue.tryUndoLastStrokeAsync(); } bool KisUpdateScheduler::wrapAroundModeSupported() const { return m_d->strokesQueue.wrapAroundModeSupported(); } void KisUpdateScheduler::setDesiredLevelOfDetail(int lod) { m_d->strokesQueue.setDesiredLevelOfDetail(lod); /** * The queue might have started an internal stroke for * cache synchronization. Process the queues to execute * it if needed. */ processQueues(); } void KisUpdateScheduler::explicitRegenerateLevelOfDetail() { m_d->strokesQueue.explicitRegenerateLevelOfDetail(); // \see a comment in setDesiredLevelOfDetail() processQueues(); } int KisUpdateScheduler::currentLevelOfDetail() const { int levelOfDetail = -1; if (levelOfDetail < 0) { levelOfDetail = m_d->updaterContext.currentLevelOfDetail(); } if (levelOfDetail < 0) { levelOfDetail = m_d->updatesQueue.overrideLevelOfDetail(); } if (levelOfDetail < 0) { levelOfDetail = 0; } return levelOfDetail; } void KisUpdateScheduler::setLod0ToNStrokeStrategyFactory(const KisLodSyncStrokeStrategyFactory &factory) { m_d->strokesQueue.setLod0ToNStrokeStrategyFactory(factory); } void KisUpdateScheduler::setSuspendUpdatesStrokeStrategyFactory(const KisSuspendResumeStrategyFactory &factory) { m_d->strokesQueue.setSuspendUpdatesStrokeStrategyFactory(factory); } void KisUpdateScheduler::setResumeUpdatesStrokeStrategyFactory(const KisSuspendResumeStrategyFactory &factory) { m_d->strokesQueue.setResumeUpdatesStrokeStrategyFactory(factory); } KisPostExecutionUndoAdapter *KisUpdateScheduler::lodNPostExecutionUndoAdapter() const { return m_d->strokesQueue.lodNPostExecutionUndoAdapter(); } void KisUpdateScheduler::updateSettings() { m_d->updatesQueue.updateSettings(); KisImageConfig config; - m_d->balancingRatio = config.schedulerBalancingRatio(); + m_d->defaultBalancingRatio = config.schedulerBalancingRatio(); setThreadsLimit(config.maxNumberOfThreads()); } void KisUpdateScheduler::lock() { m_d->processingBlocked = true; m_d->updaterContext.waitForDone(); } void KisUpdateScheduler::unlock(bool resetLodLevels) { if (resetLodLevels) { /** * Legacy strokes may have changed the image while we didn't * control it. Notify the queue to take it into account. */ m_d->strokesQueue.notifyUFOChangedImage(); } m_d->processingBlocked = false; processQueues(); } bool KisUpdateScheduler::isIdle() { bool result = false; if (tryBarrierLock()) { result = true; unlock(false); } return result; } void KisUpdateScheduler::waitForDone() { do { processQueues(); m_d->updaterContext.waitForDone(); } while(!m_d->updatesQueue.isEmpty() || !m_d->strokesQueue.isEmpty()); } bool KisUpdateScheduler::tryBarrierLock() { if(!m_d->updatesQueue.isEmpty() || !m_d->strokesQueue.isEmpty()) { return false; } m_d->processingBlocked = true; m_d->updaterContext.waitForDone(); if(!m_d->updatesQueue.isEmpty() || !m_d->strokesQueue.isEmpty()) { m_d->processingBlocked = false; processQueues(); return false; } return true; } void KisUpdateScheduler::barrierLock() { do { m_d->processingBlocked = false; processQueues(); m_d->processingBlocked = true; m_d->updaterContext.waitForDone(); } while(!m_d->updatesQueue.isEmpty() || !m_d->strokesQueue.isEmpty()); } void KisUpdateScheduler::processQueues() { wakeUpWaitingThreads(); if(m_d->processingBlocked) return; if(m_d->strokesQueue.needsExclusiveAccess()) { DEBUG_BALANCING_METRICS("STROKES", "X"); m_d->strokesQueue.processQueue(m_d->updaterContext, !m_d->updatesQueue.isEmpty()); if(!m_d->strokesQueue.needsExclusiveAccess()) { tryProcessUpdatesQueue(); } } - else if(m_d->balancingRatio * m_d->strokesQueue.sizeMetric() > m_d->updatesQueue.sizeMetric()) { + else if(m_d->balancingRatio() * m_d->strokesQueue.sizeMetric() > m_d->updatesQueue.sizeMetric()) { DEBUG_BALANCING_METRICS("STROKES", "N"); m_d->strokesQueue.processQueue(m_d->updaterContext, !m_d->updatesQueue.isEmpty()); tryProcessUpdatesQueue(); } else { DEBUG_BALANCING_METRICS("UPDATES", "N"); tryProcessUpdatesQueue(); m_d->strokesQueue.processQueue(m_d->updaterContext, !m_d->updatesQueue.isEmpty()); } progressUpdate(); } void KisUpdateScheduler::blockUpdates() { m_d->updatesFinishedCondition.initWaiting(); m_d->updatesLockCounter.ref(); while(haveUpdatesRunning()) { m_d->updatesFinishedCondition.wait(); } m_d->updatesFinishedCondition.endWaiting(); } void KisUpdateScheduler::unblockUpdates() { m_d->updatesLockCounter.deref(); processQueues(); } void KisUpdateScheduler::wakeUpWaitingThreads() { if(m_d->updatesLockCounter && !haveUpdatesRunning()) { m_d->updatesFinishedCondition.wakeAll(); } } void KisUpdateScheduler::tryProcessUpdatesQueue() { QReadLocker locker(&m_d->updatesStartLock); if(m_d->updatesLockCounter) return; m_d->updatesQueue.processQueue(m_d->updaterContext); } bool KisUpdateScheduler::haveUpdatesRunning() { QWriteLocker locker(&m_d->updatesStartLock); qint32 numMergeJobs, numStrokeJobs; m_d->updaterContext.getJobsSnapshot(numMergeJobs, numStrokeJobs); return numMergeJobs; } void KisUpdateScheduler::continueUpdate(const QRect &rect) { Q_ASSERT(m_d->projectionUpdateListener); m_d->projectionUpdateListener->notifyProjectionUpdated(rect); } void KisUpdateScheduler::doSomeUsefulWork() { m_d->updatesQueue.optimize(); } void KisUpdateScheduler::spareThreadAppeared() { processQueues(); } KisTestableUpdateScheduler::KisTestableUpdateScheduler(KisProjectionUpdateListener *projectionUpdateListener, qint32 threadCount) { Q_UNUSED(threadCount); updateSettings(); m_d->projectionUpdateListener = projectionUpdateListener; // The queue will update settings in a constructor itself // m_d->updatesQueue = new KisTestableSimpleUpdateQueue(); // m_d->strokesQueue = new KisStrokesQueue(); // m_d->updaterContext = new KisTestableUpdaterContext(threadCount); connectSignals(); } KisTestableUpdaterContext* KisTestableUpdateScheduler::updaterContext() { return dynamic_cast(&m_d->updaterContext); } KisTestableSimpleUpdateQueue* KisTestableUpdateScheduler::updateQueue() { return dynamic_cast(&m_d->updatesQueue); } diff --git a/libs/image/kis_update_scheduler.h b/libs/image/kis_update_scheduler.h index fbd394d83d..85fcc204a5 100644 --- a/libs/image/kis_update_scheduler.h +++ b/libs/image/kis_update_scheduler.h @@ -1,252 +1,253 @@ /* * Copyright (c) 2010 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_UPDATE_SCHEDULER_H #define __KIS_UPDATE_SCHEDULER_H #include #include "kritaimage_export.h" #include "kis_types.h" #include "kis_image_interfaces.h" #include "kis_stroke_strategy_factory.h" #include "kis_strokes_queue_undo_result.h" class QRect; class KoProgressProxy; class KisProjectionUpdateListener; class KisSpontaneousJob; class KisPostExecutionUndoAdapter; class KRITAIMAGE_EXPORT KisUpdateScheduler : public QObject, public KisStrokesFacade { Q_OBJECT public: KisUpdateScheduler(KisProjectionUpdateListener *projectionUpdateListener, QObject *parent = 0); ~KisUpdateScheduler() override; /** * Set the number of threads used by the scheduler */ void setThreadsLimit(int value); /** * Return the number of threads available to the scheduler */ int threadsLimit() const; /** * Sets the proxy that is going to be notified about the progress * of processing of the queues. If you want to switch the proxy * on runtime, you should do it under the lock held. * * \see lock(), unlock() */ void setProgressProxy(KoProgressProxy *progressProxy); /** * Blocks processing of the queues. * The function will wait until all the executing jobs * are finished. * NOTE: you may add new jobs while the block held, but they * will be delayed until unlock() is called. * * \see unlock() */ void lock(); /** * Unblocks the process and calls processQueues() * * \see processQueues() */ void unlock(bool resetLodLevels = true); /** * Waits until all the running jobs are finished. * * If some other thread adds jobs in parallel, then you may * wait forever. If you you don't want it, consider lock() instead. * * \see lock() */ void waitForDone(); /** * Waits until the queues become empty, then blocks the processing. * To unblock processing you should use unlock(). * * If some other thread adds jobs in parallel, then you may * wait forever. If you you don't want it, consider lock() instead. * * \see unlock(), lock() */ void barrierLock(); /** * Works like barrier lock, but returns false immediately if barrierLock * can't be acquired. * * \see barrierLock() */ bool tryBarrierLock(); /** * Tells if there are no strokes or updates are running at the * moment. Internally calls to tryBarrierLock(), so it is not O(1). */ bool isIdle(); /** * Blocks all the updates from execution. It doesn't affect * strokes execution in any way. This type of lock is supposed * to be held by the strokes themselves when they need a short * access to some parts of the projection of the image. * * From all the other places you should use usual lock()/unlock() * methods * * \see lock(), unlock() */ void blockUpdates(); /** * Unblocks updates from execution previously locked by blockUpdates() * * \see blockUpdates() */ void unblockUpdates(); - void updateProjection(KisNodeSP node, const QRect& rc, const QRect &cropRect); + void updateProjection(KisNodeSP node, const QVector &rects, const QRect &cropRect); + void updateProjection(KisNodeSP node, const QRect &rc, const QRect &cropRect); void updateProjectionNoFilthy(KisNodeSP node, const QRect& rc, const QRect &cropRect); void fullRefreshAsync(KisNodeSP root, const QRect& rc, const QRect &cropRect); void fullRefresh(KisNodeSP root, const QRect& rc, const QRect &cropRect); void addSpontaneousJob(KisSpontaneousJob *spontaneousJob); KisStrokeId startStroke(KisStrokeStrategy *strokeStrategy) override; void addJob(KisStrokeId id, KisStrokeJobData *data) override; void endStroke(KisStrokeId id) override; bool cancelStroke(KisStrokeId id) override; /** * Sets the desired level of detail on which the strokes should * work. Please note that this configuration will be applied * starting from the next stroke. Please also note that this value * is not guaranteed to coincide with the one returned by * currentLevelOfDetail() */ void setDesiredLevelOfDetail(int lod); /** * Explicitly start regeneration of LoD planes of all the devices * in the image. This call should be performed when the user is idle, * just to make the quality of image updates better. */ void explicitRegenerateLevelOfDetail(); /** * Install a factory of a stroke strategy, that will be started * every time when the scheduler needs to synchronize LOD caches * of all the paint devices of the image. */ void setLod0ToNStrokeStrategyFactory(const KisLodSyncStrokeStrategyFactory &factory); /** * Install a factory of a stroke strategy, that will be started * every time when the scheduler needs to postpone all the updates * of the *LOD0* strokes. */ void setSuspendUpdatesStrokeStrategyFactory(const KisSuspendResumeStrategyFactory &factory); /** * \see setSuspendUpdatesStrokeStrategyFactory() */ void setResumeUpdatesStrokeStrategyFactory(const KisSuspendResumeStrategyFactory &factory); KisPostExecutionUndoAdapter* lodNPostExecutionUndoAdapter() const; /** * tryCancelCurrentStrokeAsync() checks whether there is a * *running* stroke (which is being executed at this very moment) * which is not still open by the owner (endStroke() or * cancelStroke() have already been called) and cancels it. * * \return true if some stroke has been found and cancelled * * \note This method is *not* part of KisStrokesFacade! It is too * low level for KisImage. In KisImage it is combined with * more high level requestStrokeCancellation(). */ bool tryCancelCurrentStrokeAsync(); UndoResult tryUndoLastStrokeAsync(); bool wrapAroundModeSupported() const; int currentLevelOfDetail() const; protected: // Trivial constructor for testing support KisUpdateScheduler(); void connectSignals(); void processQueues(); protected Q_SLOTS: /** * Called when it is necessary to reread configuration */ void updateSettings(); private Q_SLOTS: void continueUpdate(const QRect &rect); void doSomeUsefulWork(); void spareThreadAppeared(); private: friend class UpdatesBlockTester; bool haveUpdatesRunning(); void tryProcessUpdatesQueue(); void wakeUpWaitingThreads(); void progressUpdate(); protected: struct Private; Private * const m_d; }; class KisTestableUpdaterContext; class KisTestableSimpleUpdateQueue; class KRITAIMAGE_EXPORT KisTestableUpdateScheduler : public KisUpdateScheduler { public: KisTestableUpdateScheduler(KisProjectionUpdateListener *projectionUpdateListener, qint32 threadCount); KisTestableUpdaterContext* updaterContext(); KisTestableSimpleUpdateQueue* updateQueue(); using KisUpdateScheduler::processQueues; }; #endif /* __KIS_UPDATE_SCHEDULER_H */ diff --git a/libs/image/kis_updater_context.cpp b/libs/image/kis_updater_context.cpp index 3fdc0331b3..222f95d68c 100644 --- a/libs/image/kis_updater_context.cpp +++ b/libs/image/kis_updater_context.cpp @@ -1,287 +1,322 @@ /* * Copyright (c) 2010 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_updater_context.h" #include #include #include "kis_update_job_item.h" #include "kis_stroke_job.h" const int KisUpdaterContext::useIdealThreadCountTag = -1; KisUpdaterContext::KisUpdaterContext(qint32 threadCount, QObject *parent) : QObject(parent) { if(threadCount <= 0) { threadCount = QThread::idealThreadCount(); threadCount = threadCount > 0 ? threadCount : 1; } setThreadsLimit(threadCount); } KisUpdaterContext::~KisUpdaterContext() { m_threadPool.waitForDone(); for(qint32 i = 0; i < m_jobs.size(); i++) delete m_jobs[i]; } void KisUpdaterContext::getJobsSnapshot(qint32 &numMergeJobs, qint32 &numStrokeJobs) { numMergeJobs = 0; numStrokeJobs = 0; Q_FOREACH (const KisUpdateJobItem *item, m_jobs) { - if(item->type() == KisUpdateJobItem::MERGE || - item->type() == KisUpdateJobItem::SPONTANEOUS) { + if(item->type() == KisUpdateJobItem::Type::MERGE || + item->type() == KisUpdateJobItem::Type::SPONTANEOUS) { numMergeJobs++; } - else if(item->type() == KisUpdateJobItem::STROKE) { + else if(item->type() == KisUpdateJobItem::Type::STROKE) { numStrokeJobs++; } } } +KisUpdaterContextSnapshotEx KisUpdaterContext::getContextSnapshotEx() const +{ + KisUpdaterContextSnapshotEx state = ContextEmpty; + + Q_FOREACH (const KisUpdateJobItem *item, m_jobs) { + if (item->type() == KisUpdateJobItem::Type::MERGE || + item->type() == KisUpdateJobItem::Type::SPONTANEOUS) { + state |= HasMergeJob; + } else if(item->type() == KisUpdateJobItem::Type::STROKE) { + switch (item->strokeJobSequentiality()) { + case KisStrokeJobData::SEQUENTIAL: + state |= HasSequentialJob; + break; + case KisStrokeJobData::CONCURRENT: + state |= HasConcurrentJob; + break; + case KisStrokeJobData::BARRIER: + state |= HasBarrierJob; + break; + case KisStrokeJobData::UNIQUELY_CONCURRENT: + state |= HasUniquelyConcurrentJob; + break; + } + } + } + + return state; +} + int KisUpdaterContext::currentLevelOfDetail() const { return m_lodCounter.readLod(); } bool KisUpdaterContext::hasSpareThread() { bool found = false; Q_FOREACH (const KisUpdateJobItem *item, m_jobs) { if(!item->isRunning()) { found = true; break; } } return found; } bool KisUpdaterContext::isJobAllowed(KisBaseRectsWalkerSP walker) { int lod = this->currentLevelOfDetail(); if (lod >= 0 && walker->levelOfDetail() != lod) return false; bool intersects = false; Q_FOREACH (const KisUpdateJobItem *item, m_jobs) { if(item->isRunning() && walkerIntersectsJob(walker, item)) { intersects = true; break; } } return !intersects; } /** * NOTE: In theory, isJobAllowed() and addMergeJob() should be merged into * one atomic method like `bool push()`, because this implementation * of KisUpdaterContext will not work in case of multiple * producers. But currently we have only one producer (one thread * in a time), that is guaranteed by the lock()/unlock() pair in * KisAbstractUpdateQueue::processQueue. */ void KisUpdaterContext::addMergeJob(KisBaseRectsWalkerSP walker) { m_lodCounter.addLod(walker->levelOfDetail()); qint32 jobIndex = findSpareThread(); Q_ASSERT(jobIndex >= 0); - m_jobs[jobIndex]->setWalker(walker); + const bool shouldStartThread = m_jobs[jobIndex]->setWalker(walker); // it might happen that we call this function from within // the thread itself, right when it finished its work - if (!m_jobs[jobIndex]->hasThreadAttached()) { + if (shouldStartThread) { m_threadPool.start(m_jobs[jobIndex]); } } /** * This variant is for use in a testing suite only */ void KisTestableUpdaterContext::addMergeJob(KisBaseRectsWalkerSP walker) { m_lodCounter.addLod(walker->levelOfDetail()); qint32 jobIndex = findSpareThread(); Q_ASSERT(jobIndex >= 0); - m_jobs[jobIndex]->setWalker(walker); + const bool shouldStartThread = m_jobs[jobIndex]->setWalker(walker); + // HINT: Not calling start() here + Q_UNUSED(shouldStartThread); } void KisUpdaterContext::addStrokeJob(KisStrokeJob *strokeJob) { m_lodCounter.addLod(strokeJob->levelOfDetail()); qint32 jobIndex = findSpareThread(); Q_ASSERT(jobIndex >= 0); - m_jobs[jobIndex]->setStrokeJob(strokeJob); + const bool shouldStartThread = m_jobs[jobIndex]->setStrokeJob(strokeJob); // it might happen that we call this function from within // the thread itself, right when it finished its work - if (!m_jobs[jobIndex]->hasThreadAttached()) { + if (shouldStartThread) { m_threadPool.start(m_jobs[jobIndex]); } } /** * This variant is for use in a testing suite only */ void KisTestableUpdaterContext::addStrokeJob(KisStrokeJob *strokeJob) { m_lodCounter.addLod(strokeJob->levelOfDetail()); qint32 jobIndex = findSpareThread(); Q_ASSERT(jobIndex >= 0); - m_jobs[jobIndex]->setStrokeJob(strokeJob); + const bool shouldStartThread = m_jobs[jobIndex]->setStrokeJob(strokeJob); + // HINT: Not calling start() here + Q_UNUSED(shouldStartThread); } void KisUpdaterContext::addSpontaneousJob(KisSpontaneousJob *spontaneousJob) { m_lodCounter.addLod(spontaneousJob->levelOfDetail()); qint32 jobIndex = findSpareThread(); Q_ASSERT(jobIndex >= 0); - m_jobs[jobIndex]->setSpontaneousJob(spontaneousJob); + const bool shouldStartThread = m_jobs[jobIndex]->setSpontaneousJob(spontaneousJob); // it might happen that we call this function from within // the thread itself, right when it finished its work - if (!m_jobs[jobIndex]->hasThreadAttached()) { + if (shouldStartThread) { m_threadPool.start(m_jobs[jobIndex]); } } /** * This variant is for use in a testing suite only */ void KisTestableUpdaterContext::addSpontaneousJob(KisSpontaneousJob *spontaneousJob) { m_lodCounter.addLod(spontaneousJob->levelOfDetail()); qint32 jobIndex = findSpareThread(); Q_ASSERT(jobIndex >= 0); - m_jobs[jobIndex]->setSpontaneousJob(spontaneousJob); + const bool shouldStartThread = m_jobs[jobIndex]->setSpontaneousJob(spontaneousJob); + // HINT: Not calling start() here + Q_UNUSED(shouldStartThread); } void KisUpdaterContext::waitForDone() { m_threadPool.waitForDone(); } bool KisUpdaterContext::walkerIntersectsJob(KisBaseRectsWalkerSP walker, const KisUpdateJobItem* job) { return (walker->accessRect().intersects(job->changeRect())) || (job->accessRect().intersects(walker->changeRect())); } qint32 KisUpdaterContext::findSpareThread() { for(qint32 i=0; i < m_jobs.size(); i++) if(!m_jobs[i]->isRunning()) return i; return -1; } void KisUpdaterContext::slotJobFinished() { m_lodCounter.removeLod(); // Be careful. This slot can be called asynchronously without locks. emit sigSpareThreadAppeared(); } void KisUpdaterContext::lock() { m_lock.lock(); } void KisUpdaterContext::unlock() { m_lock.unlock(); } void KisUpdaterContext::setThreadsLimit(int value) { m_threadPool.setMaxThreadCount(value); for (int i = 0; i < m_jobs.size(); i++) { KIS_SAFE_ASSERT_RECOVER_RETURN(!m_jobs[i]->isRunning()); // don't delete the jobs until all of them are checked! } for (int i = 0; i < m_jobs.size(); i++) { delete m_jobs[i]; } m_jobs.resize(value); for(qint32 i = 0; i < m_jobs.size(); i++) { m_jobs[i] = new KisUpdateJobItem(&m_exclusiveJobLock); connect(m_jobs[i], SIGNAL(sigContinueUpdate(const QRect&)), SIGNAL(sigContinueUpdate(const QRect&)), Qt::DirectConnection); connect(m_jobs[i], SIGNAL(sigDoSomeUsefulWork()), SIGNAL(sigDoSomeUsefulWork()), Qt::DirectConnection); connect(m_jobs[i], SIGNAL(sigJobFinished()), SLOT(slotJobFinished()), Qt::DirectConnection); } } int KisUpdaterContext::threadsLimit() const { KIS_SAFE_ASSERT_RECOVER_NOOP(m_jobs.size() == m_threadPool.maxThreadCount()); return m_jobs.size(); } KisTestableUpdaterContext::KisTestableUpdaterContext(qint32 threadCount) : KisUpdaterContext(threadCount) { } KisTestableUpdaterContext::~KisTestableUpdaterContext() { clear(); } const QVector KisTestableUpdaterContext::getJobs() { return m_jobs; } void KisTestableUpdaterContext::clear() { Q_FOREACH (KisUpdateJobItem *item, m_jobs) { item->testingSetDone(); } m_lodCounter.testingClear(); } diff --git a/libs/image/kis_updater_context.h b/libs/image/kis_updater_context.h index 55a02271a8..15421d6516 100644 --- a/libs/image/kis_updater_context.h +++ b/libs/image/kis_updater_context.h @@ -1,190 +1,195 @@ /* * Copyright (c) 2010 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_UPDATER_CONTEXT_H #define __KIS_UPDATER_CONTEXT_H #include #include #include #include #include "kis_base_rects_walker.h" #include "kis_async_merger.h" #include "kis_lock_free_lod_counter.h" +#include "KisUpdaterContextSnapshotEx.h" class KisUpdateJobItem; class KisSpontaneousJob; class KisStrokeJob; class KRITAIMAGE_EXPORT KisUpdaterContext : public QObject { Q_OBJECT public: static const int useIdealThreadCountTag; public: KisUpdaterContext(qint32 threadCount = useIdealThreadCountTag, QObject *parent = 0); ~KisUpdaterContext() override; /** * Returns the number of currently running jobs of each type. * To use this information you should lock the context beforehand. * * \see lock() */ void getJobsSnapshot(qint32 &numMergeJobs, qint32 &numStrokeJobs); + KisUpdaterContextSnapshotEx getContextSnapshotEx() const; + /** * Returns the current level of detail of all the running jobs in the * context. If there are no jobs, returns -1. */ int currentLevelOfDetail() const; /** * Check whether there is a spare thread for running * one more job */ bool hasSpareThread(); /** * Checks whether the walker intersects with any * of currently executing walkers. If it does, * it is not allowed to go in. It should be called * with the lock held. * * \see lock() */ bool isJobAllowed(KisBaseRectsWalkerSP walker); /** * Registers the job and starts executing it. * The caller must ensure that the context is locked * with lock(), job is allowed with isWalkerAllowed() and * there is a spare thread for running it with hasSpareThread() * * \see lock() * \see isWalkerAllowed() * \see hasSpareThread() */ virtual void addMergeJob(KisBaseRectsWalkerSP walker); /** * Adds a stroke job to the context. The prerequisites are * the same as for addMergeJob() * \see addMergeJob() */ virtual void addStrokeJob(KisStrokeJob *strokeJob); /** * Adds a spontaneous job to the context. The prerequisites are * the same as for addMergeJob() * \see addMergeJob() */ virtual void addSpontaneousJob(KisSpontaneousJob *spontaneousJob); /** * Block execution of the caller until all the jobs are finished */ void waitForDone(); /** * Locks the context to guarantee an exclusive access * to the context */ void lock(); /** * Unlocks the context * * \see lock() */ void unlock(); /** * Set the number of threads available for this updater context * WARNING: one cannot change the number of threads if there is * at least one job running in the context! So before * calling this method make sure you do two things: * 1) barrierLock() the update scheduler * 2) lock() the context */ void setThreadsLimit(int value); /** * Return the number of available threads in the context. Make sure you * lock the context before calling this function! */ int threadsLimit() const; Q_SIGNALS: void sigContinueUpdate(const QRect& rc); void sigDoSomeUsefulWork(); void sigSpareThreadAppeared(); protected Q_SLOTS: void slotJobFinished(); protected: static bool walkerIntersectsJob(KisBaseRectsWalkerSP walker, const KisUpdateJobItem* job); qint32 findSpareThread(); protected: /** * The lock is shared by all the child update job items. * When an item wants to run a usual (non-exclusive) job, * it locks the lock for read access. When an exclusive * access is requested, it locks it for write */ QReadWriteLock m_exclusiveJobLock; QMutex m_lock; QVector m_jobs; QThreadPool m_threadPool; KisLockFreeLodCounter m_lodCounter; }; class KRITAIMAGE_EXPORT KisTestableUpdaterContext : public KisUpdaterContext { public: /** * Creates an explicit number of threads */ KisTestableUpdaterContext(qint32 threadCount); ~KisTestableUpdaterContext() override; /** * The only difference - it doesn't start execution * of the job */ void addMergeJob(KisBaseRectsWalkerSP walker) override; void addStrokeJob(KisStrokeJob *strokeJob) override; void addSpontaneousJob(KisSpontaneousJob *spontaneousJob) override; const QVector getJobs(); void clear(); }; + + #endif /* __KIS_UPDATER_CONTEXT_H */ diff --git a/libs/image/krita_utils.cpp b/libs/image/krita_utils.cpp index bd419d1367..54ee2e43ae 100644 --- a/libs/image/krita_utils.cpp +++ b/libs/image/krita_utils.cpp @@ -1,444 +1,474 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "krita_utils.h" #include #include #include #include #include #include #include #include "kis_algebra_2d.h" #include #include "kis_image_config.h" #include "kis_debug.h" #include "kis_node.h" #include "kis_sequential_iterator.h" #include "kis_random_accessor_ng.h" +#include + namespace KritaUtils { QSize optimalPatchSize() { KisImageConfig cfg; return QSize(cfg.updatePatchWidth(), cfg.updatePatchHeight()); } QVector splitRectIntoPatches(const QRect &rc, const QSize &patchSize) { QVector patches; qint32 firstCol = rc.x() / patchSize.width(); qint32 firstRow = rc.y() / patchSize.height(); qint32 lastCol = (rc.x() + rc.width()) / patchSize.width(); qint32 lastRow = (rc.y() + rc.height()) / patchSize.height(); for(qint32 i = firstRow; i <= lastRow; i++) { for(qint32 j = firstCol; j <= lastCol; j++) { QRect maxPatchRect(j * patchSize.width(), i * patchSize.height(), patchSize.width(), patchSize.height()); QRect patchRect = rc & maxPatchRect; if (!patchRect.isEmpty()) { patches.append(patchRect); } } } return patches; } QVector splitRegionIntoPatches(const QRegion ®ion, const QSize &patchSize) { QVector patches; Q_FOREACH (const QRect rect, region.rects()) { patches << KritaUtils::splitRectIntoPatches(rect, patchSize); } return patches; } bool checkInTriangle(const QRectF &rect, const QPolygonF &triangle) { return triangle.intersected(rect).boundingRect().isValid(); } QRegion KRITAIMAGE_EXPORT splitTriangles(const QPointF ¢er, const QVector &points) { Q_ASSERT(points.size()); Q_ASSERT(!(points.size() & 1)); QVector triangles; QRect totalRect; for (int i = 0; i < points.size(); i += 2) { QPolygonF triangle; triangle << center; triangle << points[i]; triangle << points[i+1]; totalRect |= triangle.boundingRect().toAlignedRect(); triangles << triangle; } const int step = 64; const int right = totalRect.x() + totalRect.width(); const int bottom = totalRect.y() + totalRect.height(); QRegion dirtyRegion; for (int y = totalRect.y(); y < bottom;) { int nextY = qMin((y + step) & ~(step-1), bottom); for (int x = totalRect.x(); x < right;) { int nextX = qMin((x + step) & ~(step-1), right); QRect rect(x, y, nextX - x, nextY - y); Q_FOREACH (const QPolygonF &triangle, triangles) { if(checkInTriangle(rect, triangle)) { dirtyRegion |= rect; break; } } x = nextX; } y = nextY; } return dirtyRegion; } QRegion KRITAIMAGE_EXPORT splitPath(const QPainterPath &path) { QRect totalRect = path.boundingRect().toAlignedRect(); // adjust the rect for antialiasing to work totalRect = totalRect.adjusted(-1,-1,1,1); const int step = 64; const int right = totalRect.x() + totalRect.width(); const int bottom = totalRect.y() + totalRect.height(); QRegion dirtyRegion; for (int y = totalRect.y(); y < bottom;) { int nextY = qMin((y + step) & ~(step-1), bottom); for (int x = totalRect.x(); x < right;) { int nextX = qMin((x + step) & ~(step-1), right); QRect rect(x, y, nextX - x, nextY - y); if(path.intersects(rect)) { dirtyRegion |= rect; } x = nextX; } y = nextY; } return dirtyRegion; } QString KRITAIMAGE_EXPORT prettyFormatReal(qreal value) { return QString("%1").arg(value, 6, 'f', 1); } qreal KRITAIMAGE_EXPORT maxDimensionPortion(const QRectF &bounds, qreal portion, qreal minValue) { qreal maxDimension = qMax(bounds.width(), bounds.height()); return qMax(portion * maxDimension, minValue); } bool tryMergePoints(QPainterPath &path, const QPointF &startPoint, const QPointF &endPoint, qreal &distance, qreal distanceThreshold, bool lastSegment) { qreal length = (endPoint - startPoint).manhattanLength(); if (lastSegment || length > distanceThreshold) { if (lastSegment) { qreal wrappedLength = (endPoint - QPointF(path.elementAt(0))).manhattanLength(); if (length < distanceThreshold || wrappedLength < distanceThreshold) { return true; } } distance = 0; return false; } distance += length; if (distance > distanceThreshold) { path.lineTo(endPoint); distance = 0; } return true; } QPainterPath trySimplifyPath(const QPainterPath &path, qreal lengthThreshold) { QPainterPath newPath; QPointF startPoint; qreal distance = 0; int count = path.elementCount(); for (int i = 0; i < count; i++) { QPainterPath::Element e = path.elementAt(i); QPointF endPoint = QPointF(e.x, e.y); switch (e.type) { case QPainterPath::MoveToElement: newPath.moveTo(endPoint); break; case QPainterPath::LineToElement: if (!tryMergePoints(newPath, startPoint, endPoint, distance, lengthThreshold, i == count - 1)) { newPath.lineTo(endPoint); } break; case QPainterPath::CurveToElement: { Q_ASSERT(i + 2 < count); if (!tryMergePoints(newPath, startPoint, endPoint, distance, lengthThreshold, i == count - 1)) { e = path.elementAt(i + 1); Q_ASSERT(e.type == QPainterPath::CurveToDataElement); QPointF ctrl1 = QPointF(e.x, e.y); e = path.elementAt(i + 2); Q_ASSERT(e.type == QPainterPath::CurveToDataElement); QPointF ctrl2 = QPointF(e.x, e.y); newPath.cubicTo(ctrl1, ctrl2, endPoint); } i += 2; } default: ; } startPoint = endPoint; } return newPath; } QList splitDisjointPaths(const QPainterPath &path) { QList resultList; QList inputPolygons = path.toSubpathPolygons(); Q_FOREACH (const QPolygonF &poly, inputPolygons) { QPainterPath testPath; testPath.addPolygon(poly); if (resultList.isEmpty()) { resultList.append(testPath); continue; } QPainterPath mergedPath = testPath; for (auto it = resultList.begin(); it != resultList.end(); /*noop*/) { if (it->intersects(testPath)) { mergedPath.addPath(*it); it = resultList.erase(it); } else { ++it; } } resultList.append(mergedPath); } return resultList; } quint8 mergeOpacity(quint8 opacity, quint8 parentOpacity) { if (parentOpacity != OPACITY_OPAQUE_U8) { opacity = (int(opacity) * parentOpacity) / OPACITY_OPAQUE_U8; } return opacity; } QBitArray mergeChannelFlags(const QBitArray &childFlags, const QBitArray &parentFlags) { QBitArray flags = childFlags; if (!flags.isEmpty() && !parentFlags.isEmpty() && flags.size() == parentFlags.size()) { flags &= parentFlags; } else if (!parentFlags.isEmpty()) { flags = parentFlags; } return flags; } bool compareChannelFlags(QBitArray f1, QBitArray f2) { if (f1.isNull() && f2.isNull()) return true; if (f1.isNull()) { f1.fill(true, f2.size()); } if (f2.isNull()) { f2.fill(true, f1.size()); } return f1 == f2; } QString KRITAIMAGE_EXPORT toLocalizedOnOff(bool value) { return value ? i18n("on") : i18n("off"); } KisNodeSP nearestNodeAfterRemoval(KisNodeSP node) { KisNodeSP newNode = node->nextSibling(); if (!newNode) { newNode = node->prevSibling(); } if (!newNode) { newNode = node->parent(); } return newNode; } void renderExactRect(QPainter *p, const QRect &rc) { p->drawRect(rc.adjusted(0,0,-1,-1)); } void renderExactRect(QPainter *p, const QRect &rc, const QPen &pen) { QPen oldPen = p->pen(); p->setPen(pen); renderExactRect(p, rc); p->setPen(oldPen); } QImage convertQImageToGrayA(const QImage &image) { QImage dstImage(image.size(), QImage::Format_ARGB32); // TODO: if someone feel bored, a more optimized version of this would be welcome const QSize size = image.size(); for(int i = 0; i < size.height(); ++i) { for(int j = 0; j < size.width(); ++j) { const QRgb pixel = image.pixel(i,j); const int gray = qGray(pixel); dstImage.setPixel(i, j, qRgba(gray, gray, gray, qAlpha(pixel))); } } return dstImage; } QColor blendColors(const QColor &c1, const QColor &c2, qreal r1) { const qreal r2 = 1.0 - r1; return QColor::fromRgbF( c1.redF() * r1 + c2.redF() * r2, c1.greenF() * r1 + c2.greenF() * r2, c1.blueF() * r1 + c2.blueF() * r2); } void applyToAlpha8Device(KisPaintDeviceSP dev, const QRect &rc, std::function func) { KisSequentialConstIterator dstIt(dev, rc); do { const quint8 *dstPtr = dstIt.rawDataConst(); func(*dstPtr); } while (dstIt.nextPixel()); } void filterAlpha8Device(KisPaintDeviceSP dev, const QRect &rc, std::function func) { KisSequentialIterator dstIt(dev, rc); do { quint8 *dstPtr = dstIt.rawData(); *dstPtr = func(*dstPtr); } while (dstIt.nextPixel()); } qreal estimatePortionOfTransparentPixels(KisPaintDeviceSP dev, const QRect &rect, qreal samplePortion) { const KoColorSpace *cs = dev->colorSpace(); const qreal linearPortion = std::sqrt(samplePortion); const qreal ratio = qreal(rect.width()) / rect.height(); const int xStep = qMax(1, qRound(1.0 / linearPortion * ratio)); const int yStep = qMax(1, qRound(1.0 / linearPortion / ratio)); int numTransparentPixels = 0; int numPixels = 0; KisRandomConstAccessorSP it = dev->createRandomConstAccessorNG(rect.x(), rect.y()); for (int y = rect.y(); y <= rect.bottom(); y += yStep) { for (int x = rect.x(); x <= rect.right(); x += xStep) { it->moveTo(x, y); const quint8 alpha = cs->opacityU8(it->rawDataConst()); if (alpha != OPACITY_OPAQUE_U8) { numTransparentPixels++; } numPixels++; } } return qreal(numTransparentPixels) / numPixels; } + + void mirrorDab(Qt::Orientation dir, const QPoint ¢er, KisRenderedDab *dab) + { + const QRect rc = dab->realBounds(); + + if (dir == Qt::Horizontal) { + const int mirrorX = -((rc.x() + rc.width()) - center.x()) + center.x(); + + dab->device->mirror(true, false); + dab->offset.rx() = mirrorX; + } else /* if (dir == Qt::Vertical) */ { + const int mirrorY = -((rc.y() + rc.height()) - center.y()) + center.y(); + + dab->device->mirror(false, true); + dab->offset.ry() = mirrorY; + } + } + + void mirrorRect(Qt::Orientation dir, const QPoint ¢er, QRect *rc) + { + if (dir == Qt::Horizontal) { + const int mirrorX = -((rc->x() + rc->width()) - center.x()) + center.x(); + rc->moveLeft(mirrorX); + } else /* if (dir == Qt::Vertical) */ { + const int mirrorY = -((rc->y() + rc->height()) - center.y()) + center.y(); + rc->moveTop(mirrorY); + } + } } diff --git a/libs/image/krita_utils.h b/libs/image/krita_utils.h index 45e6fab3e0..cc3e04d740 100644 --- a/libs/image/krita_utils.h +++ b/libs/image/krita_utils.h @@ -1,101 +1,105 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KRITA_UTILS_H #define __KRITA_UTILS_H class QRect; class QRectF; class QSize; class QPen; class QPointF; class QPainterPath; class QBitArray; class QPainter; +class KisRenderedDab; #include #include "kritaimage_export.h" #include "kis_types.h" #include "krita_container_utils.h" #include namespace KritaUtils { QSize KRITAIMAGE_EXPORT optimalPatchSize(); QVector KRITAIMAGE_EXPORT splitRectIntoPatches(const QRect &rc, const QSize &patchSize); QVector KRITAIMAGE_EXPORT splitRegionIntoPatches(const QRegion ®ion, const QSize &patchSize); QRegion KRITAIMAGE_EXPORT splitTriangles(const QPointF ¢er, const QVector &points); QRegion KRITAIMAGE_EXPORT splitPath(const QPainterPath &path); QString KRITAIMAGE_EXPORT prettyFormatReal(qreal value); qreal KRITAIMAGE_EXPORT maxDimensionPortion(const QRectF &bounds, qreal portion, qreal minValue); QPainterPath KRITAIMAGE_EXPORT trySimplifyPath(const QPainterPath &path, qreal lengthThreshold); /** * Split a path \p path into a set of disjoint (non-intersectable) * paths if possible. * * It tries to follow odd-even fill rule, but has a small problem: * If you have three selections included into each other twice, * then the smallest selection will be included into the final subpath, * although it shouldn't according to odd-even-fill rule. It is still * to be fixed. */ QList KRITAIMAGE_EXPORT splitDisjointPaths(const QPainterPath &path); quint8 KRITAIMAGE_EXPORT mergeOpacity(quint8 opacity, quint8 parentOpacity); QBitArray KRITAIMAGE_EXPORT mergeChannelFlags(const QBitArray &flags, const QBitArray &parentFlags); bool KRITAIMAGE_EXPORT compareChannelFlags(QBitArray f1, QBitArray f2); QString KRITAIMAGE_EXPORT toLocalizedOnOff(bool value); KisNodeSP KRITAIMAGE_EXPORT nearestNodeAfterRemoval(KisNodeSP node); /** * When drawing a rect Qt uses quite a weird algorithm. It * draws 4 lines: * o at X-es: rect.x() and rect.right() + 1 * o at Y-s: rect.y() and rect.bottom() + 1 * * Which means that bottom and right lines of the rect are painted * outside the virtual rectangle the rect defines. This methods overcome this issue by * painting the adjusted rect. */ void KRITAIMAGE_EXPORT renderExactRect(QPainter *p, const QRect &rc); /** * \see renderExactRect(QPainter *p, const QRect &rc) */ void KRITAIMAGE_EXPORT renderExactRect(QPainter *p, const QRect &rc, const QPen &pen); QImage KRITAIMAGE_EXPORT convertQImageToGrayA(const QImage &image); QColor KRITAIMAGE_EXPORT blendColors(const QColor &c1, const QColor &c2, qreal r1); void KRITAIMAGE_EXPORT applyToAlpha8Device(KisPaintDeviceSP dev, const QRect &rc, std::function func); void KRITAIMAGE_EXPORT filterAlpha8Device(KisPaintDeviceSP dev, const QRect &rc, std::function func); qreal KRITAIMAGE_EXPORT estimatePortionOfTransparentPixels(KisPaintDeviceSP dev, const QRect &rect, qreal samplePortion); + + void KRITAIMAGE_EXPORT mirrorDab(Qt::Orientation dir, const QPoint ¢er, KisRenderedDab *dab); + void KRITAIMAGE_EXPORT mirrorRect(Qt::Orientation dir, const QPoint ¢er, QRect *rc); } #endif /* __KRITA_UTILS_H */ diff --git a/libs/image/tests/CMakeLists.txt b/libs/image/tests/CMakeLists.txt index 9c3b5f62ba..280bd167f5 100644 --- a/libs/image/tests/CMakeLists.txt +++ b/libs/image/tests/CMakeLists.txt @@ -1,238 +1,242 @@ # cmake in some versions for some not yet known reasons fails to run automoc # on random targets (changing target names already has an effect) # As temporary workaround skipping build of tests on these versions for now # See https://mail.kde.org/pipermail/kde-buildsystem/2015-June/010819.html # extend range of affected cmake versions as needed if(NOT ${CMAKE_VERSION} VERSION_LESS 3.1.3 AND NOT ${CMAKE_VERSION} VERSION_GREATER 3.2.3) message(WARNING "Skipping krita/image/tests, CMake in at least versions 3.1.3 - 3.2.3 seems to have a problem with automoc. \n(FRIENDLY REMINDER: PLEASE DON'T BREAK THE TESTS!)") set (HAVE_FAILING_CMAKE TRUE) else() set (HAVE_FAILING_CMAKE FALSE) endif() include_directories( ${CMAKE_SOURCE_DIR}/libs/image/metadata ${CMAKE_BINARY_DIR}/libs/image/ ${CMAKE_SOURCE_DIR}/libs/image/ ${CMAKE_SOURCE_DIR}/libs/image/brushengine ${CMAKE_SOURCE_DIR}/libs/image/tiles3 ${CMAKE_SOURCE_DIR}/libs/image/tiles3/swap ${CMAKE_SOURCE_DIR}/sdk/tests ) include_Directories(SYSTEM ${EIGEN3_INCLUDE_DIR} ${Boost_INCLUDE_DIRS} ) if(HAVE_VC) include_directories(${Vc_INCLUDE_DIR}) endif() include(ECMAddTests) include(KritaAddBrokenUnitTest) macro_add_unittest_definitions() set(KisRandomGeneratorDemoSources kis_random_generator_demo.cpp kimageframe.cpp) ki18n_wrap_ui(KisRandomGeneratorDemoSources kis_random_generator_demo.ui) add_executable(KisRandomGeneratorDemo ${KisRandomGeneratorDemoSources}) target_link_libraries(KisRandomGeneratorDemo kritaimage) ecm_mark_as_test(KisRandomGeneratorDemo) ecm_add_tests( kis_base_node_test.cpp kis_fast_math_test.cpp kis_node_test.cpp kis_node_facade_test.cpp kis_fixed_paint_device_test.cpp kis_layer_test.cpp kis_effect_mask_test.cpp kis_iterator_test.cpp kis_painter_test.cpp kis_selection_test.cpp kis_count_visitor_test.cpp kis_projection_test.cpp kis_properties_configuration_test.cpp kis_transaction_test.cpp kis_pixel_selection_test.cpp kis_group_layer_test.cpp kis_paint_layer_test.cpp kis_adjustment_layer_test.cpp kis_annotation_test.cpp kis_change_profile_visitor_test.cpp kis_clone_layer_test.cpp kis_colorspace_convert_visitor_test.cpp kis_convolution_painter_test.cpp kis_crop_processing_visitor_test.cpp kis_processing_applicator_test.cpp kis_datamanager_test.cpp kis_fill_painter_test.cpp kis_filter_configuration_test.cpp kis_filter_test.cpp kis_filter_processing_information_test.cpp kis_filter_registry_test.cpp kis_filter_strategy_test.cpp kis_gradient_painter_test.cpp kis_image_commands_test.cpp kis_image_test.cpp kis_image_signal_router_test.cpp kis_iterators_ng_test.cpp kis_iterator_benchmark.cpp kis_updater_context_test.cpp kis_simple_update_queue_test.cpp kis_stroke_test.cpp kis_simple_stroke_strategy_test.cpp kis_stroke_strategy_undo_command_based_test.cpp kis_strokes_queue_test.cpp kis_macro_test.cpp kis_mask_test.cpp kis_math_toolbox_test.cpp kis_name_server_test.cpp kis_node_commands_test.cpp kis_node_graph_listener_test.cpp kis_node_visitor_test.cpp kis_paint_information_test.cpp kis_distance_information_test.cpp kis_paintop_test.cpp kis_pattern_test.cpp kis_recorded_action_factory_registry_test.cpp kis_recorded_action_test.cpp kis_recorded_filter_action_test.cpp kis_selection_mask_test.cpp kis_shared_ptr_test.cpp kis_bsplines_test.cpp kis_warp_transform_worker_test.cpp kis_liquify_transform_worker_test.cpp kis_transparency_mask_test.cpp kis_types_test.cpp kis_vec_test.cpp kis_filter_config_widget_test.cpp kis_mask_generator_test.cpp kis_cubic_curve_test.cpp kis_node_query_path_test.cpp kis_fixed_point_maths_test.cpp kis_filter_weights_buffer_test.cpp kis_filter_weights_applicator_test.cpp kis_fill_interval_test.cpp kis_fill_interval_map_test.cpp kis_scanline_fill_test.cpp kis_psd_layer_style_test.cpp kis_layer_style_projection_plane_test.cpp kis_lod_capable_layer_offset_test.cpp kis_algebra_2d_test.cpp kis_marker_painter_test.cpp kis_lazy_brush_test.cpp kis_colorize_mask_test.cpp NAME_PREFIX "krita-image-" LINK_LIBRARIES kritaimage Qt5::Test) ecm_add_test(kis_layer_style_filter_environment_test.cpp TEST_NAME kritaimage-layer_style_filter_environment_test LINK_LIBRARIES ${KDE4_KDEUI_LIBS} kritaimage Qt5::Test) ecm_add_test(kis_asl_parser_test.cpp TEST_NAME kritalibpsd-asl_parser_test LINK_LIBRARIES kritapsd kritapigment kritawidgetutils kritacommand Qt5::Xml Qt5::Test) +ecm_add_test(KisPerStrokeRandomSourceTest.cpp + TEST_NAME KisPerStrokeRandomSourceTest + LINK_LIBRARIES kritaimage Qt5::Test) + # ecm_add_test(kis_dom_utils_test.cpp # TEST_NAME krita-image-DomUtils-Test # LINK_LIBRARIES kritaimage Qt5::Test) # kisdoc dep # kis_transform_worker_test.cpp # TEST_NAME krita-image-KisTransformWorkerTest #LINK_LIBRARIES kritaimage Qt5::Test) # kisdoc # kis_perspective_transform_worker_test.cpp # TEST_NAME krita-image-KisPerspectiveTransformWorkerTest #LINK_LIBRARIES kritaimage Qt5::Test) # kis_cs_conversion_test.cpp # TEST_NAME krita-image-KisCsConversionTest # LINK_LIBRARIES kritaimage Qt5::Test) # kisdoc # kis_processings_test.cpp # TEST_NAME krita-image-KisProcessingsTest #LINK_LIBRARIES kritaimage Qt5::Test) # image/tests cannot use stuff that needs kisdocument # kis_projection_leaf_test.cpp # TEST_NAME kritaimage-projection_leaf_test # LINK_LIBRARIES kritaimage Qt5::Test) if (NOT HAVE_FAILING_CMAKE) krita_add_broken_unit_test(kis_paint_device_test.cpp TEST_NAME krita-image-KisPaintDeviceTest LINK_LIBRARIES kritaimage kritaodf Qt5::Test) else() message(WARNING "Skipping KisPaintDeviceTest!!!!!!!!!!!!!!") endif() if (NOT HAVE_FAILING_CMAKE) krita_add_broken_unit_test(kis_filter_mask_test.cpp TEST_NAME krita-image-KisFilterMaskTest LINK_LIBRARIES kritaimage Qt5::Test) else() message(WARNING "Skipping KisFilterMaskTest!!!!!!!!!!!!!!") endif() krita_add_broken_unit_test(kis_transform_mask_test.cpp TEST_NAME krita-image-KisTransformMaskTest LINK_LIBRARIES kritaimage Qt5::Test) krita_add_broken_unit_test(kis_histogram_test.cpp TEST_NAME krita-image-KisHistogramTest LINK_LIBRARIES kritaimage Qt5::Test) krita_add_broken_unit_test(kis_walkers_test.cpp TEST_NAME krita-image-KisWalkersTest LINK_LIBRARIES kritaimage Qt5::Test) #krita_add_broken_unit_test(kis_async_merger_test.cpp # TEST_NAME krita-image-KisAsyncMergerTest # LINK_LIBRARIES kritaimage Qt5::Test) krita_add_broken_unit_test(kis_update_scheduler_test.cpp TEST_NAME krita-image-KisUpdateSchedulerTest LINK_LIBRARIES kritaimage Qt5::Test) krita_add_broken_unit_test(kis_queues_progress_updater_test.cpp TEST_NAME krita-image-KisQueuesProgressUpdaterTest LINK_LIBRARIES kritaimage Qt5::Test) krita_add_broken_unit_test(kis_cage_transform_worker_test.cpp TEST_NAME krita-image-KisCageTransformWorkerTest LINK_LIBRARIES kritaimage Qt5::Test) krita_add_broken_unit_test(kis_meta_data_test.cpp TEST_NAME krita-image-KisMetaDataTest LINK_LIBRARIES kritaimage Qt5::Test) krita_add_broken_unit_test(kis_random_generator_test.cpp TEST_NAME krita-image-KisRandomGeneratorTest LINK_LIBRARIES kritaimage Qt5::Test) krita_add_broken_unit_test(kis_keyframing_test.cpp TEST_NAME krita-image-Keyframing-Test LINK_LIBRARIES kritaimage Qt5::Test) krita_add_broken_unit_test(kis_image_animation_interface_test.cpp TEST_NAME krita-image-ImageAnimationInterface-Test LINK_LIBRARIES ${KDE4_KDEUI_LIBS} kritaimage Qt5::Test) krita_add_broken_unit_test(kis_onion_skin_compositor_test.cpp TEST_NAME krita-image-OnionSkinCompositor-Test LINK_LIBRARIES ${KDE4_KDEUI_LIBS} kritaimage Qt5::Test) krita_add_broken_unit_test(kis_layer_styles_test.cpp TEST_NAME krita-image-LayerStylesTest LINK_LIBRARIES kritaimage Qt5::Test) diff --git a/libs/image/tests/KisPerStrokeRandomSourceTest.cpp b/libs/image/tests/KisPerStrokeRandomSourceTest.cpp new file mode 100644 index 0000000000..bc496d3668 --- /dev/null +++ b/libs/image/tests/KisPerStrokeRandomSourceTest.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisPerStrokeRandomSourceTest.h" + +#include "brushengine/KisPerStrokeRandomSource.h" + +#include + +void KisPerStrokeRandomSourceTest::testIndependent() +{ + bool sourcesDiffer = false; + + /** + * Theoretically, the we can get two equal 1000-pcs sequences, but it is highly improbable + */ + for (int i = 0; i < 1000; i++) { + KisPerStrokeRandomSource s1; + KisPerStrokeRandomSource s2; + + if (s1.generate("mykey", 0, 1000) != s2.generate("mykey", 0, 1000)) { + sourcesDiffer = true; + break; + } + } + + QVERIFY(sourcesDiffer); +} + +void KisPerStrokeRandomSourceTest::testDependent() +{ + bool allSame = true; + + for (int i = 0; i < 1000; i++) { + KisPerStrokeRandomSource s1; + KisPerStrokeRandomSource s2(s1); + + if (s1.generate("mykey", 0, 1000) != s2.generate("mykey", 0, 1000)) { + allSame = false; + break; + } + } + + QVERIFY(allSame); +} + +void KisPerStrokeRandomSourceTest::testDifferentKeys() +{ + bool sourcesDiffer = false; + + /** + * Theoretically, the we can get two equal 1000-pcs sequences, but it is highly improbable + */ + for (int i = 0; i < 1000; i++) { + KisPerStrokeRandomSource s1; + KisPerStrokeRandomSource s2(s1); + + if (s1.generate("mykey1", 0, 1000) != s2.generate("mykey2", 0, 1000)) { + sourcesDiffer = true; + break; + } + } + + QVERIFY(sourcesDiffer); +} + +QTEST_MAIN(KisPerStrokeRandomSourceTest) diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/image/tests/KisPerStrokeRandomSourceTest.h similarity index 62% copy from libs/image/kis_projection_updates_filter.cpp copy to libs/image/tests/KisPerStrokeRandomSourceTest.h index e1493ec367..e405ce8f76 100644 --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/image/tests/KisPerStrokeRandomSourceTest.h @@ -1,36 +1,33 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#ifndef KISPERSTROKERANDOMSOURCETEST_H +#define KISPERSTROKERANDOMSOURCETEST_H +#include -#include -#include - -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +class KisPerStrokeRandomSourceTest : public QObject { -} + Q_OBJECT +private Q_SLOTS: + void testIndependent(); + void testDependent(); + void testDifferentKeys(); +}; -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} +#endif // KISPERSTROKERANDOMSOURCETEST_H diff --git a/libs/image/tests/data/kispainter_test/massive_bitblt_full_update_3.png b/libs/image/tests/data/kispainter_test/massive_bitblt_full_update_3.png new file mode 100644 index 0000000000..239c08da06 Binary files /dev/null and b/libs/image/tests/data/kispainter_test/massive_bitblt_full_update_3.png differ diff --git a/libs/image/tests/data/kispainter_test/massive_bitblt_full_update_6.png b/libs/image/tests/data/kispainter_test/massive_bitblt_full_update_6.png new file mode 100644 index 0000000000..f58815060c Binary files /dev/null and b/libs/image/tests/data/kispainter_test/massive_bitblt_full_update_6.png differ diff --git a/libs/image/tests/data/kispainter_test/massive_bitblt_full_update_6_sel.png b/libs/image/tests/data/kispainter_test/massive_bitblt_full_update_6_sel.png new file mode 100644 index 0000000000..5d31b6803a Binary files /dev/null and b/libs/image/tests/data/kispainter_test/massive_bitblt_full_update_6_sel.png differ diff --git a/libs/image/tests/data/kispainter_test/massive_bitblt_full_update_6_varyop.png b/libs/image/tests/data/kispainter_test/massive_bitblt_full_update_6_varyop.png new file mode 100644 index 0000000000..4e5c38adcc Binary files /dev/null and b/libs/image/tests/data/kispainter_test/massive_bitblt_full_update_6_varyop.png differ diff --git a/libs/image/tests/data/kispainter_test/massive_bitblt_partial_update_3.png b/libs/image/tests/data/kispainter_test/massive_bitblt_partial_update_3.png new file mode 100644 index 0000000000..6de92a53b3 Binary files /dev/null and b/libs/image/tests/data/kispainter_test/massive_bitblt_partial_update_3.png differ diff --git a/libs/image/tests/data/kispainter_test/massive_bitblt_partial_update_6.png b/libs/image/tests/data/kispainter_test/massive_bitblt_partial_update_6.png new file mode 100644 index 0000000000..87f8e58250 Binary files /dev/null and b/libs/image/tests/data/kispainter_test/massive_bitblt_partial_update_6.png differ diff --git a/libs/image/tests/data/kispainter_test/massive_bitblt_partial_update_6_sel.png b/libs/image/tests/data/kispainter_test/massive_bitblt_partial_update_6_sel.png new file mode 100644 index 0000000000..856c402a2c Binary files /dev/null and b/libs/image/tests/data/kispainter_test/massive_bitblt_partial_update_6_sel.png differ diff --git a/libs/image/tests/data/kispainter_test/massive_bitblt_partial_update_6_varyop.png b/libs/image/tests/data/kispainter_test/massive_bitblt_partial_update_6_varyop.png new file mode 100644 index 0000000000..6ff304e66e Binary files /dev/null and b/libs/image/tests/data/kispainter_test/massive_bitblt_partial_update_6_varyop.png differ diff --git a/libs/image/tests/kis_distance_information_test.cpp b/libs/image/tests/kis_distance_information_test.cpp index 6f65be1f8c..91f3085ea1 100644 --- a/libs/image/tests/kis_distance_information_test.cpp +++ b/libs/image/tests/kis_distance_information_test.cpp @@ -1,182 +1,182 @@ #include "kis_distance_information_test.h" #include #include #include #include #include "kis_algebra_2d.h" #include "kis_distance_information.h" #include "kis_spacing_information.h" #include "kis_timing_information.h" #include "kis_paint_information.h" void KisDistanceInformationTest::testInitInfo() { // Test equality checking operators. testInitInfoEquality(); // Test XML cloning. testInitInfoXMLClone(); } void KisDistanceInformationTest::testInterpolation() { // Set up a scenario for interpolation. QPointF startPos; QPointF endPos(100.0, -50.0); qreal dist = KisAlgebra2D::norm(endPos - startPos); qreal startTime = 0.0; qreal endTime = 1000.0; qreal interval = endTime - startTime; KisPaintInformation p1(startPos, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, startTime, 0.0); KisPaintInformation p2(endPos, 1.0, 0.0, 0.0, 5.0, 0.0, 1.0, endTime, 0.0); // Test interpolation with various spacing settings. static const qreal interpTolerance = 0.000001; KisDistanceInformation dist1; dist1.updateSpacing(KisSpacingInformation(dist/10.0)); dist1.updateTiming(KisTimingInformation()); testInterpolationImpl(p1, p2, dist1, 1.0/10.0, false, false, interpTolerance); KisDistanceInformation dist2(interval/2.0, interval/3.0); dist2.updateSpacing(KisSpacingInformation(dist*2.0)); dist2.updateTiming(KisTimingInformation(interval*1.5)); testInterpolationImpl(p1, p2, dist2, -1.0, true, true, interpTolerance); KisDistanceInformation dist3(interval/1.5, interval*2.0); dist3.updateSpacing(KisSpacingInformation(dist*2.1)); dist3.updateTiming(KisTimingInformation(interval*1.6)); testInterpolationImpl(p1, p2, dist3, -1.0, true, false, interpTolerance); KisDistanceInformation dist4(interval*1.3, interval/2.4); dist4.updateSpacing(KisSpacingInformation(dist*1.7)); dist4.updateTiming(KisTimingInformation(interval*1.9)); testInterpolationImpl(p1, p2, dist4, -1.0, false, true, interpTolerance); KisDistanceInformation dist5(interval*1.1, interval*1.2); dist5.updateSpacing(KisSpacingInformation(dist/40.0)); dist5.updateTiming(KisTimingInformation()); testInterpolationImpl(p1, p2, dist5, 1.0/40.0, false, false, interpTolerance); KisDistanceInformation dist6; dist6.updateSpacing(KisSpacingInformation(false, 1.0)); dist6.updateTiming(KisTimingInformation()); testInterpolationImpl(p1, p2, dist6, -1.0, false, false, interpTolerance); KisDistanceInformation dist7; dist7.updateSpacing(KisSpacingInformation(false, 1.0)); dist7.updateTiming(KisTimingInformation(interval/20.0)); testInterpolationImpl(p1, p2, dist7, 1.0/20.0, false, false, interpTolerance); KisDistanceInformation dist8; dist8.updateSpacing(KisSpacingInformation(dist/10.0)); dist8.updateTiming(KisTimingInformation(interval/15.0)); testInterpolationImpl(p1, p2, dist8, 1.0/15.0, false, false, interpTolerance); KisDistanceInformation dist9; dist9.updateSpacing(KisSpacingInformation(dist/15.0)); dist9.updateTiming(KisTimingInformation(interval/10.0)); testInterpolationImpl(p1, p2, dist9, 1.0/15.0, false, false, interpTolerance); KisDistanceInformation dist10; dist10.updateSpacing(KisSpacingInformation(dist*2.0)); dist10.updateTiming(KisTimingInformation(interval*1.5)); testInterpolationImpl(p1, p2, dist10, -1.0, false, false, interpTolerance); KisDistanceInformation dist11; qreal a = 50.0; qreal b = 25.0; dist11.updateSpacing(KisSpacingInformation(QPointF(a * 2.0, b * 2.0), 0.0, false)); dist11.updateTiming(KisTimingInformation()); // Compute the expected interpolation factor; we are using anisotropic spacing here. qreal angle = KisAlgebra2D::directionBetweenPoints(startPos, endPos, 0.0); qreal cosTermSqrt = qCos(angle) / a; qreal sinTermSqrt = qSin(angle) / b; qreal spacingDist = 2.0 / qSqrt(cosTermSqrt * cosTermSqrt + sinTermSqrt * sinTermSqrt); qreal expectedInterp = spacingDist / dist; testInterpolationImpl(p1, p2, dist11, expectedInterp, false, false, interpTolerance); } void KisDistanceInformationTest::testInitInfoEquality() const { KisDistanceInitInfo info1; KisDistanceInitInfo info2; QVERIFY(info1 == info2); QVERIFY(!(info1 != info2)); - KisDistanceInitInfo info3(0.1, 0.5); - KisDistanceInitInfo info4(0.1, 0.5); + KisDistanceInitInfo info3(0.1, 0.5, 4); + KisDistanceInitInfo info4(0.1, 0.5, 4); QVERIFY(info3 == info4); QVERIFY(!(info3 != info4)); - KisDistanceInitInfo info5(QPointF(1.1, -10.7), 100.0, 3.3); - KisDistanceInitInfo info6(QPointF(1.1, -10.7), 100.0, 3.3); + KisDistanceInitInfo info5(QPointF(1.1, -10.7), 3.3, 7); + KisDistanceInitInfo info6(QPointF(1.1, -10.7), 3.3, 7); QVERIFY(info5 == info6); QVERIFY(!(info5 != info6)); - KisDistanceInitInfo info7(QPointF(-12.3, 24.0), 104.0, 5.0, 20.1, 35.7); - KisDistanceInitInfo info8(QPointF(-12.3, 24.0), 104.0, 5.0, 20.1, 35.7); + KisDistanceInitInfo info7(QPointF(-12.3, 24.0), 5.0, 20.1, 35.7, 9); + KisDistanceInitInfo info8(QPointF(-12.3, 24.0), 5.0, 20.1, 35.7, 9); QVERIFY(info7 == info8); QVERIFY(!(info7 != info8)); QVERIFY(info1 != info3); QVERIFY(info1 != info5); QVERIFY(info1 != info7); QVERIFY(info3 != info5); QVERIFY(info3 != info7); QVERIFY(info5 != info7); } void KisDistanceInformationTest::testInitInfoXMLClone() const { // Note: Numeric values used here must be values that get serialized to XML exactly (e.g. small // integers). Otherwise, roundoff error in serialization may cause a failure. KisDistanceInitInfo info1; QDomDocument doc; QDomElement elt1 = doc.createElement("Test1"); info1.toXML(doc, elt1); KisDistanceInitInfo clone1 = KisDistanceInitInfo::fromXML(elt1); QVERIFY(clone1 == info1); - KisDistanceInitInfo info2(40.0, 2.0); + KisDistanceInitInfo info2(40.0, 2.0, 3); QDomElement elt2 = doc.createElement("Test2"); info2.toXML(doc, elt2); KisDistanceInitInfo clone2 = KisDistanceInitInfo::fromXML(elt2); QVERIFY(clone2 == info2); - KisDistanceInitInfo info3(QPointF(-8.0, -5.0), 0.0, 60.0); + KisDistanceInitInfo info3(QPointF(-8.0, -5.0), 60.0, 3); QDomElement elt3 = doc.createElement("Test3"); info3.toXML(doc, elt3); KisDistanceInitInfo clone3 = KisDistanceInitInfo::fromXML(elt3); QVERIFY(clone3 == info3); - KisDistanceInitInfo info4(QPointF(0.0, 9.0), 10.0, 6.0, 1.0, 3.0); + KisDistanceInitInfo info4(QPointF(0.0, 9.0), 6.0, 1.0, 3.0, 8); QDomElement elt4 = doc.createElement("Test4"); info4.toXML(doc, elt4); KisDistanceInitInfo clone4 = KisDistanceInitInfo::fromXML(elt4); QVERIFY(clone4 == info4); } void KisDistanceInformationTest::testInterpolationImpl(const KisPaintInformation &p1, const KisPaintInformation &p2, KisDistanceInformation &dist, qreal interpFactor, bool needSpacingUpdate, bool needTimingUpdate, qreal interpTolerance) const { qreal actualInterpFactor = dist.getNextPointPosition(p1.pos(), p2.pos(), p1.currentTime(), p2.currentTime()); QVERIFY(qAbs(interpFactor - actualInterpFactor) <= interpTolerance); QCOMPARE(dist.needsSpacingUpdate(), needSpacingUpdate); QCOMPARE(dist.needsTimingUpdate(), needTimingUpdate); } QTEST_MAIN(KisDistanceInformationTest) diff --git a/libs/image/tests/kis_painter_test.cpp b/libs/image/tests/kis_painter_test.cpp index d7b53cbe9a..7ae36d03a7 100644 --- a/libs/image/tests/kis_painter_test.cpp +++ b/libs/image/tests/kis_painter_test.cpp @@ -1,522 +1,800 @@ /* * Copyright (c) 2007 Sven Langkamp * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_painter_test.h" #include #include #include #include #include #include #include #include #include #include "kis_datamanager.h" #include "kis_types.h" #include "kis_paint_device.h" #include "kis_painter.h" #include "kis_pixel_selection.h" #include "kis_fill_painter.h" #include #include "testutil.h" #include void KisPainterTest::allCsApplicator(void (KisPainterTest::* funcPtr)(const KoColorSpace*cs)) { QList colorsapces = KoColorSpaceRegistry::instance()->allColorSpaces(KoColorSpaceRegistry::AllColorSpaces, KoColorSpaceRegistry::OnlyDefaultProfile); Q_FOREACH (const KoColorSpace* cs, colorsapces) { QString csId = cs->id(); // ALL THESE COLORSPACES ARE BROKEN: WE NEED UNITTESTS FOR COLORSPACES! if (csId.startsWith("KS")) continue; if (csId.startsWith("Xyz")) continue; if (csId.startsWith('Y')) continue; if (csId.contains("AF")) continue; if (csId == "GRAYU16") continue; // No point in testing bounds with a cs without alpha if (csId == "GRAYU8") continue; // No point in testing bounds with a cs without alpha dbgKrita << "Testing with cs" << csId; if (cs && cs->compositeOp(COMPOSITE_OVER) != 0) { (this->*funcPtr)(cs); } else { dbgKrita << "Cannot bitBlt for cs" << csId; } } } void KisPainterTest::testSimpleBlt(const KoColorSpace * cs) { KisPaintDeviceSP dst = new KisPaintDevice(cs); KisPaintDeviceSP src = new KisPaintDevice(cs); KoColor c(Qt::red, cs); c.setOpacity(quint8(128)); src->fill(20, 20, 20, 20, c.data()); QCOMPARE(src->exactBounds(), QRect(20, 20, 20, 20)); const KoCompositeOp* op; { op = cs->compositeOp(COMPOSITE_OVER); KisPainter painter(dst); painter.setCompositeOp(op); painter.bitBlt(50, 50, src, 20, 20, 20, 20); painter.end(); QCOMPARE(dst->exactBounds(), QRect(50,50,20,20)); } dst->clear(); { op = cs->compositeOp(COMPOSITE_COPY); KisPainter painter(dst); painter.setCompositeOp(op); painter.bitBlt(50, 50, src, 20, 20, 20, 20); painter.end(); QCOMPARE(dst->exactBounds(), QRect(50,50,20,20)); } } void KisPainterTest::testSimpleBlt() { allCsApplicator(&KisPainterTest::testSimpleBlt); } /* Note: the bltSelection tests assume the following geometry: 0,0 0,30 +---------+------+ | 10,10 | | | +----+ | | |####| | | |####| | +----+----+ | | 20,20 | | | | | +----------------+ 30,30 */ void KisPainterTest::testPaintDeviceBltSelection(const KoColorSpace * cs) { KisPaintDeviceSP dst = new KisPaintDevice(cs); KisPaintDeviceSP src = new KisPaintDevice(cs); KoColor c(Qt::red, cs); c.setOpacity(quint8(128)); src->fill(0, 0, 20, 20, c.data()); QCOMPARE(src->exactBounds(), QRect(0, 0, 20, 20)); KisSelectionSP selection = new KisSelection(); selection->pixelSelection()->select(QRect(10, 10, 20, 20)); selection->updateProjection(); QCOMPARE(selection->selectedExactRect(), QRect(10, 10, 20, 20)); KisPainter painter(dst); painter.setSelection(selection); painter.bitBlt(0, 0, src, 0, 0, 30, 30); painter.end(); QImage image = dst->convertToQImage(0); image.save("blt_Selection_" + cs->name() + ".png"); QCOMPARE(dst->exactBounds(), QRect(10, 10, 10, 10)); const KoCompositeOp* op = cs->compositeOp(COMPOSITE_SUBTRACT); if (op->id() == COMPOSITE_SUBTRACT) { KisPaintDeviceSP dst2 = new KisPaintDevice(cs); KisPainter painter2(dst2); painter2.setSelection(selection); painter2.setCompositeOp(op); painter2.bitBlt(0, 0, src, 0, 0, 30, 30); painter2.end(); QCOMPARE(dst2->exactBounds(), QRect(10, 10, 10, 10)); } } void KisPainterTest::testPaintDeviceBltSelection() { allCsApplicator(&KisPainterTest::testPaintDeviceBltSelection); } void KisPainterTest::testPaintDeviceBltSelectionIrregular(const KoColorSpace * cs) { KisPaintDeviceSP dst = new KisPaintDevice(cs); KisPaintDeviceSP src = new KisPaintDevice(cs); KisFillPainter gc(src); gc.fillRect(0, 0, 20, 20, KoColor(Qt::red, cs)); gc.end(); QCOMPARE(src->exactBounds(), QRect(0, 0, 20, 20)); KisSelectionSP sel = new KisSelection(); KisPixelSelectionSP psel = sel->pixelSelection(); psel->select(QRect(10, 15, 20, 15)); psel->select(QRect(15, 10, 15, 5)); QCOMPARE(psel->selectedExactRect(), QRect(10, 10, 20, 20)); QCOMPARE(TestUtil::alphaDevicePixel(psel, 13, 13), MIN_SELECTED); KisPainter painter(dst); painter.setSelection(sel); painter.bitBlt(0, 0, src, 0, 0, 30, 30); painter.end(); QImage image = dst->convertToQImage(0); image.save("blt_Selection_irregular" + cs->name() + ".png"); QCOMPARE(dst->exactBounds(), QRect(10, 10, 10, 10)); Q_FOREACH (KoChannelInfo * channel, cs->channels()) { // Only compare alpha if there actually is an alpha channel in // this colorspace if (channel->channelType() == KoChannelInfo::ALPHA) { QColor c; dst->pixel(13, 13, &c); QCOMPARE((int) c.alpha(), (int) OPACITY_TRANSPARENT_U8); } } } void KisPainterTest::testPaintDeviceBltSelectionIrregular() { allCsApplicator(&KisPainterTest::testPaintDeviceBltSelectionIrregular); } void KisPainterTest::testPaintDeviceBltSelectionInverted(const KoColorSpace * cs) { KisPaintDeviceSP dst = new KisPaintDevice(cs); KisPaintDeviceSP src = new KisPaintDevice(cs); KisFillPainter gc(src); gc.fillRect(0, 0, 30, 30, KoColor(Qt::red, cs)); gc.end(); QCOMPARE(src->exactBounds(), QRect(0, 0, 30, 30)); KisSelectionSP sel = new KisSelection(); KisPixelSelectionSP psel = sel->pixelSelection(); psel->select(QRect(10, 10, 20, 20)); psel->invert(); sel->updateProjection(); KisPainter painter(dst); painter.setSelection(sel); painter.bitBlt(0, 0, src, 0, 0, 30, 30); painter.end(); QCOMPARE(dst->exactBounds(), QRect(0, 0, 30, 30)); } void KisPainterTest::testPaintDeviceBltSelectionInverted() { allCsApplicator(&KisPainterTest::testPaintDeviceBltSelectionInverted); } void KisPainterTest::testSelectionBltSelection() { KisPixelSelectionSP src = new KisPixelSelection(); src->select(QRect(0, 0, 20, 20)); QCOMPARE(src->selectedExactRect(), QRect(0, 0, 20, 20)); KisSelectionSP sel = new KisSelection(); KisPixelSelectionSP Selection = sel->pixelSelection(); Selection->select(QRect(10, 10, 20, 20)); QCOMPARE(Selection->selectedExactRect(), QRect(10, 10, 20, 20)); sel->updateProjection(); KisPixelSelectionSP dst = new KisPixelSelection(); KisPainter painter(dst); painter.setSelection(sel); painter.bitBlt(0, 0, src, 0, 0, 30, 30); painter.end(); QCOMPARE(dst->selectedExactRect(), QRect(10, 10, 10, 10)); KisSequentialConstIterator it(dst, QRect(10, 10, 10, 10)); do { // These are selections, so only one channel and it should // be totally selected QCOMPARE(it.oldRawData()[0], MAX_SELECTED); } while (it.nextPixel()); } /* Test with non-square selection 0,0 0,30 +-----------+------+ | 13,13 | | | x +--+ | | +--+##| | | |#####| | +-----+-----+ | | 20,20 | | | | | +------------------+ 30,30 */ void KisPainterTest::testSelectionBltSelectionIrregular() { KisPaintDeviceSP dev = new KisPaintDevice(KoColorSpaceRegistry::instance()->rgb8()); KisPixelSelectionSP src = new KisPixelSelection(); src->select(QRect(0, 0, 20, 20)); QCOMPARE(src->selectedExactRect(), QRect(0, 0, 20, 20)); KisSelectionSP sel = new KisSelection(); KisPixelSelectionSP Selection = sel->pixelSelection(); Selection->select(QRect(10, 15, 20, 15)); Selection->select(QRect(15, 10, 15, 5)); QCOMPARE(Selection->selectedExactRect(), QRect(10, 10, 20, 20)); QCOMPARE(TestUtil::alphaDevicePixel(Selection, 13, 13), MIN_SELECTED); sel->updateProjection(); KisPixelSelectionSP dst = new KisPixelSelection(); KisPainter painter(dst); painter.setSelection(sel); painter.bitBlt(0, 0, src, 0, 0, 30, 30); painter.end(); QCOMPARE(dst->selectedExactRect(), QRect(10, 10, 10, 10)); QCOMPARE(TestUtil::alphaDevicePixel(dst, 13, 13), MIN_SELECTED); } void KisPainterTest::testSelectionBitBltFixedSelection() { const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); KisPaintDeviceSP dst = new KisPaintDevice(cs); KisPaintDeviceSP src = new KisPaintDevice(cs); KoColor c(Qt::red, cs); c.setOpacity(quint8(128)); src->fill(0, 0, 20, 20, c.data()); QCOMPARE(src->exactBounds(), QRect(0, 0, 20, 20)); KisFixedPaintDeviceSP fixedSelection = new KisFixedPaintDevice(cs); fixedSelection->setRect(QRect(0, 0, 20, 20)); fixedSelection->initialize(); KoColor fill(Qt::white, cs); fixedSelection->fill(5, 5, 10, 10, fill.data()); fixedSelection->convertTo(KoColorSpaceRegistry::instance()->alpha8()); KisPainter painter(dst); painter.bitBltWithFixedSelection(0, 0, src, fixedSelection, 20, 20); painter.end(); QCOMPARE(dst->exactBounds(), QRect(5, 5, 10, 10)); /* dbgKrita << "canary1.5"; dst->clear(); painter.begin(dst); painter.bitBltWithFixedSelection(0, 0, src, fixedSelection, 10, 20); painter.end(); dbgKrita << "canary2"; QCOMPARE(dst->exactBounds(), QRect(5, 5, 5, 10)); dst->clear(); painter.begin(dst); painter.bitBltWithFixedSelection(0, 0, src, fixedSelection, 5, 5, 5, 5, 10, 20); painter.end(); dbgKrita << "canary3"; QCOMPARE(dst->exactBounds(), QRect(5, 5, 5, 10)); dst->clear(); painter.begin(dst); painter.bitBltWithFixedSelection(5, 5, src, fixedSelection, 10, 20); painter.end(); dbgKrita << "canary4"; QCOMPARE(dst->exactBounds(), QRect(10, 10, 5, 10)); */ } void KisPainterTest::testSelectionBitBltEraseCompositeOp() { const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); KisPaintDeviceSP dst = new KisPaintDevice(cs); KoColor c(Qt::red, cs); dst->fill(0, 0, 150, 150, c.data()); KisPaintDeviceSP src = new KisPaintDevice(cs); KoColor c2(Qt::black, cs); src->fill(50, 50, 50, 50, c2.data()); KisSelectionSP sel = new KisSelection(); KisPixelSelectionSP selection = sel->pixelSelection(); selection->select(QRect(25, 25, 100, 100)); sel->updateProjection(); const KoCompositeOp* op = cs->compositeOp(COMPOSITE_ERASE); KisPainter painter(dst); painter.setSelection(sel); painter.setCompositeOp(op); painter.bitBlt(0, 0, src, 0, 0, 150, 150); painter.end(); //dst->convertToQImage(0).save("result.png"); QRect erasedRect(50, 50, 50, 50); KisSequentialConstIterator it(dst, QRect(0, 0, 150, 150)); do { if(!erasedRect.contains(it.x(), it.y())) { QVERIFY(memcmp(it.oldRawData(), c.data(), cs->pixelSize()) == 0); } } while (it.nextPixel()); } void KisPainterTest::testSimpleAlphaCopy() { KisPaintDeviceSP src = new KisPaintDevice(KoColorSpaceRegistry::instance()->alpha8()); KisPaintDeviceSP dst = new KisPaintDevice(KoColorSpaceRegistry::instance()->alpha8()); quint8 p = 128; src->fill(0, 0, 100, 100, &p); QVERIFY(src->exactBounds() == QRect(0, 0, 100, 100)); KisPainter gc(dst); gc.setCompositeOp(KoColorSpaceRegistry::instance()->alpha8()->compositeOp(COMPOSITE_COPY)); gc.bitBlt(QPoint(0, 0), src, src->exactBounds()); gc.end(); QCOMPARE(dst->exactBounds(), QRect(0, 0, 100, 100)); } void KisPainterTest::checkPerformance() { KisPaintDeviceSP src = new KisPaintDevice(KoColorSpaceRegistry::instance()->alpha8()); KisPaintDeviceSP dst = new KisPaintDevice(KoColorSpaceRegistry::instance()->alpha8()); quint8 p = 128; src->fill(0, 0, 10000, 5000, &p); KisSelectionSP sel = new KisSelection(); sel->pixelSelection()->select(QRect(0, 0, 10000, 5000), 128); sel->updateProjection(); QTime t; t.start(); for (int i = 0; i < 10; ++i) { KisPainter gc(dst); gc.bitBlt(0, 0, src, 0, 0, 10000, 5000); } t.restart(); for (int i = 0; i < 10; ++i) { KisPainter gc(dst, sel); gc.bitBlt(0, 0, src, 0, 0, 10000, 5000); } } void KisPainterTest::testBitBltOldData() { const KoColorSpace *cs = KoColorSpaceRegistry::instance()->alpha8(); KisPaintDeviceSP src = new KisPaintDevice(cs); KisPaintDeviceSP dst = new KisPaintDevice(cs); quint8 defaultPixel = 0; quint8 p1 = 128; quint8 p2 = 129; quint8 p3 = 130; KoColor defaultColor(&defaultPixel, cs); KoColor color1(&p1, cs); KoColor color2(&p2, cs); KoColor color3(&p3, cs); QRect fillRect(0,0,5000,5000); src->fill(fillRect, color1); KisPainter srcGc(src); srcGc.beginTransaction(); src->fill(fillRect, color2); KisPainter dstGc(dst); dstGc.bitBltOldData(QPoint(), src, fillRect); QVERIFY(TestUtil::checkAlphaDeviceFilledWithPixel(dst, fillRect, p1)); dstGc.end(); srcGc.deleteTransaction(); } void KisPainterTest::benchmarkBitBlt() { quint8 p = 128; const KoColorSpace *cs = KoColorSpaceRegistry::instance()->alpha8(); KisPaintDeviceSP src = new KisPaintDevice(cs); KisPaintDeviceSP dst = new KisPaintDevice(cs); KoColor color(&p, cs); QRect fillRect(0,0,5000,5000); src->fill(fillRect, color); QBENCHMARK { KisPainter gc(dst); gc.bitBlt(QPoint(), src, fillRect); } } void KisPainterTest::benchmarkBitBltOldData() { quint8 p = 128; const KoColorSpace *cs = KoColorSpaceRegistry::instance()->alpha8(); KisPaintDeviceSP src = new KisPaintDevice(cs); KisPaintDeviceSP dst = new KisPaintDevice(cs); KoColor color(&p, cs); QRect fillRect(0,0,5000,5000); src->fill(fillRect, color); QBENCHMARK { KisPainter gc(dst); gc.bitBltOldData(QPoint(), src, fillRect); } } +#include "kis_paint_device_debug_utils.h" +#include "KisRenderedDab.h" + +void testMassiveBltFixedImpl(int numRects, bool varyOpacity = false, bool useSelection = false) +{ + const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); + KisPaintDeviceSP dst = new KisPaintDevice(cs); + + QList colors; + colors << Qt::red; + colors << Qt::green; + colors << Qt::blue; + + QRect devicesRect; + QList devices; + + for (int i = 0; i < numRects; i++) { + const QRect rc(10 + i * 10, 10 + i * 10, 30, 30); + KisFixedPaintDeviceSP dev = new KisFixedPaintDevice(cs); + dev->setRect(rc); + dev->initialize(); + dev->fill(rc, KoColor(colors[i % 3], cs)); + dev->fill(kisGrowRect(rc, -5), KoColor(Qt::white, cs)); + + KisRenderedDab dab; + dab.device = dev; + dab.offset = dev->bounds().topLeft(); + dab.opacity = varyOpacity ? qreal(1 + i) / numRects : 1.0; + dab.flow = 1.0; + + devices << dab; + devicesRect |= rc; + } + + KisSelectionSP selection; + + if (useSelection) { + selection = new KisSelection(); + selection->pixelSelection()->select(kisGrowRect(devicesRect, -7)); + } + + const QString opacityPostfix = varyOpacity ? "_varyop" : ""; + const QString selectionPostfix = useSelection ? "_sel" : ""; + + const QRect fullRect = kisGrowRect(devicesRect, 10); + + { + KisPainter painter(dst); + painter.setSelection(selection); + painter.bltFixed(fullRect, devices); + painter.end(); + QVERIFY(TestUtil::checkQImage(dst->convertToQImage(0, fullRect), + "kispainter_test", + "massive_bitblt", + QString("full_update_%1%2%3") + .arg(numRects) + .arg(opacityPostfix) + .arg(selectionPostfix))); + } + + dst->clear(); + + { + KisPainter painter(dst); + painter.setSelection(selection); + + for (int i = fullRect.x(); i <= fullRect.center().x(); i += 10) { + const QRect rc(i, fullRect.y(), 10, fullRect.height()); + painter.bltFixed(rc, devices); + } + + painter.end(); + + QVERIFY(TestUtil::checkQImage(dst->convertToQImage(0, fullRect), + "kispainter_test", + "massive_bitblt", + QString("partial_update_%1%2%3") + .arg(numRects) + .arg(opacityPostfix) + .arg(selectionPostfix))); + + } +} + +void KisPainterTest::testMassiveBltFixedSingleTile() +{ + testMassiveBltFixedImpl(3); +} + +void KisPainterTest::testMassiveBltFixedMultiTile() +{ + testMassiveBltFixedImpl(6); +} + +void KisPainterTest::testMassiveBltFixedMultiTileWithOpacity() +{ + testMassiveBltFixedImpl(6, true); +} + +void KisPainterTest::testMassiveBltFixedMultiTileWithSelection() +{ + testMassiveBltFixedImpl(6, false, true); +} + +void KisPainterTest::testMassiveBltFixedCornerCases() +{ + const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); + KisPaintDeviceSP dst = new KisPaintDevice(cs); + + QList devices; + + QVERIFY(dst->extent().isEmpty()); + + { + // empty devices, shouldn't crash + KisPainter painter(dst); + painter.bltFixed(QRect(60,60,20,20), devices); + painter.end(); + } + + QVERIFY(dst->extent().isEmpty()); + + const QRect rc(10,10,20,20); + KisFixedPaintDeviceSP dev = new KisFixedPaintDevice(cs); + dev->setRect(rc); + dev->initialize(); + dev->fill(rc, KoColor(Qt::white, cs)); + + devices.append(KisRenderedDab(dev)); + + { + // rect outside the devices bounds, shouldn't crash + KisPainter painter(dst); + painter.bltFixed(QRect(60,60,20,20), devices); + painter.end(); + } + + QVERIFY(dst->extent().isEmpty()); +} + + +#include "kis_paintop_utils.h" +#include "kis_algebra_2d.h" + +void benchmarkMassiveBltFixedImpl(int numDabs, int size, qreal spacing, int idealNumPatches, Qt::Orientations direction) +{ + const KoColorSpace* cs = KoColorSpaceRegistry::instance()->rgb8(); + KisPaintDeviceSP dst = new KisPaintDevice(cs); + + QList colors; + colors << QColor(255, 0, 0, 200); + colors << QColor(0, 255, 0, 200); + colors << QColor(0, 0, 255, 200); + + QRect devicesRect; + QList devices; + + const int step = spacing * size; + + for (int i = 0; i < numDabs; i++) { + const QRect rc = + direction == Qt::Horizontal ? QRect(10 + i * step, 0, size, size) : + direction == Qt::Vertical ? QRect(0, 10 + i * step, size, size) : + QRect(10 + i * step, 10 + i * step, size, size); + + KisFixedPaintDeviceSP dev = new KisFixedPaintDevice(cs); + dev->setRect(rc); + dev->initialize(); + dev->fill(rc, KoColor(colors[i % 3], cs)); + dev->fill(kisGrowRect(rc, -5), KoColor(Qt::white, cs)); + + KisRenderedDab dab; + dab.device = dev; + dab.offset = dev->bounds().topLeft(); + dab.opacity = 1.0; + dab.flow = 1.0; + + devices << dab; + devicesRect |= rc; + } + + const QRect fullRect = kisGrowRect(devicesRect, 10); + + { + KisPainter painter(dst); + painter.bltFixed(fullRect, devices); + painter.end(); + //QVERIFY(TestUtil::checkQImage(dst->convertToQImage(0, fullRect), + // "kispainter_test", + // "massive_bitblt_benchmark", + // "initial")); + dst->clear(); + } + + QElapsedTimer t; + + qint64 massiveTime = 0; + int massiveTries = 0; + int numRects = 0; + int avgPatchSize = 0; + + for (int i = 0; i < 50 || massiveTime > 5000000; i++) { + QVector rects = KisPaintOpUtils::splitDabsIntoRects(devices, idealNumPatches, size, spacing); + numRects = rects.size(); + + // HACK: please calculate real *average*! + avgPatchSize = KisAlgebra2D::maxDimension(rects.first()); + + t.start(); + + KisPainter painter(dst); + Q_FOREACH (const QRect &rc, rects) { + painter.bltFixed(rc, devices); + } + painter.end(); + + massiveTime += t.nsecsElapsed() / 1000; + massiveTries++; + dst->clear(); + } + + qint64 linearTime = 0; + int linearTries = 0; + + for (int i = 0; i < 50 || linearTime > 5000000; i++) { + t.start(); + + KisPainter painter(dst); + Q_FOREACH (const KisRenderedDab &dab, devices) { + painter.setOpacity(255 * dab.opacity); + painter.setFlow(255 * dab.flow); + painter.bltFixed(dab.offset, dab.device, dab.device->bounds()); + } + painter.end(); + + linearTime += t.nsecsElapsed() / 1000; + linearTries++; + dst->clear(); + } + + const qreal avgMassive = qreal(massiveTime) / massiveTries; + const qreal avgLinear = qreal(linearTime) / linearTries; + + const QString directionMark = + direction == Qt::Horizontal ? "H" : + direction == Qt::Vertical ? "V" : "D"; + + qDebug() + << "D:" << size + << "S:" << spacing + << "N:" << numDabs + << "P (px):" << avgPatchSize + << "R:" << numRects + << "Dir:" << directionMark + << "\t" + << qPrintable(QString("Massive (usec): %1").arg(QString::number(avgMassive, 'f', 2), 8)) + << "\t" + << qPrintable(QString("Linear (usec): %1").arg(QString::number(avgLinear, 'f', 2), 8)) + << (avgMassive < avgLinear ? "*" : " ") + << qPrintable(QString("%1") + .arg(QString::number((avgMassive - avgLinear) / avgLinear * 100.0, 'f', 2), 8)) + << qRound(size + size * spacing * (numDabs - 1)); +} + + +void KisPainterTest::benchmarkMassiveBltFixed() +{ + const qreal sp = 0.14; + const int idealThreadCount = 8; + + for (int d = 50; d < 301; d += 50) { + for (int n = 1; n < 150; n = qCeil(n * 1.5)) { + benchmarkMassiveBltFixedImpl(n, d, sp, idealThreadCount, Qt::Horizontal); + benchmarkMassiveBltFixedImpl(n, d, sp, idealThreadCount, Qt::Vertical); + benchmarkMassiveBltFixedImpl(n, d, sp, idealThreadCount, Qt::Vertical | Qt::Horizontal); + } + } +} QTEST_MAIN(KisPainterTest) diff --git a/libs/image/tests/kis_painter_test.h b/libs/image/tests/kis_painter_test.h index 3bcaf109ad..4aa0763786 100644 --- a/libs/image/tests/kis_painter_test.h +++ b/libs/image/tests/kis_painter_test.h @@ -1,60 +1,70 @@ /* * Copyright (c) 2007 Sven Langkamp * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_PAINTER_TEST_H #define KIS_PAINTER_TEST_H #include class KoColorSpace; class KisPainterTest : public QObject { Q_OBJECT private: void allCsApplicator(void (KisPainterTest::* funcPtr)(const KoColorSpace*cs)); void testSimpleBlt(const KoColorSpace * cs); void testPaintDeviceBltSelection(const KoColorSpace * cs); void testPaintDeviceBltSelectionIrregular(const KoColorSpace * cs); void testPaintDeviceBltSelectionInverted(const KoColorSpace * cs); void checkPerformance(); private Q_SLOTS: void testSimpleBlt(); void testSelectionBltSelectionIrregular(); // Irregular selection void testPaintDeviceBltSelectionInverted(); // Inverted selection void testPaintDeviceBltSelectionIrregular(); // Irregular selection void testPaintDeviceBltSelection(); // Square selection void testSelectionBltSelection(); // Square selection void testSimpleAlphaCopy(); void testSelectionBitBltFixedSelection(); void testSelectionBitBltEraseCompositeOp(); void testBitBltOldData(); void benchmarkBitBlt(); void benchmarkBitBltOldData(); + void testMassiveBltFixedSingleTile(); + void testMassiveBltFixedMultiTile(); + + void testMassiveBltFixedMultiTileWithOpacity(); + + void testMassiveBltFixedMultiTileWithSelection(); + + void testMassiveBltFixedCornerCases(); + + void benchmarkMassiveBltFixed(); }; #endif diff --git a/libs/image/tests/kis_strokes_queue_test.cpp b/libs/image/tests/kis_strokes_queue_test.cpp index 8960b80206..59c91e13ee 100644 --- a/libs/image/tests/kis_strokes_queue_test.cpp +++ b/libs/image/tests/kis_strokes_queue_test.cpp @@ -1,647 +1,838 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_strokes_queue_test.h" #include #include "scheduler_utils.h" #include "kis_strokes_queue.h" #include "kis_updater_context.h" #include "kis_update_job_item.h" #include "kis_merge_walker.h" void KisStrokesQueueTest::testSequentialJobs() { KisStrokesQueue queue; KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy("tri_", false)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id); KisTestableUpdaterContext context(2); QVector jobs; queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "tri_init"); VERIFY_EMPTY(jobs[1]); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "tri_dab"); COMPARE_NAME(jobs[1], "tri_dab"); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "tri_dab"); VERIFY_EMPTY(jobs[1]); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "tri_finish"); VERIFY_EMPTY(jobs[1]); } void KisStrokesQueueTest::testConcurrentSequentialBarrier() { KisStrokesQueue queue; KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy("tri_", false)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id); // make the number of threads higher KisTestableUpdaterContext context(3); QVector jobs; queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "tri_init"); VERIFY_EMPTY(jobs[1]); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "tri_dab"); COMPARE_NAME(jobs[1], "tri_dab"); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "tri_finish"); VERIFY_EMPTY(jobs[1]); } void KisStrokesQueueTest::testExclusiveStrokes() { KisStrokesQueue queue; KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy("excl_", true)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id); // well, this walker is not initialized... but who cares? KisBaseRectsWalkerSP walker = new KisMergeWalker(QRect()); KisTestableUpdaterContext context(2); QVector jobs; context.addMergeJob(walker); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_WALKER(jobs[0], walker); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), true); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "excl_init"); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), true); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "excl_dab"); COMPARE_NAME(jobs[1], "excl_dab"); QCOMPARE(queue.needsExclusiveAccess(), true); context.clear(); context.addMergeJob(walker); queue.processQueue(context, false); COMPARE_WALKER(jobs[0], walker); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), true); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "excl_dab"); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), true); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "excl_finish"); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), true); context.clear(); queue.processQueue(context, false); QCOMPARE(queue.needsExclusiveAccess(), false); } void KisStrokesQueueTest::testBarrierStrokeJobs() { KisStrokesQueue queue; KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy("nor_", false)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::BARRIER)); queue.addJob(id, new KisStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id); // yes, this walker is not initialized again... but who cares? KisBaseRectsWalkerSP walker = new KisMergeWalker(QRect()); bool externalJobsPending = false; KisTestableUpdaterContext context(3); QVector jobs; queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "nor_init"); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); context.clear(); queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "nor_dab"); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); // Now some updates has come... context.addMergeJob(walker); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "nor_dab"); COMPARE_WALKER(jobs[1], walker); VERIFY_EMPTY(jobs[2]); // No difference for the queue queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "nor_dab"); COMPARE_WALKER(jobs[1], walker); VERIFY_EMPTY(jobs[2]); // Even more updates has come... externalJobsPending = true; // Still no difference for the queue queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "nor_dab"); COMPARE_WALKER(jobs[1], walker); VERIFY_EMPTY(jobs[2]); // Now clear the context context.clear(); // And still no difference for the queue queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); VERIFY_EMPTY(jobs[0]); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); // Process the last update... context.addMergeJob(walker); externalJobsPending = false; // Yep, the queue is still waiting queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); COMPARE_WALKER(jobs[0], walker); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); context.clear(); // Finally, we can do our work. Barrier job is executed alone queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "nor_dab"); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); // Barrier job has finished context.clear(); jobs = context.getJobs(); VERIFY_EMPTY(jobs[0]); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); // fetch the last (concurrent) one queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "nor_dab"); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); context.clear(); // finish the stroke queue.processQueue(context, externalJobsPending); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "nor_finish"); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); context.clear(); } void KisStrokesQueueTest::testStrokesOverlapping() { KisStrokesQueue queue; KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy("1_", false, true)); queue.addJob(id, 0); // comment out this line to catch an assert queue.endStroke(id); id = queue.startStroke(new KisTestingStrokeStrategy("2_", false, true)); queue.addJob(id, 0); queue.endStroke(id); // uncomment this line to catch an assert // queue.addJob(id, 0); KisTestableUpdaterContext context(2); QVector jobs; queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "1_dab"); VERIFY_EMPTY(jobs[1]); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "2_dab"); VERIFY_EMPTY(jobs[1]); } void KisStrokesQueueTest::testImmediateCancel() { KisStrokesQueue queue; KisTestableUpdaterContext context(2); KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy("1_", false, false)); queue.cancelStroke(id); // this should not crash queue.processQueue(context, false); } void KisStrokesQueueTest::testOpenedStrokeCounter() { KisStrokesQueue queue; QVERIFY(!queue.hasOpenedStrokes()); KisStrokeId id0 = queue.startStroke(new KisTestingStrokeStrategy("0")); QVERIFY(queue.hasOpenedStrokes()); KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy("1")); QVERIFY(queue.hasOpenedStrokes()); queue.endStroke(id0); QVERIFY(queue.hasOpenedStrokes()); queue.endStroke(id1); QVERIFY(!queue.hasOpenedStrokes()); KisTestableUpdaterContext context(2); queue.processQueue(context, false); context.clear(); queue.processQueue(context, false); context.clear(); queue.processQueue(context, false); context.clear(); queue.processQueue(context, false); context.clear(); } void KisStrokesQueueTest::testAsyncCancelWhileOpenedStroke() { KisStrokesQueue queue; KisStrokeId id = queue.startStroke(new KisTestingStrokeStrategy("nor_", false)); queue.addJob(id, 0); queue.addJob(id, 0); queue.addJob(id, 0); // no async cancelling until the stroke is ended by the owner QVERIFY(!queue.tryCancelCurrentStrokeAsync()); queue.endStroke(id); QVERIFY(queue.tryCancelCurrentStrokeAsync()); bool externalJobsPending = false; KisTestableUpdaterContext context(3); QVector jobs; queue.processQueue(context, externalJobsPending); // no? really? jobs = context.getJobs(); VERIFY_EMPTY(jobs[0]); VERIFY_EMPTY(jobs[1]); VERIFY_EMPTY(jobs[2]); } struct KisStrokesQueueTest::LodStrokesQueueTester { LodStrokesQueueTester(bool real = false) : fakeContext(2), realContext(2), context(!real ? fakeContext : realContext) { queue.setSuspendUpdatesStrokeStrategyFactory( []() { return KisSuspendResumePair( new KisTestingStrokeStrategy("susp_u_", false, true, true), QList()); }); queue.setResumeUpdatesStrokeStrategyFactory( []() { return KisSuspendResumePair( new KisTestingStrokeStrategy("resu_u_", false, true, true), QList()); }); queue.setLod0ToNStrokeStrategyFactory( [](bool forgettable) { Q_UNUSED(forgettable); return KisSuspendResumePair( new KisTestingStrokeStrategy("sync_u_", false, true, true), QList()); }); } KisStrokesQueue queue; KisTestableUpdaterContext fakeContext; KisUpdaterContext realContext; KisUpdaterContext &context; QVector jobs; void processQueueNoAdd() { if (&context != &fakeContext) return; fakeContext.clear(); jobs = fakeContext.getJobs(); VERIFY_EMPTY(jobs[0]); VERIFY_EMPTY(jobs[1]); } + void processQueueNoContextClear() { + queue.processQueue(context, false); + + if (&context == &realContext) { + context.waitForDone(); + } + } + void processQueue() { processQueueNoAdd(); queue.processQueue(context, false); if (&context == &realContext) { context.waitForDone(); } } + void checkNothing() { + KIS_ASSERT(&context == &fakeContext); + + jobs = fakeContext.getJobs(); + VERIFY_EMPTY(jobs[0]); + VERIFY_EMPTY(jobs[1]); + } + + void checkJobs(const QStringList &list) { + KIS_ASSERT(&context == &fakeContext); + + jobs = fakeContext.getJobs(); + + for (int i = 0; i < 2; i++) { + if (list.size() <= i) { + VERIFY_EMPTY(jobs[i]); + } else { + QVERIFY(jobs[i]->isRunning()); + COMPARE_NAME(jobs[i], list[i]); + } + } + + QCOMPARE(queue.needsExclusiveAccess(), false); + } + void checkOnlyJob(const QString &name) { KIS_ASSERT(&context == &fakeContext); jobs = fakeContext.getJobs(); COMPARE_NAME(jobs[0], name); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), false); } void checkOnlyExecutedJob(const QString &name) { realContext.waitForDone(); QVERIFY(!globalExecutedDabs.isEmpty()); QCOMPARE(globalExecutedDabs[0], name); QCOMPARE(globalExecutedDabs.size(), 1); globalExecutedDabs.clear(); } + + void checkExecutedJobs(const QStringList &list) { + realContext.waitForDone(); + + QCOMPARE(globalExecutedDabs, list); + globalExecutedDabs.clear(); + } + + void checkNothingExecuted() { + realContext.waitForDone(); + QVERIFY(globalExecutedDabs.isEmpty()); + } }; void KisStrokesQueueTest::testStrokesLevelOfDetail() { LodStrokesQueueTester t; KisStrokesQueue &queue = t.queue; // create a stroke with LOD0 + LOD2 queue.setDesiredLevelOfDetail(2); + + // process sync-lodn-planes stroke + t.processQueue(); + t.checkOnlyJob("sync_u_init"); + KisStrokeId id2 = queue.startStroke(new KisTestingStrokeStrategy("lod_", false, true)); queue.addJob(id2, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id2); // create a update with LOD == 0 (default one) // well, this walker is not initialized... but who cares? KisBaseRectsWalkerSP walker = new KisMergeWalker(QRect()); KisTestableUpdaterContext context(2); QVector jobs; context.addMergeJob(walker); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_WALKER(jobs[0], walker); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), false); context.clear(); jobs = context.getJobs(); VERIFY_EMPTY(jobs[0]); VERIFY_EMPTY(jobs[1]); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "clone2_lod_dab"); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), false); // walker of a different LOD must not be allowed QCOMPARE(context.isJobAllowed(walker), false); context.clear(); context.addMergeJob(walker); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_WALKER(jobs[0], walker); COMPARE_NAME(jobs[1], "susp_u_init"); QCOMPARE(queue.needsExclusiveAccess(), false); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "lod_dab"); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), false); context.clear(); queue.processQueue(context, false); jobs = context.getJobs(); COMPARE_NAME(jobs[0], "resu_u_init"); VERIFY_EMPTY(jobs[1]); QCOMPARE(queue.needsExclusiveAccess(), false); context.clear(); } #include #include struct TestUndoCommand : public KUndo2Command { TestUndoCommand(const QString &text) : KUndo2Command(kundo2_noi18n(text)) {} void undo() override { ENTER_FUNCTION(); undoCount++; } void redo() override { ENTER_FUNCTION(); redoCount++; } int undoCount = 0; int redoCount = 0; }; void KisStrokesQueueTest::testLodUndoBase() { LodStrokesQueueTester t; KisStrokesQueue &queue = t.queue; // create a stroke with LOD0 + LOD2 queue.setDesiredLevelOfDetail(2); + + // process sync-lodn-planes stroke + t.processQueue(); + t.checkOnlyJob("sync_u_init"); + KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy("str1_", false, true)); queue.addJob(id1, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id1); KisStrokeId id2 = queue.startStroke(new KisTestingStrokeStrategy("str2_", false, true)); queue.addJob(id2, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id2); t.processQueue(); t.checkOnlyJob("clone2_str1_dab"); QSharedPointer undoStr1(new TestUndoCommand("str1_undo")); queue.lodNPostExecutionUndoAdapter()->addCommand(undoStr1); t.processQueue(); t.checkOnlyJob("clone2_str2_dab"); QSharedPointer undoStr2(new TestUndoCommand("str2_undo")); queue.lodNPostExecutionUndoAdapter()->addCommand(undoStr2); t.processQueue(); t.checkOnlyJob("susp_u_init"); t.processQueue(); t.checkOnlyJob("str1_dab"); t.processQueue(); t.checkOnlyJob("str2_dab"); t.processQueue(); t.checkOnlyJob("resu_u_init"); } void KisStrokesQueueTest::testLodUndoBase2() { LodStrokesQueueTester t(true); KisStrokesQueue &queue = t.queue; // create a stroke with LOD0 + LOD2 queue.setDesiredLevelOfDetail(2); KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy("str1_", false, true, false, true)); queue.addJob(id1, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id1); KisStrokeId id2 = queue.startStroke(new KisTestingStrokeStrategy("str2_", false, true, false, true)); queue.addJob(id2, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); queue.endStroke(id2); t.processQueue(); t.checkOnlyExecutedJob("sync_u_init"); t.processQueue(); t.checkOnlyExecutedJob("clone2_str1_dab"); QSharedPointer undoStr1(new TestUndoCommand("str1_undo")); queue.lodNPostExecutionUndoAdapter()->addCommand(undoStr1); t.processQueue(); t.checkOnlyExecutedJob("clone2_str2_dab"); QSharedPointer undoStr2(new TestUndoCommand("str2_undo")); queue.lodNPostExecutionUndoAdapter()->addCommand(undoStr2); t.processQueue(); t.checkOnlyExecutedJob("susp_u_init"); queue.tryUndoLastStrokeAsync(); t.processQueue(); while (queue.currentStrokeName() == kundo2_noi18n("str2_undo")) { //queue.debugPrintStrokes(); t.processQueue(); } QCOMPARE(undoStr2->undoCount, 1); t.checkOnlyExecutedJob("str1_dab"); t.processQueue(); t.checkOnlyExecutedJob("str2_cancel"); t.processQueue(); t.checkOnlyExecutedJob("resu_u_init"); } +void KisStrokesQueueTest::testMutatedJobs() +{ + LodStrokesQueueTester t(true); + KisStrokesQueue &queue = t.queue; + + KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy("str1_", false, true, false, true)); + + queue.addJob(id1, + new KisTestingStrokeJobData( + KisStrokeJobData::CONCURRENT, + KisStrokeJobData::NORMAL, + true, "1")); + + queue.addJob(id1, + new KisTestingStrokeJobData( + KisStrokeJobData::SEQUENTIAL, + KisStrokeJobData::NORMAL, + false, "2")); + + queue.endStroke(id1); + + t.processQueue(); + + t.checkOnlyExecutedJob("str1_dab_1"); + + t.processQueue(); + + QStringList refList; + refList << "str1_dab_mutated" << "str1_dab_mutated"; + t.checkExecutedJobs(refList); + + t.processQueue(); + t.checkOnlyExecutedJob("str1_dab_mutated"); + + t.processQueue(); + t.checkOnlyExecutedJob("str1_dab_2"); + + t.processQueue(); + t.checkNothingExecuted(); +} + +QString sequentialityToString(KisStrokeJobData::Sequentiality seq) { + QString result = ""; + + switch (seq) { + case KisStrokeJobData::SEQUENTIAL: + result = "SEQUENTIAL"; + break; + case KisStrokeJobData::UNIQUELY_CONCURRENT: + result = "UNIQUELY_CONCURRENT"; + break; + case KisStrokeJobData::BARRIER: + result = "BARRIER"; + break; + case KisStrokeJobData::CONCURRENT: + result = "CONCURRENT"; + break; + } + + return result; +} + +void KisStrokesQueueTest::checkJobsOverlapping(LodStrokesQueueTester &t, + KisStrokeId id, + KisStrokeJobData::Sequentiality first, + KisStrokeJobData::Sequentiality second, + bool allowed) +{ + t.queue.addJob(id, new KisTestingStrokeJobData(first, + KisStrokeJobData::NORMAL, false, "first")); + t.processQueue(); + t.checkJobs({"str1_dab_first"}); + + t.queue.addJob(id, new KisTestingStrokeJobData(second, + KisStrokeJobData::NORMAL, false, "second")); + + qDebug() << QString(" test %1 after %2 allowed: %3 ") + .arg(sequentialityToString(second), 24) + .arg(sequentialityToString(first), 24) + .arg(allowed); + + if (allowed) { + t.processQueueNoContextClear(); + t.checkJobs({"str1_dab_first", "str1_dab_second"}); + } else { + t.processQueueNoContextClear(); + t.checkJobs({"str1_dab_first"}); + + t.processQueue(); + t.checkJobs({"str1_dab_second"}); + } + + t.processQueueNoAdd(); + t.checkNothing(); +} + +void KisStrokesQueueTest::testUniquelyConcurrentJobs() +{ + LodStrokesQueueTester t; + KisStrokesQueue &queue = t.queue; + + KisStrokeId id1 = queue.startStroke(new KisTestingStrokeStrategy("str1_", false, true)); + queue.addJob(id1, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); + queue.addJob(id1, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); + queue.endStroke(id1); + + { // manual test + t.processQueue(); + t.checkJobs({"str1_dab", "str1_dab"}); + + queue.addJob(id1, new KisTestingStrokeJobData(KisStrokeJobData::CONCURRENT)); + t.processQueue(); + t.checkJobs({"str1_dab"}); + + queue.addJob(id1, new KisTestingStrokeJobData(KisStrokeJobData::UNIQUELY_CONCURRENT, + KisStrokeJobData::NORMAL, false, "ucon")); + t.processQueueNoContextClear(); + t.checkJobs({"str1_dab", "str1_dab_ucon"}); + + t.processQueueNoAdd(); + t.checkNothing(); + } + + // Test various cases of overlapping + + checkJobsOverlapping(t, id1, KisStrokeJobData::UNIQUELY_CONCURRENT, KisStrokeJobData::CONCURRENT, true); + checkJobsOverlapping(t, id1, KisStrokeJobData::UNIQUELY_CONCURRENT, KisStrokeJobData::UNIQUELY_CONCURRENT, false); + checkJobsOverlapping(t, id1, KisStrokeJobData::UNIQUELY_CONCURRENT, KisStrokeJobData::SEQUENTIAL, false); + checkJobsOverlapping(t, id1, KisStrokeJobData::UNIQUELY_CONCURRENT, KisStrokeJobData::BARRIER, false); + + checkJobsOverlapping(t, id1, KisStrokeJobData::CONCURRENT, KisStrokeJobData::UNIQUELY_CONCURRENT , true); + checkJobsOverlapping(t, id1, KisStrokeJobData::UNIQUELY_CONCURRENT, KisStrokeJobData::UNIQUELY_CONCURRENT, false); + checkJobsOverlapping(t, id1, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::UNIQUELY_CONCURRENT, false); + checkJobsOverlapping(t, id1, KisStrokeJobData::BARRIER, KisStrokeJobData::UNIQUELY_CONCURRENT, false); +} + QTEST_MAIN(KisStrokesQueueTest) diff --git a/libs/image/tests/kis_strokes_queue_test.h b/libs/image/tests/kis_strokes_queue_test.h index 3fdefdf84c..b412132025 100644 --- a/libs/image/tests/kis_strokes_queue_test.h +++ b/libs/image/tests/kis_strokes_queue_test.h @@ -1,46 +1,50 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_STROKES_QUEUE_TEST_H #define __KIS_STROKES_QUEUE_TEST_H #include - +#include "kis_types.h" +#include "kis_stroke_job_strategy.h" class KisStrokesQueueTest : public QObject { Q_OBJECT private Q_SLOTS: void testSequentialJobs(); void testConcurrentSequentialBarrier(); void testExclusiveStrokes(); void testBarrierStrokeJobs(); void testStrokesOverlapping(); void testImmediateCancel(); void testOpenedStrokeCounter(); void testAsyncCancelWhileOpenedStroke(); void testStrokesLevelOfDetail(); void testLodUndoBase(); void testLodUndoBase2(); + void testMutatedJobs(); + void testUniquelyConcurrentJobs(); private: struct LodStrokesQueueTester; + static void checkJobsOverlapping(LodStrokesQueueTester &t, KisStrokeId id, KisStrokeJobData::Sequentiality first, KisStrokeJobData::Sequentiality second, bool allowed); }; #endif /* __KIS_STROKES_QUEUE_TEST_H */ diff --git a/libs/image/tests/scheduler_utils.h b/libs/image/tests/scheduler_utils.h index 13ff5be0e3..6b5d683969 100644 --- a/libs/image/tests/scheduler_utils.h +++ b/libs/image/tests/scheduler_utils.h @@ -1,231 +1,281 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __SCHEDULER_UTILS_H #define __SCHEDULER_UTILS_H #include #include "kis_merge_walker.h" #include "kis_stroke_strategy.h" #include "kis_stroke_job.h" #include "kis_spontaneous_job.h" #include "kis_stroke.h" #include "kis_image.h" #define SCOMPARE(s1, s2) QCOMPARE(QString(s1), QString(s2)) #define COMPARE_WALKER(item, walker) \ QCOMPARE(item->walker(), walker) #define COMPARE_NAME(item, name) \ QCOMPARE(getJobName(item->strokeJob()), QString(name)) #define VERIFY_EMPTY(item) \ QVERIFY(!item->isRunning()) void executeStrokeJobs(KisStroke *stroke) { KisStrokeJob *job; while((job = stroke->popOneJob())) { job->run(); delete job; } } bool checkWalker(KisBaseRectsWalkerSP walker, const QRect &rect, int lod = 0) { if(walker->requestedRect() == rect && walker->levelOfDetail() == lod) { return true; } else { dbgKrita << "walker rect:" << walker->requestedRect(); dbgKrita << "expected rect:" << rect; dbgKrita << "walker lod:" << walker->levelOfDetail(); dbgKrita << "expected lod:" << lod; return false; } } class KisNoopSpontaneousJob : public KisSpontaneousJob { public: KisNoopSpontaneousJob(bool overridesEverything = false, int lod = 0) : m_overridesEverything(overridesEverything), m_lod(lod) { } void run() override { } bool overrides(const KisSpontaneousJob *otherJob) override { Q_UNUSED(otherJob); return m_overridesEverything; } int levelOfDetail() const override { return m_lod; } private: bool m_overridesEverything; int m_lod; }; static QStringList globalExecutedDabs; class KisNoopDabStrategy : public KisStrokeJobStrategy { public: KisNoopDabStrategy(QString name) : m_name(name), m_isMarked(false) {} void run(KisStrokeJobData *data) override { Q_UNUSED(data); globalExecutedDabs << m_name; } - QString name() { + virtual QString name(KisStrokeJobData *data) const { + Q_UNUSED(data); return m_name; } void setMarked() { m_isMarked = true; } bool isMarked() const { return m_isMarked; } private: QString m_name; bool m_isMarked; }; class KisTestingStrokeJobData : public KisStrokeJobData { public: KisTestingStrokeJobData(Sequentiality sequentiality = SEQUENTIAL, - Exclusivity exclusivity = NORMAL) - : KisStrokeJobData(sequentiality, exclusivity) + Exclusivity exclusivity = NORMAL, + bool addMutatedJobs = false, + const QString &customSuffix = QString()) + : KisStrokeJobData(sequentiality, exclusivity), + m_addMutatedJobs(addMutatedJobs), + m_customSuffix(customSuffix) { } KisTestingStrokeJobData(const KisTestingStrokeJobData &rhs) - : KisStrokeJobData(rhs) + : KisStrokeJobData(rhs), + m_addMutatedJobs(rhs.m_addMutatedJobs) { } KisStrokeJobData* createLodClone(int levelOfDetail) override { Q_UNUSED(levelOfDetail); return new KisTestingStrokeJobData(*this); } + + bool m_addMutatedJobs = false; + bool m_isMutated = false; + QString m_customSuffix; +}; + +class KisMutatableDabStrategy : public KisNoopDabStrategy +{ +public: + KisMutatableDabStrategy(const QString &name, KisStrokeStrategy *parentStrokeStrategy) + : KisNoopDabStrategy(name), + m_parentStrokeStrategy(parentStrokeStrategy) + { + } + + void run(KisStrokeJobData *data) override { + KisTestingStrokeJobData *td = dynamic_cast(data); + + if (td && td->m_isMutated) { + globalExecutedDabs << QString("%1_mutated").arg(name(data)); + } else if (td && td->m_addMutatedJobs) { + globalExecutedDabs << name(data); + + for (int i = 0; i < 3; i++) { + KisTestingStrokeJobData *newData = + new KisTestingStrokeJobData(td->sequentiality(), td->exclusivity(), false); + newData->m_isMutated = true; + m_parentStrokeStrategy->addMutatedJob(newData); + } + } else { + globalExecutedDabs << name(data); + } + } + + virtual QString name(KisStrokeJobData *data) const { + const QString baseName = KisNoopDabStrategy::name(data); + + KisTestingStrokeJobData *td = dynamic_cast(data); + return !td || td->m_customSuffix.isEmpty() ? baseName : QString("%1_%2").arg(baseName).arg(td->m_customSuffix); + } + +private: + KisStrokeStrategy *m_parentStrokeStrategy = 0; }; + class KisTestingStrokeStrategy : public KisStrokeStrategy { public: KisTestingStrokeStrategy(const QString &prefix = QString(), bool exclusive = false, bool inhibitServiceJobs = false, bool forceAllowInitJob = false, bool forceAllowCancelJob = false) : KisStrokeStrategy(prefix, kundo2_noi18n(prefix)), m_prefix(prefix), m_inhibitServiceJobs(inhibitServiceJobs), m_forceAllowInitJob(forceAllowInitJob), m_forceAllowCancelJob(forceAllowCancelJob), m_cancelSeqNo(0) { setExclusive(exclusive); } KisTestingStrokeStrategy(const KisTestingStrokeStrategy &rhs, int levelOfDetail) : KisStrokeStrategy(rhs), m_prefix(rhs.m_prefix), m_inhibitServiceJobs(rhs.m_inhibitServiceJobs), m_forceAllowInitJob(rhs.m_forceAllowInitJob), m_cancelSeqNo(rhs.m_cancelSeqNo) { m_prefix = QString("clone%1_%2").arg(levelOfDetail).arg(m_prefix); } KisStrokeJobStrategy* createInitStrategy() override { return m_forceAllowInitJob || !m_inhibitServiceJobs ? new KisNoopDabStrategy(m_prefix + "init") : 0; } KisStrokeJobStrategy* createFinishStrategy() override { return !m_inhibitServiceJobs ? new KisNoopDabStrategy(m_prefix + "finish") : 0; } KisStrokeJobStrategy* createCancelStrategy() override { return m_forceAllowCancelJob || !m_inhibitServiceJobs ? new KisNoopDabStrategy(m_prefix + "cancel") : 0; } KisStrokeJobStrategy* createDabStrategy() override { - return new KisNoopDabStrategy(m_prefix + "dab"); + return new KisMutatableDabStrategy(m_prefix + "dab", this); } KisStrokeStrategy* createLodClone(int levelOfDetail) override { return new KisTestingStrokeStrategy(*this, levelOfDetail); } class CancelData : public KisStrokeJobData { public: CancelData(int seqNo) : m_seqNo(seqNo) {} int seqNo() const { return m_seqNo; } private: int m_seqNo; }; KisStrokeJobData* createCancelData() override { return new CancelData(m_cancelSeqNo++); } private: QString m_prefix; bool m_inhibitServiceJobs; int m_forceAllowInitJob; bool m_forceAllowCancelJob; int m_cancelSeqNo; }; inline QString getJobName(KisStrokeJob *job) { KisNoopDabStrategy *pointer = dynamic_cast(job->testingGetDabStrategy()); - Q_ASSERT(pointer); + KIS_ASSERT(pointer); - return pointer->name(); + return pointer->name(job->testingGetDabData()); } inline int cancelSeqNo(KisStrokeJob *job) { KisTestingStrokeStrategy::CancelData *pointer = dynamic_cast (job->testingGetDabData()); Q_ASSERT(pointer); return pointer->seqNo(); } #endif /* __SCHEDULER_UTILS_H */ diff --git a/libs/image/tiles3/kis_tiled_data_manager.cc b/libs/image/tiles3/kis_tiled_data_manager.cc index 8bcf684f8d..df29afcb77 100644 --- a/libs/image/tiles3/kis_tiled_data_manager.cc +++ b/libs/image/tiles3/kis_tiled_data_manager.cc @@ -1,812 +1,818 @@ /* * Copyright (c) 2004 C. Boemann * (c) 2009 Dmitry Kazakov * (c) 2010 Cyrille Berger * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include "kis_tile.h" #include "kis_tiled_data_manager.h" #include "kis_tile_data_wrapper.h" #include "kis_tiled_data_manager_p.h" #include "kis_memento_manager.h" #include "swap/kis_legacy_tile_compressor.h" #include "swap/kis_tile_compressor_factory.h" #include "kis_paint_device_writer.h" #include "kis_global.h" /* The data area is divided into tiles each say 64x64 pixels (defined at compiletime) * The tiles are laid out in a matrix that can have negative indexes. * The matrix grows automatically if needed (a call for writeacces to a tile * outside the current extent) * Even though the matrix has grown it may still not contain tiles at specific positions. * They are created on demand */ KisTiledDataManager::KisTiledDataManager(quint32 pixelSize, const quint8 *defaultPixel) { /* See comment in destructor for details */ m_mementoManager = new KisMementoManager(); m_hashTable = new KisTileHashTable(m_mementoManager); m_pixelSize = pixelSize; m_defaultPixel = new quint8[m_pixelSize]; setDefaultPixel(defaultPixel); m_extentMinX = qint32_MAX; m_extentMinY = qint32_MAX; m_extentMaxX = qint32_MIN; m_extentMaxY = qint32_MIN; } KisTiledDataManager::KisTiledDataManager(const KisTiledDataManager &dm) : KisShared() { /* See comment in destructor for details */ /* We do not clone the history of the device, there is no usecase for it */ m_mementoManager = new KisMementoManager(); m_mementoManager->setDefaultTileData(dm.m_hashTable->defaultTileData()); m_hashTable = new KisTileHashTable(*dm.m_hashTable, m_mementoManager); m_pixelSize = dm.m_pixelSize; m_defaultPixel = new quint8[m_pixelSize]; /** * We won't call setDefaultTileData here, as defaultTileDatas * has already been made shared in m_hashTable(dm->m_hashTable) */ memcpy(m_defaultPixel, dm.m_defaultPixel, m_pixelSize); m_extentMinX = dm.m_extentMinX; m_extentMinY = dm.m_extentMinY; m_extentMaxX = dm.m_extentMaxX; m_extentMaxY = dm.m_extentMaxY; } KisTiledDataManager::~KisTiledDataManager() { /** * Here is an explanation why we use hash table and The Memento Manager * dynamically allocated We need to destroy them in that very order. The * reason is that when hash table destroying all her child tiles they all * cry about it to The Memento Manager using a pointer. So The Memento * Manager sould be alive during that destruction. We could use shared * pointers instead, but they create too much overhead. */ delete m_hashTable; delete m_mementoManager; delete[] m_defaultPixel; } void KisTiledDataManager::setDefaultPixel(const quint8 *defaultPixel) { QWriteLocker locker(&m_lock); setDefaultPixelImpl(defaultPixel); } void KisTiledDataManager::setDefaultPixelImpl(const quint8 *defaultPixel) { KisTileData *td = KisTileDataStore::instance()->createDefaultTileData(pixelSize(), defaultPixel); m_hashTable->setDefaultTileData(td); m_mementoManager->setDefaultTileData(td); memcpy(m_defaultPixel, defaultPixel, pixelSize()); } bool KisTiledDataManager::write(KisPaintDeviceWriter &store) { QReadLocker locker(&m_lock); bool retval = true; if(CURRENT_VERSION == LEGACY_VERSION) { char str[80]; sprintf(str, "%d\n", m_hashTable->numTiles()); retval = store.write(str, strlen(str)); } else { retval = writeTilesHeader(store, m_hashTable->numTiles()); } KisTileHashTableConstIterator iter(m_hashTable); KisTileSP tile; KisAbstractTileCompressorSP compressor = KisTileCompressorFactory::create(CURRENT_VERSION); while ((tile = iter.tile())) { retval = compressor->writeTile(tile, store); if (!retval) { warnFile << "Failed to write tile"; break; } iter.next(); } return retval; } bool KisTiledDataManager::read(QIODevice *stream) { if (!stream) return false; clear(); QWriteLocker locker(&m_lock); KisMementoSP nothing = m_mementoManager->getMemento(); if (!stream) { m_mementoManager->commit(); return false; } const qint32 maxLineLength = 79; // Legacy magic QByteArray line = stream->readLine(maxLineLength); line = line.trimmed(); quint32 numTiles; qint32 tilesVersion = LEGACY_VERSION; if (line[0] == 'V') { QList lineItems = line.split(' '); QString keyword = lineItems.takeFirst(); Q_ASSERT(keyword == "VERSION"); tilesVersion = lineItems.takeFirst().toInt(); if(!processTilesHeader(stream, numTiles)) return false; } else { numTiles = line.toUInt(); } KisAbstractTileCompressorSP compressor = KisTileCompressorFactory::create(tilesVersion); bool readSuccess = true; for (quint32 i = 0; i < numTiles; i++) { if (!compressor->readTile(stream, this)) { readSuccess = false; } } m_mementoManager->commit(); return readSuccess; } bool KisTiledDataManager::writeTilesHeader(KisPaintDeviceWriter &store, quint32 numTiles) { QString buffer; buffer = QString("VERSION %1\n" "TILEWIDTH %2\n" "TILEHEIGHT %3\n" "PIXELSIZE %4\n" "DATA %5\n") .arg(CURRENT_VERSION) .arg(KisTileData::WIDTH) .arg(KisTileData::HEIGHT) .arg(pixelSize()) .arg(numTiles); return store.write(buffer.toLatin1()); } #define takeOneLine(stream, maxLine, keyword, value) \ do { \ QByteArray line = stream->readLine(maxLine); \ line = line.trimmed(); \ QList lineItems = line.split(' '); \ keyword = lineItems.takeFirst(); \ value = lineItems.takeFirst().toInt(); \ } while(0) \ bool KisTiledDataManager::processTilesHeader(QIODevice *stream, quint32 &numTiles) { /** * We assume that there is only one version of this header * possible. In case we invent something new, it'll be quite easy * to modify the behavior */ const qint32 maxLineLength = 25; const qint32 totalNumTests = 4; bool foundDataMark = false; qint32 testsPassed = 0; QString keyword; qint32 value; while(!foundDataMark && stream->canReadLine()) { takeOneLine(stream, maxLineLength, keyword, value); if (keyword == "TILEWIDTH") { if(value != KisTileData::WIDTH) goto wrongString; } else if (keyword == "TILEHEIGHT") { if(value != KisTileData::HEIGHT) goto wrongString; } else if (keyword == "PIXELSIZE") { if((quint32)value != pixelSize()) goto wrongString; } else if (keyword == "DATA") { numTiles = value; foundDataMark = true; } else { goto wrongString; } testsPassed++; } if(testsPassed != totalNumTests) { warnTiles << "Not enough fields of tiles header present" << testsPassed << "of" << totalNumTests; } return testsPassed == totalNumTests; wrongString: warnTiles << "Wrong string in tiles header:" << keyword << value; return false; } void KisTiledDataManager::purge(const QRect& area) { QWriteLocker locker(&m_lock); QList tilesToDelete; { const qint32 tileDataSize = KisTileData::HEIGHT * KisTileData::WIDTH * pixelSize(); KisTileData *tileData = m_hashTable->defaultTileData(); tileData->blockSwapping(); const quint8 *defaultData = tileData->data(); KisTileHashTableConstIterator iter(m_hashTable); KisTileSP tile; while ((tile = iter.tile())) { if (tile->extent().intersects(area)) { tile->lockForRead(); if(memcmp(defaultData, tile->data(), tileDataSize) == 0) { tilesToDelete.push_back(tile); } tile->unlock(); } iter.next(); } tileData->unblockSwapping(); } Q_FOREACH (KisTileSP tile, tilesToDelete) { m_hashTable->deleteTile(tile); } recalculateExtent(); } quint8* KisTiledDataManager::duplicatePixel(qint32 num, const quint8 *pixel) { const qint32 pixelSize = this->pixelSize(); /* FIXME: Make a fun filling here */ quint8 *dstBuf = new quint8[num * pixelSize]; quint8 *dstIt = dstBuf; for (qint32 i = 0; i < num; i++) { memcpy(dstIt, pixel, pixelSize); dstIt += pixelSize; } return dstBuf; } void KisTiledDataManager::clear(QRect clearRect, const quint8 *clearPixel) { QWriteLocker locker(&m_lock); if (clearPixel == 0) clearPixel = m_defaultPixel; if (clearRect.isEmpty()) return; const qint32 pixelSize = this->pixelSize(); bool pixelBytesAreDefault = !memcmp(clearPixel, m_defaultPixel, pixelSize); bool pixelBytesAreTheSame = true; for (qint32 i = 0; i < pixelSize; ++i) { if (clearPixel[i] != clearPixel[0]) { pixelBytesAreTheSame = false; break; } } if (pixelBytesAreDefault) { clearRect &= extentImpl(); } qint32 firstColumn = xToCol(clearRect.left()); qint32 lastColumn = xToCol(clearRect.right()); qint32 firstRow = yToRow(clearRect.top()); qint32 lastRow = yToRow(clearRect.bottom()); const quint32 rowStride = KisTileData::WIDTH * pixelSize; // Generate one row quint8 *clearPixelData = 0; quint32 maxRunLength = qMin(clearRect.width(), KisTileData::WIDTH); clearPixelData = duplicatePixel(maxRunLength, clearPixel); KisTileData *td = 0; if (!pixelBytesAreDefault && clearRect.width() >= KisTileData::WIDTH && clearRect.height() >= KisTileData::HEIGHT) { td = KisTileDataStore::instance()->createDefaultTileData(pixelSize, clearPixel); td->acquire(); } bool needsRecalculateExtent = false; for (qint32 row = firstRow; row <= lastRow; ++row) { for (qint32 column = firstColumn; column <= lastColumn; ++column) { QRect tileRect(column*KisTileData::WIDTH, row*KisTileData::HEIGHT, KisTileData::WIDTH, KisTileData::HEIGHT); QRect clearTileRect = clearRect & tileRect; if (clearTileRect == tileRect) { // Clear whole tile m_hashTable->deleteTile(column, row); - needsRecalculateExtent = true; + + if (!needsRecalculateExtent && + (m_extentMinX == tileRect.left() || m_extentMaxX == tileRect.right() || + m_extentMinY == tileRect.top() || m_extentMaxY == tileRect.bottom())) { + + needsRecalculateExtent = true; + } if (!pixelBytesAreDefault) { KisTileSP clearedTile = KisTileSP(new KisTile(column, row, td, m_mementoManager)); m_hashTable->addTile(clearedTile); updateExtent(column, row); } } else { const qint32 lineSize = clearTileRect.width() * pixelSize; qint32 rowsRemaining = clearTileRect.height(); KisTileDataWrapper tw(this, clearTileRect.left(), clearTileRect.top(), KisTileDataWrapper::WRITE); quint8* tileIt = tw.data(); if (pixelBytesAreTheSame) { while (rowsRemaining > 0) { memset(tileIt, *clearPixelData, lineSize); tileIt += rowStride; rowsRemaining--; } } else { while (rowsRemaining > 0) { memcpy(tileIt, clearPixelData, lineSize); tileIt += rowStride; rowsRemaining--; } } } } } if (needsRecalculateExtent) { recalculateExtent(); } if (td) td->release(); delete[] clearPixelData; } void KisTiledDataManager::clear(QRect clearRect, quint8 clearValue) { quint8 *buf = new quint8[pixelSize()]; memset(buf, clearValue, pixelSize()); clear(clearRect, buf); delete[] buf; } void KisTiledDataManager::clear(qint32 x, qint32 y, qint32 w, qint32 h, const quint8 *clearPixel) { clear(QRect(x, y, w, h), clearPixel); } void KisTiledDataManager::clear(qint32 x, qint32 y, qint32 w, qint32 h, quint8 clearValue) { clear(QRect(x, y, w, h), clearValue); } void KisTiledDataManager::clear() { QWriteLocker locker(&m_lock); m_hashTable->clear(); m_extentMinX = qint32_MAX; m_extentMinY = qint32_MAX; m_extentMaxX = qint32_MIN; m_extentMaxY = qint32_MIN; } template void KisTiledDataManager::bitBltImpl(KisTiledDataManager *srcDM, const QRect &rect) { QWriteLocker locker(&m_lock); if (rect.isEmpty()) return; const qint32 pixelSize = this->pixelSize(); const quint32 rowStride = KisTileData::WIDTH * pixelSize; qint32 firstColumn = xToCol(rect.left()); qint32 lastColumn = xToCol(rect.right()); qint32 firstRow = yToRow(rect.top()); qint32 lastRow = yToRow(rect.bottom()); for (qint32 row = firstRow; row <= lastRow; ++row) { for (qint32 column = firstColumn; column <= lastColumn; ++column) { // this is the only variation in the template KisTileSP srcTile = useOldSrcData ? srcDM->getOldTile(column, row) : srcDM->getTile(column, row, false); QRect tileRect(column*KisTileData::WIDTH, row*KisTileData::HEIGHT, KisTileData::WIDTH, KisTileData::HEIGHT); QRect cloneTileRect = rect & tileRect; if (cloneTileRect == tileRect) { // Clone whole tile m_hashTable->deleteTile(column, row); srcTile->lockForRead(); KisTileData *td = srcTile->tileData(); KisTileSP clonedTile = KisTileSP(new KisTile(column, row, td, m_mementoManager)); srcTile->unlock(); m_hashTable->addTile(clonedTile); updateExtent(column, row); } else { const qint32 lineSize = cloneTileRect.width() * pixelSize; qint32 rowsRemaining = cloneTileRect.height(); KisTileDataWrapper tw(this, cloneTileRect.left(), cloneTileRect.top(), KisTileDataWrapper::WRITE); srcTile->lockForRead(); // We suppose that the shift in both tiles is the same const quint8* srcTileIt = srcTile->data() + tw.offset(); quint8* dstTileIt = tw.data(); while (rowsRemaining > 0) { memcpy(dstTileIt, srcTileIt, lineSize); srcTileIt += rowStride; dstTileIt += rowStride; rowsRemaining--; } srcTile->unlock(); } } } } template void KisTiledDataManager::bitBltRoughImpl(KisTiledDataManager *srcDM, const QRect &rect) { QWriteLocker locker(&m_lock); if (rect.isEmpty()) return; qint32 firstColumn = xToCol(rect.left()); qint32 lastColumn = xToCol(rect.right()); qint32 firstRow = yToRow(rect.top()); qint32 lastRow = yToRow(rect.bottom()); for (qint32 row = firstRow; row <= lastRow; ++row) { for (qint32 column = firstColumn; column <= lastColumn; ++column) { /** * We are cloning whole tiles here so let's not be so boring * to check any borders :) */ // this is the only variation in the template KisTileSP srcTile = useOldSrcData ? srcDM->getOldTile(column, row) : srcDM->getTile(column, row, false); m_hashTable->deleteTile(column, row); srcTile->lockForRead(); KisTileData *td = srcTile->tileData(); KisTileSP clonedTile = KisTileSP(new KisTile(column, row, td, m_mementoManager)); srcTile->unlock(); m_hashTable->addTile(clonedTile); updateExtent(column, row); } } } void KisTiledDataManager::bitBlt(KisTiledDataManager *srcDM, const QRect &rect) { bitBltImpl(srcDM, rect); } void KisTiledDataManager::bitBltOldData(KisTiledDataManager *srcDM, const QRect &rect) { bitBltImpl(srcDM, rect); } void KisTiledDataManager::bitBltRough(KisTiledDataManager *srcDM, const QRect &rect) { bitBltRoughImpl(srcDM, rect); } void KisTiledDataManager::bitBltRoughOldData(KisTiledDataManager *srcDM, const QRect &rect) { bitBltRoughImpl(srcDM, rect); } void KisTiledDataManager::setExtent(qint32 x, qint32 y, qint32 w, qint32 h) { setExtent(QRect(x, y, w, h)); } void KisTiledDataManager::setExtent(QRect newRect) { QRect oldRect = extent(); newRect = newRect.normalized(); // Do nothing if the desired size is bigger than we currently are: // that is handled by the autoextending automatically if (newRect.contains(oldRect)) return; QWriteLocker locker(&m_lock); KisTileSP tile; QRect tileRect; { KisTileHashTableIterator iter(m_hashTable); while (!iter.isDone()) { tile = iter.tile(); tileRect = tile->extent(); if (newRect.contains(tileRect)) { //do nothing iter.next(); } else if (newRect.intersects(tileRect)) { QRect intersection = newRect & tileRect; intersection.translate(- tileRect.topLeft()); const qint32 pixelSize = this->pixelSize(); tile->lockForWrite(); quint8* data = tile->data(); quint8* ptr; /* FIXME: make it faster */ for (int y = 0; y < KisTileData::HEIGHT; y++) { for (int x = 0; x < KisTileData::WIDTH; x++) { if (!intersection.contains(x, y)) { ptr = data + pixelSize * (y * KisTileData::WIDTH + x); memcpy(ptr, m_defaultPixel, pixelSize); } } } tile->unlock(); iter.next(); } else { iter.deleteCurrent(); } } } recalculateExtent(); } void KisTiledDataManager::recalculateExtent() { m_extentMinX = qint32_MAX; m_extentMinY = qint32_MAX; m_extentMaxX = qint32_MIN; m_extentMaxY = qint32_MIN; KisTileHashTableConstIterator iter(m_hashTable); KisTileSP tile; while ((tile = iter.tile())) { updateExtent(tile->col(), tile->row()); iter.next(); } } void KisTiledDataManager::updateExtent(qint32 col, qint32 row) { const qint32 tileMinX = col * KisTileData::WIDTH; const qint32 tileMinY = row * KisTileData::HEIGHT; const qint32 tileMaxX = tileMinX + KisTileData::WIDTH - 1; const qint32 tileMaxY = tileMinY + KisTileData::HEIGHT - 1; m_extentMinX = qMin(m_extentMinX, tileMinX); m_extentMaxX = qMax(m_extentMaxX, tileMaxX); m_extentMinY = qMin(m_extentMinY, tileMinY); m_extentMaxY = qMax(m_extentMaxY, tileMaxY); } QRect KisTiledDataManager::extentImpl() const { qint32 x = m_extentMinX; qint32 y = m_extentMinY; qint32 w = (m_extentMaxX >= m_extentMinX) ? m_extentMaxX - m_extentMinX + 1 : 0; qint32 h = (m_extentMaxY >= m_extentMinY) ? m_extentMaxY - m_extentMinY + 1 : 0; return QRect(x, y, w, h); } void KisTiledDataManager::extent(qint32 &x, qint32 &y, qint32 &w, qint32 &h) const { QRect rect = extent(); rect.getRect(&x, &y, &w, &h); } QRect KisTiledDataManager::extent() const { QReadLocker locker(&m_lock); return extentImpl(); } QRegion KisTiledDataManager::region() const { QRegion region; KisTileHashTableConstIterator iter(m_hashTable); KisTileSP tile; while ((tile = iter.tile())) { region += tile->extent(); iter.next(); } return region; } void KisTiledDataManager::setPixel(qint32 x, qint32 y, const quint8 * data) { QWriteLocker locker(&m_lock); KisTileDataWrapper tw(this, x, y, KisTileDataWrapper::WRITE); memcpy(tw.data(), data, pixelSize()); } void KisTiledDataManager::writeBytes(const quint8 *data, qint32 x, qint32 y, qint32 width, qint32 height, qint32 dataRowStride) { QWriteLocker locker(&m_lock); // Actial bytes reading/writing is done in private header writeBytesBody(data, x, y, width, height, dataRowStride); } void KisTiledDataManager::readBytes(quint8 *data, qint32 x, qint32 y, qint32 width, qint32 height, qint32 dataRowStride) const { QReadLocker locker(&m_lock); // Actual bytes reading/writing is done in private header readBytesBody(data, x, y, width, height, dataRowStride); } QVector KisTiledDataManager::readPlanarBytes(QVector channelSizes, qint32 x, qint32 y, qint32 width, qint32 height) const { QReadLocker locker(&m_lock); // Actial bytes reading/writing is done in private header return readPlanarBytesBody(channelSizes, x, y, width, height); } void KisTiledDataManager::writePlanarBytes(QVector planes, QVector channelSizes, qint32 x, qint32 y, qint32 width, qint32 height) { QWriteLocker locker(&m_lock); // Actial bytes reading/writing is done in private header bool allChannelsPresent = true; Q_FOREACH (const quint8* plane, planes) { if (!plane) { allChannelsPresent = false; break; } } if (allChannelsPresent) { writePlanarBytesBody(planes, channelSizes, x, y, width, height); } else { writePlanarBytesBody(planes, channelSizes, x, y, width, height); } } qint32 KisTiledDataManager::numContiguousColumns(qint32 x, qint32 minY, qint32 maxY) const { qint32 numColumns; Q_UNUSED(minY); Q_UNUSED(maxY); if (x >= 0) { numColumns = KisTileData::WIDTH - (x % KisTileData::WIDTH); } else { numColumns = ((-x - 1) % KisTileData::WIDTH) + 1; } return numColumns; } qint32 KisTiledDataManager::numContiguousRows(qint32 y, qint32 minX, qint32 maxX) const { qint32 numRows; Q_UNUSED(minX); Q_UNUSED(maxX); if (y >= 0) { numRows = KisTileData::HEIGHT - (y % KisTileData::HEIGHT); } else { numRows = ((-y - 1) % KisTileData::HEIGHT) + 1; } return numRows; } qint32 KisTiledDataManager::rowStride(qint32 x, qint32 y) const { Q_UNUSED(x); Q_UNUSED(y); return KisTileData::WIDTH * pixelSize(); } void KisTiledDataManager::releaseInternalPools() { KisTileData::releaseInternalPools(); } diff --git a/libs/libqml/plugins/kritasketchplugin/models/CompositeOpModel.cpp b/libs/libqml/plugins/kritasketchplugin/models/CompositeOpModel.cpp index 9ad9f70f2e..c0f8cb65ab 100644 --- a/libs/libqml/plugins/kritasketchplugin/models/CompositeOpModel.cpp +++ b/libs/libqml/plugins/kritasketchplugin/models/CompositeOpModel.cpp @@ -1,482 +1,482 @@ /* Copyright (C) 2012 Dan Leinir Turthra Jensen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "CompositeOpModel.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class CompositeOpModel::Private { public: Private(CompositeOpModel* qq) : q(qq) , model(KisCompositeOpListModel::sharedInstance()) , view(0) , eraserMode(0) , opacity(0) , opacityEnabled(false) , flow(0) , flowEnabled(false) , size(0) , sizeEnabled(false) , presetsEnabled(true) {}; CompositeOpModel* q; KisCompositeOpListModel* model; KisViewManager* view; QString currentCompositeOpID; QString prevCompositeOpID; bool eraserMode; QMap settingsWidgets; qreal opacity; bool opacityEnabled; qreal flow; bool flowEnabled; qreal size; bool sizeEnabled; bool presetsEnabled; KisPaintOpPresetSP currentPreset; void updateCompositeOp(QString compositeOpID) { if (!view) return; KisNodeSP node = view->resourceProvider()->currentNode(); if (node && node->paintDevice()) { if (!node->paintDevice()->colorSpace()->hasCompositeOp(compositeOpID)) compositeOpID = KoCompositeOpRegistry::instance().getDefaultCompositeOp().id(); if (compositeOpID != currentCompositeOpID) { q->setEraserMode(compositeOpID == COMPOSITE_ERASE); currentPreset->settings()->setProperty("CompositeOp", compositeOpID); //m_optionWidget->setConfiguration(m_activePreset->settings().data()); view->resourceProvider()->setCurrentCompositeOp(compositeOpID); prevCompositeOpID = currentCompositeOpID; currentCompositeOpID = compositeOpID; } } emit q->currentCompositeOpIDChanged(); } void ofsChanged() { if (presetsEnabled && !currentPreset.isNull() && !currentPreset->settings().isNull()) { // IMPORTANT: set the PaintOp size before setting the other properties // it wont work the other way - qreal sizeDiff = size - currentPreset->settings()->paintOpSize(); + //qreal sizeDiff = size - currentPreset->settings()->paintOpSize(); //currentPreset->settings()->changePaintOpSize(sizeDiff, 0); if (currentPreset->settings()->hasProperty("OpacityValue")) currentPreset->settings()->setProperty("OpacityValue", opacity); if (currentPreset->settings()->hasProperty("FlowValue")) currentPreset->settings()->setProperty("FlowValue", flow); //m_optionWidget->setConfiguration(d->currentPreset->settings().data()); } if (view) { view->resourceProvider()->setOpacity(opacity); } } }; CompositeOpModel::CompositeOpModel(QObject* parent) : QAbstractListModel(parent) , d(new Private(this)) { connect(KoToolManager::instance(), SIGNAL(changedTool(KoCanvasController*,int)), this, SLOT(slotToolChanged(KoCanvasController*,int))); } CompositeOpModel::~CompositeOpModel() { delete d; } QHash CompositeOpModel::roleNames() const { QHash roles; roles[TextRole] = "text"; roles[IsCategoryRole] = "isCategory"; return roles; } QVariant CompositeOpModel::data(const QModelIndex& index, int role) const { QVariant data; if (index.isValid()) { QModelIndex otherIndex = d->model->index(index.row(), index.column(), QModelIndex()); switch(role) { case TextRole: data = d->model->data(otherIndex, Qt::DisplayRole); break; case IsCategoryRole: data = d->model->data(otherIndex, __CategorizedListModelBase::IsHeaderRole); break; default: break; } } return data; } int CompositeOpModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return d->model->rowCount(QModelIndex()); } void CompositeOpModel::activateItem(int index) { if (index > -1 && index < d->model->rowCount(QModelIndex())) { KoID compositeOp; if (d->model->entryAt(compositeOp, d->model->index(index))) d->updateCompositeOp(compositeOp.id()); } } QObject* CompositeOpModel::view() const { return d->view; } void CompositeOpModel::setView(QObject* newView) { if (d->view) { d->view->canvasBase()->disconnect(this); d->view->canvasBase()->globalInputManager()->disconnect(this); d->view->nodeManager()->disconnect(this); } d->view = qobject_cast( newView ); if (d->view) { if (d->view->canvasBase() && d->view->canvasBase()->resourceManager()) { connect(d->view->canvasBase()->resourceManager(), SIGNAL(canvasResourceChanged(int, const QVariant&)), this, SLOT(resourceChanged(int, const QVariant&))); } if (d->view->nodeManager()) { connect(d->view->nodeManager(), SIGNAL(sigLayerActivated(KisLayerSP)), this, SLOT(currentNodeChanged(KisLayerSP))); } slotToolChanged(0, 0); } emit viewChanged(); } bool CompositeOpModel::eraserMode() const { return d->eraserMode; } void CompositeOpModel::setEraserMode(bool newEraserMode) { if (d->eraserMode != newEraserMode) { d->eraserMode = newEraserMode; if (d->eraserMode) d->updateCompositeOp(COMPOSITE_ERASE); else d->updateCompositeOp(d->prevCompositeOpID); emit eraserModeChanged(); } } qreal CompositeOpModel::flow() const { return d->flow; } void CompositeOpModel::setFlow(qreal newFlow) { if (d->flow != newFlow) { d->flow = newFlow; d->ofsChanged(); emit flowChanged(); } } bool CompositeOpModel::flowEnabled() const { return d->flowEnabled; } void CompositeOpModel::setFlowEnabled(bool newFlowEnabled) { d->flowEnabled = newFlowEnabled; emit flowEnabledChanged(); } qreal CompositeOpModel::opacity() const { return d->opacity; } void CompositeOpModel::setOpacity(qreal newOpacity) { if (d->opacity != newOpacity) { d->opacity = newOpacity; d->ofsChanged(); emit opacityChanged(); } } bool CompositeOpModel::opacityEnabled() const { return d->opacityEnabled; } void CompositeOpModel::setOpacityEnabled(bool newOpacityEnabled) { d->opacityEnabled = newOpacityEnabled; emit opacityEnabledChanged(); } qreal CompositeOpModel::size() const { return d->size; } void CompositeOpModel::setSize(qreal newSize) { if (d->size != newSize) { d->size = newSize; d->ofsChanged(); emit sizeChanged(); } } bool CompositeOpModel::sizeEnabled() const { return d->sizeEnabled; } void CompositeOpModel::setSizeEnabled(bool newSizeEnabled) { d->sizeEnabled = newSizeEnabled; emit sizeEnabledChanged(); } void CompositeOpModel::changePaintopValue(QString propertyName, QVariant value) { if (propertyName == "size" && value.toReal() != d->size) setSize(value.toReal()); else if (propertyName == "opacity" && value.toReal() != d->opacity) setOpacity(value.toReal()); else if (propertyName == "flow" && value.toReal() != d->flow) setFlow(value.toReal()); } bool CompositeOpModel::mirrorHorizontally() const { if (d->view) return d->view->resourceProvider()->mirrorHorizontal(); return false; } void CompositeOpModel::setMirrorHorizontally(bool newMirrorHorizontally) { if (d->view && d->view->resourceProvider()->mirrorHorizontal() != newMirrorHorizontally) { d->view->resourceProvider()->setMirrorHorizontal(newMirrorHorizontally); emit mirrorHorizontallyChanged(); } } bool CompositeOpModel::mirrorVertically() const { if (d->view) return d->view->resourceProvider()->mirrorVertical(); return false; } void CompositeOpModel::setMirrorVertically(bool newMirrorVertically) { if (d->view && d->view->resourceProvider()->mirrorVertical() != newMirrorVertically) { d->view->resourceProvider()->setMirrorVertical(newMirrorVertically); emit mirrorVerticallyChanged(); } } void CompositeOpModel::slotToolChanged(KoCanvasController* canvas, int toolId) { Q_UNUSED(canvas); Q_UNUSED(toolId); if (!d->view) return; if (!d->view->canvasBase()) return; QString id = KoToolManager::instance()->activeToolId(); KisTool* tool = dynamic_cast(KoToolManager::instance()->toolById(d->view->canvasBase(), id)); if (tool) { int flags = tool->flags(); if (flags & KisTool::FLAG_USES_CUSTOM_COMPOSITEOP) { //setWidgetState(ENABLE_COMPOSITEOP|ENABLE_OPACITY); d->opacityEnabled = true; } else { //setWidgetState(DISABLE_COMPOSITEOP|DISABLE_OPACITY); d->opacityEnabled = false; } if (flags & KisTool::FLAG_USES_CUSTOM_PRESET) { d->flowEnabled = true; d->sizeEnabled = true; d->presetsEnabled = true; } else { d->flowEnabled = false; d->sizeEnabled = false; d->presetsEnabled = false; } } else { d->opacityEnabled = false; d->flowEnabled = false; d->sizeEnabled = false; } emit opacityEnabledChanged(); emit flowEnabledChanged(); emit sizeEnabledChanged(); } void CompositeOpModel::resourceChanged(int key, const QVariant& /*v*/) { if (d->view && d->view->canvasBase() && d->view->canvasBase()->resourceManager() && d->view->resourceProvider()) { if (key == KisCanvasResourceProvider::MirrorHorizontal) { emit mirrorHorizontallyChanged(); return; } else if(key == KisCanvasResourceProvider::MirrorVertical) { emit mirrorVerticallyChanged(); return; } KisPaintOpPresetSP preset = d->view->canvasBase()->resourceManager()->resource(KisCanvasResourceProvider::CurrentPaintOpPreset).value(); if (preset && d->currentPreset.data() != preset.data()) { d->currentPreset = preset; if (!d->settingsWidgets.contains(preset.data())) { d->settingsWidgets[preset.data()] = KisPaintOpRegistry::instance()->get(preset->paintOp().id())->createConfigWidget(0); d->settingsWidgets[preset.data()]->setImage(d->view->image()); d->settingsWidgets[preset.data()]->setConfiguration(preset->settings()); } if (d->settingsWidgets[preset.data()]) { preset->settings()->setOptionsWidget(d->settingsWidgets[preset.data()]); } d->size = preset->settings()->paintOpSize(); emit sizeChanged(); if (preset->settings()->hasProperty("OpacityValue")) { d->opacityEnabled = true; d->opacity = preset->settings()->getProperty("OpacityValue").toReal(); } else { d->opacityEnabled = false; d->opacity = 1; } d->view->resourceProvider()->setOpacity(d->opacity); emit opacityChanged(); emit opacityEnabledChanged(); if (preset->settings()->hasProperty("FlowValue")) { d->flowEnabled = true; d->flow = preset->settings()->getProperty("FlowValue").toReal(); } else { d->flowEnabled = false; d->flow = 1; } emit flowChanged(); emit flowEnabledChanged(); QString compositeOp = preset->settings()->getString("CompositeOp"); // This is a little odd, but the logic here is that the opposite of an eraser is a normal composite op (so we just select over, aka normal) // This means that you can switch your eraser over to being a painting tool by turning off the eraser again. if (compositeOp == COMPOSITE_ERASE) { d->currentCompositeOpID = COMPOSITE_OVER; d->eraserMode = true; } else { d->eraserMode = false; } emit eraserModeChanged(); d->updateCompositeOp(compositeOp); } } } void CompositeOpModel::currentNodeChanged(KisLayerSP newNode) { Q_UNUSED(newNode); if (d->eraserMode) { d->eraserMode = false; d->updateCompositeOp(d->prevCompositeOpID); emit eraserModeChanged(); } } int CompositeOpModel::indexOf(QString compositeOpId) { return d->model->indexOf(KoID(compositeOpId)).row(); } QString CompositeOpModel::currentCompositeOpID() const { return d->currentCompositeOpID; } diff --git a/libs/pigment/KoColor.cpp b/libs/pigment/KoColor.cpp index c7bb76b817..ccadc4c9bb 100644 --- a/libs/pigment/KoColor.cpp +++ b/libs/pigment/KoColor.cpp @@ -1,284 +1,298 @@ /* * Copyright (c) 2005 Boudewijn Rempt * Copyright (C) 2007 Thomas Zander * Copyright (C) 2007 Cyrille Berger * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoColor.h" #include #include #include "DebugPigment.h" #include "KoColorModelStandardIds.h" #include "KoColorProfile.h" #include "KoColorSpace.h" #include "KoColorSpaceRegistry.h" #include "KoChannelInfo.h" -const KoColor *KoColor::s_prefab = nullptr; +#include -void KoColor::init() +namespace { + +struct DefaultKoColorInitializer { - KIS_ASSERT(s_prefab == nullptr); - KoColor *prefab = new KoColor(KoColorSpaceRegistry::instance()->rgb16(0)); - prefab->m_colorSpace->fromQColor(Qt::black, prefab->m_data); - prefab->m_colorSpace->setOpacity(prefab->m_data, OPACITY_OPAQUE_U8, 1); - s_prefab = prefab; + DefaultKoColorInitializer() { + const KoColorSpace *defaultColorSpace = KoColorSpaceRegistry::instance()->rgb16(0); + KIS_ASSERT(defaultColorSpace); + + value = new KoColor(Qt::black, defaultColorSpace); #ifndef NODEBUG #ifndef QT_NO_DEBUG - // warn about rather expensive checks in assertPermanentColorspace(). - qWarning() << "KoColor debug runtime checks are active."; + // warn about rather expensive checks in assertPermanentColorspace(). + qWarning() << "KoColor debug runtime checks are active."; #endif #endif + } + + KoColor *value = 0; +}; + +Q_GLOBAL_STATIC(DefaultKoColorInitializer, s_defaultKoColor); + +} + + +KoColor::KoColor() { + *this = *s_defaultKoColor->value; } KoColor::KoColor(const KoColorSpace * colorSpace) { Q_ASSERT(colorSpace); m_colorSpace = KoColorSpaceRegistry::instance()->permanentColorspace(colorSpace); m_size = m_colorSpace->pixelSize(); Q_ASSERT(m_size <= MAX_PIXEL_SIZE); memset(m_data, 0, m_size); } KoColor::KoColor(const QColor & color, const KoColorSpace * colorSpace) { Q_ASSERT(color.isValid()); Q_ASSERT(colorSpace); m_colorSpace = KoColorSpaceRegistry::instance()->permanentColorspace(colorSpace); m_size = m_colorSpace->pixelSize(); Q_ASSERT(m_size <= MAX_PIXEL_SIZE); memset(m_data, 0, m_size); m_colorSpace->fromQColor(color, m_data); } KoColor::KoColor(const quint8 * data, const KoColorSpace * colorSpace) { Q_ASSERT(colorSpace); Q_ASSERT(data); m_colorSpace = KoColorSpaceRegistry::instance()->permanentColorspace(colorSpace); m_size = m_colorSpace->pixelSize(); Q_ASSERT(m_size <= MAX_PIXEL_SIZE); memmove(m_data, data, m_size); } KoColor::KoColor(const KoColor &src, const KoColorSpace * colorSpace) { Q_ASSERT(colorSpace); m_colorSpace = KoColorSpaceRegistry::instance()->permanentColorspace(colorSpace); m_size = m_colorSpace->pixelSize(); Q_ASSERT(m_size <= MAX_PIXEL_SIZE); memset(m_data, 0, m_size); src.colorSpace()->convertPixelsTo(src.m_data, m_data, colorSpace, 1, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); } void KoColor::convertTo(const KoColorSpace * cs, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) { //dbgPigment <<"Our colormodel:" << d->colorSpace->id().name() // << ", new colormodel: " << cs->id().name() << "\n"; if (*m_colorSpace == *cs) return; quint8 data[MAX_PIXEL_SIZE]; const size_t size = cs->pixelSize(); Q_ASSERT(size <= MAX_PIXEL_SIZE); memset(data, 0, size); m_colorSpace->convertPixelsTo(m_data, data, cs, 1, renderingIntent, conversionFlags); memcpy(m_data, data, size); m_size = size; m_colorSpace = KoColorSpaceRegistry::instance()->permanentColorspace(cs); } void KoColor::convertTo(const KoColorSpace * cs) { convertTo(cs, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); } void KoColor::setProfile(const KoColorProfile *profile) { const KoColorSpace *dstColorSpace = KoColorSpaceRegistry::instance()->colorSpace(colorSpace()->colorModelId().id(), colorSpace()->colorDepthId().id(), profile); if (!dstColorSpace) return; m_colorSpace = KoColorSpaceRegistry::instance()->permanentColorspace(dstColorSpace); } void KoColor::setColor(const quint8 * data, const KoColorSpace * colorSpace) { Q_ASSERT(colorSpace); const size_t size = colorSpace->pixelSize(); Q_ASSERT(size <= MAX_PIXEL_SIZE); memcpy(m_data, data, size); m_colorSpace = KoColorSpaceRegistry::instance()->permanentColorspace(colorSpace); } // To save the user the trouble of doing color->colorSpace()->toQColor(color->data(), &c, &a, profile void KoColor::toQColor(QColor *c) const { Q_ASSERT(c); if (m_colorSpace) { m_colorSpace->toQColor(m_data, c); } } QColor KoColor::toQColor() const { QColor c; toQColor(&c); return c; } void KoColor::fromQColor(const QColor& c) { if (m_colorSpace) { m_colorSpace->fromQColor(c, m_data); } } #ifndef NDEBUG void KoColor::dump() const { dbgPigment <<"KoColor (" << this <<")," << m_colorSpace->id() <<""; QList channels = m_colorSpace->channels(); QList::const_iterator begin = channels.constBegin(); QList::const_iterator end = channels.constEnd(); for (QList::const_iterator it = begin; it != end; ++it) { KoChannelInfo * ch = (*it); // XXX: setNum always takes a byte. if (ch->size() == sizeof(quint8)) { // Byte dbgPigment <<"Channel (byte):" << ch->name() <<":" << QString().setNum(m_data[ch->pos()]) <<""; } else if (ch->size() == sizeof(quint16)) { // Short (may also by an nvidia half) dbgPigment <<"Channel (short):" << ch->name() <<":" << QString().setNum(*((const quint16 *)(m_data+ch->pos()))) <<""; } else if (ch->size() == sizeof(quint32)) { // Integer (may also be float... Find out how to distinguish these!) dbgPigment <<"Channel (int):" << ch->name() <<":" << QString().setNum(*((const quint32 *)(m_data+ch->pos()))) <<""; } } } #endif void KoColor::fromKoColor(const KoColor& src) { src.colorSpace()->convertPixelsTo(src.m_data, m_data, colorSpace(), 1, KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); } const KoColorProfile *KoColor::profile() const { return m_colorSpace->profile(); } void KoColor::toXML(QDomDocument& doc, QDomElement& colorElt) const { m_colorSpace->colorToXML(m_data, doc, colorElt); } void KoColor::setOpacity(quint8 alpha) { m_colorSpace->setOpacity(m_data, alpha, 1); } void KoColor::setOpacity(qreal alpha) { m_colorSpace->setOpacity(m_data, alpha, 1); } quint8 KoColor::opacityU8() const { return m_colorSpace->opacityU8(m_data); } qreal KoColor::opacityF() const { return m_colorSpace->opacityF(m_data); } KoColor KoColor::fromXML(const QDomElement& elt, const QString& bitDepthId) { bool ok; return fromXML(elt, bitDepthId, &ok); } KoColor KoColor::fromXML(const QDomElement& elt, const QString& bitDepthId, bool* ok) { *ok = true; QString modelId; if (elt.tagName() == "CMYK") { modelId = CMYKAColorModelID.id(); } else if (elt.tagName() == "RGB") { modelId = RGBAColorModelID.id(); } else if (elt.tagName() == "sRGB") { modelId = RGBAColorModelID.id(); } else if (elt.tagName() == "Lab") { modelId = LABAColorModelID.id(); } else if (elt.tagName() == "XYZ") { modelId = XYZAColorModelID.id(); } else if (elt.tagName() == "Gray") { modelId = GrayAColorModelID.id(); } else if (elt.tagName() == "YCbCr") { modelId = YCbCrAColorModelID.id(); } QString profileName; if (elt.tagName() != "sRGB") { profileName = elt.attribute("space", ""); if (!KoColorSpaceRegistry::instance()->profileByName(profileName)) { profileName.clear(); } } const KoColorSpace* cs = KoColorSpaceRegistry::instance()->colorSpace(modelId, bitDepthId, profileName); if (cs == 0) { QList list = KoColorSpaceRegistry::instance()->colorDepthList(modelId, KoColorSpaceRegistry::AllColorSpaces); if (!list.empty()) { cs = KoColorSpaceRegistry::instance()->colorSpace(modelId, list[0].id(), profileName); } } if (cs) { KoColor c(cs); // TODO: Provide a way for colorFromXML() to notify the caller if parsing failed. Currently it returns default values on failure. cs->colorFromXML(c.data(), elt); return c; } else { *ok = false; return KoColor(); } } QString KoColor::toQString(const KoColor &color) { QStringList ls; Q_FOREACH (KoChannelInfo *channel, KoChannelInfo::displayOrderSorted(color.colorSpace()->channels())) { int realIndex = KoChannelInfo::displayPositionToChannelIndex(channel->displayPosition(), color.colorSpace()->channels()); ls << channel->name(); ls << color.colorSpace()->channelValueText(color.data(), realIndex); } return ls.join(" "); } diff --git a/libs/pigment/KoColor.h b/libs/pigment/KoColor.h index 5eb3859a95..3fa5e7be15 100644 --- a/libs/pigment/KoColor.h +++ b/libs/pigment/KoColor.h @@ -1,239 +1,228 @@ /* * Copyright (c) 2005 Boudewijn Rempt * Copyright (C) 2007 Thomas Zander * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOCOLOR_H #define KOCOLOR_H #include #include #include "kritapigment_export.h" #include "KoColorConversionTransformation.h" #include "KoColorSpaceRegistry.h" #include "KoColorSpaceTraits.h" #include #include "kis_assert.h" class QDomDocument; class QDomElement; class KoColorProfile; class KoColorSpace; /** * A KoColor describes a color in a certain colorspace. The color is stored in a buffer * that can be manipulated by the function of the color space. */ class KRITAPIGMENT_EXPORT KoColor : public boost::equality_comparable { public: - static void init(); - /// Create an empty KoColor. It will be valid, but also black and transparent - KoColor() { - const KoColor * const prefab = s_prefab; - - // assert that KoColor::init was called and everything is set up properly. - KIS_ASSERT_X(prefab != nullptr, "KoColor::KoColor()", "KoColor not initialized yet."); - - *this = *prefab; - } + KoColor(); /// Create a null KoColor. It will be valid, but all channels will be set to 0 explicit KoColor(const KoColorSpace * colorSpace); /// Create a KoColor from a QColor. The QColor is immediately converted to native. The QColor /// is assumed to have the current monitor profile. KoColor(const QColor & color, const KoColorSpace * colorSpace); /// Create a KoColor using a native color strategy. The data is copied. KoColor(const quint8 * data, const KoColorSpace * colorSpace); /// Create a KoColor by converting src into another colorspace KoColor(const KoColor &src, const KoColorSpace * colorSpace); /// Copy constructor -- deep copies the colors. KoColor(const KoColor & rhs) { *this = rhs; } /** * assignment operator to copy the data from the param color into this one. * @param other the color we are going to copy * @return this color */ inline KoColor &operator=(const KoColor &rhs) { if (&rhs == this) { return *this; } m_colorSpace = rhs.m_colorSpace; m_size = rhs.m_size; memcpy(m_data, rhs.m_data, m_size); assertPermanentColorspace(); return *this; } bool operator==(const KoColor &other) const { if (*colorSpace() != *other.colorSpace()) return false; Q_ASSERT(m_size == other.m_size); return memcmp(m_data, other.m_data, m_size) == 0; } /// return the current colorSpace const KoColorSpace * colorSpace() const { return m_colorSpace; } /// return the current profile const KoColorProfile *profile() const; /// Convert this KoColor to the specified colorspace. If the specified colorspace is the /// same as the original colorspace, do nothing. Returns the converted KoColor. void convertTo(const KoColorSpace * cs, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags); void convertTo(const KoColorSpace * cs); /// assign new profile without converting pixel data void setProfile(const KoColorProfile *profile); /// Replace the existing color data, and colorspace with the specified data. /// The data is copied. void setColor(const quint8 * data, const KoColorSpace * colorSpace = 0); /// Convert the color from src and replace the value of the current color with the converted data. /// Don't convert the color if src and this have the same colorspace. void fromKoColor(const KoColor& src); /// a convenience method for the above. void toQColor(QColor *c) const; /// a convenience method for the above. QColor toQColor() const; /** * Convenient function to set the opacity of the color. */ void setOpacity(quint8 alpha); void setOpacity(qreal alpha); /** * Convenient function that return the opacity of the color */ quint8 opacityU8() const; qreal opacityF() const; /// Convenient function for converting from a QColor void fromQColor(const QColor& c); /** * @return the buffer associated with this color object to be used with the * transformation object created by the color space of this KoColor * or to copy to a different buffer from the same color space */ quint8 * data() { return m_data; } /** * @return the buffer associated with this color object to be used with the * transformation object created by the color space of this KoColor * or to copy to a different buffer from the same color space */ const quint8 * data() const { return m_data; } /** * Serialize this color following Create's swatch color specification available * at http://create.freedesktop.org/wiki/index.php/Swatches_-_colour_file_format * * This function doesn't create the element but rather the , * , ... elements. It is assumed that colorElt is the * element. * * @param colorElt root element for the serialization, it is assumed that this * element is * @param doc is the document containing colorElt */ void toXML(QDomDocument& doc, QDomElement& colorElt) const; /** * Unserialize a color following Create's swatch color specification available * at http://create.freedesktop.org/wiki/index.php/Swatches_-_colour_file_format * * @param elt the element to unserialize (, , ) * @param bitDepthId the bit depth is unspecified by the spec, this allow to select * a preferred bit depth for creating the KoColor object (if that * bit depth isn't available, this function will randomly select * an other bit depth) * @return the unserialize color, or an empty color object if the function failed * to unserialize the color */ static KoColor fromXML(const QDomElement& elt, const QString & bitDepthId); /** * Unserialize a color following Create's swatch color specification available * at http://create.freedesktop.org/wiki/index.php/Swatches_-_colour_file_format * * @param elt the element to unserialize (, , ) * @param bitDepthId the bit depth is unspecified by the spec, this allow to select * a preferred bit depth for creating the KoColor object (if that * bit depth isn't available, this function will randomly select * an other bit depth) * @param ok If a an error occurs, *ok is set to false; otherwise it's set to true * @return the unserialize color, or an empty color object if the function failed * to unserialize the color */ static KoColor fromXML(const QDomElement& elt, const QString & bitDepthId, bool* ok); /** * @brief toQString create a user-visible string of the channel names and the channel values * @param color the color to create the string from * @return a string that can be used to display the values of this color to the user. */ static QString toQString(const KoColor &color); #ifndef NODEBUG /// use qDebug calls to print internal info void dump() const; #endif private: inline void assertPermanentColorspace() { #ifndef NODEBUG if (m_colorSpace) { Q_ASSERT(*m_colorSpace == *KoColorSpaceRegistry::instance()->permanentColorspace(m_colorSpace)); } #endif } const KoColorSpace *m_colorSpace; quint8 m_data[MAX_PIXEL_SIZE]; quint8 m_size; - - static const KoColor *s_prefab; }; Q_DECLARE_METATYPE(KoColor) #endif diff --git a/libs/pigment/KoCompositeOp.cpp b/libs/pigment/KoCompositeOp.cpp index 0b619a4af2..66f3e8352e 100644 --- a/libs/pigment/KoCompositeOp.cpp +++ b/libs/pigment/KoCompositeOp.cpp @@ -1,176 +1,188 @@ /* * Copyright (c) 2005 Adrian Page * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoCompositeOp.h" #include #include #include #include "KoColorSpace.h" #include "KoColorSpaceMaths.h" QString KoCompositeOp::categoryColor() { return i18n("Color"); } QString KoCompositeOp::categoryArithmetic() { return i18n("Arithmetic"); } QString KoCompositeOp::categoryNegative() { return i18n("Negative"); } QString KoCompositeOp::categoryLight() { return i18n("Lighten"); } QString KoCompositeOp::categoryDark() { return i18n("Darken"); } QString KoCompositeOp::categoryHSY() { return i18n("HSY"); } QString KoCompositeOp::categoryHSI() { return i18n("HSI"); } QString KoCompositeOp::categoryHSL() { return i18n("HSL"); } QString KoCompositeOp::categoryHSV() { return i18n("HSV"); } QString KoCompositeOp::categoryMix() { return i18n("Mix"); } QString KoCompositeOp::categoryMisc() { return i18n("Misc"); } KoCompositeOp::ParameterInfo::ParameterInfo() : opacity(1.0f), flow(1.0f), lastOpacity(&opacity) { } KoCompositeOp::ParameterInfo::ParameterInfo(const ParameterInfo &rhs) { copy(rhs); } KoCompositeOp::ParameterInfo& KoCompositeOp::ParameterInfo::operator=(const ParameterInfo &rhs) { copy(rhs); return *this; } +void KoCompositeOp::ParameterInfo::setOpacityAndAverage(float _opacity, float _averageOpacity) +{ + if (qFuzzyCompare(_opacity, _averageOpacity)) { + opacity = _opacity; + lastOpacity = &opacity; + } else { + opacity = _opacity; + _lastOpacityData = _averageOpacity; + lastOpacity = &_lastOpacityData; + } +} + void KoCompositeOp::ParameterInfo::copy(const ParameterInfo &rhs) { dstRowStart = rhs.dstRowStart; dstRowStride = rhs.dstRowStride; srcRowStart = rhs.srcRowStart; srcRowStride = rhs.srcRowStride; maskRowStart = rhs.maskRowStart; maskRowStride = rhs.maskRowStride; rows = rhs.rows; cols = rhs.cols; opacity = rhs.opacity; flow = rhs.flow; _lastOpacityData = rhs._lastOpacityData; channelFlags = rhs.channelFlags; lastOpacity = rhs.lastOpacity == &rhs.opacity ? &opacity : &_lastOpacityData; } void KoCompositeOp::ParameterInfo::updateOpacityAndAverage(float value) { const float exponent = 0.1; opacity = value; if (*lastOpacity < opacity) { lastOpacity = &opacity; } else { _lastOpacityData = exponent * opacity + (1.0 - exponent) * (*lastOpacity); lastOpacity = &_lastOpacityData; } } struct Q_DECL_HIDDEN KoCompositeOp::Private { const KoColorSpace * colorSpace; QString id; QString description; QString category; QBitArray defaultChannelFlags; }; KoCompositeOp::KoCompositeOp() : d(new Private) { } KoCompositeOp::~KoCompositeOp() { delete d; } KoCompositeOp::KoCompositeOp(const KoColorSpace * cs, const QString& id, const QString& description, const QString & category) : d(new Private) { d->colorSpace = cs; d->id = id; d->description = description; d->category = category; if (d->category.isEmpty()) { d->category = categoryMisc(); } } void KoCompositeOp::composite(quint8 *dstRowStart, qint32 dstRowStride, const quint8 *srcRowStart, qint32 srcRowStride, const quint8 *maskRowStart, qint32 maskRowStride, qint32 rows, qint32 numColumns, quint8 opacity, const QBitArray& channelFlags) const { KoCompositeOp::ParameterInfo params; params.dstRowStart = dstRowStart; params.dstRowStride = dstRowStride; params.srcRowStart = srcRowStart; params.srcRowStride = srcRowStride; params.maskRowStart = maskRowStart; params.maskRowStride = maskRowStride; params.rows = rows; params.cols = numColumns; params.opacity = float(opacity) / 255.0f; params.flow = 1.0f; params.channelFlags = channelFlags; composite(params); } void KoCompositeOp::composite(const KoCompositeOp::ParameterInfo& params) const { using namespace Arithmetic; composite(params.dstRowStart , params.dstRowStride , params.srcRowStart , params.srcRowStride , params.maskRowStart , params.maskRowStride, params.rows , params.cols , scale(params.opacity), params.channelFlags ); } QString KoCompositeOp::category() const { return d->category; } QString KoCompositeOp::id() const { return d->id; } QString KoCompositeOp::description() const { return d->description; } const KoColorSpace * KoCompositeOp::colorSpace() const { return d->colorSpace; } diff --git a/libs/pigment/KoCompositeOp.h b/libs/pigment/KoCompositeOp.h index 76a8b662d7..fb6fe18f56 100644 --- a/libs/pigment/KoCompositeOp.h +++ b/libs/pigment/KoCompositeOp.h @@ -1,144 +1,148 @@ /* * Copyright (c) 2005 Adrian Page * Copyright (c) 2011 Silvio Heinrich * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOCOMPOSITEOP_H #define KOCOMPOSITEOP_H #include #include #include #include +#include + #include "kritapigment_export.h" class KoColorSpace; class KoColorSpace; /** * Base for colorspace-specific blending modes. */ class KRITAPIGMENT_EXPORT KoCompositeOp { public: static QString categoryColor(); static QString categoryArithmetic(); static QString categoryNegative(); static QString categoryLight(); static QString categoryDark(); static QString categoryHSY(); static QString categoryHSI(); static QString categoryHSL(); static QString categoryHSV(); static QString categoryMix(); static QString categoryMisc(); struct KRITAPIGMENT_EXPORT ParameterInfo { ParameterInfo(); ParameterInfo(const ParameterInfo &rhs); ParameterInfo& operator=(const ParameterInfo &rhs); quint8* dstRowStart; qint32 dstRowStride; const quint8* srcRowStart; qint32 srcRowStride; const quint8* maskRowStart; qint32 maskRowStride; qint32 rows; qint32 cols; float opacity; float flow; float _lastOpacityData; float* lastOpacity; QBitArray channelFlags; + void setOpacityAndAverage(float _opacity, float _averageOpacity); + void updateOpacityAndAverage(float value); private: inline void copy(const ParameterInfo &rhs); }; public: /** * @param cs a pointer to the color space that can be used with this composite op * @param id the identifier for this composite op (not user visible) * @param description a user visible string describing this composite operation * @param category the name of the category where to put that composite op when displayed * @param userVisible define whether or not that composite op should be visible in a user * interface */ KoCompositeOp(const KoColorSpace * cs, const QString& id, const QString& description, const QString & category = KoCompositeOp::categoryMisc()); virtual ~KoCompositeOp(); /** * @return the identifier of this composite op */ QString id() const; /** * @return the user visible string for this composite op */ QString description() const; /** * @return the color space that can use and own this composite op */ const KoColorSpace * colorSpace() const; /** * @return the category associated with the composite op */ QString category() const; // WARNING: A derived class needs to overwrite at least one // of the following virtual methods or a call to // composite(...) will lead to an endless recursion/stack overflow /** * @param dstRowStart pointer to the start of the byte array we will composite the source on * @param dstRowStride length of the rows of the block of destination pixels in bytes * @param srcRowStart pointer to the start of the byte array we will mix with dest * @param srcRowStride length of the rows of the block of src in bytes * pixels (may be different from the rowstride of the dst pixels, * in which case the smaller value is used). If srcRowStride is null * it is assumed that the source is a constant color. * @param maskRowStart start of the byte mask that determines whether and if so, then how much of src is used for blending * @param maskRowStride length of the mask scanlines in bytes * @param rows number of scanlines to blend * @param numColumns length of the row of pixels in pixels * @param opacity transparency with which to blend * @param channelFlags a bit array that determines which channels should be processed (channels are in the order of the channels in the colorspace) */ virtual void composite(quint8 *dstRowStart, qint32 dstRowStride, const quint8 *srcRowStart, qint32 srcRowStride, const quint8 *maskRowStart, qint32 maskRowStride, qint32 rows, qint32 numColumns, quint8 opacity, const QBitArray& channelFlags=QBitArray()) const; /** * Same as previous, but uses a parameter structure */ virtual void composite(const ParameterInfo& params) const; private: KoCompositeOp(); struct Private; Private* const d; }; #endif // KOCOMPOSITEOP_H diff --git a/libs/ui/CMakeLists.txt b/libs/ui/CMakeLists.txt index 24b1d8dcc1..ccbe871f0a 100644 --- a/libs/ui/CMakeLists.txt +++ b/libs/ui/CMakeLists.txt @@ -1,575 +1,577 @@ include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/qtlockedfile ${EXIV2_INCLUDE_DIR} ) include_directories(SYSTEM ${EIGEN3_INCLUDE_DIR} ${OCIO_INCLUDE_DIR} ${Boost_INCLUDE_DIRS} ) add_subdirectory( tests ) if (APPLE) find_library(FOUNDATION_LIBRARY Foundation) find_library(APPKIT_LIBRARY AppKit) endif () set(kritaui_LIB_SRCS canvas/kis_canvas_widget_base.cpp canvas/kis_canvas2.cpp canvas/kis_canvas_updates_compressor.cpp canvas/kis_canvas_controller.cpp canvas/kis_paintop_transformation_connector.cpp canvas/kis_display_color_converter.cpp canvas/kis_display_filter.cpp canvas/kis_exposure_gamma_correction_interface.cpp canvas/kis_tool_proxy.cpp canvas/kis_canvas_decoration.cc canvas/kis_coordinates_converter.cpp canvas/kis_grid_manager.cpp canvas/kis_grid_decoration.cpp canvas/kis_grid_config.cpp canvas/kis_prescaled_projection.cpp canvas/kis_qpainter_canvas.cpp canvas/kis_projection_backend.cpp canvas/kis_update_info.cpp canvas/kis_image_patch.cpp canvas/kis_image_pyramid.cpp canvas/kis_infinity_manager.cpp canvas/kis_change_guides_command.cpp canvas/kis_guides_decoration.cpp canvas/kis_guides_manager.cpp canvas/kis_guides_config.cpp canvas/kis_snap_config.cpp canvas/kis_snap_line_strategy.cpp canvas/KisSnapPointStrategy.cpp dialogs/kis_about_application.cpp dialogs/kis_dlg_adj_layer_props.cc dialogs/kis_dlg_adjustment_layer.cc dialogs/kis_dlg_filter.cpp dialogs/kis_dlg_generator_layer.cpp dialogs/kis_dlg_file_layer.cpp dialogs/kis_dlg_filter.cpp dialogs/kis_dlg_stroke_selection_properties.cpp dialogs/kis_dlg_image_properties.cc dialogs/kis_dlg_layer_properties.cc dialogs/kis_dlg_preferences.cc dialogs/slider_and_spin_box_sync.cpp dialogs/kis_dlg_blacklist_cleanup.cpp dialogs/kis_dlg_layer_style.cpp dialogs/kis_dlg_png_import.cpp dialogs/kis_dlg_import_image_sequence.cpp dialogs/kis_delayed_save_dialog.cpp dialogs/kis_dlg_internal_color_selector.cpp flake/kis_node_dummies_graph.cpp flake/kis_dummies_facade_base.cpp flake/kis_dummies_facade.cpp flake/kis_node_shapes_graph.cpp flake/kis_node_shape.cpp flake/kis_shape_controller.cpp flake/kis_shape_layer.cc flake/kis_shape_layer_canvas.cpp flake/kis_shape_selection.cpp flake/kis_shape_selection_canvas.cpp flake/kis_shape_selection_model.cpp flake/kis_take_all_shapes_command.cpp brushhud/kis_uniform_paintop_property_widget.cpp brushhud/kis_brush_hud.cpp brushhud/kis_round_hud_button.cpp brushhud/kis_dlg_brush_hud_config.cpp brushhud/kis_brush_hud_properties_list.cpp brushhud/kis_brush_hud_properties_config.cpp kis_aspect_ratio_locker.cpp kis_autogradient.cc kis_bookmarked_configurations_editor.cc kis_bookmarked_configurations_model.cc kis_bookmarked_filter_configurations_model.cc kis_base_option.cpp kis_canvas_resource_provider.cpp kis_derived_resources.cpp kis_categories_mapper.cpp kis_categorized_list_model.cpp kis_categorized_item_delegate.cpp kis_clipboard.cc kis_config.cc kis_config_notifier.cpp kis_control_frame.cpp kis_composite_ops_model.cc kis_paint_ops_model.cpp kis_cursor.cc kis_cursor_cache.cpp kis_custom_pattern.cc kis_file_layer.cpp kis_change_file_layer_command.h kis_safe_document_loader.cpp kis_splash_screen.cpp kis_filter_manager.cc kis_filters_model.cc kis_histogram_view.cc KisImageBarrierLockerWithFeedback.cpp kis_image_manager.cc kis_image_view_converter.cpp kis_import_catcher.cc kis_layer_manager.cc kis_mask_manager.cc kis_mimedata.cpp kis_node_commands_adapter.cpp kis_node_manager.cpp kis_node_juggler_compressed.cpp kis_node_selection_adapter.cpp kis_node_insertion_adapter.cpp kis_node_model.cpp kis_node_filter_proxy_model.cpp kis_model_index_converter_base.cpp kis_model_index_converter.cpp kis_model_index_converter_show_all.cpp kis_painting_assistant.cc kis_painting_assistants_decoration.cpp kis_painting_assistants_manager.cpp kis_paintop_box.cc kis_paintop_option.cpp kis_paintop_options_model.cpp kis_paintop_settings_widget.cpp kis_popup_palette.cpp kis_png_converter.cpp kis_preference_set_registry.cpp kis_script_manager.cpp kis_resource_server_provider.cpp KisSelectedShapesProxy.cpp kis_selection_decoration.cc kis_selection_manager.cc kis_statusbar.cc kis_zoom_manager.cc kis_favorite_resource_manager.cpp kis_workspace_resource.cpp kis_action.cpp kis_action_manager.cpp kis_view_plugin.cpp kis_canvas_controls_manager.cpp kis_tooltip_manager.cpp kis_multinode_property.cpp kis_stopgradient_editor.cpp kisexiv2/kis_exif_io.cpp kisexiv2/kis_exiv2.cpp kisexiv2/kis_iptc_io.cpp kisexiv2/kis_xmp_io.cpp opengl/kis_opengl.cpp opengl/kis_opengl_canvas2.cpp opengl/kis_opengl_canvas_debugger.cpp opengl/kis_opengl_image_textures.cpp opengl/kis_texture_tile.cpp opengl/kis_opengl_shader_loader.cpp opengl/kis_texture_tile_info_pool.cpp kis_fps_decoration.cpp recorder/kis_node_query_path_editor.cc recorder/kis_recorded_action_creator.cc recorder/kis_recorded_action_creator_factory.cc recorder/kis_recorded_action_creator_factory_registry.cc recorder/kis_recorded_action_editor_factory.cc recorder/kis_recorded_action_editor_factory_registry.cc recorder/kis_recorded_filter_action_editor.cc recorder/kis_recorded_filter_action_creator.cpp recorder/kis_recorded_paint_action_editor.cc tool/kis_selection_tool_helper.cpp tool/kis_selection_tool_config_widget_helper.cpp tool/kis_rectangle_constraint_widget.cpp tool/kis_shape_tool_helper.cpp tool/kis_tool.cc tool/kis_delegated_tool_policies.cpp tool/kis_tool_freehand.cc tool/kis_speed_smoother.cpp tool/kis_painting_information_builder.cpp tool/kis_stabilized_events_sampler.cpp tool/kis_tool_freehand_helper.cpp tool/kis_tool_multihand_helper.cpp tool/kis_figure_painting_tool_helper.cpp tool/kis_recording_adapter.cpp tool/kis_tool_paint.cc tool/kis_tool_shape.cc tool/kis_tool_ellipse_base.cpp tool/kis_tool_rectangle_base.cpp tool/kis_tool_polyline_base.cpp tool/kis_tool_utils.cpp tool/kis_resources_snapshot.cpp tool/kis_smoothing_options.cpp tool/KisStabilizerDelayedPaintHelper.cpp + tool/KisStrokeSpeedMonitor.cpp tool/strokes/freehand_stroke.cpp + tool/strokes/KisStrokeEfficiencyMeasurer.cpp tool/strokes/kis_painter_based_stroke_strategy.cpp tool/strokes/kis_filter_stroke_strategy.cpp tool/strokes/kis_color_picker_stroke_strategy.cpp widgets/kis_cmb_composite.cc widgets/kis_cmb_contour.cpp widgets/kis_cmb_gradient.cpp widgets/kis_paintop_list_widget.cpp widgets/kis_cmb_idlist.cc widgets/kis_color_space_selector.cc widgets/kis_advanced_color_space_selector.cc widgets/kis_cie_tongue_widget.cpp widgets/kis_tone_curve_widget.cpp widgets/kis_curve_widget.cpp widgets/kis_custom_image_widget.cc widgets/kis_image_from_clipboard_widget.cpp widgets/kis_double_widget.cc widgets/kis_filter_selector_widget.cc widgets/kis_gradient_chooser.cc widgets/kis_gradient_slider_widget.cc widgets/kis_gradient_slider.cpp widgets/kis_iconwidget.cc widgets/kis_mask_widgets.cpp widgets/kis_meta_data_merge_strategy_chooser_widget.cc widgets/kis_multi_bool_filter_widget.cc widgets/kis_multi_double_filter_widget.cc widgets/kis_multi_integer_filter_widget.cc widgets/kis_multipliers_double_slider_spinbox.cpp widgets/kis_paintop_presets_popup.cpp widgets/kis_tool_options_popup.cpp widgets/kis_paintop_presets_chooser_popup.cpp widgets/kis_paintop_presets_save.cpp widgets/kis_pattern_chooser.cc widgets/kis_popup_button.cc widgets/kis_preset_chooser.cpp widgets/kis_progress_widget.cpp widgets/kis_selection_options.cc widgets/kis_scratch_pad.cpp widgets/kis_scratch_pad_event_filter.cpp widgets/kis_preset_selector_strip.cpp widgets/kis_slider_spin_box.cpp widgets/kis_size_group.cpp widgets/kis_size_group_p.cpp widgets/kis_wdg_generator.cpp widgets/kis_workspace_chooser.cpp widgets/squeezedcombobox.cpp widgets/kis_categorized_list_view.cpp widgets/kis_widget_chooser.cpp widgets/kis_tool_button.cpp widgets/kis_floating_message.cpp widgets/kis_lod_availability_widget.cpp widgets/kis_color_label_selector_widget.cpp widgets/kis_color_filter_combo.cpp widgets/kis_elided_label.cpp widgets/kis_stopgradient_slider_widget.cpp widgets/kis_spinbox_color_selector.cpp widgets/kis_screen_color_picker.cpp widgets/kis_preset_live_preview_view.cpp widgets/KoDualColorButton.cpp widgets/kis_color_input.cpp widgets/kis_color_button.cpp widgets/KisVisualColorSelector.cpp widgets/KisVisualColorSelectorShape.cpp widgets/KisVisualEllipticalSelectorShape.cpp widgets/KisVisualRectangleSelectorShape.cpp widgets/KisVisualTriangleSelectorShape.cpp widgets/KoStrokeConfigWidget.cpp widgets/KoFillConfigWidget.cpp widgets/KoShapeFillWrapper.cpp utils/kis_document_aware_spin_box_unit_manager.cpp input/kis_input_manager.cpp input/kis_input_manager_p.cpp input/kis_extended_modifiers_mapper.cpp input/kis_abstract_input_action.cpp input/kis_tool_invocation_action.cpp input/kis_pan_action.cpp input/kis_alternate_invocation_action.cpp input/kis_rotate_canvas_action.cpp input/kis_zoom_action.cpp input/kis_change_frame_action.cpp input/kis_gamma_exposure_action.cpp input/kis_show_palette_action.cpp input/kis_change_primary_setting_action.cpp input/kis_abstract_shortcut.cpp input/kis_native_gesture_shortcut.cpp input/kis_single_action_shortcut.cpp input/kis_stroke_shortcut.cpp input/kis_shortcut_matcher.cpp input/kis_select_layer_action.cpp input/KisQtWidgetsTweaker.cpp operations/kis_operation.cpp operations/kis_operation_configuration.cpp operations/kis_operation_registry.cpp operations/kis_operation_ui_factory.cpp operations/kis_operation_ui_widget.cpp operations/kis_filter_selection_operation.cpp actions/kis_selection_action_factories.cpp actions/KisPasteActionFactory.cpp input/kis_touch_shortcut.cpp kis_document_undo_store.cpp kis_transaction_based_command.cpp kis_gui_context_command.cpp kis_gui_context_command_p.cpp input/kis_tablet_debugger.cpp input/kis_input_profile_manager.cpp input/kis_input_profile.cpp input/kis_shortcut_configuration.cpp input/config/kis_input_configuration_page.cpp input/config/kis_edit_profiles_dialog.cpp input/config/kis_input_profile_model.cpp input/config/kis_input_configuration_page_item.cpp input/config/kis_action_shortcuts_model.cpp input/config/kis_input_type_delegate.cpp input/config/kis_input_mode_delegate.cpp input/config/kis_input_button.cpp input/config/kis_input_editor_delegate.cpp input/config/kis_mouse_input_editor.cpp input/config/kis_wheel_input_editor.cpp input/config/kis_key_input_editor.cpp processing/fill_processing_visitor.cpp kis_asl_layer_style_serializer.cpp kis_psd_layer_style_resource.cpp canvas/kis_mirror_axis.cpp kis_abstract_perspective_grid.cpp KisApplication.cpp KisAutoSaveRecoveryDialog.cpp KisDetailsPane.cpp KisDocument.cpp KisNodeDelegate.cpp kis_node_view_visibility_delegate.cpp KisNodeToolTip.cpp KisNodeView.cpp kis_node_view_color_scheme.cpp KisImportExportFilter.cpp KisFilterEntry.cpp KisImportExportManager.cpp KisImportExportUtils.cpp kis_async_action_feedback.cpp KisMainWindow.cpp KisOpenPane.cpp KisPart.cpp KisPrintJob.cpp KisTemplate.cpp KisTemplateCreateDia.cpp KisTemplateGroup.cpp KisTemplates.cpp KisTemplatesPane.cpp KisTemplateTree.cpp KisUndoStackAction.cpp KisView.cpp thememanager.cpp kis_mainwindow_observer.cpp KisViewManager.cpp kis_mirror_manager.cpp qtlockedfile/qtlockedfile.cpp qtsingleapplication/qtlocalpeer.cpp qtsingleapplication/qtsingleapplication.cpp KisResourceBundle.cpp KisResourceBundleManifest.cpp kis_md5_generator.cpp KisApplicationArguments.cpp KisNetworkAccessManager.cpp KisMultiFeedRSSModel.cpp KisRemoteFileFetcher.cpp KisPaletteModel.cpp kis_palette_delegate.cpp kis_palette_view.cpp KisColorsetChooser.cpp KisSaveGroupVisitor.cpp ) if(WIN32) if (NOT Qt5Gui_PRIVATE_INCLUDE_DIRS) message(FATAL_ERROR "Qt5Gui Private header are missing!") endif() set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} input/kis_tablet_event.cpp input/wintab/kis_tablet_support_win.cpp input/wintab/kis_screen_size_choice_dialog.cpp qtlockedfile/qtlockedfile_win.cpp input/wintab/kis_tablet_support_win8.cpp ) include_directories(${Qt5Gui_PRIVATE_INCLUDE_DIRS}) endif() set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} kis_animation_frame_cache.cpp kis_animation_cache_populator.cpp KisAsyncAnimationRendererBase.cpp KisAsyncAnimationCacheRenderer.cpp KisAsyncAnimationFramesSavingRenderer.cpp dialogs/KisAsyncAnimationRenderDialogBase.cpp dialogs/KisAsyncAnimationCacheRenderDialog.cpp dialogs/KisAsyncAnimationFramesSaveDialog.cpp canvas/kis_animation_player.cpp kis_animation_importer.cpp KisSyncedAudioPlayback.cpp ) if(UNIX) set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} input/kis_tablet_event.cpp input/wintab/kis_tablet_support.cpp qtlockedfile/qtlockedfile_unix.cpp ) if(NOT APPLE) set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} input/wintab/kis_tablet_support_x11.cpp input/wintab/qxcbconnection_xi2.cpp input/wintab/qxcbconnection.cpp input/wintab/kis_xi2_event_filter.cpp ) endif() endif() if(APPLE) set(kritaui_LIB_SRCS ${kritaui_LIB_SRCS} osx.mm ) endif() ki18n_wrap_ui(kritaui_LIB_SRCS widgets/KoFillConfigWidget.ui widgets/KoStrokeConfigWidget.ui forms/wdgdlgpngimport.ui forms/wdgfullscreensettings.ui forms/wdgautogradient.ui forms/wdggeneralsettings.ui forms/wdgperformancesettings.ui forms/wdggenerators.ui forms/wdgbookmarkedconfigurationseditor.ui forms/wdgapplyprofile.ui forms/wdgcustompattern.ui forms/wdglayerproperties.ui forms/wdgcolorsettings.ui forms/wdgtabletsettings.ui forms/wdgcolorspaceselector.ui forms/wdgcolorspaceselectoradvanced.ui forms/wdgdisplaysettings.ui forms/kis_previewwidgetbase.ui forms/kis_matrix_widget.ui forms/wdgselectionoptions.ui forms/wdggeometryoptions.ui forms/wdgnewimage.ui forms/wdgimageproperties.ui forms/wdgmaskfromselection.ui forms/wdgmasksource.ui forms/wdgfilterdialog.ui forms/wdgmetadatamergestrategychooser.ui forms/wdgpaintoppresets.ui forms/wdgpaintopsettings.ui forms/wdgdlggeneratorlayer.ui forms/wdgdlgfilelayer.ui forms/wdgfilterselector.ui forms/wdgfilternodecreation.ui forms/wdgpaintactioneditor.ui forms/wdgmultipliersdoublesliderspinbox.ui forms/wdgnodequerypatheditor.ui forms/wdgpresetselectorstrip.ui forms/wdgsavebrushpreset.ui forms/wdgdlgblacklistcleanup.ui forms/wdgrectangleconstraints.ui forms/wdgimportimagesequence.ui forms/wdgstrokeselectionproperties.ui forms/KisDetailsPaneBase.ui forms/KisOpenPaneBase.ui forms/wdgstopgradienteditor.ui brushhud/kis_dlg_brush_hud_config.ui forms/wdgdlginternalcolorselector.ui dialogs/kis_delayed_save_dialog.ui input/config/kis_input_configuration_page.ui input/config/kis_edit_profiles_dialog.ui input/config/kis_input_configuration_page_item.ui input/config/kis_mouse_input_editor.ui input/config/kis_wheel_input_editor.ui input/config/kis_key_input_editor.ui layerstyles/wdgBevelAndEmboss.ui layerstyles/wdgblendingoptions.ui layerstyles/WdgColorOverlay.ui layerstyles/wdgContour.ui layerstyles/wdgdropshadow.ui layerstyles/WdgGradientOverlay.ui layerstyles/wdgInnerGlow.ui layerstyles/wdglayerstyles.ui layerstyles/WdgPatternOverlay.ui layerstyles/WdgSatin.ui layerstyles/WdgStroke.ui layerstyles/wdgstylesselector.ui layerstyles/wdgTexture.ui wdgsplash.ui input/wintab/kis_screen_size_choice_dialog.ui ) QT5_WRAP_CPP(kritaui_HEADERS_MOC KisNodePropertyAction_p.h) add_library(kritaui SHARED ${kritaui_HEADERS_MOC} ${kritaui_LIB_SRCS} ) generate_export_header(kritaui BASE_NAME kritaui) target_link_libraries(kritaui KF5::CoreAddons KF5::Completion KF5::I18n KF5::ItemViews Qt5::Network kritaimpex kritacolor kritaimage kritalibbrush kritawidgets kritawidgetutils ${PNG_LIBRARIES} ${EXIV2_LIBRARIES} ) if (HAVE_QT_MULTIMEDIA) target_link_libraries(kritaui Qt5::Multimedia) endif() if (HAVE_KIO) target_link_libraries(kritaui KF5::KIOCore) endif() if (NOT WIN32 AND NOT APPLE) target_link_libraries(kritaui ${X11_X11_LIB} ${X11_Xinput_LIB} ${XCB_LIBRARIES}) endif() if(APPLE) target_link_libraries(kritaui ${FOUNDATION_LIBRARY}) target_link_libraries(kritaui ${APPKIT_LIBRARY}) endif () target_link_libraries(kritaui ${OPENEXR_LIBRARIES}) # Add VSync disable workaround if(NOT WIN32 AND NOT APPLE) target_link_libraries(kritaui ${CMAKE_DL_LIBS} Qt5::X11Extras) endif() if(X11_FOUND) target_link_libraries(kritaui Qt5::X11Extras ${X11_LIBRARIES}) endif() target_include_directories(kritaui PUBLIC $ $ $ $ $ $ $ ) set_target_properties(kritaui PROPERTIES VERSION ${GENERIC_KRITA_LIB_VERSION} SOVERSION ${GENERIC_KRITA_LIB_SOVERSION} ) install(TARGETS kritaui ${INSTALL_TARGETS_DEFAULT_ARGS}) if (APPLE) install(FILES osx.stylesheet DESTINATION ${DATA_INSTALL_DIR}/krita) endif () diff --git a/libs/ui/KisApplication.cpp b/libs/ui/KisApplication.cpp index a3e5bf70fa..3bf41e75cd 100644 --- a/libs/ui/KisApplication.cpp +++ b/libs/ui/KisApplication.cpp @@ -1,812 +1,808 @@ /* * Copyright (C) 1998, 1999 Torben Weis * Copyright (C) 2012 Boudewijn Rempt * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KisApplication.h" #include #ifdef Q_OS_WIN #include #include #endif #ifdef Q_OS_OSX #include "osx.h" #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KoGlobal.h" #include "KoConfig.h" #include #include #include #include "thememanager.h" #include "KisPrintJob.h" #include "KisDocument.h" #include "KisMainWindow.h" #include "KisAutoSaveRecoveryDialog.h" #include "KisPart.h" #include #include "kis_md5_generator.h" #include "kis_splash_screen.h" #include "kis_config.h" #include "flake/kis_shape_selection.h" #include #include #include #include #include #include #include #include "kisexiv2/kis_exiv2.h" #include "KisApplicationArguments.h" #include #include "kis_action_registry.h" #include #include #include #include "kis_image_barrier_locker.h" #include "opengl/kis_opengl.h" #include "kis_spin_box_unit_manager.h" #include "kis_document_aware_spin_box_unit_manager.h" #include "KisViewManager.h" #include "kis_workspace_resource.h" #include namespace { const QTime appStartTime(QTime::currentTime()); } class KisApplicationPrivate { public: KisApplicationPrivate() : splashScreen(0) {} QPointer splashScreen; }; class KisApplication::ResetStarting { public: ResetStarting(KisSplashScreen *splash, int fileCount) : m_splash(splash) , m_fileCount(fileCount) { } ~ResetStarting() { if (m_splash) { KConfigGroup cfg( KSharedConfig::openConfig(), "SplashScreen"); bool hideSplash = cfg.readEntry("HideSplashAfterStartup", false); if (m_fileCount > 0 || hideSplash) { m_splash->hide(); } else { m_splash->setWindowFlags(Qt::Dialog); QRect r(QPoint(), m_splash->size()); m_splash->move(QApplication::desktop()->availableGeometry().center() - r.center()); m_splash->setWindowTitle(qAppName()); m_splash->setParent(0); Q_FOREACH (QObject *o, m_splash->children()) { QWidget *w = qobject_cast(o); if (w && w->isHidden()) { w->setVisible(true); } } m_splash->show(); m_splash->activateWindow(); } } } QPointer m_splash; int m_fileCount; }; KisApplication::KisApplication(const QString &key, int &argc, char **argv) : QtSingleApplication(key, argc, argv) , d(new KisApplicationPrivate) , m_autosaveDialog(0) , m_mainWindow(0) , m_batchRun(false) { #ifdef Q_OS_OSX setMouseCoalescingEnabled(false); #endif QCoreApplication::addLibraryPath(QCoreApplication::applicationDirPath()); setApplicationDisplayName("Krita"); setApplicationName("krita"); // Note: Qt docs suggest we set this, but if we do, we get resource paths of the form of krita/krita, which is weird. // setOrganizationName("krita"); setOrganizationDomain("krita.org"); QString version = KritaVersionWrapper::versionString(true); setApplicationVersion(version); setWindowIcon(KisIconUtils::loadIcon("calligrakrita")); if (qgetenv("KRITA_NO_STYLE_OVERRIDE").isEmpty()) { QStringList styles = QStringList() << "breeze" << "fusion" << "plastique"; if (!styles.contains(style()->objectName().toLower())) { Q_FOREACH (const QString & style, styles) { if (!setStyle(style)) { qDebug() << "No" << style << "available."; } else { qDebug() << "Set style" << style; break; } } } } else { qDebug() << "Style override disabled, using" << style()->objectName(); } KisOpenGL::initialize(); qDebug() << "krita has opengl" << KisOpenGL::hasOpenGL(); } #if defined(Q_OS_WIN) && defined(ENV32BIT) typedef BOOL (WINAPI *LPFN_ISWOW64PROCESS) (HANDLE, PBOOL); LPFN_ISWOW64PROCESS fnIsWow64Process; BOOL isWow64() { BOOL bIsWow64 = FALSE; //IsWow64Process is not available on all supported versions of Windows. //Use GetModuleHandle to get a handle to the DLL that contains the function //and GetProcAddress to get a pointer to the function if available. fnIsWow64Process = (LPFN_ISWOW64PROCESS) GetProcAddress( GetModuleHandle(TEXT("kernel32")),"IsWow64Process"); if(0 != fnIsWow64Process) { if (!fnIsWow64Process(GetCurrentProcess(),&bIsWow64)) { //handle error } } return bIsWow64; } #endif void KisApplication::initializeGlobals(const KisApplicationArguments &args) { int dpiX = args.dpiX(); int dpiY = args.dpiY(); if (dpiX > 0 && dpiY > 0) { KoDpi::setDPI(dpiX, dpiY); } } void KisApplication::addResourceTypes() { // All Krita's resource types KoResourcePaths::addResourceType("kis_pics", "data", "/pics/"); KoResourcePaths::addResourceType("kis_images", "data", "/images/"); KoResourcePaths::addResourceType("icc_profiles", "data", "/profiles/"); KoResourcePaths::addResourceType("metadata_schema", "data", "/metadata/schemas/"); KoResourcePaths::addResourceType("kis_brushes", "data", "/brushes/"); KoResourcePaths::addResourceType("kis_taskset", "data", "/taskset/"); KoResourcePaths::addResourceType("kis_taskset", "data", "/taskset/"); KoResourcePaths::addResourceType("gmic_definitions", "data", "/gmic/"); KoResourcePaths::addResourceType("kis_resourcebundles", "data", "/bundles/"); KoResourcePaths::addResourceType("kis_defaultpresets", "data", "/defaultpresets/"); KoResourcePaths::addResourceType("kis_paintoppresets", "data", "/paintoppresets/"); KoResourcePaths::addResourceType("kis_workspaces", "data", "/workspaces/"); KoResourcePaths::addResourceType("psd_layer_style_collections", "data", "/asl"); KoResourcePaths::addResourceType("ko_patterns", "data", "/patterns/", true); KoResourcePaths::addResourceType("ko_gradients", "data", "/gradients/"); KoResourcePaths::addResourceType("ko_gradients", "data", "/gradients/", true); KoResourcePaths::addResourceType("ko_palettes", "data", "/palettes/", true); KoResourcePaths::addResourceType("kis_shortcuts", "data", "/shortcuts/"); KoResourcePaths::addResourceType("kis_actions", "data", "/actions"); KoResourcePaths::addResourceType("icc_profiles", "data", "/color/icc"); KoResourcePaths::addResourceType("ko_effects", "data", "/effects/"); KoResourcePaths::addResourceType("tags", "data", "/tags/"); KoResourcePaths::addResourceType("templates", "data", "/templates"); KoResourcePaths::addResourceType("pythonscripts", "data", "/pykrita"); KoResourcePaths::addResourceType("symbols", "data", "/symbols"); // // Extra directories to look for create resources. (Does anyone actually use that anymore?) // KoResourcePaths::addResourceDir("ko_gradients", "/usr/share/create/gradients/gimp"); // KoResourcePaths::addResourceDir("ko_gradients", QDir::homePath() + QString("/.create/gradients/gimp")); // KoResourcePaths::addResourceDir("ko_patterns", "/usr/share/create/patterns/gimp"); // KoResourcePaths::addResourceDir("ko_patterns", QDir::homePath() + QString("/.create/patterns/gimp")); // KoResourcePaths::addResourceDir("kis_brushes", "/usr/share/create/brushes/gimp"); // KoResourcePaths::addResourceDir("kis_brushes", QDir::homePath() + QString("/.create/brushes/gimp")); // KoResourcePaths::addResourceDir("ko_palettes", "/usr/share/create/swatches"); // KoResourcePaths::addResourceDir("ko_palettes", QDir::homePath() + QString("/.create/swatches")); // Make directories for all resources we can save, and tags QDir d; d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/tags/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/asl/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/bundles/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/gradients/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/paintoppresets/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/palettes/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/patterns/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/taskset/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/workspaces/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/input/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/pykrita/"); d.mkpath(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/symbols/"); // Indicate that it is now safe for users of KoResourcePaths to load resources KoResourcePaths::setReady(); } void KisApplication::loadResources() { setSplashScreenLoadingText(i18n("Loading Gradients...")); processEvents(); KoResourceServerProvider::instance()->gradientServer(true); // Load base resources setSplashScreenLoadingText(i18n("Loading Patterns...")); processEvents(); KoResourceServerProvider::instance()->patternServer(true); setSplashScreenLoadingText(i18n("Loading Palettes...")); processEvents(); KoResourceServerProvider::instance()->paletteServer(false); setSplashScreenLoadingText(i18n("Loading Brushes...")); processEvents(); KisBrushServer::instance()->brushServer(true); // load paintop presets setSplashScreenLoadingText(i18n("Loading Paint Operations...")); processEvents(); KisResourceServerProvider::instance()->paintOpPresetServer(true); // load symbols setSplashScreenLoadingText(i18n("Loading SVG Symbol Collections...")); processEvents(); KoResourceServerProvider::instance()->svgSymbolCollectionServer(true); setSplashScreenLoadingText(i18n("Loading Resource Bundles...")); processEvents(); KisResourceServerProvider::instance()->resourceBundleServer(); } void KisApplication::loadPlugins() { KoShapeRegistry* r = KoShapeRegistry::instance(); r->add(new KisShapeSelectionFactory()); KisActionRegistry::instance(); KisFilterRegistry::instance(); KisGeneratorRegistry::instance(); KisPaintOpRegistry::instance(); KoColorSpaceRegistry::instance(); // Load the krita-specific tools setSplashScreenLoadingText(i18n("Loading Plugins for Krita/Tool...")); processEvents(); KoPluginLoader::instance()->load(QString::fromLatin1("Krita/Tool"), QString::fromLatin1("[X-Krita-Version] == 28")); // Load dockers setSplashScreenLoadingText(i18n("Loading Plugins for Krita/Dock...")); processEvents(); KoPluginLoader::instance()->load(QString::fromLatin1("Krita/Dock"), QString::fromLatin1("[X-Krita-Version] == 28")); // XXX_EXIV: make the exiv io backends real plugins setSplashScreenLoadingText(i18n("Loading Plugins Exiv/IO...")); processEvents(); KisExiv2::initialize(); } bool KisApplication::start(const KisApplicationArguments &args) { KisConfig cfg; #if defined(Q_OS_WIN) #ifdef ENV32BIT if (isWow64() && !cfg.readEntry("WarnedAbout32Bits", false)) { QMessageBox::information(0, i18nc("@title:window", "Krita: Warning"), i18n("You are running a 32 bits build on a 64 bits Windows.\n" "This is not recommended.\n" "Please download and install the x64 build instead.")); cfg.writeEntry("WarnedAbout32Bits", true); } #endif #endif QString opengl = cfg.canvasState(); if (opengl == "OPENGL_NOT_TRIED" ) { cfg.setCanvasState("TRY_OPENGL"); } else if (opengl != "OPENGL_SUCCESS") { cfg.setCanvasState("OPENGL_FAILED"); } setSplashScreenLoadingText(i18n("Initializing Globals")); processEvents(); initializeGlobals(args); const bool doNewImage = args.doNewImage(); const bool doTemplate = args.doTemplate(); const bool print = args.print(); const bool exportAs = args.exportAs(); const bool exportAsPdf = args.exportAsPdf(); const QString exportFileName = args.exportFileName(); m_batchRun = (print || exportAs || exportAsPdf || !exportFileName.isEmpty()); // print & exportAsPdf do user interaction ATM const bool needsMainWindow = !exportAs; // only show the mainWindow when no command-line mode option is passed // TODO: fix print & exportAsPdf to work without mainwindow shown const bool showmainWindow = !exportAs; // would be !batchRun; const bool showSplashScreen = !m_batchRun && qEnvironmentVariableIsEmpty("NOSPLASH"); if (showSplashScreen && d->splashScreen) { d->splashScreen->show(); d->splashScreen->repaint(); processEvents(); } KoHashGeneratorProvider::instance()->setGenerator("MD5", new KisMD5Generator()); // Initialize all Krita directories etc. KoGlobal::initialize(); KConfigGroup group(KSharedConfig::openConfig(), "theme"); Digikam::ThemeManager themeManager; themeManager.setCurrentTheme(group.readEntry("Theme", "Krita dark")); ResetStarting resetStarting(d->splashScreen, args.filenames().count()); // remove the splash when done Q_UNUSED(resetStarting); // Make sure we can save resources and tags setSplashScreenLoadingText(i18n("Adding resource types")); processEvents(); addResourceTypes(); - // now we're set up, and the LcmsEnginePlugin will have access to resource paths for color management, - // we can finally initialize KoColor. - KoColor::init(); - // Load all resources and tags before the plugins do that loadResources(); // Load the plugins loadPlugins(); if (needsMainWindow) { // show a mainWindow asap, if we want that setSplashScreenLoadingText(i18n("Loading Main Window...")); processEvents(); m_mainWindow = KisPart::instance()->createMainWindow(); if (showmainWindow) { m_mainWindow->initializeGeometry(); if (!args.workspace().isEmpty()) { KoResourceServer * rserver = KisResourceServerProvider::instance()->workspaceServer(); KisWorkspaceResource* workspace = rserver->resourceByName(args.workspace()); if (workspace) { m_mainWindow->restoreWorkspace(workspace->dockerState()); } } if (args.canvasOnly()) { m_mainWindow->viewManager()->switchCanvasOnly(true); } if (args.fullScreen()) { m_mainWindow->showFullScreen(); } else { m_mainWindow->show(); } } } short int numberOfOpenDocuments = 0; // number of documents open // Check for autosave files that can be restored, if we're not running a batchrun (test, print, export to pdf) if (!m_batchRun) { checkAutosaveFiles(); } setSplashScreenLoadingText(QString()); // done loading, so clear out label processEvents(); //configure the unit manager KisSpinBoxUnitManagerFactory::setDefaultUnitManagerBuilder(new KisDocumentAwareSpinBoxUnitManagerBuilder()); connect(this, &KisApplication::aboutToQuit, &KisSpinBoxUnitManagerFactory::clearUnitManagerBuilder); //ensure the builder is destroyed when the application leave. //the new syntax slot syntax allow to connect to a non q_object static method. // Create a new image, if needed if (doNewImage) { KisDocument *doc = args.image(); if (doc) { KisPart::instance()->addDocument(doc); m_mainWindow->addViewAndNotifyLoadingCompleted(doc); } } // Get the command line arguments which we have to parse int argsCount = args.filenames().count(); if (argsCount > 0) { // Loop through arguments short int nPrinted = 0; for (int argNumber = 0; argNumber < argsCount; argNumber++) { QString fileName = args.filenames().at(argNumber); // are we just trying to open a template? if (doTemplate) { // called in mix with batch options? ignore and silently skip if (m_batchRun) { continue; } if (createNewDocFromTemplate(fileName, m_mainWindow)) { ++numberOfOpenDocuments; } // now try to load } else { if (exportAs) { QString outputMimetype = KisMimeDatabase::mimeTypeForFile(exportFileName); if (outputMimetype == "application/octetstream") { dbgKrita << i18n("Mimetype not found, try using the -mimetype option") << endl; return 1; } KisDocument *doc = KisPart::instance()->createDocument(); doc->setFileBatchMode(m_batchRun); doc->openUrl(QUrl::fromLocalFile(fileName)); qApp->processEvents(); // For vector layers to be updated doc->setFileBatchMode(true); if (!doc->exportDocumentSync(QUrl::fromLocalFile(exportFileName), outputMimetype.toLatin1())) { dbgKrita << "Could not export " << fileName << "to" << exportFileName << ":" << doc->errorMessage(); } nPrinted++; QTimer::singleShot(0, this, SLOT(quit())); } else if (m_mainWindow) { KisMainWindow::OpenFlags flags = m_batchRun ? KisMainWindow::BatchMode : KisMainWindow::None; if (m_mainWindow->openDocument(QUrl::fromLocalFile(fileName), flags)) { if (print) { m_mainWindow->slotFilePrint(); nPrinted++; // TODO: trigger closing of app once printing is done } else if (exportAsPdf) { KisPrintJob *job = m_mainWindow->exportToPdf(exportFileName); if (job) connect (job, SIGNAL(destroyed(QObject*)), m_mainWindow, SLOT(slotFileQuit()), Qt::QueuedConnection); nPrinted++; } else { // Normal case, success numberOfOpenDocuments++; } } else { // .... if failed // delete doc; done by openDocument } } } } if (m_batchRun) { return nPrinted > 0; } } // fixes BUG:369308 - Krita crashing on splash screen when loading. // trying to open a file before Krita has loaded can cause it to hang and crash if (d->splashScreen) { d->splashScreen->displayLinks(); d->splashScreen->displayRecentFiles(); } // not calling this before since the program will quit there. return true; } KisApplication::~KisApplication() { delete d; } void KisApplication::setSplashScreen(QWidget *splashScreen) { d->splashScreen = qobject_cast(splashScreen); } void KisApplication::setSplashScreenLoadingText(QString textToLoad) { if (d->splashScreen) { d->splashScreen->loadingLabel->setText(textToLoad); d->splashScreen->repaint(); } } void KisApplication::hideSplashScreen() { if (d->splashScreen) { // hide the splashscreen to see the dialog d->splashScreen->hide(); } } bool KisApplication::notify(QObject *receiver, QEvent *event) { try { return QApplication::notify(receiver, event); } catch (std::exception &e) { qWarning("Error %s sending event %i to object %s", e.what(), event->type(), qPrintable(receiver->objectName())); } catch (...) { qWarning("Error sending event %i to object %s", event->type(), qPrintable(receiver->objectName())); } return false; } void KisApplication::remoteArguments(QByteArray message, QObject *socket) { Q_UNUSED(socket); // check if we have any mainwindow KisMainWindow *mw = qobject_cast(qApp->activeWindow()); if (!mw) { mw = KisPart::instance()->mainWindows().first(); } if (!mw) { return; } KisApplicationArguments args = KisApplicationArguments::deserialize(message); const bool doTemplate = args.doTemplate(); const int argsCount = args.filenames().count(); if (argsCount > 0) { // Loop through arguments for (int argNumber = 0; argNumber < argsCount; ++argNumber) { QString filename = args.filenames().at(argNumber); // are we just trying to open a template? if (doTemplate) { createNewDocFromTemplate(filename, mw); } else if (QFile(filename).exists()) { KisMainWindow::OpenFlags flags = m_batchRun ? KisMainWindow::BatchMode : KisMainWindow::None; mw->openDocument(QUrl::fromLocalFile(filename), flags); } } } } void KisApplication::fileOpenRequested(const QString &url) { KisMainWindow *mainWindow = KisPart::instance()->mainWindows().first(); if (mainWindow) { KisMainWindow::OpenFlags flags = m_batchRun ? KisMainWindow::BatchMode : KisMainWindow::None; mainWindow->openDocument(QUrl::fromLocalFile(url), flags); } } void KisApplication::checkAutosaveFiles() { if (m_batchRun) return; // Check for autosave files from a previous run. There can be several, and // we want to offer a restore for every one. Including a nice thumbnail! QStringList filters; filters << QString(".krita-*-*-autosave.kra"); #ifdef Q_OS_WIN QDir dir = QDir::temp(); #else QDir dir = QDir::home(); #endif // all autosave files for our application QStringList autosaveFiles = dir.entryList(filters, QDir::Files | QDir::Hidden); // Allow the user to make their selection if (autosaveFiles.size() > 0) { if (d->splashScreen) { // hide the splashscreen to see the dialog d->splashScreen->hide(); } m_autosaveDialog = new KisAutoSaveRecoveryDialog(autosaveFiles, activeWindow()); QDialog::DialogCode result = (QDialog::DialogCode) m_autosaveDialog->exec(); if (result == QDialog::Accepted) { QStringList filesToRecover = m_autosaveDialog->recoverableFiles(); Q_FOREACH (const QString &autosaveFile, autosaveFiles) { if (!filesToRecover.contains(autosaveFile)) { QFile::remove(dir.absolutePath() + "/" + autosaveFile); } } autosaveFiles = filesToRecover; } else { autosaveFiles.clear(); } if (autosaveFiles.size() > 0) { QList autosaveUrls; Q_FOREACH (const QString &autoSaveFile, autosaveFiles) { const QUrl url = QUrl::fromLocalFile(dir.absolutePath() + QLatin1Char('/') + autoSaveFile); autosaveUrls << url; } if (m_mainWindow) { Q_FOREACH (const QUrl &url, autosaveUrls) { KisMainWindow::OpenFlags flags = m_batchRun ? KisMainWindow::BatchMode : KisMainWindow::None; m_mainWindow->openDocument(url, flags | KisMainWindow::RecoveryFile); } } } // cleanup delete m_autosaveDialog; m_autosaveDialog = nullptr; } } bool KisApplication::createNewDocFromTemplate(const QString &fileName, KisMainWindow *mainWindow) { QString templatePath; const QUrl templateUrl = QUrl::fromLocalFile(fileName); if (QFile::exists(fileName)) { templatePath = templateUrl.toLocalFile(); dbgUI << "using full path..."; } else { QString desktopName(fileName); const QString templatesResourcePath = QStringLiteral("templates/"); QStringList paths = KoResourcePaths::findAllResources("data", templatesResourcePath + "*/" + desktopName); if (paths.isEmpty()) { paths = KoResourcePaths::findAllResources("data", templatesResourcePath + desktopName); } if (paths.isEmpty()) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("No template found for: %1", desktopName)); } else if (paths.count() > 1) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Too many templates found for: %1", desktopName)); } else { templatePath = paths.at(0); } } if (!templatePath.isEmpty()) { QUrl templateBase; templateBase.setPath(templatePath); KDesktopFile templateInfo(templatePath); QString templateName = templateInfo.readUrl(); QUrl templateURL; templateURL.setPath(templateBase.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path() + '/' + templateName); KisMainWindow::OpenFlags batchFlags = m_batchRun ? KisMainWindow::BatchMode : KisMainWindow::None; if (mainWindow->openDocument(templateURL, KisMainWindow::Import | batchFlags)) { dbgUI << "Template loaded..."; return true; } else { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Template %1 failed to load.", templateURL.toDisplayString())); } } return false; } void KisApplication::clearConfig() { KIS_ASSERT_RECOVER_RETURN(qApp->thread() == QThread::currentThread()); KSharedConfigPtr config = KSharedConfig::openConfig(); // find user settings file bool createDir = false; QString kritarcPath = KoResourcePaths::locateLocal("config", "kritarc", createDir); QFile configFile(kritarcPath); if (configFile.exists()) { // clear file if (configFile.open(QFile::WriteOnly)) { configFile.close(); } else { QMessageBox::warning(0, i18nc("@title:window", "Krita"), i18n("Failed to clear %1\n\n" "Please make sure no other program is using the file and try again.", kritarcPath), QMessageBox::Ok, QMessageBox::Ok); } } // reload from disk; with the user file settings cleared, // this should load any default configuration files shipping with the program config->reparseConfiguration(); config->sync(); } void KisApplication::askClearConfig() { Qt::KeyboardModifiers mods = QApplication::queryKeyboardModifiers(); bool askClearConfig = (mods & Qt::ControlModifier) && (mods & Qt::ShiftModifier) && (mods & Qt::AltModifier); if (askClearConfig) { bool ok = QMessageBox::question(0, i18nc("@title:window", "Krita"), i18n("Do you want to clear the settings file?"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes; if (ok) { clearConfig(); } } } diff --git a/libs/ui/KisViewManager.cpp b/libs/ui/KisViewManager.cpp index 2f3a5b05be..59eb5fe1ea 100644 --- a/libs/ui/KisViewManager.cpp +++ b/libs/ui/KisViewManager.cpp @@ -1,1370 +1,1373 @@ /* * This file is part of KimageShop^WKrayon^WKrita * * Copyright (c) 1999 Matthias Elter * 1999 Michael Koch * 1999 Carsten Pfeiffer * 2002 Patrick Julien * 2003-2011 Boudewijn Rempt * 2004 Clarence Dang * 2011 José Luis Vergara * 2017 L. E. Segovia * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include "KisViewManager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "input/kis_input_manager.h" #include "canvas/kis_canvas2.h" #include "canvas/kis_canvas_controller.h" #include "canvas/kis_grid_manager.h" #include "dialogs/kis_dlg_blacklist_cleanup.h" #include "input/kis_input_profile_manager.h" #include "kis_action_manager.h" #include "kis_action.h" #include "kis_canvas_controls_manager.h" #include "kis_canvas_resource_provider.h" #include "kis_composite_progress_proxy.h" #include #include "kis_config.h" #include "kis_config_notifier.h" #include "kis_control_frame.h" #include "kis_coordinates_converter.h" #include "KisDocument.h" #include "kis_favorite_resource_manager.h" #include "kis_filter_manager.h" #include "kis_group_layer.h" #include #include #include "kis_image_manager.h" #include #include "kis_mainwindow_observer.h" #include "kis_mask_manager.h" #include "kis_mimedata.h" #include "kis_mirror_manager.h" #include "kis_node_commands_adapter.h" #include "kis_node.h" #include "kis_node_manager.h" #include "kis_painting_assistants_manager.h" #include #include "kis_paintop_box.h" #include #include "KisPart.h" #include "KisPrintJob.h" #include #include "kis_resource_server_provider.h" #include "kis_selection.h" #include "kis_selection_manager.h" #include "kis_shape_controller.h" #include "kis_shape_layer.h" #include #include "kis_statusbar.h" #include #include #include "kis_tooltip_manager.h" #include #include "KisView.h" #include "kis_zoom_manager.h" #include "widgets/kis_floating_message.h" #include "kis_signal_auto_connection.h" #include "kis_script_manager.h" #include "kis_icon_utils.h" #include "kis_guides_manager.h" #include "kis_derived_resources.h" #include "dialogs/kis_delayed_save_dialog.h" #include class BlockingUserInputEventFilter : public QObject { bool eventFilter(QObject *watched, QEvent *event) override { Q_UNUSED(watched); if(dynamic_cast(event) || dynamic_cast(event) || dynamic_cast(event)) { return true; } else { return false; } } }; class KisViewManager::KisViewManagerPrivate { public: KisViewManagerPrivate(KisViewManager *_q, KActionCollection *_actionCollection, QWidget *_q_parent) : filterManager(_q) , createTemplate(0) , saveIncremental(0) , saveIncrementalBackup(0) , openResourcesDirectory(0) , rotateCanvasRight(0) , rotateCanvasLeft(0) , resetCanvasRotation(0) , wrapAroundAction(0) , levelOfDetailAction(0) , showRulersAction(0) , rulersTrackMouseAction(0) , zoomTo100pct(0) , zoomIn(0) , zoomOut(0) , selectionManager(_q) , statusBar(_q) , controlFrame(_q, _q_parent) , nodeManager(_q) , imageManager(_q) , gridManager(_q) , canvasControlsManager(_q) , paintingAssistantsManager(_q) , actionManager(_q, _actionCollection) , mainWindow(0) , showFloatingMessage(true) , currentImageView(0) , canvasResourceProvider(_q) , canvasResourceManager() , guiUpdateCompressor(30, KisSignalCompressor::POSTPONE, _q) , actionCollection(_actionCollection) , mirrorManager(_q) , inputManager(_q) , scriptManager(_q) , actionAuthor(0) , showPixelGrid(0) { - canvasResourceManager.addDerivedResourceConverter(toQShared(new KisCompositeOpResourceConverter)); - canvasResourceManager.addDerivedResourceConverter(toQShared(new KisEffectiveCompositeOpResourceConverter)); - canvasResourceManager.addDerivedResourceConverter(toQShared(new KisOpacityResourceConverter)); - canvasResourceManager.addDerivedResourceConverter(toQShared(new KisFlowResourceConverter)); - canvasResourceManager.addDerivedResourceConverter(toQShared(new KisSizeResourceConverter)); - canvasResourceManager.addDerivedResourceConverter(toQShared(new KisLodAvailabilityResourceConverter)); - canvasResourceManager.addDerivedResourceConverter(toQShared(new KisEraserModeResourceConverter)); - canvasResourceManager.addResourceUpdateMediator(toQShared(new KisPresetUpdateMediator)); + KisViewManager::initializeResourceManager(&canvasResourceManager); } public: KisFilterManager filterManager; KisAction *createTemplate; KisAction *createCopy; KisAction *saveIncremental; KisAction *saveIncrementalBackup; KisAction *openResourcesDirectory; KisAction *rotateCanvasRight; KisAction *rotateCanvasLeft; KisAction *resetCanvasRotation; KisAction *wrapAroundAction; KisAction *levelOfDetailAction; KisAction *showRulersAction; KisAction *rulersTrackMouseAction; KisAction *zoomTo100pct; KisAction *zoomIn; KisAction *zoomOut; KisAction *softProof; KisAction *gamutCheck; KisSelectionManager selectionManager; KisGuidesManager guidesManager; KisStatusBar statusBar; QPointer persistentImageProgressUpdater; QScopedPointer persistentUnthreadedProgressUpdaterRouter; QPointer persistentUnthreadedProgressUpdater; KisControlFrame controlFrame; KisNodeManager nodeManager; KisImageManager imageManager; KisGridManager gridManager; KisCanvasControlsManager canvasControlsManager; KisPaintingAssistantsManager paintingAssistantsManager; BlockingUserInputEventFilter blockingEventFilter; KisActionManager actionManager; QMainWindow* mainWindow; QPointer savedFloatingMessage; bool showFloatingMessage; QPointer currentImageView; KisCanvasResourceProvider canvasResourceProvider; KoCanvasResourceManager canvasResourceManager; KisSignalCompressor guiUpdateCompressor; KActionCollection *actionCollection; KisMirrorManager mirrorManager; KisInputManager inputManager; KisSignalAutoConnectionsStore viewConnections; KisScriptManager scriptManager; KSelectAction *actionAuthor; // Select action for author profile. KisAction *showPixelGrid; QByteArray canvasState; #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) QFlags windowFlags; #endif bool blockUntilOperationsFinishedImpl(KisImageSP image, bool force); }; - KisViewManager::KisViewManager(QWidget *parent, KActionCollection *_actionCollection) : d(new KisViewManagerPrivate(this, _actionCollection, parent)) { d->actionCollection = _actionCollection; d->mainWindow = dynamic_cast(parent); d->canvasResourceProvider.setResourceManager(&d->canvasResourceManager); connect(&d->guiUpdateCompressor, SIGNAL(timeout()), this, SLOT(guiUpdateTimeout())); createActions(); setupManagers(); // These initialization functions must wait until KisViewManager ctor is complete. d->statusBar.setup(); d->persistentImageProgressUpdater = d->statusBar.progressUpdater()->startSubtask(1, "", true); // reset state to "completed" d->persistentImageProgressUpdater->setRange(0,100); d->persistentImageProgressUpdater->setValue(100); d->persistentUnthreadedProgressUpdater = d->statusBar.progressUpdater()->startSubtask(1, "", true); // reset state to "completed" d->persistentUnthreadedProgressUpdater->setRange(0,100); d->persistentUnthreadedProgressUpdater->setValue(100); d->persistentUnthreadedProgressUpdaterRouter.reset( new KoProgressUpdater(d->persistentUnthreadedProgressUpdater, KoProgressUpdater::Unthreaded)); d->persistentUnthreadedProgressUpdaterRouter->setAutoNestNames(true); d->controlFrame.setup(parent); //Check to draw scrollbars after "Canvas only mode" toggle is created. this->showHideScrollbars(); QScopedPointer dummy(new KoDummyCanvasController(actionCollection())); KoToolManager::instance()->registerToolActions(actionCollection(), dummy.data()); QTimer::singleShot(0, this, SLOT(initializeStatusBarVisibility())); connect(KoToolManager::instance(), SIGNAL(inputDeviceChanged(KoInputDevice)), d->controlFrame.paintopBox(), SLOT(slotInputDeviceChanged(KoInputDevice))); connect(KoToolManager::instance(), SIGNAL(changedTool(KoCanvasController*,int)), d->controlFrame.paintopBox(), SLOT(slotToolChanged(KoCanvasController*,int))); connect(&d->nodeManager, SIGNAL(sigNodeActivated(KisNodeSP)), resourceProvider(), SLOT(slotNodeActivated(KisNodeSP))); connect(resourceProvider()->resourceManager(), SIGNAL(canvasResourceChanged(int,QVariant)), d->controlFrame.paintopBox(), SLOT(slotCanvasResourceChanged(int,QVariant))); connect(KisPart::instance(), SIGNAL(sigViewAdded(KisView*)), SLOT(slotViewAdded(KisView*))); connect(KisPart::instance(), SIGNAL(sigViewRemoved(KisView*)), SLOT(slotViewRemoved(KisView*))); connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotUpdateAuthorProfileActions())); KisInputProfileManager::instance()->loadProfiles(); KisConfig cfg; d->showFloatingMessage = cfg.showCanvasMessages(); } KisViewManager::~KisViewManager() { KisConfig cfg; if (resourceProvider() && resourceProvider()->currentPreset()) { cfg.writeEntry("LastPreset", resourceProvider()->currentPreset()->name()); cfg.writeKoColor("LastForeGroundColor",resourceProvider()->fgColor()); cfg.writeKoColor("LastBackGroundColor",resourceProvider()->bgColor()); } cfg.writeEntry("baseLength", KoResourceItemChooserSync::instance()->baseLength()); delete d; } +void KisViewManager::initializeResourceManager(KoCanvasResourceManager *resourceManager) +{ + resourceManager->addDerivedResourceConverter(toQShared(new KisCompositeOpResourceConverter)); + resourceManager->addDerivedResourceConverter(toQShared(new KisEffectiveCompositeOpResourceConverter)); + resourceManager->addDerivedResourceConverter(toQShared(new KisOpacityResourceConverter)); + resourceManager->addDerivedResourceConverter(toQShared(new KisFlowResourceConverter)); + resourceManager->addDerivedResourceConverter(toQShared(new KisSizeResourceConverter)); + resourceManager->addDerivedResourceConverter(toQShared(new KisLodAvailabilityResourceConverter)); + resourceManager->addDerivedResourceConverter(toQShared(new KisEraserModeResourceConverter)); + resourceManager->addResourceUpdateMediator(toQShared(new KisPresetUpdateMediator)); +} + KActionCollection *KisViewManager::actionCollection() const { return d->actionCollection; } void KisViewManager::slotViewAdded(KisView *view) { // WARNING: this slot is called even when a view from another main windows is added! // Don't expect \p view be a child of this view manager! Q_UNUSED(view); if (viewCount() == 0) { d->statusBar.showAllStatusBarItems(); } } void KisViewManager::slotViewRemoved(KisView *view) { // WARNING: this slot is called even when a view from another main windows is removed! // Don't expect \p view be a child of this view manager! Q_UNUSED(view); if (viewCount() == 0) { d->statusBar.hideAllStatusBarItems(); } } void KisViewManager::setCurrentView(KisView *view) { bool first = true; if (d->currentImageView) { d->currentImageView->notifyCurrentStateChanged(false); d->currentImageView->canvasBase()->setCursor(QCursor(Qt::ArrowCursor)); first = false; KisDocument* doc = d->currentImageView->document(); if (doc) { doc->image()->compositeProgressProxy()->removeProxy(d->persistentImageProgressUpdater); doc->disconnect(this); } d->currentImageView->canvasController()->proxyObject->disconnect(&d->statusBar); d->viewConnections.clear(); } QPointer imageView = qobject_cast(view); d->currentImageView = imageView; if (imageView) { d->softProof->setChecked(imageView->softProofing()); d->gamutCheck->setChecked(imageView->gamutCheck()); // Wait for the async image to have loaded KisDocument* doc = view->document(); // connect(canvasController()->proxyObject, SIGNAL(documentMousePositionChanged(QPointF)), d->statusBar, SLOT(documentMousePositionChanged(QPointF))); // Restore the last used brush preset, color and background color. if (first) { KisConfig cfg; KisPaintOpPresetResourceServer * rserver = KisResourceServerProvider::instance()->paintOpPresetServer(); QString lastPreset = cfg.readEntry("LastPreset", QString("Basic_tip_default")); KisPaintOpPresetSP preset = rserver->resourceByName(lastPreset); if (!preset) { preset = rserver->resourceByName("Basic_tip_default"); } if (!preset) { preset = rserver->resources().first(); } if (preset) { paintOpBox()->restoreResource(preset.data()); } const KoColorSpace *cs = KoColorSpaceRegistry::instance()->rgb8(); KoColor foreground(Qt::black, cs); d->canvasResourceProvider.setFGColor(cfg.readKoColor("LastForeGroundColor",foreground)); KoColor background(Qt::white, cs); d->canvasResourceProvider.setBGColor(cfg.readKoColor("LastBackGroundColor",background)); } KisCanvasController *canvasController = dynamic_cast(d->currentImageView->canvasController()); d->viewConnections.addUniqueConnection(&d->nodeManager, SIGNAL(sigNodeActivated(KisNodeSP)), doc->image(), SLOT(requestStrokeEndActiveNode())); d->viewConnections.addUniqueConnection(d->rotateCanvasRight, SIGNAL(triggered()), canvasController, SLOT(rotateCanvasRight15())); d->viewConnections.addUniqueConnection(d->rotateCanvasLeft, SIGNAL(triggered()),canvasController, SLOT(rotateCanvasLeft15())); d->viewConnections.addUniqueConnection(d->resetCanvasRotation, SIGNAL(triggered()),canvasController, SLOT(resetCanvasRotation())); d->viewConnections.addUniqueConnection(d->wrapAroundAction, SIGNAL(toggled(bool)), canvasController, SLOT(slotToggleWrapAroundMode(bool))); d->wrapAroundAction->setChecked(canvasController->wrapAroundMode()); d->viewConnections.addUniqueConnection(d->levelOfDetailAction, SIGNAL(toggled(bool)), canvasController, SLOT(slotToggleLevelOfDetailMode(bool))); d->levelOfDetailAction->setChecked(canvasController->levelOfDetailMode()); d->viewConnections.addUniqueConnection(d->currentImageView->image(), SIGNAL(sigColorSpaceChanged(const KoColorSpace*)), d->controlFrame.paintopBox(), SLOT(slotColorSpaceChanged(const KoColorSpace*))); d->viewConnections.addUniqueConnection(d->showRulersAction, SIGNAL(toggled(bool)), imageView->zoomManager(), SLOT(setShowRulers(bool))); d->viewConnections.addUniqueConnection(d->rulersTrackMouseAction, SIGNAL(toggled(bool)), imageView->zoomManager(), SLOT(setRulersTrackMouse(bool))); d->viewConnections.addUniqueConnection(d->zoomTo100pct, SIGNAL(triggered()), imageView->zoomManager(), SLOT(zoomTo100())); d->viewConnections.addUniqueConnection(d->zoomIn, SIGNAL(triggered()), imageView->zoomController()->zoomAction(), SLOT(zoomIn())); d->viewConnections.addUniqueConnection(d->zoomOut, SIGNAL(triggered()), imageView->zoomController()->zoomAction(), SLOT(zoomOut())); d->viewConnections.addUniqueConnection(d->softProof, SIGNAL(toggled(bool)), view, SLOT(slotSoftProofing(bool)) ); d->viewConnections.addUniqueConnection(d->gamutCheck, SIGNAL(toggled(bool)), view, SLOT(slotGamutCheck(bool)) ); // set up progrress reporting doc->image()->compositeProgressProxy()->addProxy(d->persistentImageProgressUpdater); d->viewConnections.addUniqueConnection(&d->statusBar, SIGNAL(sigCancellationRequested()), doc->image(), SLOT(requestStrokeCancellation())); d->viewConnections.addUniqueConnection(d->showPixelGrid, SIGNAL(toggled(bool)), canvasController, SLOT(slotTogglePixelGrid(bool))); imageView->zoomManager()->setShowRulers(d->showRulersAction->isChecked()); imageView->zoomManager()->setRulersTrackMouse(d->rulersTrackMouseAction->isChecked()); showHideScrollbars(); } d->filterManager.setView(imageView); d->selectionManager.setView(imageView); d->guidesManager.setView(imageView); d->nodeManager.setView(imageView); d->imageManager.setView(imageView); d->canvasControlsManager.setView(imageView); d->actionManager.setView(imageView); d->gridManager.setView(imageView); d->statusBar.setView(imageView); d->paintingAssistantsManager.setView(imageView); d->mirrorManager.setView(imageView); if (d->currentImageView) { d->currentImageView->notifyCurrentStateChanged(true); d->currentImageView->canvasController()->activate(); d->currentImageView->canvasController()->setFocus(); d->viewConnections.addUniqueConnection( image(), SIGNAL(sigSizeChanged(const QPointF&, const QPointF&)), resourceProvider(), SLOT(slotImageSizeChanged())); d->viewConnections.addUniqueConnection( image(), SIGNAL(sigResolutionChanged(double,double)), resourceProvider(), SLOT(slotOnScreenResolutionChanged())); d->viewConnections.addUniqueConnection( image(), SIGNAL(sigNodeChanged(KisNodeSP)), this, SLOT(updateGUI())); d->viewConnections.addUniqueConnection( d->currentImageView->zoomManager()->zoomController(), SIGNAL(zoomChanged(KoZoomMode::Mode,qreal)), resourceProvider(), SLOT(slotOnScreenResolutionChanged())); } d->actionManager.updateGUI(); resourceProvider()->slotImageSizeChanged(); resourceProvider()->slotOnScreenResolutionChanged(); Q_EMIT viewChanged(); } KoZoomController *KisViewManager::zoomController() const { if (d->currentImageView) { return d->currentImageView->zoomController(); } return 0; } KisImageWSP KisViewManager::image() const { if (document()) { return document()->image(); } return 0; } KisCanvasResourceProvider * KisViewManager::resourceProvider() { return &d->canvasResourceProvider; } KisCanvas2 * KisViewManager::canvasBase() const { if (d && d->currentImageView) { return d->currentImageView->canvasBase(); } return 0; } QWidget* KisViewManager::canvas() const { if (d && d->currentImageView && d->currentImageView->canvasBase()->canvasWidget()) { return d->currentImageView->canvasBase()->canvasWidget(); } return 0; } KisStatusBar * KisViewManager::statusBar() const { return &d->statusBar; } void KisViewManager::addStatusBarItem(QWidget *widget, int stretch, bool permanent) { d->statusBar.addStatusBarItem(widget, stretch, permanent); } void KisViewManager::removeStatusBarItem(QWidget *widget) { d->statusBar.removeStatusBarItem(widget); } KisPaintopBox* KisViewManager::paintOpBox() const { return d->controlFrame.paintopBox(); } QPointer KisViewManager::createUnthreadedUpdater(const QString &name) { return d->persistentUnthreadedProgressUpdaterRouter->startSubtask(1, name, false); } QPointer KisViewManager::createThreadedUpdater(const QString &name) { return d->statusBar.progressUpdater()->startSubtask(1, name, false); } KisSelectionManager * KisViewManager::selectionManager() { return &d->selectionManager; } KisNodeSP KisViewManager::activeNode() { return d->nodeManager.activeNode(); } KisLayerSP KisViewManager::activeLayer() { return d->nodeManager.activeLayer(); } KisPaintDeviceSP KisViewManager::activeDevice() { return d->nodeManager.activePaintDevice(); } KisZoomManager * KisViewManager::zoomManager() { if (d->currentImageView) { return d->currentImageView->zoomManager(); } return 0; } KisFilterManager * KisViewManager::filterManager() { return &d->filterManager; } KisImageManager * KisViewManager::imageManager() { return &d->imageManager; } KisInputManager* KisViewManager::inputManager() const { return &d->inputManager; } KisSelectionSP KisViewManager::selection() { if (d->currentImageView) { return d->currentImageView->selection(); } return 0; } bool KisViewManager::selectionEditable() { KisLayerSP layer = activeLayer(); if (layer) { KoProperties properties; QList masks = layer->childNodes(QStringList("KisSelectionMask"), properties); if (masks.size() == 1) { return masks[0]->isEditable(); } } // global selection is always editable return true; } KisUndoAdapter * KisViewManager::undoAdapter() { if (!document()) return 0; KisImageWSP image = document()->image(); Q_ASSERT(image); return image->undoAdapter(); } void KisViewManager::createActions() { KisConfig cfg; d->saveIncremental = actionManager()->createAction("save_incremental_version"); connect(d->saveIncremental, SIGNAL(triggered()), this, SLOT(slotSaveIncremental())); d->saveIncrementalBackup = actionManager()->createAction("save_incremental_backup"); connect(d->saveIncrementalBackup, SIGNAL(triggered()), this, SLOT(slotSaveIncrementalBackup())); connect(mainWindow(), SIGNAL(documentSaved()), this, SLOT(slotDocumentSaved())); d->saveIncremental->setEnabled(false); d->saveIncrementalBackup->setEnabled(false); KisAction *tabletDebugger = actionManager()->createAction("tablet_debugger"); connect(tabletDebugger, SIGNAL(triggered()), this, SLOT(toggleTabletLogger())); d->createTemplate = actionManager()->createAction("create_template"); connect(d->createTemplate, SIGNAL(triggered()), this, SLOT(slotCreateTemplate())); d->createCopy = actionManager()->createAction("create_copy"); connect(d->createCopy, SIGNAL(triggered()), this, SLOT(slotCreateCopy())); d->openResourcesDirectory = actionManager()->createAction("open_resources_directory"); connect(d->openResourcesDirectory, SIGNAL(triggered()), SLOT(openResourcesDirectory())); d->rotateCanvasRight = actionManager()->createAction("rotate_canvas_right"); d->rotateCanvasLeft = actionManager()->createAction("rotate_canvas_left"); d->resetCanvasRotation = actionManager()->createAction("reset_canvas_rotation"); d->wrapAroundAction = actionManager()->createAction("wrap_around_mode"); d->levelOfDetailAction = actionManager()->createAction("level_of_detail_mode"); d->softProof = actionManager()->createAction("softProof"); d->gamutCheck = actionManager()->createAction("gamutCheck"); KisAction *tAction = actionManager()->createAction("showStatusBar"); tAction->setChecked(cfg.showStatusBar()); connect(tAction, SIGNAL(toggled(bool)), this, SLOT(showStatusBar(bool))); tAction = actionManager()->createAction("view_show_canvas_only"); tAction->setChecked(false); connect(tAction, SIGNAL(toggled(bool)), this, SLOT(switchCanvasOnly(bool))); //Workaround, by default has the same shortcut as mirrorCanvas KisAction *a = dynamic_cast(actionCollection()->action("format_italic")); if (a) { a->setDefaultShortcut(QKeySequence()); } a = actionManager()->createAction("edit_blacklist_cleanup"); connect(a, SIGNAL(triggered()), this, SLOT(slotBlacklistCleanup())); d->showRulersAction = actionManager()->createAction("view_ruler"); d->showRulersAction->setChecked(cfg.showRulers()); connect(d->showRulersAction, SIGNAL(toggled(bool)), SLOT(slotSaveShowRulersState(bool))); d->rulersTrackMouseAction = actionManager()->createAction("rulers_track_mouse"); d->rulersTrackMouseAction->setChecked(cfg.rulersTrackMouse()); connect(d->rulersTrackMouseAction, SIGNAL(toggled(bool)), SLOT(slotSaveRulersTrackMouseState(bool))); d->zoomTo100pct = actionManager()->createAction("zoom_to_100pct"); d->zoomIn = actionManager()->createStandardAction(KStandardAction::ZoomIn, 0, ""); d->zoomOut = actionManager()->createStandardAction(KStandardAction::ZoomOut, 0, ""); d->actionAuthor = new KSelectAction(KisIconUtils::loadIcon("im-user"), i18n("Active Author Profile"), this); connect(d->actionAuthor, SIGNAL(triggered(const QString &)), this, SLOT(changeAuthorProfile(const QString &))); actionCollection()->addAction("settings_active_author", d->actionAuthor); slotUpdateAuthorProfileActions(); d->showPixelGrid = actionManager()->createAction("view_pixel_grid"); d->showPixelGrid->setChecked(cfg.pixelGridEnabled()); } void KisViewManager::setupManagers() { // Create the managers for filters, selections, layers etc. // XXX: When the currentlayer changes, call updateGUI on all // managers d->filterManager.setup(actionCollection(), actionManager()); d->selectionManager.setup(actionManager()); d->guidesManager.setup(actionManager()); d->nodeManager.setup(actionCollection(), actionManager()); d->imageManager.setup(actionManager()); d->gridManager.setup(actionManager()); d->paintingAssistantsManager.setup(actionManager()); d->canvasControlsManager.setup(actionManager()); d->mirrorManager.setup(actionCollection()); d->scriptManager.setup(actionCollection(), actionManager()); } void KisViewManager::updateGUI() { d->guiUpdateCompressor.start(); } void KisViewManager::slotBlacklistCleanup() { KisDlgBlacklistCleanup dialog; dialog.exec(); } KisNodeManager * KisViewManager::nodeManager() const { return &d->nodeManager; } KisActionManager* KisViewManager::actionManager() const { return &d->actionManager; } KisGridManager * KisViewManager::gridManager() const { return &d->gridManager; } KisGuidesManager * KisViewManager::guidesManager() const { return &d->guidesManager; } KisDocument *KisViewManager::document() const { if (d->currentImageView && d->currentImageView->document()) { return d->currentImageView->document(); } return 0; } KisScriptManager *KisViewManager::scriptManager() const { return &d->scriptManager; } int KisViewManager::viewCount() const { KisMainWindow *mw = qobject_cast(d->mainWindow); if (mw) { return mw->viewCount(); } return 0; } bool KisViewManager::KisViewManagerPrivate::blockUntilOperationsFinishedImpl(KisImageSP image, bool force) { const int busyWaitDelay = 1000; KisDelayedSaveDialog dialog(image, !force ? KisDelayedSaveDialog::GeneralDialog : KisDelayedSaveDialog::ForcedDialog, busyWaitDelay, mainWindow); dialog.blockIfImageIsBusy(); return dialog.result() == QDialog::Accepted; } bool KisViewManager::blockUntilOperationsFinished(KisImageSP image) { return d->blockUntilOperationsFinishedImpl(image, false); } void KisViewManager::blockUntilOperationsFinishedForced(KisImageSP image) { d->blockUntilOperationsFinishedImpl(image, true); } void KisViewManager::slotCreateTemplate() { if (!document()) return; KisTemplateCreateDia::createTemplate( QStringLiteral("templates/"), ".kra", document(), mainWindow()); } void KisViewManager::slotCreateCopy() { KisDocument *srcDoc = document(); if (!srcDoc) return; if (!this->blockUntilOperationsFinished(srcDoc->image())) return; KisDocument *doc = 0; { KisImageBarrierLocker l(srcDoc->image()); doc = srcDoc->clone(); } KIS_SAFE_ASSERT_RECOVER_RETURN(doc); QString name = srcDoc->documentInfo()->aboutInfo("name"); if (name.isEmpty()) { name = document()->url().toLocalFile(); } name = i18n("%1 (Copy)", name); doc->documentInfo()->setAboutInfo("title", name); KisPart::instance()->addDocument(doc); KisMainWindow *mw = qobject_cast(d->mainWindow); mw->addViewAndNotifyLoadingCompleted(doc); } QMainWindow* KisViewManager::qtMainWindow() const { if (d->mainWindow) return d->mainWindow; //Fallback for when we have not yet set the main window. QMainWindow* w = qobject_cast(qApp->activeWindow()); if(w) return w; return mainWindow(); } void KisViewManager::setQtMainWindow(QMainWindow* newMainWindow) { d->mainWindow = newMainWindow; } - void KisViewManager::slotDocumentSaved() { d->saveIncremental->setEnabled(true); d->saveIncrementalBackup->setEnabled(true); } void KisViewManager::slotSaveIncremental() { if (!document()) return; bool foundVersion; bool fileAlreadyExists; bool isBackup; QString version = "000"; QString newVersion; QString letter; QString fileName = document()->localFilePath(); // Find current version filenames // v v Regexp to find incremental versions in the filename, taking our backup scheme into account as well // Considering our incremental version and backup scheme, format is filename_001~001.ext QRegExp regex("_\\d{1,4}[.]|_\\d{1,4}[a-z][.]|_\\d{1,4}[~]|_\\d{1,4}[a-z][~]"); regex.indexIn(fileName); // Perform the search QStringList matches = regex.capturedTexts(); foundVersion = matches.at(0).isEmpty() ? false : true; // Ensure compatibility with Save Incremental Backup // If this regex is not kept separate, the entire algorithm needs modification; // It's simpler to just add this. QRegExp regexAux("_\\d{1,4}[~]|_\\d{1,4}[a-z][~]"); regexAux.indexIn(fileName); // Perform the search QStringList matchesAux = regexAux.capturedTexts(); isBackup = matchesAux.at(0).isEmpty() ? false : true; // If the filename has a version, prepare it for incrementation if (foundVersion) { version = matches.at(matches.count() - 1); // Look at the last index, we don't care about other matches if (version.contains(QRegExp("[a-z]"))) { version.chop(1); // Trim "." letter = version.right(1); // Save letter version.chop(1); // Trim letter } else { version.chop(1); // Trim "." } version.remove(0, 1); // Trim "_" } else { // TODO: this will not work with files extensions like jp2 // ...else, simply add a version to it so the next loop works QRegExp regex2("[.][a-z]{2,4}$"); // Heuristic to find file extension regex2.indexIn(fileName); QStringList matches2 = regex2.capturedTexts(); QString extensionPlusVersion = matches2.at(0); extensionPlusVersion.prepend(version); extensionPlusVersion.prepend("_"); fileName.replace(regex2, extensionPlusVersion); } // Prepare the base for new version filename int intVersion = version.toInt(0); ++intVersion; QString baseNewVersion = QString::number(intVersion); while (baseNewVersion.length() < version.length()) { baseNewVersion.prepend("0"); } // Check if the file exists under the new name and search until options are exhausted (test appending a to z) do { newVersion = baseNewVersion; newVersion.prepend("_"); if (!letter.isNull()) newVersion.append(letter); if (isBackup) { newVersion.append("~"); } else { newVersion.append("."); } fileName.replace(regex, newVersion); fileAlreadyExists = QFile(fileName).exists(); if (fileAlreadyExists) { if (!letter.isNull()) { char letterCh = letter.at(0).toLatin1(); ++letterCh; letter = QString(QChar(letterCh)); } else { letter = 'a'; } } } while (fileAlreadyExists && letter != "{"); // x, y, z, {... if (letter == "{") { QMessageBox::critical(mainWindow(), i18nc("@title:window", "Couldn't save incremental version"), i18n("Alternative names exhausted, try manually saving with a higher number")); return; } document()->setFileBatchMode(true); document()->saveAs(QUrl::fromUserInput(fileName), document()->mimeType(), true); document()->setFileBatchMode(false); if (mainWindow()) { mainWindow()->updateCaption(); } } void KisViewManager::slotSaveIncrementalBackup() { if (!document()) return; bool workingOnBackup; bool fileAlreadyExists; QString version = "000"; QString newVersion; QString letter; QString fileName = document()->localFilePath(); // First, discover if working on a backup file, or a normal file QRegExp regex("~\\d{1,4}[.]|~\\d{1,4}[a-z][.]"); regex.indexIn(fileName); // Perform the search QStringList matches = regex.capturedTexts(); workingOnBackup = matches.at(0).isEmpty() ? false : true; if (workingOnBackup) { // Try to save incremental version (of backup), use letter for alt versions version = matches.at(matches.count() - 1); // Look at the last index, we don't care about other matches if (version.contains(QRegExp("[a-z]"))) { version.chop(1); // Trim "." letter = version.right(1); // Save letter version.chop(1); // Trim letter } else { version.chop(1); // Trim "." } version.remove(0, 1); // Trim "~" // Prepare the base for new version filename int intVersion = version.toInt(0); ++intVersion; QString baseNewVersion = QString::number(intVersion); QString backupFileName = document()->localFilePath(); while (baseNewVersion.length() < version.length()) { baseNewVersion.prepend("0"); } // Check if the file exists under the new name and search until options are exhausted (test appending a to z) do { newVersion = baseNewVersion; newVersion.prepend("~"); if (!letter.isNull()) newVersion.append(letter); newVersion.append("."); backupFileName.replace(regex, newVersion); fileAlreadyExists = QFile(backupFileName).exists(); if (fileAlreadyExists) { if (!letter.isNull()) { char letterCh = letter.at(0).toLatin1(); ++letterCh; letter = QString(QChar(letterCh)); } else { letter = 'a'; } } } while (fileAlreadyExists && letter != "{"); // x, y, z, {... if (letter == "{") { QMessageBox::critical(mainWindow(), i18nc("@title:window", "Couldn't save incremental backup"), i18n("Alternative names exhausted, try manually saving with a higher number")); return; } QFile::copy(fileName, backupFileName); document()->saveAs(QUrl::fromUserInput(fileName), document()->mimeType(), true); if (mainWindow()) mainWindow()->updateCaption(); } else { // if NOT working on a backup... // Navigate directory searching for latest backup version, ignore letters const quint8 HARDCODED_DIGIT_COUNT = 3; QString baseNewVersion = "000"; QString backupFileName = document()->localFilePath(); QRegExp regex2("[.][a-z]{2,4}$"); // Heuristic to find file extension regex2.indexIn(backupFileName); QStringList matches2 = regex2.capturedTexts(); QString extensionPlusVersion = matches2.at(0); extensionPlusVersion.prepend(baseNewVersion); extensionPlusVersion.prepend("~"); backupFileName.replace(regex2, extensionPlusVersion); // Save version with 1 number higher than the highest version found ignoring letters do { newVersion = baseNewVersion; newVersion.prepend("~"); newVersion.append("."); backupFileName.replace(regex, newVersion); fileAlreadyExists = QFile(backupFileName).exists(); if (fileAlreadyExists) { // Prepare the base for new version filename, increment by 1 int intVersion = baseNewVersion.toInt(0); ++intVersion; baseNewVersion = QString::number(intVersion); while (baseNewVersion.length() < HARDCODED_DIGIT_COUNT) { baseNewVersion.prepend("0"); } } } while (fileAlreadyExists); // Save both as backup and on current file for interapplication workflow document()->setFileBatchMode(true); QFile::copy(fileName, backupFileName); document()->saveAs(QUrl::fromUserInput(fileName), document()->mimeType(), true); document()->setFileBatchMode(false); if (mainWindow()) mainWindow()->updateCaption(); } } void KisViewManager::disableControls() { // prevents possible crashes, if somebody changes the paintop during dragging by using the mousewheel // this is for Bug 250944 // the solution blocks all wheel, mouse and key event, while dragging with the freehand tool // see KisToolFreehand::initPaint() and endPaint() d->controlFrame.paintopBox()->installEventFilter(&d->blockingEventFilter); Q_FOREACH (QObject* child, d->controlFrame.paintopBox()->children()) { child->installEventFilter(&d->blockingEventFilter); } } void KisViewManager::enableControls() { d->controlFrame.paintopBox()->removeEventFilter(&d->blockingEventFilter); Q_FOREACH (QObject* child, d->controlFrame.paintopBox()->children()) { child->removeEventFilter(&d->blockingEventFilter); } } void KisViewManager::showStatusBar(bool toggled) { KisMainWindow *mw = mainWindow(); if(mw && mw->statusBar()) { mw->statusBar()->setVisible(toggled); KisConfig cfg; cfg.setShowStatusBar(toggled); } } void KisViewManager::switchCanvasOnly(bool toggled) { KisConfig cfg; KisMainWindow* main = mainWindow(); if(!main) { dbgUI << "Unable to switch to canvas-only mode, main window not found"; return; } if (toggled) { d->canvasState = qtMainWindow()->saveState(); #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) d->windowFlags = main->windowState(); #endif } if (cfg.hideStatusbarFullscreen()) { if (main->statusBar()) { if (!toggled) { if (main->statusBar()->dynamicPropertyNames().contains("wasvisible")) { if (main->statusBar()->property("wasvisible").toBool()) { main->statusBar()->setVisible(true); } } } else { main->statusBar()->setProperty("wasvisible", main->statusBar()->isVisible()); main->statusBar()->setVisible(false); } } } if (cfg.hideDockersFullscreen()) { KisAction* action = qobject_cast(main->actionCollection()->action("view_toggledockers")); if (action) { action->setCheckable(true); if (toggled) { if (action->isChecked()) { cfg.setShowDockers(action->isChecked()); action->setChecked(false); } else { cfg.setShowDockers(false); } } else { action->setChecked(cfg.showDockers()); } } } // QT in windows does not return to maximized upon 4th tab in a row // https://bugreports.qt.io/browse/QTBUG-57882, https://bugreports.qt.io/browse/QTBUG-52555, https://codereview.qt-project.org/#/c/185016/ if (cfg.hideTitlebarFullscreen() && !cfg.fullscreenMode()) { if(toggled) { main->setWindowState( main->windowState() | Qt::WindowFullScreen); } else { main->setWindowState( main->windowState() & ~Qt::WindowFullScreen); #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0) // If window was maximized prior to fullscreen, restore that if (d->windowFlags & Qt::WindowMaximized) { main->setWindowState( main->windowState() | Qt::WindowMaximized); } #endif } } if (cfg.hideMenuFullscreen()) { if (!toggled) { if (main->menuBar()->dynamicPropertyNames().contains("wasvisible")) { if (main->menuBar()->property("wasvisible").toBool()) { main->menuBar()->setVisible(true); } } } else { main->menuBar()->setProperty("wasvisible", main->menuBar()->isVisible()); main->menuBar()->setVisible(false); } } if (cfg.hideToolbarFullscreen()) { QList toolBars = main->findChildren(); Q_FOREACH (QToolBar* toolbar, toolBars) { if (!toggled) { if (toolbar->dynamicPropertyNames().contains("wasvisible")) { if (toolbar->property("wasvisible").toBool()) { toolbar->setVisible(true); } } } else { toolbar->setProperty("wasvisible", toolbar->isVisible()); toolbar->setVisible(false); } } } showHideScrollbars(); if (toggled) { // show a fading heads-up display about the shortcut to go back showFloatingMessage(i18n("Going into Canvas-Only mode.\nPress %1 to go back.", actionCollection()->action("view_show_canvas_only")->shortcut().toString()), QIcon()); } else { main->restoreState(d->canvasState); } } void KisViewManager::toggleTabletLogger() { d->inputManager.toggleTabletLogger(); } void KisViewManager::openResourcesDirectory() { QString dir = KoResourcePaths::locateLocal("data", ""); QDesktopServices::openUrl(QUrl::fromLocalFile(dir)); } void KisViewManager::updateIcons() { if (mainWindow()) { QList dockers = mainWindow()->dockWidgets(); Q_FOREACH (QDockWidget* dock, dockers) { dbgKrita << "name " << dock->objectName(); KoDockWidgetTitleBar* titlebar = dynamic_cast(dock->titleBarWidget()); if (titlebar) { titlebar->updateIcons(); } QObjectList objects; objects.append(dock); while (!objects.isEmpty()) { QObject* object = objects.takeFirst(); objects.append(object->children()); KisIconUtils::updateIconCommon(object); } } } } void KisViewManager::initializeStatusBarVisibility() { KisConfig cfg; d->mainWindow->statusBar()->setVisible(cfg.showStatusBar()); } void KisViewManager::guiUpdateTimeout() { d->nodeManager.updateGUI(); d->selectionManager.updateGUI(); d->filterManager.updateGUI(); if (zoomManager()) { zoomManager()->updateGUI(); } d->gridManager.updateGUI(); d->actionManager.updateGUI(); } void KisViewManager::showFloatingMessage(const QString &message, const QIcon& icon, int timeout, KisFloatingMessage::Priority priority, int alignment) { if (!d->currentImageView) return; d->currentImageView->showFloatingMessageImpl(message, icon, timeout, priority, alignment); emit floatingMessageRequested(message, icon.name()); } KisMainWindow *KisViewManager::mainWindow() const { return qobject_cast(d->mainWindow); } void KisViewManager::showHideScrollbars() { if (!d->currentImageView) return; if (!d->currentImageView->canvasController()) return; KisConfig cfg; bool toggled = actionCollection()->action("view_show_canvas_only")->isChecked(); if ( (toggled && cfg.hideScrollbarsFullscreen()) || (!toggled && cfg.hideScrollbars()) ) { d->currentImageView->canvasController()->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); d->currentImageView->canvasController()->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } else { d->currentImageView->canvasController()->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); d->currentImageView->canvasController()->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOn); } } void KisViewManager::slotSaveShowRulersState(bool value) { KisConfig cfg; cfg.setShowRulers(value); } void KisViewManager::slotSaveRulersTrackMouseState(bool value) { KisConfig cfg; cfg.setRulersTrackMouse(value); } void KisViewManager::setShowFloatingMessage(bool show) { d->showFloatingMessage = show; } void KisViewManager::changeAuthorProfile(const QString &profileName) { KConfigGroup appAuthorGroup(KoGlobal::calligraConfig(), "Author"); if (profileName.isEmpty()) { appAuthorGroup.writeEntry("active-profile", ""); } else if (profileName == i18nc("choice for author profile", "Anonymous")) { appAuthorGroup.writeEntry("active-profile", "anonymous"); } else { appAuthorGroup.writeEntry("active-profile", profileName); } appAuthorGroup.sync(); Q_FOREACH (KisDocument *doc, KisPart::instance()->documents()) { doc->documentInfo()->updateParameters(); } } void KisViewManager::slotUpdateAuthorProfileActions() { Q_ASSERT(d->actionAuthor); if (!d->actionAuthor) { return; } d->actionAuthor->clear(); d->actionAuthor->addAction(i18n("Default Author Profile")); d->actionAuthor->addAction(i18nc("choice for author profile", "Anonymous")); KConfigGroup authorGroup(KoGlobal::calligraConfig(), "Author"); QStringList profiles = authorGroup.readEntry("profile-names", QStringList()); Q_FOREACH (const QString &profile , profiles) { d->actionAuthor->addAction(profile); } KConfigGroup appAuthorGroup(KoGlobal::calligraConfig(), "Author"); QString profileName = appAuthorGroup.readEntry("active-profile", ""); if (profileName == "anonymous") { d->actionAuthor->setCurrentItem(1); } else if (profiles.contains(profileName)) { d->actionAuthor->setCurrentAction(profileName); } else { d->actionAuthor->setCurrentItem(0); } } diff --git a/libs/ui/KisViewManager.h b/libs/ui/KisViewManager.h index 9da2f795e6..20b46b512e 100644 --- a/libs/ui/KisViewManager.h +++ b/libs/ui/KisViewManager.h @@ -1,270 +1,272 @@ /* * Copyright (c) 2006 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_GUI_CLIENT_H #define KIS_GUI_CLIENT_H #include #include #include #include #include #include #include #include "kis_floating_message.h" class QPoint; class KisView; class KisCanvas2; class KisCanvasResourceProvider; class KisDocument; class KisFilterManager; class KisGridManager; class KisGuidesManager; class KisImageManager; class KisNodeManager; class KisPaintingAssistantsManager; class KisPaintopBox; class KisSelectionManager; class KisStatusBar; class KisUndoAdapter; class KisZoomManager; class KisPaintopBox; class KisActionManager; class KisScriptManager; class KisInputManager; class KoUpdater; class KoProgressUpdater; /** * KisViewManager manages the collection of views shown in a single mainwindow. */ class KRITAUI_EXPORT KisViewManager : public QObject { Q_OBJECT public: /** * Construct a new view on the krita document. * @param document the document we show. * @param parent a parent widget we show ourselves in. */ KisViewManager(QWidget *parent, KActionCollection *actionCollection); ~KisViewManager() override; /** * Retrieves the entire action collection. */ virtual KActionCollection* actionCollection() const; public: // Krita specific interfaces void setCurrentView(KisView *view); /// Return the image this view is displaying KisImageWSP image() const; KoZoomController *zoomController() const; /// The resource provider contains all per-view settings, such as /// current color, current paint op etc. KisCanvasResourceProvider *resourceProvider(); /// Return the canvasbase class KisCanvas2 *canvasBase() const; /// Return the actual widget that is displaying the current image QWidget* canvas() const; /// Return the wrapper class around the statusbar KisStatusBar *statusBar() const; /** * This adds a widget to the statusbar for this view. * If you use this method instead of using statusBar() directly, * KisView will take care of removing the items when the view GUI is deactivated * and readding them when it is reactivated. * The parameters are the same as QStatusBar::addWidget(). */ void addStatusBarItem(QWidget * widget, int stretch = 0, bool permanent = false); /** * Remove a widget from the statusbar for this view. */ void removeStatusBarItem(QWidget * widget); KisPaintopBox* paintOpBox() const; /// create a new progress updater QPointer createUnthreadedUpdater(const QString &name); QPointer createThreadedUpdater(const QString &name); /// The selection manager handles everything action related to /// selections. KisSelectionManager *selectionManager(); /// The node manager handles everything about nodes KisNodeManager *nodeManager() const; KisActionManager *actionManager() const; /** * Convenience method to get at the active node, which may be * a layer or a mask or a selection */ KisNodeSP activeNode(); /// Convenience method to get at the active layer KisLayerSP activeLayer(); /// Convenience method to get at the active paint device KisPaintDeviceSP activeDevice(); /// The filtermanager handles everything action-related to filters KisFilterManager *filterManager(); /// The image manager handles everything action-related to the /// current image KisImageManager *imageManager(); /// Filters events and sends them to canvas actions KisInputManager *inputManager() const; /// Convenience method to get at the active selection (the /// selection of the current layer, or, if that does not exist, /// the global selection. KisSelectionSP selection(); /// Checks if the current global or local selection is editable bool selectionEditable(); /// The undo adapter is used to add commands to the undo stack KisUndoAdapter *undoAdapter(); KisDocument *document() const; KisScriptManager *scriptManager() const; int viewCount() const; /** * @brief blockUntilOperationsFinished blocks the GUI of the application until execution * of actions on \p image is finished * @param image the image which we should wait for * @return true if the image has finished execution of the actions, false if * the user cancelled operation */ bool blockUntilOperationsFinished(KisImageSP image); /** * @brief blockUntilOperationsFinished blocks the GUI of the application until execution * of actions on \p image is finished. Does *not* provide a "Cancel" button. So the * user is forced to wait. * @param image the image which we should wait for */ void blockUntilOperationsFinishedForced(KisImageSP image); public: KisGridManager * gridManager() const; KisGuidesManager * guidesManager() const; /// disable and enable toolbar controls. used for disabling them during painting. void enableControls(); void disableControls(); /// shows a floating message in the top right corner of the canvas void showFloatingMessage(const QString &message, const QIcon& icon, int timeout = 4500, KisFloatingMessage::Priority priority = KisFloatingMessage::Medium, int alignment = Qt::AlignCenter | Qt::TextWordWrap); /// @return the KoMaindow this view is in, or 0 KisMainWindow *mainWindow() const; /// The QMainWindow associated with this view. This is most likely going to be shell(), but /// when running as Gemini or Sketch, this will be set to the applications' own QMainWindow. /// This can be checked by qobject_casting to KisMainWindow to check the difference. QMainWindow* qtMainWindow() const; /// The mainWindow function will return the shell() value, unless this function is called /// with a non-null value. To make it return shell() again, simply pass null to this function. void setQtMainWindow(QMainWindow* newMainWindow); + static void initializeResourceManager(KoCanvasResourceManager *resourceManager); + public Q_SLOTS: void switchCanvasOnly(bool toggled); void setShowFloatingMessage(bool show); void showHideScrollbars(); /// Visit all managers to update gui elements, e.g. enable / disable actions. /// This is heavy-duty call, so it uses a compressor. void updateGUI(); /// Update the style of all the icons void updateIcons(); void slotViewAdded(KisView *view); void slotViewRemoved(KisView *view); Q_SIGNALS: void floatingMessageRequested(const QString &message, const QString &iconName); /** * @brief viewChanged * sent out when the view has changed. */ void viewChanged(); private Q_SLOTS: void slotBlacklistCleanup(); void slotCreateTemplate(); void slotCreateCopy(); void slotDocumentSaved(); void slotSaveIncremental(); void slotSaveIncrementalBackup(); void showStatusBar(bool toggled); void toggleTabletLogger(); void openResourcesDirectory(); void initializeStatusBarVisibility(); void guiUpdateTimeout(); void changeAuthorProfile(const QString &profileName); void slotUpdateAuthorProfileActions(); void slotSaveShowRulersState(bool value); void slotSaveRulersTrackMouseState(bool value); private: void createActions(); void setupManagers(); /// The zoommanager handles everything action-related to zooming KisZoomManager * zoomManager(); private: class KisViewManagerPrivate; KisViewManagerPrivate * const d; }; #endif diff --git a/libs/ui/canvas/kis_abstract_canvas_widget.h b/libs/ui/canvas/kis_abstract_canvas_widget.h index 15ef84ed3b..46e2cb4401 100644 --- a/libs/ui/canvas/kis_abstract_canvas_widget.h +++ b/libs/ui/canvas/kis_abstract_canvas_widget.h @@ -1,91 +1,92 @@ /* * Copyright (C) 2007 Boudewijn Rempt , (C) * Copyright (C) 2015 Michael Abrahams * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_ABSTRACT_CANVAS_WIDGET_ #define _KIS_ABSTRACT_CANVAS_WIDGET_ class QWidget; class QRect; class QPainter; class QRect; class KoToolProxy; #include class KisDisplayFilter; class KisDisplayColorConverter; class QBitArray; #include "kis_types.h" #include "kis_ui_types.h" class KisAbstractCanvasWidget { public: KisAbstractCanvasWidget() {} virtual ~KisAbstractCanvasWidget() {} virtual QWidget * widget() = 0; virtual KoToolProxy * toolProxy() const = 0; /// Draw the specified decorations on the view. virtual void drawDecorations(QPainter & gc, const QRect &updateWidgetRect) const = 0; virtual void addDecoration(KisCanvasDecorationSP deco) = 0; + virtual void removeDecoration(const QString& id) = 0; virtual KisCanvasDecorationSP decoration(const QString& id) const = 0; virtual void setDecorations(const QList &) = 0; virtual QList decorations() const = 0; /// set the specified display filter on the canvas virtual void setDisplayFilter(QSharedPointer displayFilter) = 0; virtual void setWrapAroundViewingMode(bool value) = 0; // Called from KisCanvas2::channelSelectionChanged virtual void channelSelectionChanged(const QBitArray &channelFlags) = 0; // Called from KisCanvas2::slotSetDisplayProfile virtual void setDisplayProfile(KisDisplayColorConverter *colorConverter) = 0; // Called from KisCanvas2::finishResizingImage virtual void finishResizingImage(qint32 w, qint32 h) = 0; // Called from KisCanvas2::startUpdateProjection virtual KisUpdateInfoSP startUpdateCanvasProjection(const QRect & rc, const QBitArray &channelFlags) = 0; // Called from KisCanvas2::updateCanvasProjection virtual QRect updateCanvasProjection(KisUpdateInfoSP info) = 0; /** * Returns true if the asynchromous engine of the canvas * (e.g. openGL pipeline) is busy with processing of the previous * update events. This will make KisCanvas2 to postpone and * compress update events. */ virtual bool isBusy() const = 0; }; #endif // _KIS_ABSTRACT_CANVAS_WIDGET_ diff --git a/libs/ui/canvas/kis_animation_player.cpp b/libs/ui/canvas/kis_animation_player.cpp index ec03f40900..e23c59450f 100644 --- a/libs/ui/canvas/kis_animation_player.cpp +++ b/libs/ui/canvas/kis_animation_player.cpp @@ -1,552 +1,544 @@ /* * Copyright (c) 2015 Jouni Pentikäinen * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_animation_player.h" #include #include #include //#define PLAYER_DEBUG_FRAMERATE #include "kis_global.h" #include "kis_config.h" #include "kis_config_notifier.h" #include "kis_image.h" #include "kis_canvas2.h" #include "kis_animation_frame_cache.h" #include "kis_signal_auto_connection.h" #include "kis_image_animation_interface.h" #include "kis_time_range.h" #include "kis_signal_compressor.h" #include #include - -#include -#include -#include - #include "KisSyncedAudioPlayback.h" #include "kis_signal_compressor_with_param.h" #include "KisViewManager.h" #include "kis_icon_utils.h" #include "KisPart.h" #include "dialogs/KisAsyncAnimationCacheRenderDialog.h" - - -using namespace boost::accumulators; -typedef accumulator_set > FpsAccumulator; +#include "KisRollingMeanAccumulatorWrapper.h" struct KisAnimationPlayer::Private { public: Private(KisAnimationPlayer *_q) : q(_q), - realFpsAccumulator(tag::rolling_window::window_size = 24), - droppedFpsAccumulator(tag::rolling_window::window_size = 24), - droppedFramesPortion(tag::rolling_window::window_size = 24), + realFpsAccumulator(24), + droppedFpsAccumulator(24), + droppedFramesPortion(24), dropFramesMode(true), nextFrameExpectedTime(0), expectedInterval(0), expectedFrame(0), lastTimerInterval(0), lastPaintedFrame(0), playbackStatisticsCompressor(1000, KisSignalCompressor::FIRST_INACTIVE), stopAudioOnScrubbingCompressor(100, KisSignalCompressor::POSTPONE), audioOffsetTolerance(-1) {} KisAnimationPlayer *q; bool useFastFrameUpload; bool playing; QTimer *timer; int initialFrame; int firstFrame; int lastFrame; qreal playbackSpeed; KisCanvas2 *canvas; KisSignalAutoConnectionsStore cancelStrokeConnections; QElapsedTimer realFpsTimer; - FpsAccumulator realFpsAccumulator; - FpsAccumulator droppedFpsAccumulator; - FpsAccumulator droppedFramesPortion; + KisRollingMeanAccumulatorWrapper realFpsAccumulator; + KisRollingMeanAccumulatorWrapper droppedFpsAccumulator; + KisRollingMeanAccumulatorWrapper droppedFramesPortion; bool dropFramesMode; QElapsedTimer playbackTime; int nextFrameExpectedTime; int expectedInterval; int expectedFrame; int lastTimerInterval; int lastPaintedFrame; KisSignalCompressor playbackStatisticsCompressor; QScopedPointer syncedAudio; QScopedPointer > audioSyncScrubbingCompressor; KisSignalCompressor stopAudioOnScrubbingCompressor; int audioOffsetTolerance; void stopImpl(bool doUpdates); int incFrame(int frame, int inc) { frame += inc; if (frame > lastFrame) { frame = firstFrame + frame - lastFrame - 1; } return frame; } qint64 frameToMSec(int value, int fps) { return qreal(value) / fps * 1000.0; } int msecToFrame(qint64 value, int fps) { return qreal(value) * fps / 1000.0; } }; KisAnimationPlayer::KisAnimationPlayer(KisCanvas2 *canvas) : QObject(canvas) , m_d(new Private(this)) { m_d->useFastFrameUpload = false; m_d->playing = false; m_d->canvas = canvas; m_d->playbackSpeed = 1.0; m_d->timer = new QTimer(this); connect(m_d->timer, SIGNAL(timeout()), this, SLOT(slotUpdate())); m_d->timer->setSingleShot(true); connect(KisConfigNotifier::instance(), SIGNAL(dropFramesModeChanged()), SLOT(slotUpdateDropFramesMode())); slotUpdateDropFramesMode(); connect(&m_d->playbackStatisticsCompressor, SIGNAL(timeout()), this, SIGNAL(sigPlaybackStatisticsUpdated())); using namespace std::placeholders; std::function callback( std::bind(&KisAnimationPlayer::slotSyncScrubbingAudio, this, _1)); const int defaultScrubbingUdpatesDelay = 40; /* 40 ms == 25 fps */ m_d->audioSyncScrubbingCompressor.reset( new KisSignalCompressorWithParam(defaultScrubbingUdpatesDelay, callback, KisSignalCompressor::FIRST_ACTIVE)); m_d->stopAudioOnScrubbingCompressor.setDelay(defaultScrubbingUdpatesDelay); connect(&m_d->stopAudioOnScrubbingCompressor, SIGNAL(timeout()), SLOT(slotTryStopScrubbingAudio())); connect(m_d->canvas->image()->animationInterface(), SIGNAL(sigFramerateChanged()), SLOT(slotUpdateAudioChunkLength())); slotUpdateAudioChunkLength(); connect(m_d->canvas->image()->animationInterface(), SIGNAL(sigAudioChannelChanged()), SLOT(slotAudioChannelChanged())); connect(m_d->canvas->image()->animationInterface(), SIGNAL(sigAudioVolumeChanged()), SLOT(slotAudioVolumeChanged())); slotAudioChannelChanged(); } KisAnimationPlayer::~KisAnimationPlayer() {} void KisAnimationPlayer::slotUpdateDropFramesMode() { KisConfig cfg; m_d->dropFramesMode = cfg.animationDropFrames(); } void KisAnimationPlayer::slotSyncScrubbingAudio(int msecTime) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->syncedAudio); if (!m_d->syncedAudio->isPlaying()) { m_d->syncedAudio->play(msecTime); } else { m_d->syncedAudio->syncWithVideo(msecTime); } if (!isPlaying()) { m_d->stopAudioOnScrubbingCompressor.start(); } } void KisAnimationPlayer::slotTryStopScrubbingAudio() { KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->syncedAudio); if (m_d->syncedAudio && !isPlaying()) { m_d->syncedAudio->stop(); } } void KisAnimationPlayer::slotAudioChannelChanged() { KisImageAnimationInterface *interface = m_d->canvas->image()->animationInterface(); QString fileName = interface->audioChannelFileName(); QFileInfo info(fileName); if (info.exists() && !interface->isAudioMuted()) { m_d->syncedAudio.reset(new KisSyncedAudioPlayback(info.absoluteFilePath())); m_d->syncedAudio->setVolume(interface->audioVolume()); m_d->syncedAudio->setSoundOffsetTolerance(m_d->audioOffsetTolerance); connect(m_d->syncedAudio.data(), SIGNAL(error(const QString &, const QString &)), SLOT(slotOnAudioError(const QString &, const QString &))); } else { m_d->syncedAudio.reset(); } } void KisAnimationPlayer::slotAudioVolumeChanged() { KisImageAnimationInterface *interface = m_d->canvas->image()->animationInterface(); if (m_d->syncedAudio) { m_d->syncedAudio->setVolume(interface->audioVolume()); } } void KisAnimationPlayer::slotOnAudioError(const QString &fileName, const QString &message) { QString errorMessage(i18nc("floating on-canvas message", "Cannot open audio: \"%1\"\nError: %2", fileName, message)); m_d->canvas->viewManager()->showFloatingMessage(errorMessage, KisIconUtils::loadIcon("warning")); } void KisAnimationPlayer::connectCancelSignals() { m_d->cancelStrokeConnections.addConnection( m_d->canvas->image().data(), SIGNAL(sigUndoDuringStrokeRequested()), this, SLOT(slotCancelPlayback())); m_d->cancelStrokeConnections.addConnection( m_d->canvas->image().data(), SIGNAL(sigStrokeCancellationRequested()), this, SLOT(slotCancelPlayback())); m_d->cancelStrokeConnections.addConnection( m_d->canvas->image().data(), SIGNAL(sigStrokeEndRequested()), this, SLOT(slotCancelPlaybackSafe())); m_d->cancelStrokeConnections.addConnection( m_d->canvas->image()->animationInterface(), SIGNAL(sigFramerateChanged()), this, SLOT(slotUpdatePlaybackTimer())); m_d->cancelStrokeConnections.addConnection( m_d->canvas->image()->animationInterface(), SIGNAL(sigFullClipRangeChanged()), this, SLOT(slotUpdatePlaybackTimer())); m_d->cancelStrokeConnections.addConnection( m_d->canvas->image()->animationInterface(), SIGNAL(sigPlaybackRangeChanged()), this, SLOT(slotUpdatePlaybackTimer())); } void KisAnimationPlayer::disconnectCancelSignals() { m_d->cancelStrokeConnections.clear(); } void KisAnimationPlayer::slotUpdateAudioChunkLength() { const KisImageAnimationInterface *animation = m_d->canvas->image()->animationInterface(); const int animationFramePeriod = qMax(1, 1000 / animation->framerate()); KisConfig cfg; int scrubbingAudioUdpatesDelay = cfg.scrubbingAudioUpdatesDelay(); if (scrubbingAudioUdpatesDelay < 0) { scrubbingAudioUdpatesDelay = qMax(1, animationFramePeriod); } m_d->audioSyncScrubbingCompressor->setDelay(scrubbingAudioUdpatesDelay); m_d->stopAudioOnScrubbingCompressor.setDelay(scrubbingAudioUdpatesDelay); m_d->audioOffsetTolerance = cfg.audioOffsetTolerance(); if (m_d->audioOffsetTolerance < 0) { m_d->audioOffsetTolerance = animationFramePeriod; } if (m_d->syncedAudio) { m_d->syncedAudio->setSoundOffsetTolerance(m_d->audioOffsetTolerance); } } void KisAnimationPlayer::slotUpdatePlaybackTimer() { m_d->timer->stop(); const KisImageAnimationInterface *animation = m_d->canvas->image()->animationInterface(); const KisTimeRange &range = animation->playbackRange(); if (!range.isValid()) return; const int fps = animation->framerate(); m_d->initialFrame = animation->currentUITime(); m_d->firstFrame = range.start(); m_d->lastFrame = range.end(); m_d->expectedFrame = qBound(m_d->firstFrame, m_d->expectedFrame, m_d->lastFrame); m_d->expectedInterval = qreal(1000) / fps / m_d->playbackSpeed; m_d->lastTimerInterval = m_d->expectedInterval; if (m_d->syncedAudio) { m_d->syncedAudio->setSpeed(m_d->playbackSpeed); } m_d->timer->start(m_d->expectedInterval); if (m_d->playbackTime.isValid()) { m_d->playbackTime.restart(); } else { m_d->playbackTime.start(); } m_d->nextFrameExpectedTime = m_d->playbackTime.elapsed() + m_d->expectedInterval; } void KisAnimationPlayer::play() { { const KisImageAnimationInterface *animation = m_d->canvas->image()->animationInterface(); const KisTimeRange &range = animation->playbackRange(); if (!range.isValid()) return; // when openGL is disabled, there is no animation cache if (m_d->canvas->frameCache()) { KisAsyncAnimationCacheRenderDialog dlg(m_d->canvas->frameCache(), range, 200); KisAsyncAnimationCacheRenderDialog::Result result = dlg.regenerateRange(m_d->canvas->viewManager()); if (result != KisAsyncAnimationCacheRenderDialog::RenderComplete) { return; } } } m_d->playing = true; slotUpdatePlaybackTimer(); m_d->expectedFrame = m_d->firstFrame; m_d->lastPaintedFrame = m_d->firstFrame; connectCancelSignals(); if (m_d->syncedAudio) { KisImageAnimationInterface *animationInterface = m_d->canvas->image()->animationInterface(); m_d->syncedAudio->play(m_d->frameToMSec(m_d->firstFrame, animationInterface->framerate())); } } void KisAnimationPlayer::Private::stopImpl(bool doUpdates) { if (syncedAudio) { syncedAudio->stop(); } q->disconnectCancelSignals(); timer->stop(); playing = false; if (doUpdates) { KisImageAnimationInterface *animation = canvas->image()->animationInterface(); if (animation->currentUITime() == initialFrame) { canvas->refetchDataFromImage(); } else { animation->switchCurrentTimeAsync(initialFrame); } } emit q->sigPlaybackStopped(); } void KisAnimationPlayer::stop() { m_d->stopImpl(true); } void KisAnimationPlayer::forcedStopOnExit() { m_d->stopImpl(false); } bool KisAnimationPlayer::isPlaying() { return m_d->playing; } int KisAnimationPlayer::currentTime() { return m_d->lastPaintedFrame; } void KisAnimationPlayer::displayFrame(int time) { uploadFrame(time); } void KisAnimationPlayer::slotUpdate() { uploadFrame(-1); } void KisAnimationPlayer::uploadFrame(int frame) { KisImageAnimationInterface *animationInterface = m_d->canvas->image()->animationInterface(); if (frame < 0) { const int currentTime = m_d->playbackTime.elapsed(); const int framesDiff = currentTime - m_d->nextFrameExpectedTime; const qreal framesDiffNorm = qreal(framesDiff) / m_d->expectedInterval; // qDebug() << ppVar(framesDiff) // << ppVar(m_d->expectedFrame) // << ppVar(framesDiffNorm) // << ppVar(m_d->lastTimerInterval); if (m_d->dropFramesMode) { const int numDroppedFrames = qMax(0, qRound(framesDiffNorm)); frame = m_d->incFrame(m_d->expectedFrame, numDroppedFrames); } else { frame = m_d->expectedFrame; } m_d->nextFrameExpectedTime = currentTime + m_d->expectedInterval; m_d->lastTimerInterval = qMax(0.0, m_d->lastTimerInterval - 0.5 * framesDiff); m_d->expectedFrame = m_d->incFrame(frame, 1); m_d->timer->start(m_d->lastTimerInterval); m_d->playbackStatisticsCompressor.start(); } if (m_d->syncedAudio) { const int msecTime = m_d->frameToMSec(frame, animationInterface->framerate()); if (isPlaying()) { slotSyncScrubbingAudio(msecTime); } else { m_d->audioSyncScrubbingCompressor->start(msecTime); } } if (m_d->canvas->frameCache() && m_d->canvas->frameCache()->uploadFrame(frame)) { m_d->canvas->updateCanvas(); m_d->useFastFrameUpload = true; emit sigFrameChanged(); } else { m_d->useFastFrameUpload = false; m_d->canvas->image()->barrierLock(true); m_d->canvas->image()->unlock(); // no OpenGL cache or the frame just not cached yet animationInterface->switchCurrentTimeAsync(frame); emit sigFrameChanged(); } if (!m_d->realFpsTimer.isValid()) { m_d->realFpsTimer.start(); } else { const int elapsed = m_d->realFpsTimer.restart(); m_d->realFpsAccumulator(elapsed); int numFrames = frame - m_d->lastPaintedFrame; if (numFrames < 0) { numFrames += m_d->lastFrame - m_d->firstFrame + 1; } m_d->droppedFramesPortion(qreal(int(numFrames != 1))); if (numFrames > 0) { m_d->droppedFpsAccumulator(qreal(elapsed) / numFrames); } #ifdef PLAYER_DEBUG_FRAMERATE - qDebug() << " RFPS:" << 1000.0 / rolling_mean(m_d->realFpsAccumulator) - << "DFPS:" << 1000.0 / rolling_mean(m_d->droppedFpsAccumulator) << ppVar(numFrames); + qDebug() << " RFPS:" << 1000.0 / m_d->realFpsAccumulator.rollingMean() + << "DFPS:" << 1000.0 / m_d->droppedFpsAccumulator.rollingMean() << ppVar(numFrames); #endif /* PLAYER_DEBUG_FRAMERATE */ } m_d->lastPaintedFrame = frame; } qreal KisAnimationPlayer::effectiveFps() const { - return 1000.0 / rolling_mean(m_d->droppedFpsAccumulator); + return 1000.0 / m_d->droppedFpsAccumulator.rollingMean(); } qreal KisAnimationPlayer::realFps() const { - return 1000.0 / rolling_mean(m_d->realFpsAccumulator); + return 1000.0 / m_d->realFpsAccumulator.rollingMean(); } qreal KisAnimationPlayer::framesDroppedPortion() const { - return rolling_mean(m_d->droppedFramesPortion); + return m_d->droppedFramesPortion.rollingMean(); } void KisAnimationPlayer::slotCancelPlayback() { stop(); } void KisAnimationPlayer::slotCancelPlaybackSafe() { /** * If there is no openGL support, then we have no (!) cache at * all. Therefore we should regenerate frame on every time switch, * which, yeah, can be very slow. What is more important, when * regenerating a frame animation interface will emit a * sigStrokeEndRequested() signal and we should ignore it. That is * not an ideal solution, because the user will be able to paint * on random frames while playing, but it lets users with faulty * GPUs see at least some preview of their animation. */ if (m_d->useFastFrameUpload) { stop(); } } qreal KisAnimationPlayer::playbackSpeed() { return m_d->playbackSpeed; } void KisAnimationPlayer::slotUpdatePlaybackSpeed(double value) { m_d->playbackSpeed = value; if (m_d->playing) { slotUpdatePlaybackTimer(); } } diff --git a/libs/ui/canvas/kis_canvas2.cpp b/libs/ui/canvas/kis_canvas2.cpp index 89f81ef581..eb766f0fe9 100644 --- a/libs/ui/canvas/kis_canvas2.cpp +++ b/libs/ui/canvas/kis_canvas2.cpp @@ -1,1016 +1,1037 @@ /* This file is part of the KDE project * * Copyright (C) 2006, 2010 Boudewijn Rempt * Copyright (C) Lukáš Tvrdý , (C) 2010 * Copyright (C) 2011 Silvio Heinrich * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.. */ #include "kis_canvas2.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_tool_proxy.h" #include "kis_coordinates_converter.h" #include "kis_prescaled_projection.h" #include "kis_image.h" #include "kis_image_barrier_locker.h" #include "kis_undo_adapter.h" #include "KisDocument.h" #include "flake/kis_shape_layer.h" #include "kis_canvas_resource_provider.h" #include "KisViewManager.h" #include "kis_config.h" #include "kis_config_notifier.h" #include "kis_abstract_canvas_widget.h" #include "kis_qpainter_canvas.h" #include "kis_group_layer.h" #include "flake/kis_shape_controller.h" #include "kis_node_manager.h" #include "kis_selection.h" #include "kis_selection_component.h" #include "flake/kis_shape_selection.h" #include "kis_image_config.h" #include "kis_infinity_manager.h" #include "kis_signal_compressor.h" #include "kis_display_color_converter.h" #include "kis_exposure_gamma_correction_interface.h" #include "KisView.h" #include "kis_canvas_controller.h" #include "kis_grid_config.h" #include "kis_animation_player.h" #include "kis_animation_frame_cache.h" #include "opengl/kis_opengl_canvas2.h" #include "opengl/kis_opengl.h" #include "kis_fps_decoration.h" #include "KoColorConversionTransformation.h" #include "KisProofingConfiguration.h" #include #include #include "input/kis_input_manager.h" #include "kis_painting_assistants_decoration.h" #include "kis_canvas_updates_compressor.h" #include "KoZoomController.h" +#include +#include "opengl/kis_opengl_canvas_debugger.h" class Q_DECL_HIDDEN KisCanvas2::KisCanvas2Private { public: KisCanvas2Private(KoCanvasBase *parent, KisCoordinatesConverter* coordConverter, QPointer view, KoCanvasResourceManager* resourceManager) : coordinatesConverter(coordConverter) , view(view) , shapeManager(parent) , selectedShapesProxy(&shapeManager) , toolProxy(parent) , displayColorConverter(resourceManager, view) { } KisCoordinatesConverter *coordinatesConverter; QPointerview; KisAbstractCanvasWidget *canvasWidget = 0; KoShapeManager shapeManager; KisSelectedShapesProxy selectedShapesProxy; bool currentCanvasIsOpenGL; int openGLFilterMode; KisToolProxy toolProxy; KisPrescaledProjectionSP prescaledProjection; bool vastScrolling; KisSignalCompressor updateSignalCompressor; QRect savedUpdateRect; QBitArray channelFlags; KisProofingConfigurationSP proofingConfig; bool softProofing = false; bool gamutCheck = false; bool proofingConfigUpdated = false; KisPopupPalette *popupPalette = 0; KisDisplayColorConverter displayColorConverter; KisCanvasUpdatesCompressor projectionUpdatesCompressor; KisAnimationPlayer *animationPlayer; KisAnimationFrameCacheSP frameCache; bool lodAllowedInCanvas; bool bootstrapLodBlocked; QPointer currentlyActiveShapeManager; bool effectiveLodAllowedInCanvas() { return lodAllowedInCanvas && !bootstrapLodBlocked; } }; namespace { KoShapeManager* fetchShapeManagerFromNode(KisNodeSP node) { KoShapeManager *shapeManager = 0; KisLayer *layer = dynamic_cast(node.data()); if (layer) { KisShapeLayer *shapeLayer = dynamic_cast(layer); if (shapeLayer) { shapeManager = shapeLayer->shapeManager(); } else { KisSelectionSP selection = layer->selection(); if (selection && selection->hasShapeSelection()) { KisShapeSelection *shapeSelection = dynamic_cast(selection->shapeSelection()); KIS_ASSERT_RECOVER_RETURN_VALUE(shapeSelection, 0); shapeManager = shapeSelection->shapeManager(); } } } return shapeManager; } } KisCanvas2::KisCanvas2(KisCoordinatesConverter *coordConverter, KoCanvasResourceManager *resourceManager, KisView *view, KoShapeBasedDocumentBase *sc) : KoCanvasBase(sc, resourceManager) , m_d(new KisCanvas2Private(this, coordConverter, view, resourceManager)) { /** * While loading LoD should be blocked. Only when GUI has finished * loading and zoom level settled down, LoD is given a green * light. */ m_d->bootstrapLodBlocked = true; connect(view->mainWindow(), SIGNAL(guiLoadingFinished()), SLOT(bootstrapFinished())); KisImageConfig config; m_d->updateSignalCompressor.setDelay(1000 / config.fpsLimit()); m_d->updateSignalCompressor.setMode(KisSignalCompressor::FIRST_ACTIVE); } void KisCanvas2::setup() { // a bit of duplication from slotConfigChanged() KisConfig cfg; m_d->vastScrolling = cfg.vastScrolling(); m_d->lodAllowedInCanvas = cfg.levelOfDetailEnabled(); createCanvas(cfg.useOpenGL()); setLodAllowedInCanvas(m_d->lodAllowedInCanvas); m_d->animationPlayer = new KisAnimationPlayer(this); connect(m_d->view->canvasController()->proxyObject, SIGNAL(moveDocumentOffset(QPoint)), SLOT(documentOffsetMoved(QPoint))); connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); /** * We switch the shape manager every time vector layer or * shape selection is activated. Flake does not expect this * and connects all the signals of the global shape manager * to the clients in the constructor. To workaround this we * forward the signals of local shape managers stored in the * vector layers to the signals of global shape manager. So the * sequence of signal deliveries is the following: * * shapeLayer.m_d.canvas.m_shapeManager.selection() -> * shapeLayer -> * shapeController -> * globalShapeManager.selection() */ KisShapeController *kritaShapeController = static_cast(shapeController()->documentBase()); connect(kritaShapeController, SIGNAL(selectionChanged()), this, SLOT(slotSelectionChanged())); connect(kritaShapeController, SIGNAL(selectionContentChanged()), globalShapeManager(), SIGNAL(selectionContentChanged())); connect(kritaShapeController, SIGNAL(currentLayerChanged(const KoShapeLayer*)), globalShapeManager()->selection(), SIGNAL(currentLayerChanged(const KoShapeLayer*))); connect(&m_d->updateSignalCompressor, SIGNAL(timeout()), SLOT(slotDoCanvasUpdate())); + + initializeFpsDecoration(); +} + +void KisCanvas2::initializeFpsDecoration() +{ + KisConfig cfg; + + const bool shouldShowDebugOverlay = + (canvasIsOpenGL() && cfg.enableOpenGLFramerateLogging()) || + cfg.enableBrushSpeedLogging(); + + if (shouldShowDebugOverlay && !decoration(KisFpsDecoration::idTag)) { + addDecoration(new KisFpsDecoration(imageView())); + + if (cfg.enableBrushSpeedLogging()) { + connect(KisStrokeSpeedMonitor::instance(), SIGNAL(sigStatsUpdated()), this, SLOT(updateCanvas())); + } + } else if (!shouldShowDebugOverlay && decoration(KisFpsDecoration::idTag)) { + m_d->canvasWidget->removeDecoration(KisFpsDecoration::idTag); + disconnect(KisStrokeSpeedMonitor::instance(), SIGNAL(sigStatsUpdated()), this, SLOT(updateCanvas())); + } } KisCanvas2::~KisCanvas2() { if (m_d->animationPlayer->isPlaying()) { m_d->animationPlayer->forcedStopOnExit(); } delete m_d; } void KisCanvas2::setCanvasWidget(QWidget * widget) { KisAbstractCanvasWidget *tmp = dynamic_cast(widget); Q_ASSERT_X(tmp, "setCanvasWidget", "Cannot cast the widget to a KisAbstractCanvasWidget"); if (m_d->popupPalette) { m_d->popupPalette->setParent(widget); } if(m_d->canvasWidget != 0) { tmp->setDecorations(m_d->canvasWidget->decorations()); // Redundant check for the constructor case, see below if(viewManager() != 0) viewManager()->inputManager()->removeTrackedCanvas(this); } m_d->canvasWidget = tmp; // Either tmp was null or we are being called by KisCanvas2 constructor that is called by KisView // constructor, so the view manager still doesn't exists. if(m_d->canvasWidget != 0 && viewManager() != 0) viewManager()->inputManager()->addTrackedCanvas(this); if (!m_d->canvasWidget->decoration(INFINITY_DECORATION_ID)) { KisInfinityManager *manager = new KisInfinityManager(m_d->view, this); manager->setVisible(true); m_d->canvasWidget->addDecoration(manager); } widget->setAutoFillBackground(false); widget->setAttribute(Qt::WA_OpaquePaintEvent); widget->setMouseTracking(true); widget->setAcceptDrops(true); KoCanvasControllerWidget *controller = dynamic_cast(canvasController()); if (controller) { Q_ASSERT(controller->canvas() == this); controller->changeCanvasWidget(widget); } } bool KisCanvas2::canvasIsOpenGL() const { return m_d->currentCanvasIsOpenGL; } KisOpenGL::FilterMode KisCanvas2::openGLFilterMode() const { return KisOpenGL::FilterMode(m_d->openGLFilterMode); } void KisCanvas2::gridSize(QPointF *offset, QSizeF *spacing) const { QTransform transform = coordinatesConverter()->imageToDocumentTransform(); const QPoint intSpacing = m_d->view->document()->gridConfig().spacing(); const QPoint intOffset = m_d->view->document()->gridConfig().offset(); QPointF size = transform.map(QPointF(intSpacing)); spacing->rwidth() = size.x(); spacing->rheight() = size.y(); *offset = transform.map(QPointF(intOffset)); } bool KisCanvas2::snapToGrid() const { return m_d->view->document()->gridConfig().snapToGrid(); } qreal KisCanvas2::rotationAngle() const { return m_d->coordinatesConverter->rotationAngle(); } bool KisCanvas2::xAxisMirrored() const { return m_d->coordinatesConverter->xAxisMirrored(); } bool KisCanvas2::yAxisMirrored() const { return m_d->coordinatesConverter->yAxisMirrored(); } void KisCanvas2::channelSelectionChanged() { KisImageSP image = this->image(); m_d->channelFlags = image->rootLayer()->channelFlags(); m_d->view->viewManager()->blockUntilOperationsFinishedForced(image); image->barrierLock(); m_d->canvasWidget->channelSelectionChanged(m_d->channelFlags); startUpdateInPatches(image->bounds()); image->unlock(); } void KisCanvas2::addCommand(KUndo2Command *command) { // This method exists to support flake-related operations m_d->view->document()->addCommand(command); } KoShapeManager* KisCanvas2::shapeManager() const { KisNodeSP node = m_d->view->currentNode(); KoShapeManager *localShapeManager = fetchShapeManagerFromNode(node); // sanity check for consistency of the local shape manager KIS_SAFE_ASSERT_RECOVER (localShapeManager == m_d->currentlyActiveShapeManager) { localShapeManager = globalShapeManager(); } return localShapeManager ? localShapeManager : globalShapeManager(); } KoSelectedShapesProxy* KisCanvas2::selectedShapesProxy() const { return &m_d->selectedShapesProxy; } KoShapeManager* KisCanvas2::globalShapeManager() const { return &m_d->shapeManager; } void KisCanvas2::updateInputMethodInfo() { // TODO call (the protected) QWidget::updateMicroFocus() on the proper canvas widget... } const KisCoordinatesConverter* KisCanvas2::coordinatesConverter() const { return m_d->coordinatesConverter; } KoViewConverter* KisCanvas2::viewConverter() const { return m_d->coordinatesConverter; } KisInputManager* KisCanvas2::globalInputManager() const { return m_d->view->globalInputManager(); } QWidget* KisCanvas2::canvasWidget() { return m_d->canvasWidget->widget(); } const QWidget* KisCanvas2::canvasWidget() const { return m_d->canvasWidget->widget(); } KoUnit KisCanvas2::unit() const { KoUnit unit(KoUnit::Pixel); KisImageWSP image = m_d->view->image(); if (image) { if (!qFuzzyCompare(image->xRes(), image->yRes())) { warnKrita << "WARNING: resolution of the image is anisotropic" << ppVar(image->xRes()) << ppVar(image->yRes()); } const qreal resolution = image->xRes(); unit.setFactor(resolution); } return unit; } KoToolProxy * KisCanvas2::toolProxy() const { return &m_d->toolProxy; } void KisCanvas2::createQPainterCanvas() { m_d->currentCanvasIsOpenGL = false; KisQPainterCanvas * canvasWidget = new KisQPainterCanvas(this, m_d->coordinatesConverter, m_d->view); m_d->prescaledProjection = new KisPrescaledProjection(); m_d->prescaledProjection->setCoordinatesConverter(m_d->coordinatesConverter); m_d->prescaledProjection->setMonitorProfile(m_d->displayColorConverter.monitorProfile(), m_d->displayColorConverter.renderingIntent(), m_d->displayColorConverter.conversionFlags()); m_d->prescaledProjection->setDisplayFilter(m_d->displayColorConverter.displayFilter()); canvasWidget->setPrescaledProjection(m_d->prescaledProjection); setCanvasWidget(canvasWidget); } void KisCanvas2::createOpenGLCanvas() { KisConfig cfg; m_d->openGLFilterMode = cfg.openGLFilteringMode(); m_d->currentCanvasIsOpenGL = true; KisOpenGLCanvas2 *canvasWidget = new KisOpenGLCanvas2(this, m_d->coordinatesConverter, 0, m_d->view->image(), &m_d->displayColorConverter); m_d->frameCache = KisAnimationFrameCache::getFrameCache(canvasWidget->openGLImageTextures()); setCanvasWidget(canvasWidget); - - if (canvasWidget->needsFpsDebugging() && !decoration(KisFpsDecoration::idTag)) { - addDecoration(new KisFpsDecoration(imageView())); - } } void KisCanvas2::createCanvas(bool useOpenGL) { // deinitialize previous canvas structures m_d->prescaledProjection = 0; m_d->frameCache = 0; KisConfig cfg; QDesktopWidget dw; const KoColorProfile *profile = cfg.displayProfile(dw.screenNumber(imageView())); m_d->displayColorConverter.setMonitorProfile(profile); if (useOpenGL) { if (KisOpenGL::hasOpenGL()) { createOpenGLCanvas(); if (cfg.canvasState() == "OPENGL_FAILED") { // Creating the opengl canvas failed, fall back warnKrita << "OpenGL Canvas initialization returned OPENGL_FAILED. Falling back to QPainter."; createQPainterCanvas(); } } else { warnKrita << "Tried to create OpenGL widget when system doesn't have OpenGL\n"; createQPainterCanvas(); } } else { createQPainterCanvas(); } if (m_d->popupPalette) { m_d->popupPalette->setParent(m_d->canvasWidget->widget()); } } void KisCanvas2::initializeImage() { KisImageSP image = m_d->view->image(); m_d->coordinatesConverter->setImage(image); m_d->toolProxy.initializeImage(image); connect(image, SIGNAL(sigImageUpdated(QRect)), SLOT(startUpdateCanvasProjection(QRect)), Qt::DirectConnection); connect(this, SIGNAL(sigCanvasCacheUpdated()), SLOT(updateCanvasProjection())); connect(image, SIGNAL(sigProofingConfigChanged()), SLOT(slotChangeProofingConfig())); connect(image, SIGNAL(sigSizeChanged(const QPointF&, const QPointF&)), SLOT(startResizingImage()), Qt::DirectConnection); connect(this, SIGNAL(sigContinueResizeImage(qint32,qint32)), SLOT(finishResizingImage(qint32,qint32))); connect(image->undoAdapter(), SIGNAL(selectionChanged()), SLOT(slotTrySwitchShapeManager())); connectCurrentCanvas(); } void KisCanvas2::connectCurrentCanvas() { KisImageWSP image = m_d->view->image(); if (!m_d->currentCanvasIsOpenGL) { Q_ASSERT(m_d->prescaledProjection); m_d->prescaledProjection->setImage(image); } startResizingImage(); emit imageChanged(image); setLodAllowedInCanvas(m_d->lodAllowedInCanvas); } void KisCanvas2::resetCanvas(bool useOpenGL) { // we cannot reset the canvas before it's created, but this method might be called, // for instance when setting the monitor profile. if (!m_d->canvasWidget) { return; } KisConfig cfg; bool needReset = (m_d->currentCanvasIsOpenGL != useOpenGL) || (m_d->currentCanvasIsOpenGL && m_d->openGLFilterMode != cfg.openGLFilteringMode()); if (needReset) { createCanvas(useOpenGL); connectCurrentCanvas(); notifyZoomChanged(); } updateCanvasWidgetImpl(); } void KisCanvas2::startUpdateInPatches(const QRect &imageRect) { if (m_d->currentCanvasIsOpenGL) { startUpdateCanvasProjection(imageRect); } else { KisImageConfig imageConfig; int patchWidth = imageConfig.updatePatchWidth(); int patchHeight = imageConfig.updatePatchHeight(); for (int y = 0; y < imageRect.height(); y += patchHeight) { for (int x = 0; x < imageRect.width(); x += patchWidth) { QRect patchRect(x, y, patchWidth, patchHeight); startUpdateCanvasProjection(patchRect); } } } } void KisCanvas2::setDisplayFilter(QSharedPointer displayFilter) { m_d->displayColorConverter.setDisplayFilter(displayFilter); KisImageSP image = this->image(); m_d->view->viewManager()->blockUntilOperationsFinishedForced(image); image->barrierLock(); m_d->canvasWidget->setDisplayFilter(displayFilter); image->unlock(); } QSharedPointer KisCanvas2::displayFilter() const { return m_d->displayColorConverter.displayFilter(); } KisDisplayColorConverter* KisCanvas2::displayColorConverter() const { return &m_d->displayColorConverter; } KisExposureGammaCorrectionInterface* KisCanvas2::exposureGammaCorrectionInterface() const { QSharedPointer displayFilter = m_d->displayColorConverter.displayFilter(); return displayFilter ? displayFilter->correctionInterface() : KisDumbExposureGammaCorrectionInterface::instance(); } void KisCanvas2::setProofingOptions(bool softProof, bool gamutCheck) { m_d->proofingConfig = this->image()->proofingConfiguration(); if (!m_d->proofingConfig) { qDebug()<<"Canvas: No proofing config found, generating one."; KisImageConfig cfg; m_d->proofingConfig = cfg.defaultProofingconfiguration(); } KoColorConversionTransformation::ConversionFlags conversionFlags = m_d->proofingConfig->conversionFlags; if (softProof && this->image()->colorSpace()->colorDepthId().id().contains("U")) { conversionFlags |= KoColorConversionTransformation::SoftProofing; } else { conversionFlags = conversionFlags & ~KoColorConversionTransformation::SoftProofing; } if (gamutCheck && softProof && this->image()->colorSpace()->colorDepthId().id().contains("U")) { conversionFlags |= KoColorConversionTransformation::GamutCheck; } else { conversionFlags = conversionFlags & ~KoColorConversionTransformation::GamutCheck; } m_d->proofingConfig->conversionFlags = conversionFlags; m_d->proofingConfigUpdated = true; startUpdateInPatches(this->image()->bounds()); } void KisCanvas2::slotSoftProofing(bool softProofing) { m_d->softProofing = softProofing; setProofingOptions(m_d->softProofing, m_d->gamutCheck); } void KisCanvas2::slotGamutCheck(bool gamutCheck) { m_d->gamutCheck = gamutCheck; setProofingOptions(m_d->softProofing, m_d->gamutCheck); } void KisCanvas2::slotChangeProofingConfig() { setProofingOptions(m_d->softProofing, m_d->gamutCheck); } void KisCanvas2::setProofingConfigUpdated(bool updated) { m_d->proofingConfigUpdated = updated; } bool KisCanvas2::proofingConfigUpdated() { return m_d->proofingConfigUpdated; } KisProofingConfigurationSP KisCanvas2::proofingConfiguration() const { if (!m_d->proofingConfig) { m_d->proofingConfig = this->image()->proofingConfiguration(); if (!m_d->proofingConfig) { qDebug()<<"Canvas: No proofing config found, generating one."; KisImageConfig cfg; m_d->proofingConfig = cfg.defaultProofingconfiguration(); } } return m_d->proofingConfig; } void KisCanvas2::startResizingImage() { KisImageWSP image = this->image(); qint32 w = image->width(); qint32 h = image->height(); emit sigContinueResizeImage(w, h); QRect imageBounds(0, 0, w, h); startUpdateInPatches(imageBounds); } void KisCanvas2::finishResizingImage(qint32 w, qint32 h) { m_d->canvasWidget->finishResizingImage(w, h); } void KisCanvas2::startUpdateCanvasProjection(const QRect & rc) { KisUpdateInfoSP info = m_d->canvasWidget->startUpdateCanvasProjection(rc, m_d->channelFlags); if (m_d->projectionUpdatesCompressor.putUpdateInfo(info)) { emit sigCanvasCacheUpdated(); } } void KisCanvas2::updateCanvasProjection() { while (KisUpdateInfoSP info = m_d->projectionUpdatesCompressor.takeUpdateInfo()) { QRect vRect = m_d->canvasWidget->updateCanvasProjection(info); if (!vRect.isEmpty()) { updateCanvasWidgetImpl(m_d->coordinatesConverter->viewportToWidget(vRect).toAlignedRect()); } } // TODO: Implement info->dirtyViewportRect() for KisOpenGLCanvas2 to avoid updating whole canvas if (m_d->currentCanvasIsOpenGL) { updateCanvasWidgetImpl(); } } void KisCanvas2::slotDoCanvasUpdate() { if (m_d->canvasWidget->isBusy()) { // just restarting the timer updateCanvasWidgetImpl(m_d->savedUpdateRect); return; } if (m_d->savedUpdateRect.isEmpty()) { m_d->canvasWidget->widget()->update(); emit updateCanvasRequested(m_d->canvasWidget->widget()->rect()); } else { emit updateCanvasRequested(m_d->savedUpdateRect); m_d->canvasWidget->widget()->update(m_d->savedUpdateRect); } m_d->savedUpdateRect = QRect(); } void KisCanvas2::updateCanvasWidgetImpl(const QRect &rc) { if (!m_d->updateSignalCompressor.isActive() || !m_d->savedUpdateRect.isEmpty()) { m_d->savedUpdateRect |= rc; } m_d->updateSignalCompressor.start(); } void KisCanvas2::updateCanvas() { updateCanvasWidgetImpl(); } void KisCanvas2::updateCanvas(const QRectF& documentRect) { if (m_d->currentCanvasIsOpenGL && m_d->canvasWidget->decorations().size() > 0) { updateCanvasWidgetImpl(); } else { // updateCanvas is called from tools, never from the projection // updates, so no need to prescale! QRect widgetRect = m_d->coordinatesConverter->documentToWidget(documentRect).toAlignedRect(); widgetRect.adjust(-2, -2, 2, 2); if (!widgetRect.isEmpty()) { updateCanvasWidgetImpl(widgetRect); } } } void KisCanvas2::disconnectCanvasObserver(QObject *object) { KoCanvasBase::disconnectCanvasObserver(object); m_d->view->disconnect(object); } void KisCanvas2::notifyZoomChanged() { if (!m_d->currentCanvasIsOpenGL) { Q_ASSERT(m_d->prescaledProjection); m_d->prescaledProjection->notifyZoomChanged(); } notifyLevelOfDetailChange(); updateCanvas(); // update the canvas, because that isn't done when zooming using KoZoomAction } void KisCanvas2::slotTrySwitchShapeManager() { QPointer oldManager = m_d->currentlyActiveShapeManager; KisNodeSP node = m_d->view->currentNode(); QPointer newManager; newManager = fetchShapeManagerFromNode(node); if (newManager != oldManager) { m_d->currentlyActiveShapeManager = newManager; m_d->selectedShapesProxy.setShapeManager(newManager); } } void KisCanvas2::notifyLevelOfDetailChange() { if (!m_d->effectiveLodAllowedInCanvas()) return; KisImageSP image = this->image(); const qreal effectiveZoom = m_d->coordinatesConverter->effectiveZoom(); KisConfig cfg; const int maxLod = cfg.numMipmapLevels(); int lod = KisLodTransform::scaleToLod(effectiveZoom, maxLod); image->setDesiredLevelOfDetail(lod); } const KoColorProfile * KisCanvas2::monitorProfile() { return m_d->displayColorConverter.monitorProfile(); } KisViewManager* KisCanvas2::viewManager() const { if (m_d->view) { return m_d->view->viewManager(); } return 0; } QPointerKisCanvas2::imageView() const { return m_d->view; } KisImageWSP KisCanvas2::image() const { return m_d->view->image(); } KisImageWSP KisCanvas2::currentImage() const { return m_d->view->image(); } void KisCanvas2::documentOffsetMoved(const QPoint &documentOffset) { QPointF offsetBefore = m_d->coordinatesConverter->imageRectInViewportPixels().topLeft(); m_d->coordinatesConverter->setDocumentOffset(documentOffset); QPointF offsetAfter = m_d->coordinatesConverter->imageRectInViewportPixels().topLeft(); QPointF moveOffset = offsetAfter - offsetBefore; if (!m_d->currentCanvasIsOpenGL) m_d->prescaledProjection->viewportMoved(moveOffset); emit documentOffsetUpdateFinished(); updateCanvas(); } void KisCanvas2::slotConfigChanged() { KisConfig cfg; m_d->vastScrolling = cfg.vastScrolling(); resetCanvas(cfg.useOpenGL()); slotSetDisplayProfile(cfg.displayProfile(QApplication::desktop()->screenNumber(this->canvasWidget()))); + initializeFpsDecoration(); } void KisCanvas2::refetchDataFromImage() { KisImageSP image = this->image(); KisImageBarrierLocker l(image); startUpdateInPatches(image->bounds()); } void KisCanvas2::slotSetDisplayProfile(const KoColorProfile *monitorProfile) { if (m_d->displayColorConverter.monitorProfile() == monitorProfile) return; m_d->displayColorConverter.setMonitorProfile(monitorProfile); { KisImageSP image = this->image(); KisImageBarrierLocker l(image); m_d->canvasWidget->setDisplayProfile(&m_d->displayColorConverter); } refetchDataFromImage(); } void KisCanvas2::addDecoration(KisCanvasDecorationSP deco) { m_d->canvasWidget->addDecoration(deco); } KisCanvasDecorationSP KisCanvas2::decoration(const QString& id) const { return m_d->canvasWidget->decoration(id); } QPoint KisCanvas2::documentOrigin() const { /** * In Krita we don't use document origin anymore. * All the centering when needed (vastScrolling < 0.5) is done * automatically by the KisCoordinatesConverter. */ return QPoint(); } QPoint KisCanvas2::documentOffset() const { return m_d->coordinatesConverter->documentOffset(); } void KisCanvas2::setFavoriteResourceManager(KisFavoriteResourceManager* favoriteResourceManager) { m_d->popupPalette = new KisPopupPalette(viewManager(), m_d->coordinatesConverter, favoriteResourceManager, displayColorConverter()->displayRendererInterface(), m_d->view->resourceProvider(), m_d->canvasWidget->widget()); connect(m_d->popupPalette, SIGNAL(zoomLevelChanged(int)), this, SLOT(slotZoomChanged(int))); connect(m_d->popupPalette, SIGNAL(sigUpdateCanvas()), this, SLOT(updateCanvas())); m_d->popupPalette->showPopupPalette(false); } void KisCanvas2::slotZoomChanged(int zoom ) { m_d->view->viewManager()->zoomController()->setZoom(KoZoomMode::ZOOM_CONSTANT, (qreal)(zoom/100.0)); // 1.0 is 100% zoom notifyZoomChanged(); } void KisCanvas2::setCursor(const QCursor &cursor) { canvasWidget()->setCursor(cursor); } KisAnimationFrameCacheSP KisCanvas2::frameCache() const { return m_d->frameCache; } KisAnimationPlayer *KisCanvas2::animationPlayer() const { return m_d->animationPlayer; } void KisCanvas2::slotSelectionChanged() { KisShapeLayer* shapeLayer = dynamic_cast(viewManager()->activeLayer().data()); if (!shapeLayer) { return; } m_d->shapeManager.selection()->deselectAll(); Q_FOREACH (KoShape* shape, shapeLayer->shapeManager()->selection()->selectedShapes()) { m_d->shapeManager.selection()->select(shape); } } bool KisCanvas2::isPopupPaletteVisible() const { if (!m_d->popupPalette) { return false; } return m_d->popupPalette->isVisible(); } void KisCanvas2::setWrapAroundViewingMode(bool value) { KisCanvasDecorationSP infinityDecoration = m_d->canvasWidget->decoration(INFINITY_DECORATION_ID); if (infinityDecoration) { infinityDecoration->setVisible(!value); } m_d->canvasWidget->setWrapAroundViewingMode(value); } bool KisCanvas2::wrapAroundViewingMode() const { KisCanvasDecorationSP infinityDecoration = m_d->canvasWidget->decoration(INFINITY_DECORATION_ID); if (infinityDecoration) { return !(infinityDecoration->visible()); } return false; } void KisCanvas2::bootstrapFinished() { if (!m_d->bootstrapLodBlocked) return; m_d->bootstrapLodBlocked = false; setLodAllowedInCanvas(m_d->lodAllowedInCanvas); } void KisCanvas2::setLodAllowedInCanvas(bool value) { if (!KisOpenGL::supportsLoD()) { qWarning() << "WARNING: Level of Detail functionality is available only with openGL + GLSL 1.3 support"; } m_d->lodAllowedInCanvas = value && m_d->currentCanvasIsOpenGL && KisOpenGL::supportsLoD() && (m_d->openGLFilterMode == KisOpenGL::TrilinearFilterMode || m_d->openGLFilterMode == KisOpenGL::HighQualityFiltering); KisImageSP image = this->image(); if (m_d->effectiveLodAllowedInCanvas() != !image->levelOfDetailBlocked()) { image->setLevelOfDetailBlocked(!m_d->effectiveLodAllowedInCanvas()); notifyLevelOfDetailChange(); } KisConfig cfg; cfg.setLevelOfDetailEnabled(m_d->lodAllowedInCanvas); } bool KisCanvas2::lodAllowedInCanvas() const { return m_d->lodAllowedInCanvas; } void KisCanvas2::slotShowPopupPalette(const QPoint &p) { if (!m_d->popupPalette) { return; } m_d->popupPalette->showPopupPalette(p); } KisPaintingAssistantsDecorationSP KisCanvas2::paintingAssistantsDecoration() const { KisCanvasDecorationSP deco = decoration("paintingAssistantsDecoration"); return qobject_cast(deco.data()); } diff --git a/libs/ui/canvas/kis_canvas2.h b/libs/ui/canvas/kis_canvas2.h index 0dc22b29f7..ae5b270f27 100644 --- a/libs/ui/canvas/kis_canvas2.h +++ b/libs/ui/canvas/kis_canvas2.h @@ -1,304 +1,306 @@ /* This file is part of the KDE project * Copyright (C) 2006, 2010 Boudewijn Rempt * Copyright (C) 2011 Silvio Heinrich * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_CANVAS_H #define KIS_CANVAS_H #include #include #include #include #include #include #include #include #include #include #include "opengl/kis_opengl.h" #include "kis_ui_types.h" #include "kis_coordinates_converter.h" #include "kis_canvas_decoration.h" #include "kis_painting_assistants_decoration.h" class KoToolProxy; class KoColorProfile; class KisViewManager; class KisFavoriteResourceManager; class KisDisplayFilter; class KisDisplayColorConverter; struct KisExposureGammaCorrectionInterface; class KisView; class KisInputManager; class KisAnimationPlayer; class KisShapeController; class KisCoordinatesConverter; class KoViewConverter; /** * KisCanvas2 is not an actual widget class, but rather an adapter for * the widget it contains, which may be either a QPainter based * canvas, or an OpenGL based canvas: that are the real widgets. */ class KRITAUI_EXPORT KisCanvas2 : public KoCanvasBase { Q_OBJECT public: /** * Create a new canvas. The canvas manages a widget that will do * the actual painting: the canvas itself is not a widget. * * @param viewConverter the viewconverter for converting between * window and document coordinates. */ KisCanvas2(KisCoordinatesConverter *coordConverter, KoCanvasResourceManager *resourceManager, KisView *view, KoShapeBasedDocumentBase *sc); ~KisCanvas2() override; void notifyZoomChanged(); void disconnectCanvasObserver(QObject *object) override; public: // KoCanvasBase implementation bool canvasIsOpenGL() const override; KisOpenGL::FilterMode openGLFilterMode() const; void gridSize(QPointF *offset, QSizeF *spacing) const override; bool snapToGrid() const override; // This method only exists to support flake-related operations void addCommand(KUndo2Command *command) override; QPoint documentOrigin() const override; QPoint documentOffset() const; /** * Return the right shape manager for the current layer. That is * to say, if the current layer is a vector layer, return the shape * layer's canvas' shapemanager, else the shapemanager associated * with the global krita canvas. */ KoShapeManager * shapeManager() const override; /** * Since shapeManager() may change, we need a persistent object where we can * connect to and thack the selection. See more comments in KoCanvasBase. */ KoSelectedShapesProxy *selectedShapesProxy() const override; /** * Return the shape manager associated with this canvas */ KoShapeManager *globalShapeManager() const; void updateCanvas(const QRectF& rc) override; void updateInputMethodInfo() override; const KisCoordinatesConverter* coordinatesConverter() const; KoViewConverter *viewConverter() const override; QWidget* canvasWidget() override; const QWidget* canvasWidget() const override; KoUnit unit() const override; KoToolProxy* toolProxy() const override; const KoColorProfile* monitorProfile(); // FIXME: // Temporary! Either get the current layer and image from the // resource provider, or use this, which gets them from the // current shape selection. KisImageWSP currentImage() const; /** * Filters events and sends them to canvas actions. Shared * among all the views/canvases * * NOTE: May be null while initialization! */ KisInputManager* globalInputManager() const; KisPaintingAssistantsDecorationSP paintingAssistantsDecoration() const; public: // KisCanvas2 methods KisImageWSP image() const; KisViewManager* viewManager() const; QPointer imageView() const; /// @return true if the canvas image should be displayed in vertically mirrored mode void addDecoration(KisCanvasDecorationSP deco); KisCanvasDecorationSP decoration(const QString& id) const; void setDisplayFilter(QSharedPointer displayFilter); QSharedPointer displayFilter() const; KisDisplayColorConverter *displayColorConverter() const; KisExposureGammaCorrectionInterface* exposureGammaCorrectionInterface() const; /** * @brief setProofingOptions * set the options for softproofing, without affecting the proofing options as stored inside the image. */ void setProofingOptions(bool softProof, bool gamutCheck); KisProofingConfigurationSP proofingConfiguration() const; /** * @brief setProofingConfigUpdated This function is to set whether the proofing config is updated, * this is needed for determining whether or not to generate a new proofing transform. * @param updated whether it's updated. Just set it to false in normal usage. */ void setProofingConfigUpdated(bool updated); /** * @brief proofingConfigUpdated ask the canvas whether or not it updated the proofing config. * @return whether or not the proofing config is updated, if so, a new proofing transform needs to be made * in KisOpenGL canvas. */ bool proofingConfigUpdated(); void setCursor(const QCursor &cursor) override; KisAnimationFrameCacheSP frameCache() const; KisAnimationPlayer *animationPlayer() const; void refetchDataFromImage(); Q_SIGNALS: void imageChanged(KisImageWSP image); void sigCanvasCacheUpdated(); void sigContinueResizeImage(qint32 w, qint32 h); void documentOffsetUpdateFinished(); // emitted whenever the canvas widget thinks sketch should update void updateCanvasRequested(const QRect &rc); public Q_SLOTS: /// Update the entire canvas area void updateCanvas(); void startResizingImage(); void finishResizingImage(qint32 w, qint32 h); /// canvas rotation in degrees qreal rotationAngle() const; /// Bools indicating canvasmirroring. bool xAxisMirrored() const; bool yAxisMirrored() const; void slotSoftProofing(bool softProofing); void slotGamutCheck(bool gamutCheck); void slotChangeProofingConfig(); void slotZoomChanged(int zoom); void channelSelectionChanged(); /** * Called whenever the display monitor profile resource changes */ void slotSetDisplayProfile(const KoColorProfile *profile); void startUpdateInPatches(const QRect &imageRect); void slotTrySwitchShapeManager(); /** * Called whenever the configuration settings change. */ void slotConfigChanged(); private Q_SLOTS: /// The image projection has changed, now start an update /// of the canvas representation. void startUpdateCanvasProjection(const QRect & rc); void updateCanvasProjection(); /** * Called whenever the view widget needs to show a different part of * the document * * @param documentOffset the offset in widget pixels */ void documentOffsetMoved(const QPoint &documentOffset); void slotSelectionChanged(); void slotDoCanvasUpdate(); void bootstrapFinished(); public: bool isPopupPaletteVisible() const; void slotShowPopupPalette(const QPoint& = QPoint(0,0)); // interface for KisCanvasController only void setWrapAroundViewingMode(bool value); bool wrapAroundViewingMode() const; void setLodAllowedInCanvas(bool value); bool lodAllowedInCanvas() const; void initializeImage(); void setFavoriteResourceManager(KisFavoriteResourceManager* favoriteResourceManager); private: Q_DISABLE_COPY(KisCanvas2) void connectCurrentCanvas(); void createCanvas(bool useOpenGL); void createQPainterCanvas(); void createOpenGLCanvas(); void updateCanvasWidgetImpl(const QRect &rc = QRect()); void setCanvasWidget(QWidget *widget); void resetCanvas(bool useOpenGL); void notifyLevelOfDetailChange(); // Completes construction of canvas. // To be called by KisView in its constructor, once it has been setup enough // (to be defined what that means) for things KisCanvas2 expects from KisView // TODO: see to avoid that void setup(); + void initializeFpsDecoration(); + private: friend class KisView; // calls setup() class KisCanvas2Private; KisCanvas2Private * const m_d; }; #endif diff --git a/libs/ui/canvas/kis_canvas_updates_compressor.cpp b/libs/ui/canvas/kis_canvas_updates_compressor.cpp index 918924d358..c016e66d0e 100644 --- a/libs/ui/canvas/kis_canvas_updates_compressor.cpp +++ b/libs/ui/canvas/kis_canvas_updates_compressor.cpp @@ -1,61 +1,54 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_canvas_updates_compressor.h" bool KisCanvasUpdatesCompressor::putUpdateInfo(KisUpdateInfoSP info) { const int levelOfDetail = info->levelOfDetail(); const QRect newUpdateRect = info->dirtyImageRect(); if (newUpdateRect.isEmpty()) return false; QMutexLocker l(&m_mutex); - bool updateOverridden = false; - UpdateInfoList::iterator it = m_updatesList.begin(); + while (it != m_updatesList.end()) { if (levelOfDetail == (*it)->levelOfDetail() && newUpdateRect.contains((*it)->dirtyImageRect())) { - if (info) { - *it = info; - info = 0; - ++it; - } else { - it = m_updatesList.erase(it); - } - - updateOverridden = true; + /** + * We should always remove the overridden update and put 'info' to the end + * of the queue. Otherwise, the updates will become reordered and the canvas + * may have tiles artifacts with "outdated" data + */ + it = m_updatesList.erase(it); } else { ++it; } } - if (!updateOverridden) { - Q_ASSERT(info); - m_updatesList.append(info); - } + m_updatesList.append(info); - return !updateOverridden; + return m_updatesList.size() <= 1; } KisUpdateInfoSP KisCanvasUpdatesCompressor::takeUpdateInfo() { QMutexLocker l(&m_mutex); return !m_updatesList.isEmpty() ? m_updatesList.takeFirst() : 0; } diff --git a/libs/ui/canvas/kis_canvas_widget_base.cpp b/libs/ui/canvas/kis_canvas_widget_base.cpp index 614dcb4467..482d63e034 100644 --- a/libs/ui/canvas/kis_canvas_widget_base.cpp +++ b/libs/ui/canvas/kis_canvas_widget_base.cpp @@ -1,248 +1,258 @@ /* * Copyright (C) 2007, 2010 Adrian Page * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_canvas_widget_base.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_coordinates_converter.h" #include "kis_canvas_decoration.h" #include "kis_config.h" #include "kis_canvas2.h" #include "KisViewManager.h" #include "kis_selection_manager.h" #include "KisDocument.h" struct KisCanvasWidgetBase::Private { public: Private(KisCanvas2 *newCanvas, KisCoordinatesConverter *newCoordinatesConverter) : canvas(newCanvas) , coordinatesConverter(newCoordinatesConverter) , viewConverter(newCanvas->viewConverter()) , toolProxy(newCanvas->toolProxy()) , ignorenextMouseEventExceptRightMiddleClick(0) , borderColor(Qt::gray) {} QList decorations; KisCanvas2 * canvas; KisCoordinatesConverter *coordinatesConverter; const KoViewConverter * viewConverter; KoToolProxy * toolProxy; QTimer blockMouseEvent; bool ignorenextMouseEventExceptRightMiddleClick; // HACK work around Qt bug not sending tablet right/dblclick http://bugreports.qt.nokia.com/browse/QTBUG-8598 QColor borderColor; }; KisCanvasWidgetBase::KisCanvasWidgetBase(KisCanvas2 * canvas, KisCoordinatesConverter *coordinatesConverter) : m_d(new Private(canvas, coordinatesConverter)) { m_d->blockMouseEvent.setSingleShot(true); } KisCanvasWidgetBase::~KisCanvasWidgetBase() { /** * Clear all the attached decoration. Oherwise they might decide * to process some events or signals after the canvas has been * destroyed */ //5qDeleteAll(m_d->decorations); m_d->decorations.clear(); delete m_d; } void KisCanvasWidgetBase::drawDecorations(QPainter & gc, const QRect &updateWidgetRect) const { if (!m_d->canvas) { dbgFile<<"canvas doesn't exist, in canvas widget base!"; return; } gc.save(); // Setup the painter to take care of the offset; all that the // classes that do painting need to keep track of is resolution gc.setRenderHint(QPainter::Antialiasing); gc.setRenderHint(QPainter::TextAntialiasing); // This option does not do anything anymore with Qt4.6, so don't reenable it since it seems to break display // http://www.archivum.info/qt-interest@trolltech.com/2010-01/00481/Re:-(Qt-interest)-Is-QPainter::HighQualityAntialiasing-render-hint-broken-in-Qt-4.6.html // gc.setRenderHint(QPainter::HighQualityAntialiasing); gc.setRenderHint(QPainter::SmoothPixmapTransform); gc.save(); gc.setClipRect(updateWidgetRect); QTransform transform = m_d->coordinatesConverter->flakeToWidgetTransform(); gc.setTransform(transform); // Paint the shapes (other than the layers) m_d->canvas->globalShapeManager()->paint(gc, *m_d->viewConverter, false); // draw green selection outlines around text shapes that are edited, so the user sees where they end gc.save(); QTransform worldTransform = gc.worldTransform(); gc.setPen( Qt::green ); Q_FOREACH (KoShape *shape, canvas()->shapeManager()->selection()->selectedShapes()) { if (shape->shapeId() == "ArtisticText" || shape->shapeId() == "TextShapeID") { gc.setWorldTransform(shape->absoluteTransformation(m_d->viewConverter) * worldTransform); KoShape::applyConversion(gc, *m_d->viewConverter); gc.drawRect(QRectF(QPointF(), shape->size())); } } gc.restore(); // Draw text shape over canvas while editing it, that's needs to show the text selection correctly QString toolId = KoToolManager::instance()->activeToolId(); if (toolId == "ArtisticTextTool" || toolId == "TextTool") { gc.save(); gc.setPen(Qt::NoPen); gc.setBrush(Qt::NoBrush); Q_FOREACH (KoShape *shape, canvas()->shapeManager()->selection()->selectedShapes()) { if (shape->shapeId() == "ArtisticText" || shape->shapeId() == "TextShapeID") { KoShapePaintingContext paintContext(canvas(), false); gc.save(); gc.setTransform(shape->absoluteTransformation(m_d->viewConverter) * gc.transform()); canvas()->shapeManager()->paintShape(shape, gc, *m_d->viewConverter, paintContext); gc.restore(); } } gc.restore(); } // - some tools do not restore gc, but that is not important here // - we need to disable clipping to draw handles properly gc.setClipping(false); toolProxy()->paint(gc, *m_d->viewConverter); gc.restore(); // ask the decorations to paint themselves Q_FOREACH (KisCanvasDecorationSP deco, m_d->decorations) { deco->paint(gc, m_d->coordinatesConverter->widgetToDocument(updateWidgetRect), m_d->coordinatesConverter,m_d->canvas); } gc.restore(); } void KisCanvasWidgetBase::addDecoration(KisCanvasDecorationSP deco) { m_d->decorations.push_back(deco); } +void KisCanvasWidgetBase::removeDecoration(const QString &id) +{ + for (auto it = m_d->decorations.begin(); it != m_d->decorations.end(); ++it) { + if ((*it)->id() == id) { + it = m_d->decorations.erase(it); + break; + } + } +} + KisCanvasDecorationSP KisCanvasWidgetBase::decoration(const QString& id) const { Q_FOREACH (KisCanvasDecorationSP deco, m_d->decorations) { if (deco->id() == id) { return deco; } } return 0; } void KisCanvasWidgetBase::setDecorations(const QList &decorations) { m_d->decorations=decorations; } QList KisCanvasWidgetBase::decorations() const { return m_d->decorations; } void KisCanvasWidgetBase::setWrapAroundViewingMode(bool value) { Q_UNUSED(value); } QImage KisCanvasWidgetBase::createCheckersImage(qint32 checkSize) { KisConfig cfg; if(checkSize < 0) checkSize = cfg.checkSize(); QColor checkColor1 = cfg.checkersColor1(); QColor checkColor2 = cfg.checkersColor2(); QImage tile(checkSize * 2, checkSize * 2, QImage::Format_RGB32); QPainter pt(&tile); pt.fillRect(tile.rect(), checkColor2); pt.fillRect(0, 0, checkSize, checkSize, checkColor1); pt.fillRect(checkSize, checkSize, checkSize, checkSize, checkColor1); pt.end(); return tile; } void KisCanvasWidgetBase::notifyConfigChanged() { KisConfig cfg; m_d->borderColor = cfg.canvasBorderColor(); } QColor KisCanvasWidgetBase::borderColor() const { return m_d->borderColor; } KisCanvas2 *KisCanvasWidgetBase::canvas() const { return m_d->canvas; } KisCoordinatesConverter* KisCanvasWidgetBase::coordinatesConverter() const { return m_d->coordinatesConverter; } KoToolProxy *KisCanvasWidgetBase::toolProxy() const { return m_d->toolProxy; } QVariant KisCanvasWidgetBase::processInputMethodQuery(Qt::InputMethodQuery query) const { if (query == Qt::ImMicroFocus) { QRectF rect = m_d->toolProxy->inputMethodQuery(query, *m_d->viewConverter).toRectF(); return m_d->coordinatesConverter->flakeToWidget(rect); } return m_d->toolProxy->inputMethodQuery(query, *m_d->viewConverter); } void KisCanvasWidgetBase::processInputMethodEvent(QInputMethodEvent *event) { m_d->toolProxy->inputMethodEvent(event); } diff --git a/libs/ui/canvas/kis_canvas_widget_base.h b/libs/ui/canvas/kis_canvas_widget_base.h index a97be68e81..011e65e1be 100644 --- a/libs/ui/canvas/kis_canvas_widget_base.h +++ b/libs/ui/canvas/kis_canvas_widget_base.h @@ -1,102 +1,103 @@ /* * Copyright (C) 2007 Boudewijn Rempt , (C) * Copyright (C) 2010 Adrian Page * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_CANVAS_WIDGET_BASE_ #define _KIS_CANVAS_WIDGET_BASE_ #include #include #include "kis_abstract_canvas_widget.h" class QColor; class QImage; class QInputMethodEvent; class QVariant; class KisCoordinatesConverter; class KisDisplayFilter; class KisCanvas2; #include "kritaui_export.h" class KRITAUI_EXPORT KisCanvasWidgetBase : public KisAbstractCanvasWidget { public: KisCanvasWidgetBase(KisCanvas2 * canvas, KisCoordinatesConverter *coordinatesConverter); ~KisCanvasWidgetBase() override; public: // KisAbstractCanvasWidget KoToolProxy *toolProxy() const override; /// set the specified display filter on the canvas void setDisplayFilter(QSharedPointer displayFilter) override = 0; /** * Draw the specified decorations on the view. */ void drawDecorations(QPainter & gc, const QRect &updateWidgetRect) const override; void addDecoration(KisCanvasDecorationSP deco) override; + void removeDecoration(const QString& id) override; KisCanvasDecorationSP decoration(const QString& id) const override; void setDecorations(const QList &) override; QList decorations() const override; void setWrapAroundViewingMode(bool value) override; /** * Returns the color of the border, i.e. the part of the canvas * outside the image contents. * */ QColor borderColor() const; /** * Returns one check of the background checkerboard pattern. */ static QImage createCheckersImage(qint32 checkSize = -1); KisCoordinatesConverter* coordinatesConverter() const; protected: KisCanvas2 *canvas() const; /** * Event handlers to be called by derived canvas event handlers. * All common event processing is carried out by these * functions. */ QVariant processInputMethodQuery(Qt::InputMethodQuery query) const; void processInputMethodEvent(QInputMethodEvent *event); void notifyConfigChanged(); /// To be implemented by the derived canvas virtual bool callFocusNextPrevChild(bool next) = 0; private: struct Private; Private * const m_d; }; #endif // _KIS_CANVAS_WIDGET_BASE_ diff --git a/libs/ui/dialogs/kis_dlg_preferences.cc b/libs/ui/dialogs/kis_dlg_preferences.cc index bd16e5c117..90f0693638 100644 --- a/libs/ui/dialogs/kis_dlg_preferences.cc +++ b/libs/ui/dialogs/kis_dlg_preferences.cc @@ -1,1269 +1,1271 @@ /* * preferencesdlg.cc - part of KImageShop * * Copyright (c) 1999 Michael Koch * Copyright (c) 2003-2011 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_dlg_preferences.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "KoID.h" #include #include #include #include #include #include "kis_action_registry.h" #include "widgets/squeezedcombobox.h" #include "kis_clipboard.h" #include "widgets/kis_cmb_idlist.h" #include "KoColorSpace.h" #include "KoColorSpaceRegistry.h" #include "KoColorConversionTransformation.h" #include "kis_cursor.h" #include "kis_config.h" #include "kis_canvas_resource_provider.h" #include "kis_preference_set_registry.h" #include "kis_color_manager.h" #include "KisProofingConfiguration.h" #include "kis_image_config.h" #include "slider_and_spin_box_sync.h" // for the performance update #include #include #include "input/config/kis_input_configuration_page.h" #ifdef Q_OS_WIN # include #endif GeneralTab::GeneralTab(QWidget *_parent, const char *_name) : WdgGeneralSettings(_parent, _name) { KisConfig cfg; m_cmbCursorShape->addItem(i18n("No Cursor")); m_cmbCursorShape->addItem(i18n("Tool Icon")); m_cmbCursorShape->addItem(i18n("Arrow")); m_cmbCursorShape->addItem(i18n("Small Circle")); m_cmbCursorShape->addItem(i18n("Crosshair")); m_cmbCursorShape->addItem(i18n("Triangle Righthanded")); m_cmbCursorShape->addItem(i18n("Triangle Lefthanded")); m_cmbCursorShape->addItem(i18n("Black Pixel")); m_cmbCursorShape->addItem(i18n("White Pixel")); m_cmbOutlineShape->addItem(i18n("No Outline")); m_cmbOutlineShape->addItem(i18n("Circle Outline")); m_cmbOutlineShape->addItem(i18n("Preview Outline")); m_cmbOutlineShape->addItem(i18n("Tilt Outline")); m_cmbCursorShape->setCurrentIndex(cfg.newCursorStyle()); m_cmbOutlineShape->setCurrentIndex(cfg.newOutlineStyle()); chkShowRootLayer->setChecked(cfg.showRootLayer()); int autosaveInterval = cfg.autoSaveInterval(); //convert to minutes m_autosaveSpinBox->setValue(autosaveInterval / 60); m_autosaveCheckBox->setChecked(autosaveInterval > 0); m_undoStackSize->setValue(cfg.undoStackLimit()); m_backupFileCheckBox->setChecked(cfg.backupFile()); m_showOutlinePainting->setChecked(cfg.showOutlineWhilePainting()); m_hideSplashScreen->setChecked(cfg.hideSplashScreen()); KConfigGroup group = KSharedConfig::openConfig()->group("File Dialogs"); m_chkNativeFileDialog->setChecked(!group.readEntry("DontUseNativeFileDialog", true)); intMaxBrushSize->setValue(cfg.readEntry("maximumBrushSize", 1000)); m_cmbMDIType->setCurrentIndex(cfg.readEntry("mdi_viewmode", (int)QMdiArea::TabbedView)); m_chkRubberBand->setChecked(cfg.readEntry("mdi_rubberband", cfg.useOpenGL())); m_favoritePresetsSpinBox->setValue(cfg.favoritePresets()); KoColor mdiColor; mdiColor.fromQColor(cfg.getMDIBackgroundColor()); m_mdiColor->setColor(mdiColor); m_backgroundimage->setText(cfg.getMDIBackgroundImage()); m_chkCanvasMessages->setChecked(cfg.showCanvasMessages()); m_chkCompressKra->setChecked(cfg.compressKra()); const QString configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); QSettings kritarc(configPath + QStringLiteral("/kritadisplayrc"), QSettings::IniFormat); m_chkHiDPI->setChecked(kritarc.value("EnableHiDPI", false).toBool()); m_chkSingleApplication->setChecked(kritarc.value("EnableSingleApplication", true).toBool()); m_radioToolOptionsInDocker->setChecked(cfg.toolOptionsInDocker()); m_chkSwitchSelectionCtrlAlt->setChecked(cfg.switchSelectionCtrlAlt()); chkEnableTouch->setChecked(!cfg.disableTouchOnCanvas()); m_chkConvertOnImport->setChecked(cfg.convertToImageColorspaceOnImport()); m_chkCacheAnimatioInBackground->setChecked(cfg.calculateAnimationCacheInBackground()); KoColor cursorColor(KoColorSpaceRegistry::instance()->rgb8()); cursorColor.fromQColor(cfg.getCursorMainColor()); cursorColorBtutton->setColor(cursorColor); connect(m_bnFileName, SIGNAL(clicked()), SLOT(getBackgroundImage())); connect(clearBgImageButton, SIGNAL(clicked()), SLOT(clearBackgroundImage())); } void GeneralTab::setDefault() { KisConfig cfg; m_cmbCursorShape->setCurrentIndex(cfg.newCursorStyle(true)); m_cmbOutlineShape->setCurrentIndex(cfg.newOutlineStyle(true)); chkShowRootLayer->setChecked(cfg.showRootLayer(true)); m_autosaveCheckBox->setChecked(cfg.autoSaveInterval(true) > 0); //convert to minutes m_autosaveSpinBox->setValue(cfg.autoSaveInterval(true) / 60); m_undoStackSize->setValue(cfg.undoStackLimit(true)); m_backupFileCheckBox->setChecked(cfg.backupFile(true)); m_showOutlinePainting->setChecked(cfg.showOutlineWhilePainting(true)); m_hideSplashScreen->setChecked(cfg.hideSplashScreen(true)); m_chkNativeFileDialog->setChecked(false); intMaxBrushSize->setValue(1000); m_cmbMDIType->setCurrentIndex((int)QMdiArea::TabbedView); m_chkRubberBand->setChecked(cfg.useOpenGL(true)); m_favoritePresetsSpinBox->setValue(cfg.favoritePresets(true)); KoColor mdiColor; mdiColor.fromQColor(cfg.getMDIBackgroundColor(true)); m_mdiColor->setColor(mdiColor); m_backgroundimage->setText(cfg.getMDIBackgroundImage(true)); m_chkCanvasMessages->setChecked(cfg.showCanvasMessages(true)); m_chkCompressKra->setChecked(cfg.compressKra(true)); m_chkHiDPI->setChecked(false); m_chkSingleApplication->setChecked(true); m_chkHiDPI->setChecked(true); m_radioToolOptionsInDocker->setChecked(cfg.toolOptionsInDocker(true)); m_chkSwitchSelectionCtrlAlt->setChecked(cfg.switchSelectionCtrlAlt(true)); chkEnableTouch->setChecked(!cfg.disableTouchOnCanvas(true)); m_chkConvertOnImport->setChecked(cfg.convertToImageColorspaceOnImport(true)); m_chkCacheAnimatioInBackground->setChecked(cfg.calculateAnimationCacheInBackground(true)); KoColor cursorColor(KoColorSpaceRegistry::instance()->rgb8()); cursorColor.fromQColor(cfg.getCursorMainColor(true)); cursorColorBtutton->setColor(cursorColor); } CursorStyle GeneralTab::cursorStyle() { return (CursorStyle)m_cmbCursorShape->currentIndex(); } OutlineStyle GeneralTab::outlineStyle() { return (OutlineStyle)m_cmbOutlineShape->currentIndex(); } bool GeneralTab::showRootLayer() { return chkShowRootLayer->isChecked(); } int GeneralTab::autoSaveInterval() { //convert to seconds return m_autosaveCheckBox->isChecked() ? m_autosaveSpinBox->value() * 60 : 0; } int GeneralTab::undoStackSize() { return m_undoStackSize->value(); } bool GeneralTab::showOutlineWhilePainting() { return m_showOutlinePainting->isChecked(); } bool GeneralTab::hideSplashScreen() { return m_hideSplashScreen->isChecked(); } int GeneralTab::mdiMode() { return m_cmbMDIType->currentIndex(); } int GeneralTab::favoritePresets() { return m_favoritePresetsSpinBox->value(); } bool GeneralTab::showCanvasMessages() { return m_chkCanvasMessages->isChecked(); } bool GeneralTab::compressKra() { return m_chkCompressKra->isChecked(); } bool GeneralTab::toolOptionsInDocker() { return m_radioToolOptionsInDocker->isChecked(); } bool GeneralTab::switchSelectionCtrlAlt() { return m_chkSwitchSelectionCtrlAlt->isChecked(); } bool GeneralTab::convertToImageColorspaceOnImport() { return m_chkConvertOnImport->isChecked(); } bool GeneralTab::calculateAnimationCacheInBackground() { return m_chkCacheAnimatioInBackground->isChecked(); } void GeneralTab::getBackgroundImage() { KoFileDialog dialog(this, KoFileDialog::OpenFile, "BackgroundImages"); dialog.setCaption(i18n("Select a Background Image")); dialog.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)); dialog.setImageFilters(); QString fn = dialog.filename(); // dialog box was canceled or somehow no file was selected if (fn.isEmpty()) { return; } QImage image(fn); if (image.isNull()) { QMessageBox::warning(this, i18nc("@title:window", "Krita"), i18n("%1 is not a valid image file!", fn)); } else { m_backgroundimage->setText(fn); } } void GeneralTab::clearBackgroundImage() { // clearing the background image text will implicitly make the background color be used m_backgroundimage->setText(""); } #include "kactioncollection.h" #include "KisActionsSnapshot.h" ShortcutSettingsTab::ShortcutSettingsTab(QWidget *parent, const char *name) : QWidget(parent) { setObjectName(name); QGridLayout * l = new QGridLayout(this); l->setMargin(0); m_page = new WdgShortcutSettings(this); l->addWidget(m_page, 0, 0); m_snapshot.reset(new KisActionsSnapshot); KActionCollection *collection = KisPart::instance()->currentMainwindow()->actionCollection(); Q_FOREACH (QAction *action, collection->actions()) { m_snapshot->addAction(action->objectName(), action); } QMap sortedCollections = m_snapshot->actionCollections(); for (auto it = sortedCollections.constBegin(); it != sortedCollections.constEnd(); ++it) { m_page->addCollection(it.value(), it.key()); } } ShortcutSettingsTab::~ShortcutSettingsTab() { } void ShortcutSettingsTab::setDefault() { m_page->allDefault(); } void ShortcutSettingsTab::saveChanges() { m_page->save(); KisActionRegistry::instance()->settingsPageSaved(); } void ShortcutSettingsTab::cancelChanges() { m_page->undo(); } ColorSettingsTab::ColorSettingsTab(QWidget *parent, const char *name) : QWidget(parent) { setObjectName(name); // XXX: Make sure only profiles that fit the specified color model // are shown in the profile combos QGridLayout * l = new QGridLayout(this); l->setMargin(0); m_page = new WdgColorSettings(this); l->addWidget(m_page, 0, 0); KisConfig cfg; m_page->chkUseSystemMonitorProfile->setChecked(cfg.useSystemMonitorProfile()); connect(m_page->chkUseSystemMonitorProfile, SIGNAL(toggled(bool)), this, SLOT(toggleAllowMonitorProfileSelection(bool))); m_page->cmbWorkingColorSpace->setIDList(KoColorSpaceRegistry::instance()->listKeys()); m_page->cmbWorkingColorSpace->setCurrent(cfg.workingColorSpace()); m_page->bnAddColorProfile->setIcon(KisIconUtils::loadIcon("document-open")); m_page->bnAddColorProfile->setToolTip( i18n("Open Color Profile") ); connect(m_page->bnAddColorProfile, SIGNAL(clicked()), SLOT(installProfile())); QFormLayout *monitorProfileGrid = new QFormLayout(m_page->monitorprofileholder); for(int i = 0; i < QApplication::desktop()->screenCount(); ++i) { QLabel *lbl = new QLabel(i18nc("The number of the screen", "Screen %1:", i + 1)); m_monitorProfileLabels << lbl; SqueezedComboBox *cmb = new SqueezedComboBox(); cmb->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed); monitorProfileGrid->addRow(lbl, cmb); m_monitorProfileWidgets << cmb; } refillMonitorProfiles(KoID("RGBA", "")); for(int i = 0; i < QApplication::desktop()->screenCount(); ++i) { if (m_monitorProfileWidgets[i]->contains(cfg.monitorProfile(i))) { m_monitorProfileWidgets[i]->setCurrent(cfg.monitorProfile(i)); } } m_page->chkBlackpoint->setChecked(cfg.useBlackPointCompensation()); m_page->chkAllowLCMSOptimization->setChecked(cfg.allowLCMSOptimization()); KisImageConfig cfgImage; KisProofingConfigurationSP proofingConfig = cfgImage.defaultProofingconfiguration(); m_page->sldAdaptationState->setMaximum(20); m_page->sldAdaptationState->setMinimum(0); m_page->sldAdaptationState->setValue((int)proofingConfig->adaptationState*20); //probably this should become the screenprofile? KoColor ga(KoColorSpaceRegistry::instance()->rgb8()); ga.fromKoColor(proofingConfig->warningColor); m_page->gamutAlarm->setColor(ga); const KoColorSpace *proofingSpace = KoColorSpaceRegistry::instance()->colorSpace(proofingConfig->proofingModel, proofingConfig->proofingDepth, proofingConfig->proofingProfile); if (proofingSpace) { m_page->proofingSpaceSelector->setCurrentColorSpace(proofingSpace); } m_page->cmbProofingIntent->setCurrentIndex((int)proofingConfig->intent); m_page->ckbProofBlackPoint->setChecked(proofingConfig->conversionFlags.testFlag(KoColorConversionTransformation::BlackpointCompensation)); m_pasteBehaviourGroup.addButton(m_page->radioPasteWeb, PASTE_ASSUME_WEB); m_pasteBehaviourGroup.addButton(m_page->radioPasteMonitor, PASTE_ASSUME_MONITOR); m_pasteBehaviourGroup.addButton(m_page->radioPasteAsk, PASTE_ASK); QAbstractButton *button = m_pasteBehaviourGroup.button(cfg.pasteBehaviour()); Q_ASSERT(button); if (button) { button->setChecked(true); } m_page->cmbMonitorIntent->setCurrentIndex(cfg.monitorRenderIntent()); toggleAllowMonitorProfileSelection(cfg.useSystemMonitorProfile()); } void ColorSettingsTab::installProfile() { KoFileDialog dialog(this, KoFileDialog::OpenFiles, "OpenDocumentICC"); dialog.setCaption(i18n("Install Color Profiles")); dialog.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation)); dialog.setMimeTypeFilters(QStringList() << "application/vnd.iccprofile", "application/vnd.iccprofile"); QStringList profileNames = dialog.filenames(); KoColorSpaceEngine *iccEngine = KoColorSpaceEngineRegistry::instance()->get("icc"); Q_ASSERT(iccEngine); QString saveLocation = KoResourcePaths::saveLocation("icc_profiles"); Q_FOREACH (const QString &profileName, profileNames) { if (!QFile::copy(profileName, saveLocation + QFileInfo(profileName).fileName())) { qWarning() << "Could not install profile!" << saveLocation + QFileInfo(profileName).fileName(); continue; } iccEngine->addProfile(saveLocation + QFileInfo(profileName).fileName()); } KisConfig cfg; refillMonitorProfiles(KoID("RGBA", "")); for(int i = 0; i < QApplication::desktop()->screenCount(); ++i) { if (m_monitorProfileWidgets[i]->contains(cfg.monitorProfile(i))) { m_monitorProfileWidgets[i]->setCurrent(cfg.monitorProfile(i)); } } } void ColorSettingsTab::toggleAllowMonitorProfileSelection(bool useSystemProfile) { if (useSystemProfile) { KisConfig cfg; QStringList devices = KisColorManager::instance()->devices(); if (devices.size() == QApplication::desktop()->screenCount()) { for(int i = 0; i < QApplication::desktop()->screenCount(); ++i) { m_monitorProfileWidgets[i]->clear(); QString monitorForScreen = cfg.monitorForScreen(i, devices[i]); Q_FOREACH (const QString &device, devices) { m_monitorProfileLabels[i]->setText(i18nc("The display/screen we got from Qt", "Screen %1:", i + 1)); m_monitorProfileWidgets[i]->addSqueezedItem(KisColorManager::instance()->deviceName(device), device); if (devices[i] == monitorForScreen) { m_monitorProfileWidgets[i]->setCurrentIndex(i); } } } } } else { KisConfig cfg; refillMonitorProfiles(KoID("RGBA", "")); for(int i = 0; i < QApplication::desktop()->screenCount(); ++i) { if (m_monitorProfileWidgets[i]->contains(cfg.monitorProfile(i))) { m_monitorProfileWidgets[i]->setCurrent(cfg.monitorProfile(i)); } } } } void ColorSettingsTab::setDefault() { m_page->cmbWorkingColorSpace->setCurrent("RGBA"); refillMonitorProfiles(KoID("RGBA", "")); KisConfig cfg; KisImageConfig cfgImage; KisProofingConfigurationSP proofingConfig = cfgImage.defaultProofingconfiguration(); const KoColorSpace *proofingSpace = KoColorSpaceRegistry::instance()->colorSpace(proofingConfig->proofingModel,proofingConfig->proofingDepth,proofingConfig->proofingProfile); if (proofingSpace) { m_page->proofingSpaceSelector->setCurrentColorSpace(proofingSpace); } m_page->cmbProofingIntent->setCurrentIndex((int)proofingConfig->intent); m_page->ckbProofBlackPoint->setChecked(proofingConfig->conversionFlags.testFlag(KoColorConversionTransformation::BlackpointCompensation)); m_page->sldAdaptationState->setValue(0); //probably this should become the screenprofile? KoColor ga(KoColorSpaceRegistry::instance()->rgb8()); ga.fromKoColor(proofingConfig->warningColor); m_page->gamutAlarm->setColor(ga); m_page->chkBlackpoint->setChecked(cfg.useBlackPointCompensation(true)); m_page->chkAllowLCMSOptimization->setChecked(cfg.allowLCMSOptimization(true)); m_page->cmbMonitorIntent->setCurrentIndex(cfg.monitorRenderIntent(true)); m_page->chkUseSystemMonitorProfile->setChecked(cfg.useSystemMonitorProfile(true)); QAbstractButton *button = m_pasteBehaviourGroup.button(cfg.pasteBehaviour(true)); Q_ASSERT(button); if (button) { button->setChecked(true); } } void ColorSettingsTab::refillMonitorProfiles(const KoID & colorSpaceId) { for (int i = 0; i < QApplication::desktop()->screenCount(); ++i) { m_monitorProfileWidgets[i]->clear(); } QMap profileList; Q_FOREACH(const KoColorProfile *profile, KoColorSpaceRegistry::instance()->profilesFor(colorSpaceId.id())) { profileList[profile->name()] = profile; } Q_FOREACH (const KoColorProfile *profile, profileList.values()) { //qDebug() << "Profile" << profile->name() << profile->isSuitableForDisplay() << csf->defaultProfile(); if (profile->isSuitableForDisplay()) { for (int i = 0; i < QApplication::desktop()->screenCount(); ++i) { m_monitorProfileWidgets[i]->addSqueezedItem(profile->name()); } } } for (int i = 0; i < QApplication::desktop()->screenCount(); ++i) { m_monitorProfileLabels[i]->setText(i18nc("The number of the screen", "Screen %1:", i + 1)); m_monitorProfileWidgets[i]->setCurrent(KoColorSpaceRegistry::instance()->defaultProfileForColorSpace(colorSpaceId.id())); } } //--------------------------------------------------------------------------------------------------- void TabletSettingsTab::setDefault() { KisCubicCurve curve; curve.fromString(DEFAULT_CURVE_STRING); m_page->pressureCurve->setCurve(curve); #ifdef Q_OS_WIN if (KisTabletSupportWin8::isAvailable()) { KisConfig cfg; m_page->radioWintab->setChecked(!cfg.useWin8PointerInput(true)); m_page->radioWin8PointerInput->setChecked(cfg.useWin8PointerInput(true)); } else { m_page->radioWintab->setChecked(true); m_page->radioWin8PointerInput->setChecked(false); } #endif } TabletSettingsTab::TabletSettingsTab(QWidget* parent, const char* name): QWidget(parent) { setObjectName(name); QGridLayout * l = new QGridLayout(this); l->setMargin(0); m_page = new WdgTabletSettings(this); l->addWidget(m_page, 0, 0); KisConfig cfg; KisCubicCurve curve; curve.fromString( cfg.pressureTabletCurve() ); m_page->pressureCurve->setMaximumSize(QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)); m_page->pressureCurve->setCurve(curve); #ifdef Q_OS_WIN if (KisTabletSupportWin8::isAvailable()) { m_page->radioWintab->setChecked(!cfg.useWin8PointerInput()); m_page->radioWin8PointerInput->setChecked(cfg.useWin8PointerInput()); } else { m_page->radioWintab->setChecked(true); m_page->radioWin8PointerInput->setChecked(false); m_page->grpTabletApi->setVisible(false); } #else m_page->grpTabletApi->setVisible(false); #endif } //--------------------------------------------------------------------------------------------------- #include "kis_acyclic_signal_connector.h" int getTotalRAM() { KisImageConfig cfg; return cfg.totalRAM(); } int PerformanceTab::realTilesRAM() { return intMemoryLimit->value() - intPoolLimit->value(); } PerformanceTab::PerformanceTab(QWidget *parent, const char *name) : WdgPerformanceSettings(parent, name) { KisImageConfig cfg; const int totalRAM = cfg.totalRAM(); lblTotalMemory->setText(i18n("%1 MiB", totalRAM)); sliderMemoryLimit->setSuffix(i18n(" %")); sliderMemoryLimit->setRange(1, 100, 2); sliderMemoryLimit->setSingleStep(0.01); sliderPoolLimit->setSuffix(i18n(" %")); sliderPoolLimit->setRange(0, 20, 2); sliderMemoryLimit->setSingleStep(0.01); sliderUndoLimit->setSuffix(i18n(" %")); sliderUndoLimit->setRange(0, 50, 2); sliderMemoryLimit->setSingleStep(0.01); intMemoryLimit->setMinimumWidth(80); intPoolLimit->setMinimumWidth(80); intUndoLimit->setMinimumWidth(80); SliderAndSpinBoxSync *sync1 = new SliderAndSpinBoxSync(sliderMemoryLimit, intMemoryLimit, getTotalRAM); sync1->slotParentValueChanged(); m_syncs << sync1; SliderAndSpinBoxSync *sync2 = new SliderAndSpinBoxSync(sliderPoolLimit, intPoolLimit, std::bind(&KisIntParseSpinBox::value, intMemoryLimit)); connect(intMemoryLimit, SIGNAL(valueChanged(int)), sync2, SLOT(slotParentValueChanged())); sync2->slotParentValueChanged(); m_syncs << sync2; SliderAndSpinBoxSync *sync3 = new SliderAndSpinBoxSync(sliderUndoLimit, intUndoLimit, std::bind(&PerformanceTab::realTilesRAM, this)); connect(intPoolLimit, SIGNAL(valueChanged(int)), sync3, SLOT(slotParentValueChanged())); sync3->slotParentValueChanged(); m_syncs << sync3; sliderSwapSize->setSuffix(i18n(" GiB")); sliderSwapSize->setRange(1, 64); intSwapSize->setRange(1, 64); KisAcyclicSignalConnector *swapSizeConnector = new KisAcyclicSignalConnector(this); swapSizeConnector->connectForwardInt(sliderSwapSize, SIGNAL(valueChanged(int)), intSwapSize, SLOT(setValue(int))); swapSizeConnector->connectBackwardInt(intSwapSize, SIGNAL(valueChanged(int)), sliderSwapSize, SLOT(setValue(int))); lblSwapFileLocation->setText(cfg.swapDir()); connect(bnSwapFile, SIGNAL(clicked()), SLOT(selectSwapDir())); sliderThreadsLimit->setRange(1, QThread::idealThreadCount()); sliderFrameClonesLimit->setRange(1, QThread::idealThreadCount()); sliderFpsLimit->setRange(20, 100); sliderFpsLimit->setSuffix(i18n(" fps")); connect(sliderThreadsLimit, SIGNAL(valueChanged(int)), SLOT(slotThreadsLimitChanged(int))); connect(sliderFrameClonesLimit, SIGNAL(valueChanged(int)), SLOT(slotFrameClonesLimitChanged(int))); connect(sliderFpsLimit, SIGNAL(valueChanged(int)), SLOT(slotFpsLimitChanged(int))); load(false); } PerformanceTab::~PerformanceTab() { qDeleteAll(m_syncs); } void PerformanceTab::load(bool requestDefault) { KisImageConfig cfg; sliderMemoryLimit->setValue(cfg.memoryHardLimitPercent(requestDefault)); sliderPoolLimit->setValue(cfg.memoryPoolLimitPercent(requestDefault)); sliderUndoLimit->setValue(cfg.memorySoftLimitPercent(requestDefault)); chkPerformanceLogging->setChecked(cfg.enablePerfLog(requestDefault)); chkProgressReporting->setChecked(cfg.enableProgressReporting(requestDefault)); sliderSwapSize->setValue(cfg.maxSwapSize(requestDefault) / 1024); lblSwapFileLocation->setText(cfg.swapDir(requestDefault)); m_lastUsedThreadsLimit = cfg.maxNumberOfThreads(requestDefault); m_lastUsedClonesLimit = cfg.frameRenderingClones(requestDefault); m_lastUsedFpsLimit = cfg.fpsLimit(requestDefault); sliderThreadsLimit->setValue(m_lastUsedThreadsLimit); sliderFrameClonesLimit->setValue(m_lastUsedClonesLimit); sliderFpsLimit->setValue(m_lastUsedFpsLimit); { KisConfig cfg2; chkOpenGLFramerateLogging->setChecked(cfg2.enableOpenGLFramerateLogging(requestDefault)); + chkBrushSpeedLogging->setChecked(cfg2.enableBrushSpeedLogging(requestDefault)); chkDisableVectorOptimizations->setChecked(cfg2.enableAmdVectorizationWorkaround(requestDefault)); } } void PerformanceTab::save() { KisImageConfig cfg; cfg.setMemoryHardLimitPercent(sliderMemoryLimit->value()); cfg.setMemorySoftLimitPercent(sliderUndoLimit->value()); cfg.setMemoryPoolLimitPercent(sliderPoolLimit->value()); cfg.setEnablePerfLog(chkPerformanceLogging->isChecked()); cfg.setEnableProgressReporting(chkProgressReporting->isChecked()); cfg.setMaxSwapSize(sliderSwapSize->value() * 1024); cfg.setSwapDir(lblSwapFileLocation->text()); cfg.setMaxNumberOfThreads(sliderThreadsLimit->value()); cfg.setFrameRenderingClones(sliderFrameClonesLimit->value()); cfg.setFpsLimit(sliderFpsLimit->value()); { KisConfig cfg2; cfg2.setEnableOpenGLFramerateLogging(chkOpenGLFramerateLogging->isChecked()); + cfg2.setEnableBrushSpeedLogging(chkBrushSpeedLogging->isChecked()); cfg2.setEnableAmdVectorizationWorkaround(chkDisableVectorOptimizations->isChecked()); } } void PerformanceTab::selectSwapDir() { KisImageConfig cfg; QString swapDir = cfg.swapDir(); swapDir = QFileDialog::getExistingDirectory(0, i18nc("@title:window", "Select a swap directory"), swapDir); if (swapDir.isEmpty()) { return; } lblSwapFileLocation->setText(swapDir); } void PerformanceTab::slotThreadsLimitChanged(int value) { KisSignalsBlocker b(sliderFrameClonesLimit); sliderFrameClonesLimit->setValue(qMin(m_lastUsedClonesLimit, value)); m_lastUsedThreadsLimit = value; } void PerformanceTab::slotFrameClonesLimitChanged(int value) { KisSignalsBlocker b(sliderThreadsLimit); sliderThreadsLimit->setValue(qMax(m_lastUsedThreadsLimit, value)); m_lastUsedClonesLimit = value; } void PerformanceTab::slotFpsLimitChanged(int value) { KisSignalsBlocker b(sliderFrameClonesLimit); sliderFrameClonesLimit->setValue(qMax(m_lastUsedFpsLimit, value)); m_lastUsedFpsLimit = value; } //--------------------------------------------------------------------------------------------------- #include "KoColor.h" DisplaySettingsTab::DisplaySettingsTab(QWidget *parent, const char *name) : WdgDisplaySettings(parent, name) { KisConfig cfg; const QString rendererOpenGLText = i18nc("canvas renderer", "OpenGL"); const QString rendererAngleText = i18nc("canvas renderer", "Direct3D 11 via ANGLE"); #ifdef Q_OS_WIN cmbRenderer->clear(); QString qtPreferredRendererText; if (KisOpenGL::getQtPreferredOpenGLRenderer() == KisOpenGL::RendererAngle) { qtPreferredRendererText = rendererAngleText; } else { qtPreferredRendererText = rendererOpenGLText; } cmbRenderer->addItem(i18nc("canvas renderer", "Auto (%1)", qtPreferredRendererText), KisOpenGL::RendererAuto); cmbRenderer->setCurrentIndex(0); if (KisOpenGL::getSupportedOpenGLRenderers() & KisOpenGL::RendererDesktopGL) { cmbRenderer->addItem(rendererOpenGLText, KisOpenGL::RendererDesktopGL); if (KisOpenGL::getNextUserOpenGLRendererConfig() == KisOpenGL::RendererDesktopGL) { cmbRenderer->setCurrentIndex(cmbRenderer->count() - 1); } } if (KisOpenGL::getSupportedOpenGLRenderers() & KisOpenGL::RendererAngle) { cmbRenderer->addItem(rendererAngleText, KisOpenGL::RendererAngle); if (KisOpenGL::getNextUserOpenGLRendererConfig() == KisOpenGL::RendererAngle) { cmbRenderer->setCurrentIndex(cmbRenderer->count() - 1); } } #else lblRenderer->setEnabled(false); cmbRenderer->setEnabled(false); cmbRenderer->clear(); cmbRenderer->addItem(rendererOpenGLText); cmbRenderer->setCurrentIndex(0); #endif #ifdef Q_OS_WIN if (!(KisOpenGL::getSupportedOpenGLRenderers() & (KisOpenGL::RendererDesktopGL | KisOpenGL::RendererAngle))) { #else if (!KisOpenGL::hasOpenGL()) { #endif grpOpenGL->setEnabled(false); grpOpenGL->setChecked(false); chkUseTextureBuffer->setEnabled(false); chkDisableVsync->setEnabled(false); cmbFilterMode->setEnabled(false); } else { grpOpenGL->setEnabled(true); grpOpenGL->setChecked(cfg.useOpenGL()); chkUseTextureBuffer->setEnabled(cfg.useOpenGL()); chkUseTextureBuffer->setChecked(cfg.useOpenGLTextureBuffer()); chkDisableVsync->setVisible(cfg.showAdvancedOpenGLSettings()); chkDisableVsync->setEnabled(cfg.useOpenGL()); chkDisableVsync->setChecked(cfg.disableVSync()); cmbFilterMode->setEnabled(cfg.useOpenGL()); cmbFilterMode->setCurrentIndex(cfg.openGLFilteringMode()); // Don't show the high quality filtering mode if it's not available if (!KisOpenGL::supportsLoD()) { cmbFilterMode->removeItem(3); } } if (qApp->applicationName() == "kritasketch" || qApp->applicationName() == "kritagemini") { grpOpenGL->setVisible(false); grpOpenGL->setMaximumHeight(0); } KoColor c; c.fromQColor(cfg.selectionOverlayMaskColor()); c.setOpacity(1.0); btnSelectionOverlayColor->setColor(c); sldSelectionOverlayOpacity->setRange(0.0, 1.0, 2); sldSelectionOverlayOpacity->setSingleStep(0.05); sldSelectionOverlayOpacity->setValue(cfg.selectionOverlayMaskColor().alphaF()); intCheckSize->setValue(cfg.checkSize()); chkMoving->setChecked(cfg.scrollCheckers()); KoColor ck1(KoColorSpaceRegistry::instance()->rgb8()); ck1.fromQColor(cfg.checkersColor1()); colorChecks1->setColor(ck1); KoColor ck2(KoColorSpaceRegistry::instance()->rgb8()); ck2.fromQColor(cfg.checkersColor2()); colorChecks2->setColor(ck2); KoColor cb(KoColorSpaceRegistry::instance()->rgb8()); cb.fromQColor(cfg.canvasBorderColor()); canvasBorder->setColor(cb); hideScrollbars->setChecked(cfg.hideScrollbars()); chkCurveAntialiasing->setChecked(cfg.antialiasCurves()); chkSelectionOutlineAntialiasing->setChecked(cfg.antialiasSelectionOutline()); chkChannelsAsColor->setChecked(cfg.showSingleChannelAsColor()); chkHidePopups->setChecked(cfg.hidePopups()); connect(grpOpenGL, SIGNAL(toggled(bool)), SLOT(slotUseOpenGLToggled(bool))); KoColor gridColor(KoColorSpaceRegistry::instance()->rgb8()); gridColor.fromQColor(cfg.getPixelGridColor()); pixelGridColorButton->setColor(gridColor); pixelGridDrawingThresholdBox->setValue(cfg.getPixelGridDrawingThreshold() * 100); } void DisplaySettingsTab::setDefault() { KisConfig cfg; cmbRenderer->setCurrentIndex(0); #ifdef Q_OS_WIN if (!(KisOpenGL::getSupportedOpenGLRenderers() & (KisOpenGL::RendererDesktopGL | KisOpenGL::RendererAngle))) { #else if (!KisOpenGL::hasOpenGL()) { #endif grpOpenGL->setEnabled(false); grpOpenGL->setChecked(false); chkUseTextureBuffer->setEnabled(false); chkDisableVsync->setEnabled(false); cmbFilterMode->setEnabled(false); } else { grpOpenGL->setEnabled(true); grpOpenGL->setChecked(cfg.useOpenGL(true)); chkUseTextureBuffer->setChecked(cfg.useOpenGLTextureBuffer(true)); chkUseTextureBuffer->setEnabled(true); chkDisableVsync->setEnabled(true); chkDisableVsync->setChecked(cfg.disableVSync(true)); cmbFilterMode->setEnabled(true); cmbFilterMode->setCurrentIndex(cfg.openGLFilteringMode(true)); } chkMoving->setChecked(cfg.scrollCheckers(true)); intCheckSize->setValue(cfg.checkSize(true)); KoColor ck1(KoColorSpaceRegistry::instance()->rgb8()); ck1.fromQColor(cfg.checkersColor1(true)); colorChecks1->setColor(ck1); KoColor ck2(KoColorSpaceRegistry::instance()->rgb8()); ck2.fromQColor(cfg.checkersColor2(true)); colorChecks2->setColor(ck2); KoColor cvb(KoColorSpaceRegistry::instance()->rgb8()); cvb.fromQColor(cfg.canvasBorderColor(true)); canvasBorder->setColor(cvb); hideScrollbars->setChecked(cfg.hideScrollbars(true)); chkCurveAntialiasing->setChecked(cfg.antialiasCurves(true)); chkSelectionOutlineAntialiasing->setChecked(cfg.antialiasSelectionOutline(true)); chkChannelsAsColor->setChecked(cfg.showSingleChannelAsColor(true)); chkHidePopups->setChecked(cfg.hidePopups(true)); KoColor gridColor(KoColorSpaceRegistry::instance()->rgb8()); gridColor.fromQColor(cfg.getPixelGridColor(true)); pixelGridColorButton->setColor(gridColor); pixelGridDrawingThresholdBox->setValue(cfg.getPixelGridDrawingThreshold(true) * 100); } void DisplaySettingsTab::slotUseOpenGLToggled(bool isChecked) { chkUseTextureBuffer->setEnabled(isChecked); chkDisableVsync->setEnabled(isChecked); cmbFilterMode->setEnabled(isChecked); } //--------------------------------------------------------------------------------------------------- FullscreenSettingsTab::FullscreenSettingsTab(QWidget* parent) : WdgFullscreenSettingsBase(parent) { KisConfig cfg; chkDockers->setChecked(cfg.hideDockersFullscreen()); chkMenu->setChecked(cfg.hideMenuFullscreen()); chkScrollbars->setChecked(cfg.hideScrollbarsFullscreen()); chkStatusbar->setChecked(cfg.hideStatusbarFullscreen()); chkTitlebar->setChecked(cfg.hideTitlebarFullscreen()); chkToolbar->setChecked(cfg.hideToolbarFullscreen()); } void FullscreenSettingsTab::setDefault() { KisConfig cfg; chkDockers->setChecked(cfg.hideDockersFullscreen(true)); chkMenu->setChecked(cfg.hideMenuFullscreen(true)); chkScrollbars->setChecked(cfg.hideScrollbarsFullscreen(true)); chkStatusbar->setChecked(cfg.hideStatusbarFullscreen(true)); chkTitlebar->setChecked(cfg.hideTitlebarFullscreen(true)); chkToolbar->setChecked(cfg.hideToolbarFullscreen(true)); } //--------------------------------------------------------------------------------------------------- KisDlgPreferences::KisDlgPreferences(QWidget* parent, const char* name) : KPageDialog(parent) { Q_UNUSED(name); setWindowTitle(i18n("Configure Krita")); setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::RestoreDefaults); button(QDialogButtonBox::Ok)->setDefault(true); setFaceType(KPageDialog::Tree); // General KoVBox *vbox = new KoVBox(); KPageWidgetItem *page = new KPageWidgetItem(vbox, i18n("General")); page->setObjectName("general"); page->setHeader(i18n("General")); page->setIcon(KisIconUtils::loadIcon("go-home")); addPage(page); m_general = new GeneralTab(vbox); // Shortcuts vbox = new KoVBox(); page = new KPageWidgetItem(vbox, i18n("Keyboard Shortcuts")); page->setObjectName("shortcuts"); page->setHeader(i18n("Shortcuts")); page->setIcon(KisIconUtils::loadIcon("document-export")); addPage(page); m_shortcutSettings = new ShortcutSettingsTab(vbox); connect(this, SIGNAL(accepted()), m_shortcutSettings, SLOT(saveChanges())); connect(this, SIGNAL(rejected()), m_shortcutSettings, SLOT(cancelChanges())); // Canvas input settings m_inputConfiguration = new KisInputConfigurationPage(); page = addPage(m_inputConfiguration, i18n("Canvas Input Settings")); page->setHeader(i18n("Canvas Input")); page->setObjectName("canvasinput"); page->setIcon(KisIconUtils::loadIcon("configure")); // Display vbox = new KoVBox(); page = new KPageWidgetItem(vbox, i18n("Display")); page->setObjectName("display"); page->setHeader(i18n("Display")); page->setIcon(KisIconUtils::loadIcon("preferences-desktop-display")); addPage(page); m_displaySettings = new DisplaySettingsTab(vbox); // Color vbox = new KoVBox(); page = new KPageWidgetItem(vbox, i18n("Color Management")); page->setObjectName("colormanagement"); page->setHeader(i18n("Color")); page->setIcon(KisIconUtils::loadIcon("preferences-desktop-color")); addPage(page); m_colorSettings = new ColorSettingsTab(vbox); // Performance vbox = new KoVBox(); page = new KPageWidgetItem(vbox, i18n("Performance")); page->setObjectName("performance"); page->setHeader(i18n("Performance")); page->setIcon(KisIconUtils::loadIcon("applications-system")); addPage(page); m_performanceSettings = new PerformanceTab(vbox); // Tablet vbox = new KoVBox(); page = new KPageWidgetItem(vbox, i18n("Tablet settings")); page->setObjectName("tablet"); page->setHeader(i18n("Tablet")); page->setIcon(KisIconUtils::loadIcon("document-edit")); addPage(page); m_tabletSettings = new TabletSettingsTab(vbox); // full-screen mode vbox = new KoVBox(); page = new KPageWidgetItem(vbox, i18n("Canvas-only settings")); page->setObjectName("canvasonly"); page->setHeader(i18n("Canvas-only")); page->setIcon(KisIconUtils::loadIcon("folder-pictures")); addPage(page); m_fullscreenSettings = new FullscreenSettingsTab(vbox); // Author profiles m_authorPage = new KoConfigAuthorPage(); page = addPage(m_authorPage, i18nc("@title:tab Author page", "Author" )); page->setObjectName("author"); page->setHeader(i18n("Author")); page->setIcon(KisIconUtils::loadIcon("im-user")); QPushButton *restoreDefaultsButton = button(QDialogButtonBox::RestoreDefaults); restoreDefaultsButton->setText("Restore Defaults"); connect(this, SIGNAL(accepted()), m_inputConfiguration, SLOT(saveChanges())); connect(this, SIGNAL(rejected()), m_inputConfiguration, SLOT(revertChanges())); KisPreferenceSetRegistry *preferenceSetRegistry = KisPreferenceSetRegistry::instance(); Q_FOREACH (KisAbstractPreferenceSetFactory *preferenceSetFactory, preferenceSetRegistry->values()) { KisPreferenceSet* preferenceSet = preferenceSetFactory->createPreferenceSet(); vbox = new KoVBox(); page = new KPageWidgetItem(vbox, preferenceSet->name()); page->setHeader(preferenceSet->header()); page->setIcon(preferenceSet->icon()); addPage(page); preferenceSet->setParent(vbox); preferenceSet->loadPreferences(); connect(restoreDefaultsButton, SIGNAL(clicked(bool)), preferenceSet, SLOT(loadDefaultPreferences()), Qt::UniqueConnection); connect(this, SIGNAL(accepted()), preferenceSet, SLOT(savePreferences()), Qt::UniqueConnection); } connect(restoreDefaultsButton, SIGNAL(clicked(bool)), this, SLOT(slotDefault())); } KisDlgPreferences::~KisDlgPreferences() { } void KisDlgPreferences::slotDefault() { if (currentPage()->objectName() == "general") { m_general->setDefault(); } else if (currentPage()->objectName() == "shortcuts") { m_shortcutSettings->setDefault(); } else if (currentPage()->objectName() == "display") { m_displaySettings->setDefault(); } else if (currentPage()->objectName() == "colormanagement") { m_colorSettings->setDefault(); } else if (currentPage()->objectName() == "performance") { m_performanceSettings->load(true); } else if (currentPage()->objectName() == "tablet") { m_tabletSettings->setDefault(); } else if (currentPage()->objectName() == "canvasonly") { m_fullscreenSettings->setDefault(); } else if (currentPage()->objectName() == "canvasinput") { m_inputConfiguration->setDefaults(); } } bool KisDlgPreferences::editPreferences() { KisDlgPreferences* dialog; dialog = new KisDlgPreferences(); bool baccept = (dialog->exec() == Accepted); if (baccept) { // General settings KisConfig cfg; cfg.setNewCursorStyle(dialog->m_general->cursorStyle()); cfg.setNewOutlineStyle(dialog->m_general->outlineStyle()); cfg.setShowRootLayer(dialog->m_general->showRootLayer()); cfg.setShowOutlineWhilePainting(dialog->m_general->showOutlineWhilePainting()); cfg.setHideSplashScreen(dialog->m_general->hideSplashScreen()); cfg.setCalculateAnimationCacheInBackground(dialog->m_general->calculateAnimationCacheInBackground()); KConfigGroup group = KSharedConfig::openConfig()->group("File Dialogs"); group.writeEntry("DontUseNativeFileDialog", !dialog->m_general->m_chkNativeFileDialog->isChecked()); cfg.writeEntry("maximumBrushSize", dialog->m_general->intMaxBrushSize->value()); cfg.writeEntry("mdi_viewmode", dialog->m_general->mdiMode()); cfg.setMDIBackgroundColor(dialog->m_general->m_mdiColor->color().toQColor()); cfg.setMDIBackgroundImage(dialog->m_general->m_backgroundimage->text()); cfg.setAutoSaveInterval(dialog->m_general->autoSaveInterval()); cfg.setBackupFile(dialog->m_general->m_backupFileCheckBox->isChecked()); cfg.setShowCanvasMessages(dialog->m_general->showCanvasMessages()); cfg.setCompressKra(dialog->m_general->compressKra()); const QString configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); QSettings kritarc(configPath + QStringLiteral("/kritadisplayrc"), QSettings::IniFormat); kritarc.setValue("EnableHiDPI", dialog->m_general->m_chkHiDPI->isChecked()); kritarc.setValue("EnableSingleApplication", dialog->m_general->m_chkSingleApplication->isChecked()); cfg.setToolOptionsInDocker(dialog->m_general->toolOptionsInDocker()); cfg.setSwitchSelectionCtrlAlt(dialog->m_general->switchSelectionCtrlAlt()); cfg.setDisableTouchOnCanvas(!dialog->m_general->chkEnableTouch->isChecked()); cfg.setConvertToImageColorspaceOnImport(dialog->m_general->convertToImageColorspaceOnImport()); cfg.setUndoStackLimit(dialog->m_general->undoStackSize()); cfg.setFavoritePresets(dialog->m_general->favoritePresets()); // Color settings cfg.setUseSystemMonitorProfile(dialog->m_colorSettings->m_page->chkUseSystemMonitorProfile->isChecked()); for (int i = 0; i < QApplication::desktop()->screenCount(); ++i) { if (dialog->m_colorSettings->m_page->chkUseSystemMonitorProfile->isChecked()) { int currentIndex = dialog->m_colorSettings->m_monitorProfileWidgets[i]->currentIndex(); QString monitorid = dialog->m_colorSettings->m_monitorProfileWidgets[i]->itemData(currentIndex).toString(); cfg.setMonitorForScreen(i, monitorid); } else { cfg.setMonitorProfile(i, dialog->m_colorSettings->m_monitorProfileWidgets[i]->itemHighlighted(), dialog->m_colorSettings->m_page->chkUseSystemMonitorProfile->isChecked()); } } cfg.setWorkingColorSpace(dialog->m_colorSettings->m_page->cmbWorkingColorSpace->currentItem().id()); KisImageConfig cfgImage; cfgImage.setDefaultProofingConfig(dialog->m_colorSettings->m_page->proofingSpaceSelector->currentColorSpace(), dialog->m_colorSettings->m_page->cmbProofingIntent->currentIndex(), dialog->m_colorSettings->m_page->ckbProofBlackPoint->isChecked(), dialog->m_colorSettings->m_page->gamutAlarm->color(), (double)dialog->m_colorSettings->m_page->sldAdaptationState->value()/20); cfg.setUseBlackPointCompensation(dialog->m_colorSettings->m_page->chkBlackpoint->isChecked()); cfg.setAllowLCMSOptimization(dialog->m_colorSettings->m_page->chkAllowLCMSOptimization->isChecked()); cfg.setPasteBehaviour(dialog->m_colorSettings->m_pasteBehaviourGroup.checkedId()); cfg.setRenderIntent(dialog->m_colorSettings->m_page->cmbMonitorIntent->currentIndex()); // Tablet settings cfg.setPressureTabletCurve( dialog->m_tabletSettings->m_page->pressureCurve->curve().toString() ); #ifdef Q_OS_WIN if (KisTabletSupportWin8::isAvailable()) { cfg.setUseWin8PointerInput(dialog->m_tabletSettings->m_page->radioWin8PointerInput->isChecked()); } #endif dialog->m_performanceSettings->save(); #ifdef Q_OS_WIN { KisOpenGL::OpenGLRenderer renderer = static_cast( dialog->m_displaySettings->cmbRenderer->itemData( dialog->m_displaySettings->cmbRenderer->currentIndex()).toInt()); KisOpenGL::setNextUserOpenGLRendererConfig(renderer); const QString configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); QSettings kritarc(configPath + QStringLiteral("/kritadisplayrc"), QSettings::IniFormat); kritarc.setValue("OpenGLRenderer", KisOpenGL::convertOpenGLRendererToConfig(renderer)); } #endif if (!cfg.useOpenGL() && dialog->m_displaySettings->grpOpenGL->isChecked()) cfg.setCanvasState("TRY_OPENGL"); cfg.setUseOpenGL(dialog->m_displaySettings->grpOpenGL->isChecked()); cfg.setUseOpenGLTextureBuffer(dialog->m_displaySettings->chkUseTextureBuffer->isChecked()); cfg.setOpenGLFilteringMode(dialog->m_displaySettings->cmbFilterMode->currentIndex()); cfg.setDisableVSync(dialog->m_displaySettings->chkDisableVsync->isChecked()); cfg.setCheckSize(dialog->m_displaySettings->intCheckSize->value()); cfg.setScrollingCheckers(dialog->m_displaySettings->chkMoving->isChecked()); cfg.setCheckersColor1(dialog->m_displaySettings->colorChecks1->color().toQColor()); cfg.setCheckersColor2(dialog->m_displaySettings->colorChecks2->color().toQColor()); cfg.setCanvasBorderColor(dialog->m_displaySettings->canvasBorder->color().toQColor()); cfg.setHideScrollbars(dialog->m_displaySettings->hideScrollbars->isChecked()); KoColor c = dialog->m_displaySettings->btnSelectionOverlayColor->color(); c.setOpacity(dialog->m_displaySettings->sldSelectionOverlayOpacity->value()); cfg.setSelectionOverlayMaskColor(c.toQColor()); cfg.setAntialiasCurves(dialog->m_displaySettings->chkCurveAntialiasing->isChecked()); cfg.setAntialiasSelectionOutline(dialog->m_displaySettings->chkSelectionOutlineAntialiasing->isChecked()); cfg.setShowSingleChannelAsColor(dialog->m_displaySettings->chkChannelsAsColor->isChecked()); cfg.setHidePopups(dialog->m_displaySettings->chkHidePopups->isChecked()); cfg.setHideDockersFullscreen(dialog->m_fullscreenSettings->chkDockers->checkState()); cfg.setHideMenuFullscreen(dialog->m_fullscreenSettings->chkMenu->checkState()); cfg.setHideScrollbarsFullscreen(dialog->m_fullscreenSettings->chkScrollbars->checkState()); cfg.setHideStatusbarFullscreen(dialog->m_fullscreenSettings->chkStatusbar->checkState()); cfg.setHideTitlebarFullscreen(dialog->m_fullscreenSettings->chkTitlebar->checkState()); cfg.setHideToolbarFullscreen(dialog->m_fullscreenSettings->chkToolbar->checkState()); cfg.setCursorMainColor(dialog->m_general->cursorColorBtutton->color().toQColor()); cfg.setPixelGridColor(dialog->m_displaySettings->pixelGridColorButton->color().toQColor()); cfg.setPixelGridDrawingThreshold(dialog->m_displaySettings->pixelGridDrawingThresholdBox->value() / 100); dialog->m_authorPage->apply(); } delete dialog; return baccept; } diff --git a/libs/ui/forms/wdgcolorsettings.ui b/libs/ui/forms/wdgcolorsettings.ui index b9f45eacd8..ba8713d174 100644 --- a/libs/ui/forms/wdgcolorsettings.ui +++ b/libs/ui/forms/wdgcolorsettings.ui @@ -1,416 +1,416 @@ WdgColorSettings 0 0 520 335 Color Settings - 2 + 0 General Default color model for new images: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 0 20 When Pasting Into Krita From Other Applications Assume sRGB (like images from the web are supposed to be seen) Assume &monitor profile (like you see it in the other application) As&k each time Note: When copying/pasting inside Krita color info is always preserved. Use Blackpoint Compensation true Allow Little CMS optimizations (uncheck when using linear light RGB or XYZ) true Qt::Vertical 20 40 Display Use system monitor profile false 50 50 0 0 0 0 The icm profile for your calibrated monitor &Rendering intent: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter cmbMonitorIntent 0 0 Perceptual Relative Colorimetric Saturation Absolute Colorimetric Add new color profile: 24 24 Qt::Horizontal 40 20 Qt::Vertical 20 40 Soft Proofing 100 100 Perceptual Relative Colorimetric Saturation Absolute Colorimetric Adaptation State: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Proofing Rendering Intent: Gamut Warning: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Qt::Horizontal Black Point Compensation Qt::Vertical 20 40 Qt::Vertical 20 40 KisColorButton QPushButton
kis_color_button.h
KisColorSpaceSelector QWidget
widgets/kis_color_space_selector.h
1
KComboBox QComboBox
kcombobox.h
KisCmbIDList QComboBox
widgets/kis_cmb_idlist.h
diff --git a/libs/ui/forms/wdgperformancesettings.ui b/libs/ui/forms/wdgperformancesettings.ui index cc5291ce8b..73544802cf 100644 --- a/libs/ui/forms/wdgperformancesettings.ui +++ b/libs/ui/forms/wdgperformancesettings.ui @@ -1,442 +1,442 @@ WdgPerformanceSettings 0 0 - 490 - 400 + 505 + 446 75 true Note: Krita will need to be restarted for changes to take affect - 1 + 0 General RAM Memory available: 0 0 XXX MiB Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter Krita will not use more memory than this limit. Memory Limit: 0 0 Krita will not use more memory than this limit. MiB Internal Pool: 0 0 MiB When undo information reaches this limit, it will be stored in a temporary file and memory will be freed. Undo will be slower. Swap Undo After: 0 0 When undo information reaches this limit, it will be stored in a temporary file and memory will be freed. Undo will be slower. MiB Swap File Size 6 The swap file will not be bigger than this limit. File Size Limit: 0 0 The swap file will not be bigger than this limit. GiB Swap File Location: 0 0 QFrame::Box TextLabel Select the location where Krita writes its swap files. ... Qt::Vertical 20 5 Advanced Multithreading CPU Limit: 0 0 <html><head/><body><p>Krita will not use more CPU cores than selected by this limit</p></body></html> Frame Rendering Clones Limit 0 0 <html><head/><body><p>When rendering animation frames (into files or during animation cache regeneration), Krita will make the specified number of copies of your image and will work on them in parallel. Each copy will demand more RAM for its storage (about 20% of the size of you image), so raise this limit only if you have enough RAM installed.</p><p><br/></p><p><span style=" font-weight:600;">Recommended value:</span> set Clones Limit to the number of <span style=" text-decoration: underline;">physical</span> (non-hyperthreaded) cores your CPU has</p></body></html> - label_8 - sliderThreadsLimit - label_9 - sliderFrameClonesLimit - label_fps - label_fps - sliderFpsLimit Limit frames per second while painting: 0 0 <html><head/><body><p>Krita will try to limit the number of screen updates per second to the given number. A lower number will decrease visual responsiveness but increase stylus precision on some systems like macOS.<p></body></html> Debug logging of OpenGL framerate + + + + Debug logging for brush rendering speed + + + Disable vector optimizations (for AMD CPUs) Progress reporting (might affect performance) Performance logging QFrame::NoFrame <html><head/><body><p>When performance logging is enabled Krita saves timing information into the '&lt;working_dir&gt;/log' folder. If you experience performance problems and want to help us, enable this option and add the contents of the directory to a bug report.</p></body></html> true Qt::Vertical 20 5 Qt::Vertical 20 5 KisIntParseSpinBox QSpinBox
kis_int_parse_spin_box.h
KisDoubleSliderSpinBox QWidget
kis_slider_spin_box.h
1
KisSliderSpinBox QWidget
kis_slider_spin_box.h
1
diff --git a/libs/ui/input/KisQtWidgetsTweaker.cpp b/libs/ui/input/KisQtWidgetsTweaker.cpp index ed323d3b7f..5e1153504e 100644 --- a/libs/ui/input/KisQtWidgetsTweaker.cpp +++ b/libs/ui/input/KisQtWidgetsTweaker.cpp @@ -1,333 +1,339 @@ /* This file is part of the KDE project Copyright (C) 2017 Nikita Vertikov This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KisQtWidgetsTweaker.h" #include #include #include #include #include #include #include #include #include #include "opengl/kis_opengl_canvas2.h" #include "canvas/kis_qpainter_canvas.h" #include "KisMainWindow.h" Q_GLOBAL_STATIC(KisQtWidgetsTweaker, kqwt_instance) namespace { class ShortcutOverriderBase { public: enum class DecisionOnShortcutOverride { overrideShortcut, askNext, dontOverrideShortcut }; constexpr ShortcutOverriderBase() = default; virtual ~ShortcutOverriderBase() {} virtual bool interestedInEvent(QKeyEvent *event) = 0; virtual DecisionOnShortcutOverride handleEvent(QObject *receiver, QKeyEvent *event) = 0; virtual DecisionOnShortcutOverride finishedPhysicalKeyPressHandling() { return DecisionOnShortcutOverride::askNext; } }; class LineTextEditingShortcutOverrider : public ShortcutOverriderBase { public: constexpr LineTextEditingShortcutOverrider() = default; virtual bool interestedInEvent(QKeyEvent *event) override { static constexpr QKeySequence::StandardKey actionsForQLineEdit[]{ QKeySequence::MoveToNextChar ,QKeySequence::MoveToPreviousChar ,QKeySequence::MoveToStartOfLine ,QKeySequence::MoveToEndOfLine ,QKeySequence::MoveToPreviousWord ,QKeySequence::MoveToNextWord ,QKeySequence::SelectPreviousChar ,QKeySequence::SelectNextChar ,QKeySequence::SelectNextWord ,QKeySequence::SelectPreviousWord ,QKeySequence::SelectStartOfLine ,QKeySequence::SelectEndOfLine ,QKeySequence::SelectAll ,QKeySequence::Deselect ,QKeySequence::Backspace ,QKeySequence::DeleteStartOfWord ,QKeySequence::Delete ,QKeySequence::DeleteEndOfWord ,QKeySequence::DeleteEndOfLine ,QKeySequence::Copy ,QKeySequence::Paste ,QKeySequence::Cut ,QKeySequence::Undo ,QKeySequence::Redo }; for (QKeySequence::StandardKey sk : actionsForQLineEdit) { if (event->matches(sk)) { event->accept(); return true; } } return false; } virtual DecisionOnShortcutOverride handleEvent(QObject *receiver, QKeyEvent *event) override { + Q_UNUSED(event); + if ((qobject_cast (receiver) != nullptr)|| (qobject_cast (receiver) != nullptr)|| (qobject_cast(receiver) != nullptr)) { return DecisionOnShortcutOverride::overrideShortcut; } else { return DecisionOnShortcutOverride::askNext; } } }; class SpingboxShortcutOverrider : public ShortcutOverriderBase { public: constexpr SpingboxShortcutOverrider() = default; virtual bool interestedInEvent(QKeyEvent *event) override { if (event->modifiers() != Qt::NoModifier) { return false; } switch (event->key()) { case Qt::Key_Down: case Qt::Key_Up: case Qt::Key_PageDown: case Qt::Key_PageUp: event->accept(); return true; default: return false; } } virtual DecisionOnShortcutOverride handleEvent(QObject *receiver, QKeyEvent *event) override { + Q_UNUSED(event); + if (qobject_cast (receiver) != nullptr|| qobject_cast(receiver) != nullptr) { return DecisionOnShortcutOverride::overrideShortcut; } else { return DecisionOnShortcutOverride::askNext; } } }; class TabShortcutOverrider : public ShortcutOverriderBase { public: constexpr TabShortcutOverrider() = default; virtual bool interestedInEvent(QKeyEvent *event) override { bool tab = event->modifiers() == Qt::NoModifier && ( event->key() == Qt::Key_Tab || event->key() == Qt::Key_Backtab); bool shiftTab = event->modifiers() == Qt::ShiftModifier && event->key() == Qt::Key_Backtab; if (tab || shiftTab) { return true; }else{ return false; } } virtual DecisionOnShortcutOverride handleEvent(QObject *receiver, QKeyEvent *event) override { + Q_UNUSED(event); + if (qobject_cast(receiver) != nullptr|| qobject_cast (receiver) != nullptr) { return DecisionOnShortcutOverride::dontOverrideShortcut; } else { m_nooverride = true; return DecisionOnShortcutOverride::askNext; } } virtual DecisionOnShortcutOverride finishedPhysicalKeyPressHandling() override { if (m_nooverride){ m_nooverride = false; return DecisionOnShortcutOverride::overrideShortcut; } return DecisionOnShortcutOverride::askNext; } private: bool m_nooverride = false; }; //for some reason I can't just populate constexpr //pointer array using "new" LineTextEditingShortcutOverrider overrider0; SpingboxShortcutOverrider overrider1; TabShortcutOverrider overrider2; constexpr ShortcutOverriderBase *allShortcutOverriders[] = { &overrider0, &overrider1, &overrider2 }; -constexpr size_t numOfShortcutOverriders = +constexpr int numOfShortcutOverriders = sizeof(allShortcutOverriders)/ sizeof(allShortcutOverriders[0]); } //namespace struct KisQtWidgetsTweaker::Private { public: Private(KisQtWidgetsTweaker *parent) : q(parent) + , interestedHandlers(numOfShortcutOverriders) , lastKeyPressProcessingComplete(true) , decision(ShortcutOverriderBase::DecisionOnShortcutOverride::askNext) - , interestedHandlers(numOfShortcutOverriders) { } const KisQtWidgetsTweaker *q; QBitArray interestedHandlers = QBitArray(numOfShortcutOverriders); ShortcutOverriderBase::DecisionOnShortcutOverride decision = ShortcutOverriderBase::DecisionOnShortcutOverride::askNext; //unsigned long lastEventTimestamp=0; bool lastKeyPressProcessingComplete = true; void newPhysicalKeyPressed(QKeyEvent *event) { for (int i=0; i < numOfShortcutOverriders; ++i) { if (allShortcutOverriders[i]->interestedInEvent(event)) { interestedHandlers.setBit(i); }else{ interestedHandlers.clearBit(i); } } decision = ShortcutOverriderBase::DecisionOnShortcutOverride::askNext; lastKeyPressProcessingComplete = false; } }; KisQtWidgetsTweaker::KisQtWidgetsTweaker(QObject *parent) :QObject(parent) , d(new KisQtWidgetsTweaker::Private(this)) { } KisQtWidgetsTweaker::~KisQtWidgetsTweaker() { delete d; } bool KisQtWidgetsTweaker::eventFilter(QObject *receiver, QEvent *event) { switch(event->type()) { case QEvent::ShortcutOverride:{ //QLineEdit and other widgets lets qt's shortcut system take away it's keyboard events //even is it knows them, such as ctrl+backspace //if there is application-wide shortcut, assigned to it. //The following code fixes it //by accepting ShortcutOverride events. //if you press key 'a' and then 'b', qt at first call //all handlers for 'a' key press event, and only after that //for 'b' QKeyEvent *key = static_cast(event); if (d->lastKeyPressProcessingComplete) { d->newPhysicalKeyPressed(key); } for(int i = 0; i < numOfShortcutOverriders; ++i) { if (d->decision != ShortcutOverriderBase::DecisionOnShortcutOverride::askNext) { break; } if (d->interestedHandlers.at(i)) { d->decision = allShortcutOverriders[i]->handleEvent(receiver, key); } } //if nothing said wether shortcutoverride to be accepted //last widget that qt will ask will be kismainwindow or docker if (qobject_cast(receiver)!=nullptr|| receiver->inherits(QDockWidget::staticMetaObject.className())) { for (int i = 0; i < numOfShortcutOverriders; ++i) { if (d->decision != ShortcutOverriderBase::DecisionOnShortcutOverride::askNext) { break; } if (d->interestedHandlers.at(i)) { d->decision = allShortcutOverriders[i]->finishedPhysicalKeyPressHandling(); } } d->lastKeyPressProcessingComplete = true; } bool retval = false; switch (d->decision) { case ShortcutOverriderBase::DecisionOnShortcutOverride::askNext: event->ignore(); retval = false; break; case ShortcutOverriderBase::DecisionOnShortcutOverride::dontOverrideShortcut: event->ignore(); retval = true; break; case ShortcutOverriderBase::DecisionOnShortcutOverride::overrideShortcut: event->accept(); //once shortcutoverride acepted, qt stop asking everyone //about it and proceed to handling next event d->lastKeyPressProcessingComplete = true; retval = true; break; } return retval || QObject::eventFilter(receiver, event); }break; //other event types default: break; } //code for tweaking the behavior of other qt elements will go here return QObject::eventFilter(receiver, event); } KisQtWidgetsTweaker *KisQtWidgetsTweaker::instance() { return kqwt_instance; } diff --git a/libs/ui/kis_config.cc b/libs/ui/kis_config.cc index 06cf057932..3231fd3cd4 100644 --- a/libs/ui/kis_config.cc +++ b/libs/ui/kis_config.cc @@ -1,1933 +1,1943 @@ /* * Copyright (c) 2002 Patrick Julien * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_canvas_resource_provider.h" #include "kis_config_notifier.h" #include "kis_snap_config.h" #include #include KisConfig::KisConfig() : m_cfg( KSharedConfig::openConfig()->group("")) { } KisConfig::~KisConfig() { if (qApp->thread() != QThread::currentThread()) { //dbgKrita << "WARNING: KisConfig: requested config synchronization from nonGUI thread! Skipping..."; return; } m_cfg.sync(); } bool KisConfig::disableTouchOnCanvas(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("disableTouchOnCanvas", false)); } void KisConfig::setDisableTouchOnCanvas(bool value) const { m_cfg.writeEntry("disableTouchOnCanvas", value); } bool KisConfig::useProjections(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("useProjections", true)); } void KisConfig::setUseProjections(bool useProj) const { m_cfg.writeEntry("useProjections", useProj); } bool KisConfig::undoEnabled(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("undoEnabled", true)); } void KisConfig::setUndoEnabled(bool undo) const { m_cfg.writeEntry("undoEnabled", undo); } int KisConfig::undoStackLimit(bool defaultValue) const { return (defaultValue ? 30 : m_cfg.readEntry("undoStackLimit", 30)); } void KisConfig::setUndoStackLimit(int limit) const { m_cfg.writeEntry("undoStackLimit", limit); } bool KisConfig::useCumulativeUndoRedo(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("useCumulativeUndoRedo",false)); } void KisConfig::setCumulativeUndoRedo(bool value) { m_cfg.writeEntry("useCumulativeUndoRedo", value); } qreal KisConfig::stackT1(bool defaultValue) const { return (defaultValue ? 5 : m_cfg.readEntry("stackT1",5)); } void KisConfig::setStackT1(int T1) { m_cfg.writeEntry("stackT1", T1); } qreal KisConfig::stackT2(bool defaultValue) const { return (defaultValue ? 1 : m_cfg.readEntry("stackT2",1)); } void KisConfig::setStackT2(int T2) { m_cfg.writeEntry("stackT2", T2); } int KisConfig::stackN(bool defaultValue) const { return (defaultValue ? 5 : m_cfg.readEntry("stackN",5)); } void KisConfig::setStackN(int N) { m_cfg.writeEntry("stackN", N); } qint32 KisConfig::defImageWidth(bool defaultValue) const { return (defaultValue ? 1600 : m_cfg.readEntry("imageWidthDef", 1600)); } qint32 KisConfig::defImageHeight(bool defaultValue) const { return (defaultValue ? 1200 : m_cfg.readEntry("imageHeightDef", 1200)); } qreal KisConfig::defImageResolution(bool defaultValue) const { return (defaultValue ? 100.0 : m_cfg.readEntry("imageResolutionDef", 100.0)) / 72.0; } QString KisConfig::defColorModel(bool defaultValue) const { return (defaultValue ? KoColorSpaceRegistry::instance()->rgb8()->colorModelId().id() : m_cfg.readEntry("colorModelDef", KoColorSpaceRegistry::instance()->rgb8()->colorModelId().id())); } void KisConfig::defColorModel(const QString & model) const { m_cfg.writeEntry("colorModelDef", model); } QString KisConfig::defaultColorDepth(bool defaultValue) const { return (defaultValue ? KoColorSpaceRegistry::instance()->rgb8()->colorDepthId().id() : m_cfg.readEntry("colorDepthDef", KoColorSpaceRegistry::instance()->rgb8()->colorDepthId().id())); } void KisConfig::setDefaultColorDepth(const QString & depth) const { m_cfg.writeEntry("colorDepthDef", depth); } QString KisConfig::defColorProfile(bool defaultValue) const { return (defaultValue ? KoColorSpaceRegistry::instance()->rgb8()->profile()->name() : m_cfg.readEntry("colorProfileDef", KoColorSpaceRegistry::instance()->rgb8()->profile()->name())); } void KisConfig::defColorProfile(const QString & profile) const { m_cfg.writeEntry("colorProfileDef", profile); } void KisConfig::defImageWidth(qint32 width) const { m_cfg.writeEntry("imageWidthDef", width); } void KisConfig::defImageHeight(qint32 height) const { m_cfg.writeEntry("imageHeightDef", height); } void KisConfig::defImageResolution(qreal res) const { m_cfg.writeEntry("imageResolutionDef", res*72.0); } void cleanOldCursorStyleKeys(KConfigGroup &cfg) { if (cfg.hasKey("newCursorStyle") && cfg.hasKey("newOutlineStyle")) { cfg.deleteEntry("cursorStyleDef"); } } CursorStyle KisConfig::newCursorStyle(bool defaultValue) const { if (defaultValue) { return CURSOR_STYLE_NO_CURSOR; } int style = m_cfg.readEntry("newCursorStyle", int(-1)); if (style < 0) { // old style format style = m_cfg.readEntry("cursorStyleDef", int(OLD_CURSOR_STYLE_OUTLINE)); switch (style) { case OLD_CURSOR_STYLE_TOOLICON: style = CURSOR_STYLE_TOOLICON; break; case OLD_CURSOR_STYLE_CROSSHAIR: case OLD_CURSOR_STYLE_OUTLINE_CENTER_CROSS: style = CURSOR_STYLE_CROSSHAIR; break; case OLD_CURSOR_STYLE_POINTER: style = CURSOR_STYLE_POINTER; break; case OLD_CURSOR_STYLE_OUTLINE: case OLD_CURSOR_STYLE_NO_CURSOR: style = CURSOR_STYLE_NO_CURSOR; break; case OLD_CURSOR_STYLE_SMALL_ROUND: case OLD_CURSOR_STYLE_OUTLINE_CENTER_DOT: style = CURSOR_STYLE_SMALL_ROUND; break; case OLD_CURSOR_STYLE_TRIANGLE_RIGHTHANDED: case OLD_CURSOR_STYLE_OUTLINE_TRIANGLE_RIGHTHANDED: style = CURSOR_STYLE_TRIANGLE_RIGHTHANDED; break; case OLD_CURSOR_STYLE_TRIANGLE_LEFTHANDED: case OLD_CURSOR_STYLE_OUTLINE_TRIANGLE_LEFTHANDED: style = CURSOR_STYLE_TRIANGLE_LEFTHANDED; break; default: style = -1; } } cleanOldCursorStyleKeys(m_cfg); // compatibility with future versions if (style < 0 || style >= N_CURSOR_STYLE_SIZE) { style = CURSOR_STYLE_NO_CURSOR; } return (CursorStyle) style; } void KisConfig::setNewCursorStyle(CursorStyle style) { m_cfg.writeEntry("newCursorStyle", (int)style); } QColor KisConfig::getCursorMainColor(bool defaultValue) const { QColor col; col.setRgbF(0.501961, 1.0, 0.501961); return (defaultValue ? col : m_cfg.readEntry("cursorMaincColor", col)); } void KisConfig::setCursorMainColor(const QColor &v) const { m_cfg.writeEntry("cursorMaincColor", v); } OutlineStyle KisConfig::newOutlineStyle(bool defaultValue) const { if (defaultValue) { return OUTLINE_FULL; } int style = m_cfg.readEntry("newOutlineStyle", int(-1)); if (style < 0) { // old style format style = m_cfg.readEntry("cursorStyleDef", int(OLD_CURSOR_STYLE_OUTLINE)); switch (style) { case OLD_CURSOR_STYLE_TOOLICON: case OLD_CURSOR_STYLE_CROSSHAIR: case OLD_CURSOR_STYLE_POINTER: case OLD_CURSOR_STYLE_NO_CURSOR: case OLD_CURSOR_STYLE_SMALL_ROUND: case OLD_CURSOR_STYLE_TRIANGLE_RIGHTHANDED: case OLD_CURSOR_STYLE_TRIANGLE_LEFTHANDED: style = OUTLINE_NONE; break; case OLD_CURSOR_STYLE_OUTLINE: case OLD_CURSOR_STYLE_OUTLINE_CENTER_DOT: case OLD_CURSOR_STYLE_OUTLINE_CENTER_CROSS: case OLD_CURSOR_STYLE_OUTLINE_TRIANGLE_RIGHTHANDED: case OLD_CURSOR_STYLE_OUTLINE_TRIANGLE_LEFTHANDED: style = OUTLINE_FULL; break; default: style = -1; } } cleanOldCursorStyleKeys(m_cfg); // compatibility with future versions if (style < 0 || style >= N_OUTLINE_STYLE_SIZE) { style = OUTLINE_FULL; } return (OutlineStyle) style; } void KisConfig::setNewOutlineStyle(OutlineStyle style) { m_cfg.writeEntry("newOutlineStyle", (int)style); } QRect KisConfig::colorPreviewRect() const { return m_cfg.readEntry("colorPreviewRect", QVariant(QRect(32, 32, 48, 48))).toRect(); } void KisConfig::setColorPreviewRect(const QRect &rect) { m_cfg.writeEntry("colorPreviewRect", QVariant(rect)); } bool KisConfig::useDirtyPresets(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("useDirtyPresets",false)); } void KisConfig::setUseDirtyPresets(bool value) { m_cfg.writeEntry("useDirtyPresets",value); KisConfigNotifier::instance()->notifyConfigChanged(); } bool KisConfig::useEraserBrushSize(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("useEraserBrushSize",false)); } void KisConfig::setUseEraserBrushSize(bool value) { m_cfg.writeEntry("useEraserBrushSize",value); KisConfigNotifier::instance()->notifyConfigChanged(); } bool KisConfig::useEraserBrushOpacity(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("useEraserBrushOpacity",false)); } void KisConfig::setUseEraserBrushOpacity(bool value) { m_cfg.writeEntry("useEraserBrushOpacity",value); KisConfigNotifier::instance()->notifyConfigChanged(); } QColor KisConfig::getMDIBackgroundColor(bool defaultValue) const { QColor col(77, 77, 77); return (defaultValue ? col : m_cfg.readEntry("mdiBackgroundColor", col)); } void KisConfig::setMDIBackgroundColor(const QColor &v) const { m_cfg.writeEntry("mdiBackgroundColor", v); } QString KisConfig::getMDIBackgroundImage(bool defaultValue) const { return (defaultValue ? "" : m_cfg.readEntry("mdiBackgroundImage", "")); } void KisConfig::setMDIBackgroundImage(const QString &filename) const { m_cfg.writeEntry("mdiBackgroundImage", filename); } QString KisConfig::monitorProfile(int screen) const { // Note: keep this in sync with the default profile for the RGB colorspaces! QString profile = m_cfg.readEntry("monitorProfile" + QString(screen == 0 ? "": QString("_%1").arg(screen)), "sRGB-elle-V2-srgbtrc.icc"); //dbgKrita << "KisConfig::monitorProfile()" << profile; return profile; } QString KisConfig::monitorForScreen(int screen, const QString &defaultMonitor, bool defaultValue) const { return (defaultValue ? defaultMonitor : m_cfg.readEntry(QString("monitor_for_screen_%1").arg(screen), defaultMonitor)); } void KisConfig::setMonitorForScreen(int screen, const QString& monitor) { m_cfg.writeEntry(QString("monitor_for_screen_%1").arg(screen), monitor); } void KisConfig::setMonitorProfile(int screen, const QString & monitorProfile, bool override) const { m_cfg.writeEntry("monitorProfile/OverrideX11", override); m_cfg.writeEntry("monitorProfile" + QString(screen == 0 ? "": QString("_%1").arg(screen)), monitorProfile); } const KoColorProfile *KisConfig::getScreenProfile(int screen) { if (screen < 0) return 0; KisConfig cfg; QString monitorId; if (KisColorManager::instance()->devices().size() > screen) { monitorId = cfg.monitorForScreen(screen, KisColorManager::instance()->devices()[screen]); } //dbgKrita << "getScreenProfile(). Screen" << screen << "monitor id" << monitorId; if (monitorId.isEmpty()) { return 0; } QByteArray bytes = KisColorManager::instance()->displayProfile(monitorId); //dbgKrita << "\tgetScreenProfile()" << bytes.size(); if (bytes.length() > 0) { const KoColorProfile *profile = KoColorSpaceRegistry::instance()->createColorProfile(RGBAColorModelID.id(), Integer8BitsColorDepthID.id(), bytes); //dbgKrita << "\tKisConfig::getScreenProfile for screen" << screen << profile->name(); return profile; } else { //dbgKrita << "\tCould not get a system monitor profile"; return 0; } } const KoColorProfile *KisConfig::displayProfile(int screen) const { if (screen < 0) return 0; // if the user plays with the settings, they can override the display profile, in which case // we don't want the system setting. bool override = useSystemMonitorProfile(); //dbgKrita << "KisConfig::displayProfile(). Override X11:" << override; const KoColorProfile *profile = 0; if (override) { //dbgKrita << "\tGoing to get the screen profile"; profile = KisConfig::getScreenProfile(screen); } // if it fails. check the configuration if (!profile || !profile->isSuitableForDisplay()) { //dbgKrita << "\tGoing to get the monitor profile"; QString monitorProfileName = monitorProfile(screen); //dbgKrita << "\t\tmonitorProfileName:" << monitorProfileName; if (!monitorProfileName.isEmpty()) { profile = KoColorSpaceRegistry::instance()->profileByName(monitorProfileName); } if (profile) { //dbgKrita << "\t\tsuitable for display" << profile->isSuitableForDisplay(); } else { //dbgKrita << "\t\tstill no profile"; } } // if we still don't have a profile, or the profile isn't suitable for display, // we need to get a last-resort profile. the built-in sRGB is a good choice then. if (!profile || !profile->isSuitableForDisplay()) { //dbgKrita << "\tnothing worked, going to get sRGB built-in"; profile = KoColorSpaceRegistry::instance()->profileByName("sRGB Built-in"); } if (profile) { //dbgKrita << "\tKisConfig::displayProfile for screen" << screen << "is" << profile->name(); } else { //dbgKrita << "\tCouldn't get a display profile at all"; } return profile; } QString KisConfig::workingColorSpace(bool defaultValue) const { return (defaultValue ? "RGBA" : m_cfg.readEntry("workingColorSpace", "RGBA")); } void KisConfig::setWorkingColorSpace(const QString & workingColorSpace) const { m_cfg.writeEntry("workingColorSpace", workingColorSpace); } QString KisConfig::printerColorSpace(bool /*defaultValue*/) const { //TODO currently only rgb8 is supported //return (defaultValue ? "RGBA" : m_cfg.readEntry("printerColorSpace", "RGBA")); return QString("RGBA"); } void KisConfig::setPrinterColorSpace(const QString & printerColorSpace) const { m_cfg.writeEntry("printerColorSpace", printerColorSpace); } QString KisConfig::printerProfile(bool defaultValue) const { return (defaultValue ? "" : m_cfg.readEntry("printerProfile", "")); } void KisConfig::setPrinterProfile(const QString & printerProfile) const { m_cfg.writeEntry("printerProfile", printerProfile); } bool KisConfig::useBlackPointCompensation(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("useBlackPointCompensation", true)); } void KisConfig::setUseBlackPointCompensation(bool useBlackPointCompensation) const { m_cfg.writeEntry("useBlackPointCompensation", useBlackPointCompensation); } bool KisConfig::allowLCMSOptimization(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("allowLCMSOptimization", true)); } void KisConfig::setAllowLCMSOptimization(bool allowLCMSOptimization) { m_cfg.writeEntry("allowLCMSOptimization", allowLCMSOptimization); } bool KisConfig::showRulers(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("showrulers", false)); } void KisConfig::setShowRulers(bool rulers) const { m_cfg.writeEntry("showrulers", rulers); } bool KisConfig::forceShowSaveMessages(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("forceShowSaveMessages", false)); } void KisConfig::setForceShowSaveMessages(bool value) const { m_cfg.writeEntry("forceShowSaveMessages", value); } bool KisConfig::forceShowAutosaveMessages(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("forceShowAutosaveMessages", false)); } void KisConfig::setForceShowAutosaveMessages(bool value) const { m_cfg.writeEntry("forceShowAutosaveMessages", value); } bool KisConfig::rulersTrackMouse(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("rulersTrackMouse", true)); } void KisConfig::setRulersTrackMouse(bool value) const { m_cfg.writeEntry("rulersTrackMouse", value); } qint32 KisConfig::pasteBehaviour(bool defaultValue) const { return (defaultValue ? 2 : m_cfg.readEntry("pasteBehaviour", 2)); } void KisConfig::setPasteBehaviour(qint32 renderIntent) const { m_cfg.writeEntry("pasteBehaviour", renderIntent); } qint32 KisConfig::monitorRenderIntent(bool defaultValue) const { qint32 intent = m_cfg.readEntry("renderIntent", INTENT_PERCEPTUAL); if (intent > 3) intent = 3; if (intent < 0) intent = 0; return (defaultValue ? INTENT_PERCEPTUAL : intent); } void KisConfig::setRenderIntent(qint32 renderIntent) const { if (renderIntent > 3) renderIntent = 3; if (renderIntent < 0) renderIntent = 0; m_cfg.writeEntry("renderIntent", renderIntent); } bool KisConfig::useOpenGL(bool defaultValue) const { if (defaultValue) { return true; } //dbgKrita << "use opengl" << m_cfg.readEntry("useOpenGL", true) << "success" << m_cfg.readEntry("canvasState", "OPENGL_SUCCESS"); QString cs = canvasState(); #ifdef Q_OS_WIN return (m_cfg.readEntry("useOpenGLWindows", true) && (cs == "OPENGL_SUCCESS" || cs == "TRY_OPENGL")); #else return (m_cfg.readEntry("useOpenGL", true) && (cs == "OPENGL_SUCCESS" || cs == "TRY_OPENGL")); #endif } void KisConfig::setUseOpenGL(bool useOpenGL) const { #ifdef Q_OS_WIN m_cfg.writeEntry("useOpenGLWindows", useOpenGL); #else m_cfg.writeEntry("useOpenGL", useOpenGL); #endif } int KisConfig::openGLFilteringMode(bool defaultValue) const { return (defaultValue ? 3 : m_cfg.readEntry("OpenGLFilterMode", 3)); } void KisConfig::setOpenGLFilteringMode(int filteringMode) { m_cfg.writeEntry("OpenGLFilterMode", filteringMode); } bool KisConfig::useOpenGLTextureBuffer(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("useOpenGLTextureBuffer", true)); } void KisConfig::setUseOpenGLTextureBuffer(bool useBuffer) { m_cfg.writeEntry("useOpenGLTextureBuffer", useBuffer); } int KisConfig::openGLTextureSize(bool defaultValue) const { return (defaultValue ? 256 : m_cfg.readEntry("textureSize", 256)); } bool KisConfig::disableVSync(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("disableVSync", true)); } void KisConfig::setDisableVSync(bool disableVSync) { m_cfg.writeEntry("disableVSync", disableVSync); } bool KisConfig::showAdvancedOpenGLSettings(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("showAdvancedOpenGLSettings", false)); } bool KisConfig::forceOpenGLFenceWorkaround(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("forceOpenGLFenceWorkaround", false)); } int KisConfig::numMipmapLevels(bool defaultValue) const { return (defaultValue ? 4 : m_cfg.readEntry("numMipmapLevels", 4)); } int KisConfig::textureOverlapBorder() const { return 1 << qMax(0, numMipmapLevels()); } quint32 KisConfig::getGridMainStyle(bool defaultValue) const { int v = m_cfg.readEntry("gridmainstyle", 0); v = qBound(0, v, 2); return (defaultValue ? 0 : v); } void KisConfig::setGridMainStyle(quint32 v) const { m_cfg.writeEntry("gridmainstyle", v); } quint32 KisConfig::getGridSubdivisionStyle(bool defaultValue) const { quint32 v = m_cfg.readEntry("gridsubdivisionstyle", 1); if (v > 2) v = 2; return (defaultValue ? 1 : v); } void KisConfig::setGridSubdivisionStyle(quint32 v) const { m_cfg.writeEntry("gridsubdivisionstyle", v); } QColor KisConfig::getGridMainColor(bool defaultValue) const { QColor col(99, 99, 99); return (defaultValue ? col : m_cfg.readEntry("gridmaincolor", col)); } void KisConfig::setGridMainColor(const QColor & v) const { m_cfg.writeEntry("gridmaincolor", v); } QColor KisConfig::getGridSubdivisionColor(bool defaultValue) const { QColor col(150, 150, 150); return (defaultValue ? col : m_cfg.readEntry("gridsubdivisioncolor", col)); } void KisConfig::setGridSubdivisionColor(const QColor & v) const { m_cfg.writeEntry("gridsubdivisioncolor", v); } QColor KisConfig::getPixelGridColor(bool defaultValue) const { QColor col(255, 255, 255); return (defaultValue ? col : m_cfg.readEntry("pixelGridColor", col)); } void KisConfig::setPixelGridColor(const QColor & v) const { m_cfg.writeEntry("pixelGridColor", v); } qreal KisConfig::getPixelGridDrawingThreshold(bool defaultValue) const { qreal border = 8.0f; return (defaultValue ? border : m_cfg.readEntry("pixelGridDrawingThreshold", border)); } void KisConfig::setPixelGridDrawingThreshold(qreal v) const { m_cfg.writeEntry("pixelGridDrawingThreshold", v); } bool KisConfig::pixelGridEnabled(bool defaultValue) const { bool enabled = true; return (defaultValue ? enabled : m_cfg.readEntry("pixelGridEnabled", enabled)); } void KisConfig::enablePixelGrid(bool v) const { m_cfg.writeEntry("pixelGridEnabled", v); } quint32 KisConfig::guidesLineStyle(bool defaultValue) const { int v = m_cfg.readEntry("guidesLineStyle", 0); v = qBound(0, v, 2); return (defaultValue ? 0 : v); } void KisConfig::setGuidesLineStyle(quint32 v) const { m_cfg.writeEntry("guidesLineStyle", v); } QColor KisConfig::guidesColor(bool defaultValue) const { QColor col(99, 99, 99); return (defaultValue ? col : m_cfg.readEntry("guidesColor", col)); } void KisConfig::setGuidesColor(const QColor & v) const { m_cfg.writeEntry("guidesColor", v); } void KisConfig::loadSnapConfig(KisSnapConfig *config, bool defaultValue) const { KisSnapConfig defaultConfig(false); if (defaultValue) { *config = defaultConfig; return; } config->setOrthogonal(m_cfg.readEntry("globalSnapOrthogonal", defaultConfig.orthogonal())); config->setNode(m_cfg.readEntry("globalSnapNode", defaultConfig.node())); config->setExtension(m_cfg.readEntry("globalSnapExtension", defaultConfig.extension())); config->setIntersection(m_cfg.readEntry("globalSnapIntersection", defaultConfig.intersection())); config->setBoundingBox(m_cfg.readEntry("globalSnapBoundingBox", defaultConfig.boundingBox())); config->setImageBounds(m_cfg.readEntry("globalSnapImageBounds", defaultConfig.imageBounds())); config->setImageCenter(m_cfg.readEntry("globalSnapImageCenter", defaultConfig.imageCenter())); } void KisConfig::saveSnapConfig(const KisSnapConfig &config) { m_cfg.writeEntry("globalSnapOrthogonal", config.orthogonal()); m_cfg.writeEntry("globalSnapNode", config.node()); m_cfg.writeEntry("globalSnapExtension", config.extension()); m_cfg.writeEntry("globalSnapIntersection", config.intersection()); m_cfg.writeEntry("globalSnapBoundingBox", config.boundingBox()); m_cfg.writeEntry("globalSnapImageBounds", config.imageBounds()); m_cfg.writeEntry("globalSnapImageCenter", config.imageCenter()); } qint32 KisConfig::checkSize(bool defaultValue) const { return (defaultValue ? 32 : m_cfg.readEntry("checksize", 32)); } void KisConfig::setCheckSize(qint32 checksize) const { m_cfg.writeEntry("checksize", checksize); } bool KisConfig::scrollCheckers(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("scrollingcheckers", false)); } void KisConfig::setScrollingCheckers(bool sc) const { m_cfg.writeEntry("scrollingcheckers", sc); } QColor KisConfig::canvasBorderColor(bool defaultValue) const { QColor color(QColor(128,128,128)); return (defaultValue ? color : m_cfg.readEntry("canvasBorderColor", color)); } void KisConfig::setCanvasBorderColor(const QColor& color) const { m_cfg.writeEntry("canvasBorderColor", color); } bool KisConfig::hideScrollbars(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("hideScrollbars", false)); } void KisConfig::setHideScrollbars(bool value) const { m_cfg.writeEntry("hideScrollbars", value); } QColor KisConfig::checkersColor1(bool defaultValue) const { QColor col(220, 220, 220); return (defaultValue ? col : m_cfg.readEntry("checkerscolor", col)); } void KisConfig::setCheckersColor1(const QColor & v) const { m_cfg.writeEntry("checkerscolor", v); } QColor KisConfig::checkersColor2(bool defaultValue) const { return (defaultValue ? QColor(Qt::white) : m_cfg.readEntry("checkerscolor2", QColor(Qt::white))); } void KisConfig::setCheckersColor2(const QColor & v) const { m_cfg.writeEntry("checkerscolor2", v); } bool KisConfig::antialiasCurves(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("antialiascurves", true)); } void KisConfig::setAntialiasCurves(bool v) const { m_cfg.writeEntry("antialiascurves", v); } QColor KisConfig::selectionOverlayMaskColor(bool defaultValue) const { QColor def(255, 0, 0, 220); return (defaultValue ? def : m_cfg.readEntry("selectionOverlayMaskColor", def)); } void KisConfig::setSelectionOverlayMaskColor(const QColor &color) { m_cfg.writeEntry("selectionOverlayMaskColor", color); } bool KisConfig::antialiasSelectionOutline(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("AntialiasSelectionOutline", false)); } void KisConfig::setAntialiasSelectionOutline(bool v) const { m_cfg.writeEntry("AntialiasSelectionOutline", v); } bool KisConfig::showRootLayer(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("ShowRootLayer", false)); } void KisConfig::setShowRootLayer(bool showRootLayer) const { m_cfg.writeEntry("ShowRootLayer", showRootLayer); } bool KisConfig::showGlobalSelection(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("ShowGlobalSelection", false)); } void KisConfig::setShowGlobalSelection(bool showGlobalSelection) const { m_cfg.writeEntry("ShowGlobalSelection", showGlobalSelection); } bool KisConfig::showOutlineWhilePainting(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("ShowOutlineWhilePainting", true)); } void KisConfig::setShowOutlineWhilePainting(bool showOutlineWhilePainting) const { m_cfg.writeEntry("ShowOutlineWhilePainting", showOutlineWhilePainting); } bool KisConfig::hideSplashScreen(bool defaultValue) const { KConfigGroup cfg( KSharedConfig::openConfig(), "SplashScreen"); return (defaultValue ? true : cfg.readEntry("HideSplashAfterStartup", true)); } void KisConfig::setHideSplashScreen(bool hideSplashScreen) const { KConfigGroup cfg( KSharedConfig::openConfig(), "SplashScreen"); cfg.writeEntry("HideSplashAfterStartup", hideSplashScreen); } qreal KisConfig::outlineSizeMinimum(bool defaultValue) const { return (defaultValue ? 1.0 : m_cfg.readEntry("OutlineSizeMinimum", 1.0)); } void KisConfig::setOutlineSizeMinimum(qreal outlineSizeMinimum) const { m_cfg.writeEntry("OutlineSizeMinimum", outlineSizeMinimum); } qreal KisConfig::selectionViewSizeMinimum(bool defaultValue) const { return (defaultValue ? 5.0 : m_cfg.readEntry("SelectionViewSizeMinimum", 5.0)); } void KisConfig::setSelectionViewSizeMinimum(qreal outlineSizeMinimum) const { m_cfg.writeEntry("SelectionViewSizeMinimum", outlineSizeMinimum); } int KisConfig::autoSaveInterval(bool defaultValue) const { return (defaultValue ? 15 * 60 : m_cfg.readEntry("AutoSaveInterval", 15 * 60)); } void KisConfig::setAutoSaveInterval(int seconds) const { return m_cfg.writeEntry("AutoSaveInterval", seconds); } bool KisConfig::backupFile(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("CreateBackupFile", true)); } void KisConfig::setBackupFile(bool backupFile) const { m_cfg.writeEntry("CreateBackupFile", backupFile); } bool KisConfig::showFilterGallery(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("showFilterGallery", false)); } void KisConfig::setShowFilterGallery(bool showFilterGallery) const { m_cfg.writeEntry("showFilterGallery", showFilterGallery); } bool KisConfig::showFilterGalleryLayerMaskDialog(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("showFilterGalleryLayerMaskDialog", true)); } void KisConfig::setShowFilterGalleryLayerMaskDialog(bool showFilterGallery) const { m_cfg.writeEntry("setShowFilterGalleryLayerMaskDialog", showFilterGallery); } QString KisConfig::canvasState(bool defaultValue) const { const QString configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); QSettings kritarc(configPath + QStringLiteral("/kritadisplayrc"), QSettings::IniFormat); return (defaultValue ? "OPENGL_NOT_TRIED" : kritarc.value("canvasState", "OPENGL_NOT_TRIED").toString()); } void KisConfig::setCanvasState(const QString& state) const { static QStringList acceptableStates; if (acceptableStates.isEmpty()) { acceptableStates << "OPENGL_SUCCESS" << "TRY_OPENGL" << "OPENGL_NOT_TRIED" << "OPENGL_FAILED"; } if (acceptableStates.contains(state)) { const QString configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); QSettings kritarc(configPath + QStringLiteral("/kritadisplayrc"), QSettings::IniFormat); kritarc.setValue("canvasState", state); } } bool KisConfig::toolOptionsPopupDetached(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("ToolOptionsPopupDetached", false)); } void KisConfig::setToolOptionsPopupDetached(bool detached) const { m_cfg.writeEntry("ToolOptionsPopupDetached", detached); } bool KisConfig::paintopPopupDetached(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("PaintopPopupDetached", false)); } void KisConfig::setPaintopPopupDetached(bool detached) const { m_cfg.writeEntry("PaintopPopupDetached", detached); } QString KisConfig::pressureTabletCurve(bool defaultValue) const { return (defaultValue ? "0,0;1,1" : m_cfg.readEntry("tabletPressureCurve","0,0;1,1;")); } void KisConfig::setPressureTabletCurve(const QString& curveString) const { m_cfg.writeEntry("tabletPressureCurve", curveString); } bool KisConfig::useWin8PointerInput(bool defaultValue) const { #ifdef Q_OS_WIN return (defaultValue ? false : m_cfg.readEntry("useWin8PointerInput", false)); #else Q_UNUSED(defaultValue); return false; #endif } void KisConfig::setUseWin8PointerInput(bool value) const { #ifdef Q_OS_WIN // Special handling: Only set value if changed // I don't want it to be set if the user hasn't touched it if (useWin8PointerInput() != value) { m_cfg.writeEntry("useWin8PointerInput", value); } #else Q_UNUSED(value) #endif } qreal KisConfig::vastScrolling(bool defaultValue) const { return (defaultValue ? 0.9 : m_cfg.readEntry("vastScrolling", 0.9)); } void KisConfig::setVastScrolling(const qreal factor) const { m_cfg.writeEntry("vastScrolling", factor); } int KisConfig::presetChooserViewMode(bool defaultValue) const { return (defaultValue ? 0 : m_cfg.readEntry("presetChooserViewMode", 0)); } void KisConfig::setPresetChooserViewMode(const int mode) const { m_cfg.writeEntry("presetChooserViewMode", mode); } int KisConfig::presetIconSize(bool defaultValue) const { return (defaultValue ? 30 : m_cfg.readEntry("presetIconSize", 30)); } void KisConfig::setPresetIconSize(const int value) const { m_cfg.writeEntry("presetIconSize", value); } bool KisConfig::firstRun(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("firstRun", true)); } void KisConfig::setFirstRun(const bool first) const { m_cfg.writeEntry("firstRun", first); } int KisConfig::horizontalSplitLines(bool defaultValue) const { return (defaultValue ? 1 : m_cfg.readEntry("horizontalSplitLines", 1)); } void KisConfig::setHorizontalSplitLines(const int numberLines) const { m_cfg.writeEntry("horizontalSplitLines", numberLines); } int KisConfig::verticalSplitLines(bool defaultValue) const { return (defaultValue ? 1 : m_cfg.readEntry("verticalSplitLines", 1)); } void KisConfig::setVerticalSplitLines(const int numberLines) const { m_cfg.writeEntry("verticalSplitLines", numberLines); } bool KisConfig::clicklessSpacePan(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("clicklessSpacePan", true)); } void KisConfig::setClicklessSpacePan(const bool toggle) const { m_cfg.writeEntry("clicklessSpacePan", toggle); } bool KisConfig::hideDockersFullscreen(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("hideDockersFullScreen", true)); } void KisConfig::setHideDockersFullscreen(const bool value) const { m_cfg.writeEntry("hideDockersFullScreen", value); } bool KisConfig::showDockerTitleBars(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("showDockerTitleBars", true)); } void KisConfig::setShowDockerTitleBars(const bool value) const { m_cfg.writeEntry("showDockerTitleBars", value); } bool KisConfig::showDockers(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("showDockers", true)); } void KisConfig::setShowDockers(const bool value) const { m_cfg.writeEntry("showDockers", value); } bool KisConfig::showStatusBar(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("showStatusBar", true)); } void KisConfig::setShowStatusBar(const bool value) const { m_cfg.writeEntry("showStatusBar", value); } bool KisConfig::hideMenuFullscreen(bool defaultValue) const { return (defaultValue ? true: m_cfg.readEntry("hideMenuFullScreen", true)); } void KisConfig::setHideMenuFullscreen(const bool value) const { m_cfg.writeEntry("hideMenuFullScreen", value); } bool KisConfig::hideScrollbarsFullscreen(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("hideScrollbarsFullScreen", true)); } void KisConfig::setHideScrollbarsFullscreen(const bool value) const { m_cfg.writeEntry("hideScrollbarsFullScreen", value); } bool KisConfig::hideStatusbarFullscreen(bool defaultValue) const { return (defaultValue ? true: m_cfg.readEntry("hideStatusbarFullScreen", true)); } void KisConfig::setHideStatusbarFullscreen(const bool value) const { m_cfg.writeEntry("hideStatusbarFullScreen", value); } bool KisConfig::hideTitlebarFullscreen(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("hideTitleBarFullscreen", true)); } void KisConfig::setHideTitlebarFullscreen(const bool value) const { m_cfg.writeEntry("hideTitleBarFullscreen", value); } bool KisConfig::hideToolbarFullscreen(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("hideToolbarFullscreen", true)); } void KisConfig::setHideToolbarFullscreen(const bool value) const { m_cfg.writeEntry("hideToolbarFullscreen", value); } bool KisConfig::fullscreenMode(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("fullscreenMode", true)); } void KisConfig::setFullscreenMode(const bool value) const { m_cfg.writeEntry("fullscreenMode", value); } QStringList KisConfig::favoriteCompositeOps(bool defaultValue) const { return (defaultValue ? QStringList() : m_cfg.readEntry("favoriteCompositeOps", QStringList())); } void KisConfig::setFavoriteCompositeOps(const QStringList& compositeOps) const { m_cfg.writeEntry("favoriteCompositeOps", compositeOps); } QString KisConfig::exportConfiguration(const QString &filterId, bool defaultValue) const { return (defaultValue ? QString() : m_cfg.readEntry("ExportConfiguration-" + filterId, QString())); } void KisConfig::setExportConfiguration(const QString &filterId, KisPropertiesConfigurationSP properties) const { QString exportConfig = properties->toXML(); m_cfg.writeEntry("ExportConfiguration-" + filterId, exportConfig); } QString KisConfig::importConfiguration(const QString &filterId, bool defaultValue) const { return (defaultValue ? QString() : m_cfg.readEntry("ImportConfiguration-" + filterId, QString())); } void KisConfig::setImportConfiguration(const QString &filterId, KisPropertiesConfigurationSP properties) const { QString importConfig = properties->toXML(); m_cfg.writeEntry("ImportConfiguration-" + filterId, importConfig); } bool KisConfig::useOcio(bool defaultValue) const { #ifdef HAVE_OCIO return (defaultValue ? false : m_cfg.readEntry("Krita/Ocio/UseOcio", false)); #else Q_UNUSED(defaultValue); return false; #endif } void KisConfig::setUseOcio(bool useOCIO) const { m_cfg.writeEntry("Krita/Ocio/UseOcio", useOCIO); } int KisConfig::favoritePresets(bool defaultValue) const { return (defaultValue ? 10 : m_cfg.readEntry("numFavoritePresets", 10)); } void KisConfig::setFavoritePresets(const int value) { m_cfg.writeEntry("numFavoritePresets", value); } bool KisConfig::levelOfDetailEnabled(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("levelOfDetailEnabled", false)); } void KisConfig::setLevelOfDetailEnabled(bool value) { m_cfg.writeEntry("levelOfDetailEnabled", value); } KisConfig::OcioColorManagementMode KisConfig::ocioColorManagementMode(bool defaultValue) const { return (OcioColorManagementMode)(defaultValue ? INTERNAL : m_cfg.readEntry("Krita/Ocio/OcioColorManagementMode", (int) INTERNAL)); } void KisConfig::setOcioColorManagementMode(OcioColorManagementMode mode) const { m_cfg.writeEntry("Krita/Ocio/OcioColorManagementMode", (int) mode); } QString KisConfig::ocioConfigurationPath(bool defaultValue) const { return (defaultValue ? QString() : m_cfg.readEntry("Krita/Ocio/OcioConfigPath", QString())); } void KisConfig::setOcioConfigurationPath(const QString &path) const { m_cfg.writeEntry("Krita/Ocio/OcioConfigPath", path); } QString KisConfig::ocioLutPath(bool defaultValue) const { return (defaultValue ? QString() : m_cfg.readEntry("Krita/Ocio/OcioLutPath", QString())); } void KisConfig::setOcioLutPath(const QString &path) const { m_cfg.writeEntry("Krita/Ocio/OcioLutPath", path); } int KisConfig::ocioLutEdgeSize(bool defaultValue) const { return (defaultValue ? 64 : m_cfg.readEntry("Krita/Ocio/LutEdgeSize", 64)); } void KisConfig::setOcioLutEdgeSize(int value) { m_cfg.writeEntry("Krita/Ocio/LutEdgeSize", value); } bool KisConfig::ocioLockColorVisualRepresentation(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("Krita/Ocio/OcioLockColorVisualRepresentation", false)); } void KisConfig::setOcioLockColorVisualRepresentation(bool value) { m_cfg.writeEntry("Krita/Ocio/OcioLockColorVisualRepresentation", value); } QString KisConfig::defaultPalette(bool defaultValue) const { return (defaultValue ? QString() : m_cfg.readEntry("defaultPalette", QString())); } void KisConfig::setDefaultPalette(const QString& name) const { m_cfg.writeEntry("defaultPalette", name); } QString KisConfig::toolbarSlider(int sliderNumber, bool defaultValue) const { QString def = "flow"; if (sliderNumber == 1) { def = "opacity"; } if (sliderNumber == 2) { def = "size"; } return (defaultValue ? def : m_cfg.readEntry(QString("toolbarslider_%1").arg(sliderNumber), def)); } void KisConfig::setToolbarSlider(int sliderNumber, const QString &slider) { m_cfg.writeEntry(QString("toolbarslider_%1").arg(sliderNumber), slider); } bool KisConfig::sliderLabels(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("sliderLabels", true)); } void KisConfig::setSliderLabels(bool enabled) { m_cfg.writeEntry("sliderLabels", enabled); } QString KisConfig::currentInputProfile(bool defaultValue) const { return (defaultValue ? QString() : m_cfg.readEntry("currentInputProfile", QString())); } void KisConfig::setCurrentInputProfile(const QString& name) { m_cfg.writeEntry("currentInputProfile", name); } bool KisConfig::useSystemMonitorProfile(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("ColorManagement/UseSystemMonitorProfile", false)); } void KisConfig::setUseSystemMonitorProfile(bool _useSystemMonitorProfile) const { m_cfg.writeEntry("ColorManagement/UseSystemMonitorProfile", _useSystemMonitorProfile); } bool KisConfig::presetStripVisible(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("presetStripVisible", true)); } void KisConfig::setPresetStripVisible(bool visible) { m_cfg.writeEntry("presetStripVisible", visible); } bool KisConfig::scratchpadVisible(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("scratchpadVisible", true)); } void KisConfig::setScratchpadVisible(bool visible) { m_cfg.writeEntry("scratchpadVisible", visible); } bool KisConfig::showSingleChannelAsColor(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("showSingleChannelAsColor", false)); } void KisConfig::setShowSingleChannelAsColor(bool asColor) { m_cfg.writeEntry("showSingleChannelAsColor", asColor); } bool KisConfig::hidePopups(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("hidePopups", false)); } void KisConfig::setHidePopups(bool hidepopups) { m_cfg.writeEntry("hidePopups", hidepopups); } int KisConfig::numDefaultLayers(bool defaultValue) const { return (defaultValue ? 2 : m_cfg.readEntry("NumberOfLayersForNewImage", 2)); } void KisConfig::setNumDefaultLayers(int num) { m_cfg.writeEntry("NumberOfLayersForNewImage", num); } quint8 KisConfig::defaultBackgroundOpacity(bool defaultValue) const { return (defaultValue ? (int)OPACITY_OPAQUE_U8 : m_cfg.readEntry("BackgroundOpacityForNewImage", (int)OPACITY_OPAQUE_U8)); } void KisConfig::setDefaultBackgroundOpacity(quint8 value) { m_cfg.writeEntry("BackgroundOpacityForNewImage", (int)value); } QColor KisConfig::defaultBackgroundColor(bool defaultValue) const { return (defaultValue ? QColor(Qt::white) : m_cfg.readEntry("BackgroundColorForNewImage", QColor(Qt::white))); } void KisConfig::setDefaultBackgroundColor(QColor value) { m_cfg.writeEntry("BackgroundColorForNewImage", value); } KisConfig::BackgroundStyle KisConfig::defaultBackgroundStyle(bool defaultValue) const { return (KisConfig::BackgroundStyle)(defaultValue ? LAYER : m_cfg.readEntry("BackgroundStyleForNewImage", (int)LAYER)); } void KisConfig::setDefaultBackgroundStyle(KisConfig::BackgroundStyle value) { m_cfg.writeEntry("BackgroundStyleForNewImage", (int)value); } int KisConfig::lineSmoothingType(bool defaultValue) const { return (defaultValue ? 1 : m_cfg.readEntry("LineSmoothingType", 1)); } void KisConfig::setLineSmoothingType(int value) { m_cfg.writeEntry("LineSmoothingType", value); } qreal KisConfig::lineSmoothingDistance(bool defaultValue) const { return (defaultValue ? 50.0 : m_cfg.readEntry("LineSmoothingDistance", 50.0)); } void KisConfig::setLineSmoothingDistance(qreal value) { m_cfg.writeEntry("LineSmoothingDistance", value); } qreal KisConfig::lineSmoothingTailAggressiveness(bool defaultValue) const { return (defaultValue ? 0.15 : m_cfg.readEntry("LineSmoothingTailAggressiveness", 0.15)); } void KisConfig::setLineSmoothingTailAggressiveness(qreal value) { m_cfg.writeEntry("LineSmoothingTailAggressiveness", value); } bool KisConfig::lineSmoothingSmoothPressure(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("LineSmoothingSmoothPressure", false)); } void KisConfig::setLineSmoothingSmoothPressure(bool value) { m_cfg.writeEntry("LineSmoothingSmoothPressure", value); } bool KisConfig::lineSmoothingScalableDistance(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("LineSmoothingScalableDistance", true)); } void KisConfig::setLineSmoothingScalableDistance(bool value) { m_cfg.writeEntry("LineSmoothingScalableDistance", value); } qreal KisConfig::lineSmoothingDelayDistance(bool defaultValue) const { return (defaultValue ? 50.0 : m_cfg.readEntry("LineSmoothingDelayDistance", 50.0)); } void KisConfig::setLineSmoothingDelayDistance(qreal value) { m_cfg.writeEntry("LineSmoothingDelayDistance", value); } bool KisConfig::lineSmoothingUseDelayDistance(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("LineSmoothingUseDelayDistance", true)); } void KisConfig::setLineSmoothingUseDelayDistance(bool value) { m_cfg.writeEntry("LineSmoothingUseDelayDistance", value); } bool KisConfig::lineSmoothingFinishStabilizedCurve(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("LineSmoothingFinishStabilizedCurve", true)); } void KisConfig::setLineSmoothingFinishStabilizedCurve(bool value) { m_cfg.writeEntry("LineSmoothingFinishStabilizedCurve", value); } bool KisConfig::lineSmoothingStabilizeSensors(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("LineSmoothingStabilizeSensors", true)); } void KisConfig::setLineSmoothingStabilizeSensors(bool value) { m_cfg.writeEntry("LineSmoothingStabilizeSensors", value); } int KisConfig::paletteDockerPaletteViewSectionSize(bool defaultValue) const { return (defaultValue ? 12 : m_cfg.readEntry("paletteDockerPaletteViewSectionSize", 12)); } void KisConfig::setPaletteDockerPaletteViewSectionSize(int value) const { m_cfg.writeEntry("paletteDockerPaletteViewSectionSize", value); } int KisConfig::tabletEventsDelay(bool defaultValue) const { return (defaultValue ? 10 : m_cfg.readEntry("tabletEventsDelay", 10)); } void KisConfig::setTabletEventsDelay(int value) { m_cfg.writeEntry("tabletEventsDelay", value); } bool KisConfig::trackTabletEventLatency(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("trackTabletEventLatency", false)); } void KisConfig::setTrackTabletEventLatency(bool value) { m_cfg.writeEntry("trackTabletEventLatency", value); } bool KisConfig::testingAcceptCompressedTabletEvents(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("testingAcceptCompressedTabletEvents", false)); } void KisConfig::setTestingAcceptCompressedTabletEvents(bool value) { m_cfg.writeEntry("testingAcceptCompressedTabletEvents", value); } bool KisConfig::shouldEatDriverShortcuts(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("shouldEatDriverShortcuts", false)); } bool KisConfig::testingCompressBrushEvents(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("testingCompressBrushEvents", false)); } void KisConfig::setTestingCompressBrushEvents(bool value) { m_cfg.writeEntry("testingCompressBrushEvents", value); } int KisConfig::workaroundX11SmoothPressureSteps(bool defaultValue) const { return (defaultValue ? 0 : m_cfg.readEntry("workaroundX11SmoothPressureSteps", 0)); } bool KisConfig::showCanvasMessages(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("showOnCanvasMessages", true)); } void KisConfig::setShowCanvasMessages(bool show) { m_cfg.writeEntry("showOnCanvasMessages", show); } bool KisConfig::compressKra(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("compressLayersInKra", false)); } void KisConfig::setCompressKra(bool compress) { m_cfg.writeEntry("compressLayersInKra", compress); } bool KisConfig::toolOptionsInDocker(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("ToolOptionsInDocker", true)); } void KisConfig::setToolOptionsInDocker(bool inDocker) { m_cfg.writeEntry("ToolOptionsInDocker", inDocker); } const KoColorSpace* KisConfig::customColorSelectorColorSpace(bool defaultValue) const { const KoColorSpace *cs = 0; KConfigGroup cfg = KSharedConfig::openConfig()->group("advancedColorSelector"); if (defaultValue || cfg.readEntry("useCustomColorSpace", true)) { KoColorSpaceRegistry* csr = KoColorSpaceRegistry::instance(); QString modelID = cfg.readEntry("customColorSpaceModel", "RGBA"); QString depthID = cfg.readEntry("customColorSpaceDepthID", "U8"); QString profile = cfg.readEntry("customColorSpaceProfile", "sRGB built-in - (lcms internal)"); if (profile == "default") { // qDebug() << "Falling back to default color profile."; profile = "sRGB built-in - (lcms internal)"; } cs = csr->colorSpace(modelID, depthID, profile); } return cs; } void KisConfig::setCustomColorSelectorColorSpace(const KoColorSpace *cs) { KConfigGroup cfg = KSharedConfig::openConfig()->group("advancedColorSelector"); cfg.writeEntry("useCustomColorSpace", bool(cs)); if(cs) { cfg.writeEntry("customColorSpaceModel", cs->colorModelId().id()); cfg.writeEntry("customColorSpaceDepthID", cs->colorDepthId().id()); cfg.writeEntry("customColorSpaceProfile", cs->profile()->name()); } KisConfigNotifier::instance()->notifyConfigChanged(); } bool KisConfig::enableOpenGLFramerateLogging(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("enableOpenGLFramerateLogging", false)); } void KisConfig::setEnableOpenGLFramerateLogging(bool value) const { m_cfg.writeEntry("enableOpenGLFramerateLogging", value); } +bool KisConfig::enableBrushSpeedLogging(bool defaultValue) const +{ + return (defaultValue ? false : m_cfg.readEntry("enableBrushSpeedLogging", false)); +} + +void KisConfig::setEnableBrushSpeedLogging(bool value) const +{ + m_cfg.writeEntry("enableBrushSpeedLogging", value); +} + void KisConfig::setEnableAmdVectorizationWorkaround(bool value) { m_cfg.writeEntry("amdDisableVectorWorkaround", value); } bool KisConfig::enableAmdVectorizationWorkaround(bool defaultValue) const { return (defaultValue ? false : m_cfg.readEntry("amdDisableVectorWorkaround", false)); } void KisConfig::setAnimationDropFrames(bool value) { bool oldValue = animationDropFrames(); if (value == oldValue) return; m_cfg.writeEntry("animationDropFrames", value); KisConfigNotifier::instance()->notifyDropFramesModeChanged(); } bool KisConfig::animationDropFrames(bool defaultValue) const { return (defaultValue ? true : m_cfg.readEntry("animationDropFrames", true)); } int KisConfig::scrubbingUpdatesDelay(bool defaultValue) const { return (defaultValue ? 30 : m_cfg.readEntry("scrubbingUpdatesDelay", 30)); } void KisConfig::setScrubbingUpdatesDelay(int value) { m_cfg.writeEntry("scrubbingUpdatesDelay", value); } int KisConfig::scrubbingAudioUpdatesDelay(bool defaultValue) const { return (defaultValue ? -1 : m_cfg.readEntry("scrubbingAudioUpdatesDelay", -1)); } void KisConfig::setScrubbingAudioUpdatesDelay(int value) { m_cfg.writeEntry("scrubbingAudioUpdatesDelay", value); } int KisConfig::audioOffsetTolerance(bool defaultValue) const { return (defaultValue ? -1 : m_cfg.readEntry("audioOffsetTolerance", -1)); } void KisConfig::setAudioOffsetTolerance(int value) { m_cfg.writeEntry("audioOffsetTolerance", value); } bool KisConfig::switchSelectionCtrlAlt(bool defaultValue) const { return defaultValue ? false : m_cfg.readEntry("switchSelectionCtrlAlt", false); } void KisConfig::setSwitchSelectionCtrlAlt(bool value) { m_cfg.writeEntry("switchSelectionCtrlAlt", value); KisConfigNotifier::instance()->notifyConfigChanged(); } bool KisConfig::convertToImageColorspaceOnImport(bool defaultValue) const { return defaultValue ? false : m_cfg.readEntry("ConvertToImageColorSpaceOnImport", false); } void KisConfig::setConvertToImageColorspaceOnImport(bool value) { m_cfg.writeEntry("ConvertToImageColorSpaceOnImport", value); } int KisConfig::stabilizerSampleSize(bool defaultValue) const { #ifdef Q_OS_WIN const int defaultSampleSize = 50; #else const int defaultSampleSize = 15; #endif return defaultValue ? defaultSampleSize : m_cfg.readEntry("stabilizerSampleSize", defaultSampleSize); } void KisConfig::setStabilizerSampleSize(int value) { m_cfg.writeEntry("stabilizerSampleSize", value); } bool KisConfig::stabilizerDelayedPaint(bool defaultValue) const { const bool defaultEnabled = true; return defaultValue ? defaultEnabled : m_cfg.readEntry("stabilizerDelayedPaint", defaultEnabled); } void KisConfig::setStabilizerDelayedPaint(bool value) { m_cfg.writeEntry("stabilizerDelayedPaint", value); } QString KisConfig::customFFMpegPath(bool defaultValue) const { return defaultValue ? QString() : m_cfg.readEntry("ffmpegExecutablePath", QString()); } void KisConfig::setCustomFFMpegPath(const QString &value) const { m_cfg.writeEntry("ffmpegExecutablePath", value); } bool KisConfig::showBrushHud(bool defaultValue) const { return defaultValue ? false : m_cfg.readEntry("showBrushHud", false); } void KisConfig::setShowBrushHud(bool value) { m_cfg.writeEntry("showBrushHud", value); } QString KisConfig::brushHudSetting(bool defaultValue) const { QString defaultDoc = "\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n"; return defaultValue ? defaultDoc : m_cfg.readEntry("brushHudSettings", defaultDoc); } void KisConfig::setBrushHudSetting(const QString &value) const { m_cfg.writeEntry("brushHudSettings", value); } bool KisConfig::calculateAnimationCacheInBackground(bool defaultValue) const { return defaultValue ? true : m_cfg.readEntry("calculateAnimationCacheInBackground", true); } void KisConfig::setCalculateAnimationCacheInBackground(bool value) { m_cfg.writeEntry("calculateAnimationCacheInBackground", value); } #include #include void KisConfig::writeKoColor(const QString& name, const KoColor& color) const { QDomDocument doc = QDomDocument(name); QDomElement el = doc.createElement(name); doc.appendChild(el); color.toXML(doc, el); m_cfg.writeEntry(name, doc.toString()); } //ported from kispropertiesconfig. KoColor KisConfig::readKoColor(const QString& name, const KoColor& color) const { QDomDocument doc; if (!m_cfg.readEntry(name).isNull()) { doc.setContent(m_cfg.readEntry(name)); QDomElement e = doc.documentElement().firstChild().toElement(); return KoColor::fromXML(e, Integer16BitsColorDepthID.id()); } else { QString blackColor = "\n\n \n\n"; doc.setContent(blackColor); QDomElement e = doc.documentElement().firstChild().toElement(); return KoColor::fromXML(e, Integer16BitsColorDepthID.id()); } return color; } diff --git a/libs/ui/kis_config.h b/libs/ui/kis_config.h index fe6bc014a4..557c2119f3 100644 --- a/libs/ui/kis_config.h +++ b/libs/ui/kis_config.h @@ -1,571 +1,574 @@ /* * Copyright (c) 2002 Patrick Julien * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_CONFIG_H_ #define KIS_CONFIG_H_ #include #include #include #include #include #include #include "kis_global.h" #include "kis_properties_configuration.h" #include "kritaui_export.h" class KoColorProfile; class KoColorSpace; class KisSnapConfig; class KRITAUI_EXPORT KisConfig { public: KisConfig(); ~KisConfig(); bool disableTouchOnCanvas(bool defaultValue = false) const; void setDisableTouchOnCanvas(bool value) const; bool useProjections(bool defaultValue = false) const; void setUseProjections(bool useProj) const; bool undoEnabled(bool defaultValue = false) const; void setUndoEnabled(bool undo) const; int undoStackLimit(bool defaultValue = false) const; void setUndoStackLimit(int limit) const; bool useCumulativeUndoRedo(bool defaultValue = false) const; void setCumulativeUndoRedo(bool value); double stackT1(bool defaultValue = false) const; void setStackT1(int T1); double stackT2(bool defaultValue = false) const; void setStackT2(int T2); int stackN(bool defaultValue = false) const; void setStackN(int N); qint32 defImageWidth(bool defaultValue = false) const; void defImageWidth(qint32 width) const; qint32 defImageHeight(bool defaultValue = false) const; void defImageHeight(qint32 height) const; qreal defImageResolution(bool defaultValue = false) const; void defImageResolution(qreal res) const; /** * @return the id of the default color model used for creating new images. */ QString defColorModel(bool defaultValue = false) const; /** * set the id of the default color model used for creating new images. */ void defColorModel(const QString & model) const; /** * @return the id of the default color depth used for creating new images. */ QString defaultColorDepth(bool defaultValue = false) const; /** * set the id of the default color depth used for creating new images. */ void setDefaultColorDepth(const QString & depth) const; /** * @return the id of the default color profile used for creating new images. */ QString defColorProfile(bool defaultValue = false) const; /** * set the id of the default color profile used for creating new images. */ void defColorProfile(const QString & depth) const; CursorStyle newCursorStyle(bool defaultValue = false) const; void setNewCursorStyle(CursorStyle style); QColor getCursorMainColor(bool defaultValue = false) const; void setCursorMainColor(const QColor& v) const; OutlineStyle newOutlineStyle(bool defaultValue = false) const; void setNewOutlineStyle(OutlineStyle style); QRect colorPreviewRect() const; void setColorPreviewRect(const QRect &rect); /// get the profile the user has selected for the given screen QString monitorProfile(int screen) const; void setMonitorProfile(int screen, const QString & monitorProfile, bool override) const; QString monitorForScreen(int screen, const QString &defaultMonitor, bool defaultValue = true) const; void setMonitorForScreen(int screen, const QString& monitor); /// Get the actual profile to be used for the given screen, which is /// either the screen profile set by the color management system or /// the custom monitor profile set by the user, depending on the configuration const KoColorProfile *displayProfile(int screen) const; QString workingColorSpace(bool defaultValue = false) const; void setWorkingColorSpace(const QString & workingColorSpace) const; QString importProfile(bool defaultValue = false) const; void setImportProfile(const QString & importProfile) const; QString printerColorSpace(bool defaultValue = false) const; void setPrinterColorSpace(const QString & printerColorSpace) const; QString printerProfile(bool defaultValue = false) const; void setPrinterProfile(const QString & printerProfile) const; bool useBlackPointCompensation(bool defaultValue = false) const; void setUseBlackPointCompensation(bool useBlackPointCompensation) const; bool allowLCMSOptimization(bool defaultValue = false) const; void setAllowLCMSOptimization(bool allowLCMSOptimization); void writeKoColor(const QString& name, const KoColor& color) const; KoColor readKoColor(const QString& name, const KoColor& color = KoColor()) const; bool showRulers(bool defaultValue = false) const; void setShowRulers(bool rulers) const; bool forceShowSaveMessages(bool defaultValue = true) const; void setForceShowSaveMessages(bool value) const; bool forceShowAutosaveMessages(bool defaultValue = true) const; void setForceShowAutosaveMessages(bool ShowAutosaveMessages) const; bool rulersTrackMouse(bool defaultValue = false) const; void setRulersTrackMouse(bool value) const; qint32 pasteBehaviour(bool defaultValue = false) const; void setPasteBehaviour(qint32 behaviour) const; qint32 monitorRenderIntent(bool defaultValue = false) const; void setRenderIntent(qint32 monitorRenderIntent) const; bool useOpenGL(bool defaultValue = false) const; void setUseOpenGL(bool useOpenGL) const; int openGLFilteringMode(bool defaultValue = false) const; void setOpenGLFilteringMode(int filteringMode); bool useOpenGLTextureBuffer(bool defaultValue = false) const; void setUseOpenGLTextureBuffer(bool useBuffer); bool disableVSync(bool defaultValue = false) const; void setDisableVSync(bool disableVSync); bool showAdvancedOpenGLSettings(bool defaultValue = false) const; bool forceOpenGLFenceWorkaround(bool defaultValue = false) const; int numMipmapLevels(bool defaultValue = false) const; int openGLTextureSize(bool defaultValue = false) const; int textureOverlapBorder() const; quint32 getGridMainStyle(bool defaultValue = false) const; void setGridMainStyle(quint32 v) const; quint32 getGridSubdivisionStyle(bool defaultValue = false) const; void setGridSubdivisionStyle(quint32 v) const; QColor getGridMainColor(bool defaultValue = false) const; void setGridMainColor(const QColor & v) const; QColor getGridSubdivisionColor(bool defaultValue = false) const; void setGridSubdivisionColor(const QColor & v) const; QColor getPixelGridColor(bool defaultValue = false) const; void setPixelGridColor(const QColor & v) const; qreal getPixelGridDrawingThreshold(bool defaultValue = false) const; void setPixelGridDrawingThreshold(qreal v) const; bool pixelGridEnabled(bool defaultValue = false) const; void enablePixelGrid(bool v) const; quint32 guidesLineStyle(bool defaultValue = false) const; void setGuidesLineStyle(quint32 v) const; QColor guidesColor(bool defaultValue = false) const; void setGuidesColor(const QColor & v) const; void loadSnapConfig(KisSnapConfig *config, bool defaultValue = false) const; void saveSnapConfig(const KisSnapConfig &config); qint32 checkSize(bool defaultValue = false) const; void setCheckSize(qint32 checkSize) const; bool scrollCheckers(bool defaultValue = false) const; void setScrollingCheckers(bool scollCheckers) const; QColor checkersColor1(bool defaultValue = false) const; void setCheckersColor1(const QColor & v) const; QColor checkersColor2(bool defaultValue = false) const; void setCheckersColor2(const QColor & v) const; QColor canvasBorderColor(bool defaultValue = false) const; void setCanvasBorderColor(const QColor &color) const; bool hideScrollbars(bool defaultValue = false) const; void setHideScrollbars(bool value) const; bool antialiasCurves(bool defaultValue = false) const; void setAntialiasCurves(bool v) const; QColor selectionOverlayMaskColor(bool defaultValue = false) const; void setSelectionOverlayMaskColor(const QColor &color); bool antialiasSelectionOutline(bool defaultValue = false) const; void setAntialiasSelectionOutline(bool v) const; bool showRootLayer(bool defaultValue = false) const; void setShowRootLayer(bool showRootLayer) const; bool showGlobalSelection(bool defaultValue = false) const; void setShowGlobalSelection(bool showGlobalSelection) const; bool showOutlineWhilePainting(bool defaultValue = false) const; void setShowOutlineWhilePainting(bool showOutlineWhilePainting) const; bool hideSplashScreen(bool defaultValue = false) const; void setHideSplashScreen(bool hideSplashScreen) const; qreal outlineSizeMinimum(bool defaultValue = false) const; void setOutlineSizeMinimum(qreal outlineSizeMinimum) const; qreal selectionViewSizeMinimum(bool defaultValue = false) const; void setSelectionViewSizeMinimum(qreal outlineSizeMinimum) const; int autoSaveInterval(bool defaultValue = false) const; void setAutoSaveInterval(int seconds) const; bool backupFile(bool defaultValue = false) const; void setBackupFile(bool backupFile) const; bool showFilterGallery(bool defaultValue = false) const; void setShowFilterGallery(bool showFilterGallery) const; bool showFilterGalleryLayerMaskDialog(bool defaultValue = false) const; void setShowFilterGalleryLayerMaskDialog(bool showFilterGallery) const; // OPENGL_SUCCESS, TRY_OPENGL, OPENGL_NOT_TRIED, OPENGL_FAILED QString canvasState(bool defaultValue = false) const; void setCanvasState(const QString& state) const; bool toolOptionsPopupDetached(bool defaultValue = false) const; void setToolOptionsPopupDetached(bool detached) const; bool paintopPopupDetached(bool defaultValue = false) const; void setPaintopPopupDetached(bool detached) const; QString pressureTabletCurve(bool defaultValue = false) const; void setPressureTabletCurve(const QString& curveString) const; bool useWin8PointerInput(bool defaultValue = false) const; void setUseWin8PointerInput(bool value) const; qreal vastScrolling(bool defaultValue = false) const; void setVastScrolling(const qreal factor) const; int presetChooserViewMode(bool defaultValue = false) const; void setPresetChooserViewMode(const int mode) const; int presetIconSize(bool defaultValue = false) const; void setPresetIconSize(const int value) const; bool firstRun(bool defaultValue = false) const; void setFirstRun(const bool firstRun) const; bool clicklessSpacePan(bool defaultValue = false) const; void setClicklessSpacePan(const bool toggle) const; int horizontalSplitLines(bool defaultValue = false) const; void setHorizontalSplitLines(const int numberLines) const; int verticalSplitLines(bool defaultValue = false) const; void setVerticalSplitLines(const int numberLines) const; bool hideDockersFullscreen(bool defaultValue = false) const; void setHideDockersFullscreen(const bool value) const; bool showDockerTitleBars(bool defaultValue = false) const; void setShowDockerTitleBars(const bool value) const; bool showDockers(bool defaultValue = false) const; void setShowDockers(const bool value) const; bool showStatusBar(bool defaultValue = false) const; void setShowStatusBar(const bool value) const; bool hideMenuFullscreen(bool defaultValue = false) const; void setHideMenuFullscreen(const bool value) const; bool hideScrollbarsFullscreen(bool defaultValue = false) const; void setHideScrollbarsFullscreen(const bool value) const; bool hideStatusbarFullscreen(bool defaultValue = false) const; void setHideStatusbarFullscreen(const bool value) const; bool hideTitlebarFullscreen(bool defaultValue = false) const; void setHideTitlebarFullscreen(const bool value) const; bool hideToolbarFullscreen(bool defaultValue = false) const; void setHideToolbarFullscreen(const bool value) const; bool fullscreenMode(bool defaultValue = false) const; void setFullscreenMode(const bool value) const; QStringList favoriteCompositeOps(bool defaultValue = false) const; void setFavoriteCompositeOps(const QStringList& compositeOps) const; QString exportConfiguration(const QString &filterId, bool defaultValue = false) const; void setExportConfiguration(const QString &filterId, KisPropertiesConfigurationSP properties) const; QString importConfiguration(const QString &filterId, bool defaultValue = false) const; void setImportConfiguration(const QString &filterId, KisPropertiesConfigurationSP properties) const; bool useOcio(bool defaultValue = false) const; void setUseOcio(bool useOCIO) const; int favoritePresets(bool defaultValue = false) const; void setFavoritePresets(const int value); bool levelOfDetailEnabled(bool defaultValue = false) const; void setLevelOfDetailEnabled(bool value); enum OcioColorManagementMode { INTERNAL = 0, OCIO_CONFIG, OCIO_ENVIRONMENT }; OcioColorManagementMode ocioColorManagementMode(bool defaultValue = false) const; void setOcioColorManagementMode(OcioColorManagementMode mode) const; QString ocioConfigurationPath(bool defaultValue = false) const; void setOcioConfigurationPath(const QString &path) const; QString ocioLutPath(bool defaultValue = false) const; void setOcioLutPath(const QString &path) const; int ocioLutEdgeSize(bool defaultValue = false) const; void setOcioLutEdgeSize(int value); bool ocioLockColorVisualRepresentation(bool defaultValue = false) const; void setOcioLockColorVisualRepresentation(bool value); bool useSystemMonitorProfile(bool defaultValue = false) const; void setUseSystemMonitorProfile(bool _useSystemMonitorProfile) const; QString defaultPalette(bool defaultValue = false) const; void setDefaultPalette(const QString& name) const; QString toolbarSlider(int sliderNumber, bool defaultValue = false) const; void setToolbarSlider(int sliderNumber, const QString &slider); bool sliderLabels(bool defaultValue = false) const; void setSliderLabels(bool enabled); QString currentInputProfile(bool defaultValue = false) const; void setCurrentInputProfile(const QString& name); bool presetStripVisible(bool defaultValue = false) const; void setPresetStripVisible(bool visible); bool scratchpadVisible(bool defaultValue = false) const; void setScratchpadVisible(bool visible); bool showSingleChannelAsColor(bool defaultValue = false) const; void setShowSingleChannelAsColor(bool asColor); bool hidePopups(bool defaultValue = false) const; void setHidePopups(bool hidepopups); int numDefaultLayers(bool defaultValue = false) const; void setNumDefaultLayers(int num); quint8 defaultBackgroundOpacity(bool defaultValue = false) const; void setDefaultBackgroundOpacity(quint8 value); QColor defaultBackgroundColor(bool defaultValue = false) const; void setDefaultBackgroundColor(QColor value); enum BackgroundStyle { LAYER = 0, PROJECTION = 1 }; BackgroundStyle defaultBackgroundStyle(bool defaultValue = false) const; void setDefaultBackgroundStyle(BackgroundStyle value); int lineSmoothingType(bool defaultValue = false) const; void setLineSmoothingType(int value); qreal lineSmoothingDistance(bool defaultValue = false) const; void setLineSmoothingDistance(qreal value); qreal lineSmoothingTailAggressiveness(bool defaultValue = false) const; void setLineSmoothingTailAggressiveness(qreal value); bool lineSmoothingSmoothPressure(bool defaultValue = false) const; void setLineSmoothingSmoothPressure(bool value); bool lineSmoothingScalableDistance(bool defaultValue = false) const; void setLineSmoothingScalableDistance(bool value); qreal lineSmoothingDelayDistance(bool defaultValue = false) const; void setLineSmoothingDelayDistance(qreal value); bool lineSmoothingUseDelayDistance(bool defaultValue = false) const; void setLineSmoothingUseDelayDistance(bool value); bool lineSmoothingFinishStabilizedCurve(bool defaultValue = false) const; void setLineSmoothingFinishStabilizedCurve(bool value); bool lineSmoothingStabilizeSensors(bool defaultValue = false) const; void setLineSmoothingStabilizeSensors(bool value); int paletteDockerPaletteViewSectionSize(bool defaultValue = false) const; void setPaletteDockerPaletteViewSectionSize(int value) const; int tabletEventsDelay(bool defaultValue = false) const; void setTabletEventsDelay(int value); bool trackTabletEventLatency(bool defaultValue = false) const; void setTrackTabletEventLatency(bool value); bool testingAcceptCompressedTabletEvents(bool defaultValue = false) const; void setTestingAcceptCompressedTabletEvents(bool value); bool shouldEatDriverShortcuts(bool defaultValue = false) const; bool testingCompressBrushEvents(bool defaultValue = false) const; void setTestingCompressBrushEvents(bool value); const KoColorSpace* customColorSelectorColorSpace(bool defaultValue = false) const; void setCustomColorSelectorColorSpace(const KoColorSpace *cs); bool useDirtyPresets(bool defaultValue = false) const; void setUseDirtyPresets(bool value); bool useEraserBrushSize(bool defaultValue = false) const; void setUseEraserBrushSize(bool value); bool useEraserBrushOpacity(bool defaultValue = false) const; void setUseEraserBrushOpacity(bool value); QColor getMDIBackgroundColor(bool defaultValue = false) const; void setMDIBackgroundColor(const QColor & v) const; QString getMDIBackgroundImage(bool defaultValue = false) const; void setMDIBackgroundImage(const QString & fileName) const; int workaroundX11SmoothPressureSteps(bool defaultValue = false) const; bool showCanvasMessages(bool defaultValue = false) const; void setShowCanvasMessages(bool show); bool compressKra(bool defaultValue = false) const; void setCompressKra(bool compress); bool toolOptionsInDocker(bool defaultValue = false) const; void setToolOptionsInDocker(bool inDocker); void setEnableOpenGLFramerateLogging(bool value) const; bool enableOpenGLFramerateLogging(bool defaultValue = false) const; + void setEnableBrushSpeedLogging(bool value) const; + bool enableBrushSpeedLogging(bool defaultValue = false) const; + void setEnableAmdVectorizationWorkaround(bool value); bool enableAmdVectorizationWorkaround(bool defaultValue = false) const; bool animationDropFrames(bool defaultValue = false) const; void setAnimationDropFrames(bool value); int scrubbingUpdatesDelay(bool defaultValue = false) const; void setScrubbingUpdatesDelay(int value); int scrubbingAudioUpdatesDelay(bool defaultValue = false) const; void setScrubbingAudioUpdatesDelay(int value); int audioOffsetTolerance(bool defaultValue = false) const; void setAudioOffsetTolerance(int value); bool switchSelectionCtrlAlt(bool defaultValue = false) const; void setSwitchSelectionCtrlAlt(bool value); bool convertToImageColorspaceOnImport(bool defaultValue = false) const; void setConvertToImageColorspaceOnImport(bool value); int stabilizerSampleSize(bool defaultValue = false) const; void setStabilizerSampleSize(int value); bool stabilizerDelayedPaint(bool defaultValue = false) const; void setStabilizerDelayedPaint(bool value); QString customFFMpegPath(bool defaultValue = false) const; void setCustomFFMpegPath(const QString &value) const; bool showBrushHud(bool defaultValue = false) const; void setShowBrushHud(bool value); QString brushHudSetting(bool defaultValue = false) const; void setBrushHudSetting(const QString &value) const; bool calculateAnimationCacheInBackground(bool defaultValue = false) const; void setCalculateAnimationCacheInBackground(bool value); template void writeEntry(const QString& name, const T& value) { m_cfg.writeEntry(name, value); } template void writeList(const QString& name, const QList& value) { m_cfg.writeEntry(name, value); } template T readEntry(const QString& name, const T& defaultValue=T()) { return m_cfg.readEntry(name, defaultValue); } template QList readList(const QString& name, const QList& defaultValue=QList()) { return m_cfg.readEntry(name, defaultValue); } /// get the profile the color managment system has stored for the given screen static const KoColorProfile* getScreenProfile(int screen); private: KisConfig(const KisConfig&); KisConfig& operator=(const KisConfig&) const; private: mutable KConfigGroup m_cfg; }; #endif // KIS_CONFIG_H_ diff --git a/libs/ui/kis_fps_decoration.cpp b/libs/ui/kis_fps_decoration.cpp index e5a79baf39..8e6f465a68 100644 --- a/libs/ui/kis_fps_decoration.cpp +++ b/libs/ui/kis_fps_decoration.cpp @@ -1,64 +1,99 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_fps_decoration.h" #include #include "kis_canvas2.h" #include "kis_coordinates_converter.h" #include "opengl/kis_opengl_canvas_debugger.h" +#include const QString KisFpsDecoration::idTag = "fps_decoration"; KisFpsDecoration::KisFpsDecoration(QPointer view) : KisCanvasDecoration(idTag, view) { setVisible(true); } KisFpsDecoration::~KisFpsDecoration() { } void KisFpsDecoration::drawDecoration(QPainter& gc, const QRectF& /*updateRect*/, const KisCoordinatesConverter */*converter*/, KisCanvas2* /*canvas*/) { #ifdef Q_OS_OSX - QPixmap pixmap(256, 64); + QPixmap pixmap(320, 128); pixmap.fill(Qt::transparent); { QPainter painter(&pixmap); draw(painter); } gc.drawPixmap(0, 0, pixmap); #else draw(gc); #endif } + + void KisFpsDecoration::draw(QPainter& gc) { - const qreal value = KisOpenglCanvasDebugger::instance()->accumulatedFps(); - const QString text = QString("FPS: %1").arg(QString::number(value, 'f', 1)); + QStringList lines; + + if (KisOpenglCanvasDebugger::instance()->showFpsOnCanvas()) { + const qreal value = KisOpenglCanvasDebugger::instance()->accumulatedFps(); + lines << QString("Canvas FPS: %1").arg(QString::number(value, 'f', 1)); + } + + KisStrokeSpeedMonitor *monitor = KisStrokeSpeedMonitor::instance(); + + if (monitor->haveStrokeSpeedMeasurement()) { + lines << QString("Last cursor/brush speed (px/ms): %1/%2%3") + .arg(monitor->lastCursorSpeed(), 0, 'f', 1) + .arg(monitor->lastRenderingSpeed(), 0, 'f', 1) + .arg(monitor->lastStrokeSaturated() ? " (!)" : ""); + lines << QString("Last brush framerate: %1 fps") + .arg(monitor->lastFps(), 0, 'f', 1); + + lines << QString("Average cursor/brush speed (px/ms): %1/%2") + .arg(monitor->avgCursorSpeed(), 0, 'f', 1) + .arg(monitor->avgRenderingSpeed(), 0, 'f', 1); + lines << QString("Average brush framerate: %1 fps") + .arg(monitor->avgFps(), 0, 'f', 1); + } + + + QPoint startPoint(20,30); + const int lineSpacing = QFontMetrics(gc.font()).lineSpacing(); + gc.save(); - gc.setPen(QPen(Qt::white)); - gc.drawText(QPoint(21, 31), text); - gc.setPen(QPen(Qt::black)); - gc.drawText(QPoint(20, 30), text); + + Q_FOREACH (const QString &line, lines) { + gc.setPen(QPen(Qt::white)); + gc.drawText(startPoint + QPoint(1,1), line); + gc.setPen(QPen(Qt::black)); + gc.drawText(startPoint, line); + + startPoint += QPoint(0, lineSpacing); + } + gc.restore(); } diff --git a/libs/ui/opengl/kis_opengl_canvas2.cpp b/libs/ui/opengl/kis_opengl_canvas2.cpp index 0d72cbeb85..6174bfecfe 100644 --- a/libs/ui/opengl/kis_opengl_canvas2.cpp +++ b/libs/ui/opengl/kis_opengl_canvas2.cpp @@ -1,909 +1,904 @@ /* This file is part of the KDE project * Copyright (C) Boudewijn Rempt , (C) 2006-2013 * Copyright (C) 2015 Michael Abrahams * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #define GL_GLEXT_PROTOTYPES #include "opengl/kis_opengl_canvas2.h" #include "opengl/kis_opengl_canvas2_p.h" #include "opengl/kis_opengl_shader_loader.h" #include "opengl/kis_opengl_canvas_debugger.h" #include "canvas/kis_canvas2.h" #include "canvas/kis_coordinates_converter.h" #include "canvas/kis_display_filter.h" #include "canvas/kis_display_color_converter.h" #include "kis_config.h" #include "kis_config_notifier.h" #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include #include #ifndef Q_OS_OSX #include #endif #define NEAR_VAL -1000.0 #define FAR_VAL 1000.0 #ifndef GL_CLAMP_TO_EDGE #define GL_CLAMP_TO_EDGE 0x812F #endif #define PROGRAM_VERTEX_ATTRIBUTE 0 #define PROGRAM_TEXCOORD_ATTRIBUTE 1 static bool OPENGL_SUCCESS = false; struct KisOpenGLCanvas2::Private { public: ~Private() { delete displayShader; delete checkerShader; delete solidColorShader; Sync::deleteSync(glSyncObject); } bool canvasInitialized{false}; KisOpenGLImageTexturesSP openGLImageTextures; KisOpenGLShaderLoader shaderLoader; KisShaderProgram *displayShader{0}; KisShaderProgram *checkerShader{0}; KisShaderProgram *solidColorShader{0}; bool displayShaderCompiledWithDisplayFilterSupport{false}; GLfloat checkSizeScale; bool scrollCheckers; QSharedPointer displayFilter; KisOpenGL::FilterMode filterMode; bool proofingConfigIsUpdated=false; GLsync glSyncObject{0}; bool wrapAroundMode{false}; // Stores a quad for drawing the canvas QOpenGLVertexArrayObject quadVAO; QOpenGLBuffer quadBuffers[2]; // Stores data for drawing tool outlines QOpenGLVertexArrayObject outlineVAO; QOpenGLBuffer lineBuffer; QVector3D vertices[6]; QVector2D texCoords[6]; #ifndef Q_OS_OSX QOpenGLFunctions_2_1 *glFn201; #endif qreal pixelGridDrawingThreshold; bool pixelGridEnabled; QColor gridColor; int xToColWithWrapCompensation(int x, const QRect &imageRect) { int firstImageColumn = openGLImageTextures->xToCol(imageRect.left()); int lastImageColumn = openGLImageTextures->xToCol(imageRect.right()); int colsPerImage = lastImageColumn - firstImageColumn + 1; int numWraps = floor(qreal(x) / imageRect.width()); int remainder = x - imageRect.width() * numWraps; return colsPerImage * numWraps + openGLImageTextures->xToCol(remainder); } int yToRowWithWrapCompensation(int y, const QRect &imageRect) { int firstImageRow = openGLImageTextures->yToRow(imageRect.top()); int lastImageRow = openGLImageTextures->yToRow(imageRect.bottom()); int rowsPerImage = lastImageRow - firstImageRow + 1; int numWraps = floor(qreal(y) / imageRect.height()); int remainder = y - imageRect.height() * numWraps; return rowsPerImage * numWraps + openGLImageTextures->yToRow(remainder); } }; KisOpenGLCanvas2::KisOpenGLCanvas2(KisCanvas2 *canvas, KisCoordinatesConverter *coordinatesConverter, QWidget *parent, KisImageWSP image, KisDisplayColorConverter *colorConverter) : QOpenGLWidget(parent) , KisCanvasWidgetBase(canvas, coordinatesConverter) , d(new Private()) { KisConfig cfg; cfg.setCanvasState("OPENGL_STARTED"); d->openGLImageTextures = KisOpenGLImageTextures::getImageTextures(image, colorConverter->monitorProfile(), colorConverter->renderingIntent(), colorConverter->conversionFlags()); setAcceptDrops(true); setAutoFillBackground(false); setFocusPolicy(Qt::StrongFocus); setAttribute(Qt::WA_NoSystemBackground, true); #ifdef Q_OS_OSX setAttribute(Qt::WA_AcceptTouchEvents, false); #else setAttribute(Qt::WA_AcceptTouchEvents, true); #endif setAttribute(Qt::WA_InputMethodEnabled, true); setAttribute(Qt::WA_DontCreateNativeAncestors, true); setDisplayFilterImpl(colorConverter->displayFilter(), true); connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); slotConfigChanged(); cfg.writeEntry("canvasState", "OPENGL_SUCCESS"); } KisOpenGLCanvas2::~KisOpenGLCanvas2() { delete d; } -bool KisOpenGLCanvas2::needsFpsDebugging() const -{ - return KisOpenglCanvasDebugger::instance()->showFpsOnCanvas(); -} - void KisOpenGLCanvas2::setDisplayFilter(QSharedPointer displayFilter) { setDisplayFilterImpl(displayFilter, false); } void KisOpenGLCanvas2::setDisplayFilterImpl(QSharedPointer displayFilter, bool initializing) { bool needsInternalColorManagement = !displayFilter || displayFilter->useInternalColorManagement(); bool needsFullRefresh = d->openGLImageTextures->setInternalColorManagementActive(needsInternalColorManagement); d->displayFilter = displayFilter; if (!initializing && needsFullRefresh) { canvas()->startUpdateInPatches(canvas()->image()->bounds()); } else if (!initializing) { canvas()->updateCanvas(); } } void KisOpenGLCanvas2::setWrapAroundViewingMode(bool value) { d->wrapAroundMode = value; update(); } inline void rectToVertices(QVector3D* vertices, const QRectF &rc) { vertices[0] = QVector3D(rc.left(), rc.bottom(), 0.f); vertices[1] = QVector3D(rc.left(), rc.top(), 0.f); vertices[2] = QVector3D(rc.right(), rc.bottom(), 0.f); vertices[3] = QVector3D(rc.left(), rc.top(), 0.f); vertices[4] = QVector3D(rc.right(), rc.top(), 0.f); vertices[5] = QVector3D(rc.right(), rc.bottom(), 0.f); } inline void rectToTexCoords(QVector2D* texCoords, const QRectF &rc) { texCoords[0] = QVector2D(rc.left(), rc.bottom()); texCoords[1] = QVector2D(rc.left(), rc.top()); texCoords[2] = QVector2D(rc.right(), rc.bottom()); texCoords[3] = QVector2D(rc.left(), rc.top()); texCoords[4] = QVector2D(rc.right(), rc.top()); texCoords[5] = QVector2D(rc.right(), rc.bottom()); } void KisOpenGLCanvas2::initializeGL() { KisOpenGL::initializeContext(context()); initializeOpenGLFunctions(); #ifndef Q_OS_OSX if (!KisOpenGL::hasOpenGLES()) { d->glFn201 = context()->versionFunctions(); if (!d->glFn201) { warnUI << "Cannot obtain QOpenGLFunctions_2_1, glLogicOp cannot be used"; } } else { d->glFn201 = nullptr; } #endif KisConfig cfg; d->openGLImageTextures->setProofingConfig(canvas()->proofingConfiguration()); d->openGLImageTextures->initGL(context()->functions()); d->openGLImageTextures->generateCheckerTexture(createCheckersImage(cfg.checkSize())); initializeShaders(); // If we support OpenGL 3.2, then prepare our VAOs and VBOs for drawing if (KisOpenGL::hasOpenGL3()) { d->quadVAO.create(); d->quadVAO.bind(); glEnableVertexAttribArray(PROGRAM_VERTEX_ATTRIBUTE); glEnableVertexAttribArray(PROGRAM_TEXCOORD_ATTRIBUTE); // Create the vertex buffer object, it has 6 vertices with 3 components d->quadBuffers[0].create(); d->quadBuffers[0].setUsagePattern(QOpenGLBuffer::StaticDraw); d->quadBuffers[0].bind(); d->quadBuffers[0].allocate(d->vertices, 6 * 3 * sizeof(float)); glVertexAttribPointer(PROGRAM_VERTEX_ATTRIBUTE, 3, GL_FLOAT, GL_FALSE, 0, 0); // Create the texture buffer object, it has 6 texture coordinates with 2 components d->quadBuffers[1].create(); d->quadBuffers[1].setUsagePattern(QOpenGLBuffer::StaticDraw); d->quadBuffers[1].bind(); d->quadBuffers[1].allocate(d->texCoords, 6 * 2 * sizeof(float)); glVertexAttribPointer(PROGRAM_TEXCOORD_ATTRIBUTE, 2, GL_FLOAT, GL_FALSE, 0, 0); // Create the outline buffer, this buffer will store the outlines of // tools and will frequently change data d->outlineVAO.create(); d->outlineVAO.bind(); glEnableVertexAttribArray(PROGRAM_VERTEX_ATTRIBUTE); // The outline buffer has a StreamDraw usage pattern, because it changes constantly d->lineBuffer.create(); d->lineBuffer.setUsagePattern(QOpenGLBuffer::StreamDraw); d->lineBuffer.bind(); glVertexAttribPointer(PROGRAM_VERTEX_ATTRIBUTE, 3, GL_FLOAT, GL_FALSE, 0, 0); } Sync::init(context()); d->canvasInitialized = true; } /** * Loads all shaders and reports compilation problems */ void KisOpenGLCanvas2::initializeShaders() { KIS_SAFE_ASSERT_RECOVER_RETURN(!d->canvasInitialized); delete d->checkerShader; delete d->solidColorShader; d->checkerShader = 0; d->solidColorShader = 0; try { d->checkerShader = d->shaderLoader.loadCheckerShader(); d->solidColorShader = d->shaderLoader.loadSolidColorShader(); } catch (const ShaderLoaderException &e) { reportFailedShaderCompilation(e.what()); } initializeDisplayShader(); } void KisOpenGLCanvas2::initializeDisplayShader() { KIS_SAFE_ASSERT_RECOVER_RETURN(!d->canvasInitialized); bool useHiQualityFiltering = d->filterMode == KisOpenGL::HighQualityFiltering; delete d->displayShader; d->displayShader = 0; try { d->displayShader = d->shaderLoader.loadDisplayShader(d->displayFilter, useHiQualityFiltering); d->displayShaderCompiledWithDisplayFilterSupport = d->displayFilter; } catch (const ShaderLoaderException &e) { reportFailedShaderCompilation(e.what()); } } /** * Displays a message box telling the user that * shader compilation failed and turns off OpenGL. */ void KisOpenGLCanvas2::reportFailedShaderCompilation(const QString &context) { KisConfig cfg; qDebug() << "Shader Compilation Failure: " << context; QMessageBox::critical(this, i18nc("@title:window", "Krita"), QString(i18n("Krita could not initialize the OpenGL canvas:\n\n%1\n\n Krita will disable OpenGL and close now.")).arg(context), QMessageBox::Close); cfg.setUseOpenGL(false); cfg.setCanvasState("OPENGL_FAILED"); } void KisOpenGLCanvas2::resizeGL(int width, int height) { coordinatesConverter()->setCanvasWidgetSize(QSize(width, height)); paintGL(); } void KisOpenGLCanvas2::paintGL() { if (!OPENGL_SUCCESS) { KisConfig cfg; cfg.writeEntry("canvasState", "OPENGL_PAINT_STARTED"); } KisOpenglCanvasDebugger::instance()->nofityPaintRequested(); renderCanvasGL(); if (d->glSyncObject) { Sync::deleteSync(d->glSyncObject); } d->glSyncObject = Sync::getSync(); QPainter gc(this); renderDecorations(&gc); gc.end(); if (!OPENGL_SUCCESS) { KisConfig cfg; cfg.writeEntry("canvasState", "OPENGL_SUCCESS"); OPENGL_SUCCESS = true; } } void KisOpenGLCanvas2::paintToolOutline(const QPainterPath &path) { if (!d->solidColorShader->bind()) { return; } // setup the mvp transformation QMatrix4x4 projectionMatrix; projectionMatrix.setToIdentity(); projectionMatrix.ortho(0, width(), height(), 0, NEAR_VAL, FAR_VAL); // Set view/projection matrices QMatrix4x4 modelMatrix(coordinatesConverter()->flakeToWidgetTransform()); modelMatrix.optimize(); modelMatrix = projectionMatrix * modelMatrix; d->solidColorShader->setUniformValue(d->solidColorShader->location(Uniform::ModelViewProjection), modelMatrix); if (!KisOpenGL::hasOpenGLES()) { glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); glEnable(GL_COLOR_LOGIC_OP); #ifndef Q_OS_OSX if (d->glFn201) { d->glFn201->glLogicOp(GL_XOR); } #else glLogicOp(GL_XOR); #endif } else { glEnable(GL_BLEND); glBlendFuncSeparate(GL_ONE_MINUS_DST_COLOR, GL_ZERO, GL_ONE, GL_ONE); } KisConfig cfg; QColor cursorColor = cfg.getCursorMainColor(); d->solidColorShader->setUniformValue( d->solidColorShader->location(Uniform::FragmentColor), QVector4D(cursorColor.redF(), cursorColor.greenF(), cursorColor.blueF(), 1.0f)); // Paint the tool outline if (KisOpenGL::hasOpenGL3()) { d->outlineVAO.bind(); d->lineBuffer.bind(); } // Convert every disjointed subpath to a polygon and draw that polygon QList subPathPolygons = path.toSubpathPolygons(); for (int i = 0; i < subPathPolygons.size(); i++) { const QPolygonF& polygon = subPathPolygons.at(i); QVector vertices; vertices.resize(polygon.count()); for (int j = 0; j < polygon.count(); j++) { QPointF p = polygon.at(j); vertices[j].setX(p.x()); vertices[j].setY(p.y()); } if (KisOpenGL::hasOpenGL3()) { d->lineBuffer.allocate(vertices.constData(), 3 * vertices.size() * sizeof(float)); } else { d->solidColorShader->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE); d->solidColorShader->setAttributeArray(PROGRAM_VERTEX_ATTRIBUTE, vertices.constData()); } glDrawArrays(GL_LINE_STRIP, 0, vertices.size()); } if (KisOpenGL::hasOpenGL3()) { d->lineBuffer.release(); d->outlineVAO.release(); } if (!KisOpenGL::hasOpenGLES()) { glDisable(GL_COLOR_LOGIC_OP); } else { glDisable(GL_BLEND); } d->solidColorShader->release(); } bool KisOpenGLCanvas2::isBusy() const { const bool isBusyStatus = Sync::syncStatus(d->glSyncObject) == Sync::Unsignaled; KisOpenglCanvasDebugger::instance()->nofitySyncStatus(isBusyStatus); return isBusyStatus; } void KisOpenGLCanvas2::drawCheckers() { if (!d->checkerShader) { return; } KisCoordinatesConverter *converter = coordinatesConverter(); QTransform textureTransform; QTransform modelTransform; QRectF textureRect; QRectF modelRect; QRectF viewportRect = !d->wrapAroundMode ? converter->imageRectInViewportPixels() : converter->widgetToViewport(this->rect()); converter->getOpenGLCheckersInfo(viewportRect, &textureTransform, &modelTransform, &textureRect, &modelRect, d->scrollCheckers); textureTransform *= QTransform::fromScale(d->checkSizeScale / KisOpenGLImageTextures::BACKGROUND_TEXTURE_SIZE, d->checkSizeScale / KisOpenGLImageTextures::BACKGROUND_TEXTURE_SIZE); if (!d->checkerShader->bind()) { qWarning() << "Could not bind checker shader"; return; } QMatrix4x4 projectionMatrix; projectionMatrix.setToIdentity(); projectionMatrix.ortho(0, width(), height(), 0, NEAR_VAL, FAR_VAL); // Set view/projection matrices QMatrix4x4 modelMatrix(modelTransform); modelMatrix.optimize(); modelMatrix = projectionMatrix * modelMatrix; d->checkerShader->setUniformValue(d->checkerShader->location(Uniform::ModelViewProjection), modelMatrix); QMatrix4x4 textureMatrix(textureTransform); d->checkerShader->setUniformValue(d->checkerShader->location(Uniform::TextureMatrix), textureMatrix); //Setup the geometry for rendering if (KisOpenGL::hasOpenGL3()) { rectToVertices(d->vertices, modelRect); d->quadBuffers[0].bind(); d->quadBuffers[0].write(0, d->vertices, 3 * 6 * sizeof(float)); rectToTexCoords(d->texCoords, textureRect); d->quadBuffers[1].bind(); d->quadBuffers[1].write(0, d->texCoords, 2 * 6 * sizeof(float)); } else { rectToVertices(d->vertices, modelRect); d->checkerShader->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE); d->checkerShader->setAttributeArray(PROGRAM_VERTEX_ATTRIBUTE, d->vertices); rectToTexCoords(d->texCoords, textureRect); d->checkerShader->enableAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE); d->checkerShader->setAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE, d->texCoords); } // render checkers glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, d->openGLImageTextures->checkerTexture()); glDrawArrays(GL_TRIANGLES, 0, 6); glBindTexture(GL_TEXTURE_2D, 0); d->checkerShader->release(); glBindBuffer(GL_ARRAY_BUFFER, 0); } void KisOpenGLCanvas2::drawGrid() { if (!d->solidColorShader->bind()) { return; } QMatrix4x4 projectionMatrix; projectionMatrix.setToIdentity(); projectionMatrix.ortho(0, width(), height(), 0, NEAR_VAL, FAR_VAL); // Set view/projection matrices QMatrix4x4 modelMatrix(coordinatesConverter()->imageToWidgetTransform()); modelMatrix.optimize(); modelMatrix = projectionMatrix * modelMatrix; d->solidColorShader->setUniformValue(d->solidColorShader->location(Uniform::ModelViewProjection), modelMatrix); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); d->solidColorShader->setUniformValue( d->solidColorShader->location(Uniform::FragmentColor), QVector4D(d->gridColor.redF(), d->gridColor.greenF(), d->gridColor.blueF(), 0.5f)); if (KisOpenGL::hasOpenGL3()) { d->outlineVAO.bind(); d->lineBuffer.bind(); } QRectF widgetRect(0,0, width(), height()); QRectF widgetRectInImagePixels = coordinatesConverter()->documentToImage(coordinatesConverter()->widgetToDocument(widgetRect)); QRect wr = widgetRectInImagePixels.toAlignedRect(); if (!d->wrapAroundMode) { wr &= d->openGLImageTextures->storedImageBounds(); } QPoint topLeftCorner = wr.topLeft(); QPoint bottomRightCorner = wr.bottomRight() + QPoint(1, 1); QVector grid; for (int i = topLeftCorner.x(); i <= bottomRightCorner.x(); ++i) { grid.append(QVector3D(i, topLeftCorner.y(), 0)); grid.append(QVector3D(i, bottomRightCorner.y(), 0)); } for (int i = topLeftCorner.y(); i <= bottomRightCorner.y(); ++i) { grid.append(QVector3D(topLeftCorner.x(), i, 0)); grid.append(QVector3D(bottomRightCorner.x(), i, 0)); } if (KisOpenGL::hasOpenGL3()) { d->lineBuffer.allocate(grid.constData(), 3 * grid.size() * sizeof(float)); } else { d->solidColorShader->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE); d->solidColorShader->setAttributeArray(PROGRAM_VERTEX_ATTRIBUTE, grid.constData()); } glDrawArrays(GL_LINES, 0, grid.size()); if (KisOpenGL::hasOpenGL3()) { d->lineBuffer.release(); d->outlineVAO.release(); } d->solidColorShader->release(); glDisable(GL_BLEND); } void KisOpenGLCanvas2::drawImage() { if (!d->displayShader) { return; } glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); KisCoordinatesConverter *converter = coordinatesConverter(); d->displayShader->bind(); QMatrix4x4 projectionMatrix; projectionMatrix.setToIdentity(); projectionMatrix.ortho(0, width(), height(), 0, NEAR_VAL, FAR_VAL); // Set view/projection matrices QMatrix4x4 modelMatrix(converter->imageToWidgetTransform()); modelMatrix.optimize(); modelMatrix = projectionMatrix * modelMatrix; d->displayShader->setUniformValue(d->displayShader->location(Uniform::ModelViewProjection), modelMatrix); QMatrix4x4 textureMatrix; textureMatrix.setToIdentity(); d->displayShader->setUniformValue(d->displayShader->location(Uniform::TextureMatrix), textureMatrix); QRectF widgetRect(0,0, width(), height()); QRectF widgetRectInImagePixels = converter->documentToImage(converter->widgetToDocument(widgetRect)); qreal scaleX, scaleY; converter->imageScale(&scaleX, &scaleY); d->displayShader->setUniformValue(d->displayShader->location(Uniform::ViewportScale), (GLfloat) scaleX); d->displayShader->setUniformValue(d->displayShader->location(Uniform::TexelSize), (GLfloat) d->openGLImageTextures->texelSize()); QRect ir = d->openGLImageTextures->storedImageBounds(); QRect wr = widgetRectInImagePixels.toAlignedRect(); if (!d->wrapAroundMode) { // if we don't want to paint wrapping images, just limit the // processing area, and the code will handle all the rest wr &= ir; } int firstColumn = d->xToColWithWrapCompensation(wr.left(), ir); int lastColumn = d->xToColWithWrapCompensation(wr.right(), ir); int firstRow = d->yToRowWithWrapCompensation(wr.top(), ir); int lastRow = d->yToRowWithWrapCompensation(wr.bottom(), ir); int minColumn = d->openGLImageTextures->xToCol(ir.left()); int maxColumn = d->openGLImageTextures->xToCol(ir.right()); int minRow = d->openGLImageTextures->yToRow(ir.top()); int maxRow = d->openGLImageTextures->yToRow(ir.bottom()); int imageColumns = maxColumn - minColumn + 1; int imageRows = maxRow - minRow + 1; for (int col = firstColumn; col <= lastColumn; col++) { for (int row = firstRow; row <= lastRow; row++) { int effectiveCol = col; int effectiveRow = row; QPointF tileWrappingTranslation; if (effectiveCol > maxColumn || effectiveCol < minColumn) { int translationStep = floor(qreal(col) / imageColumns); int originCol = translationStep * imageColumns; effectiveCol = col - originCol; tileWrappingTranslation.rx() = translationStep * ir.width(); } if (effectiveRow > maxRow || effectiveRow < minRow) { int translationStep = floor(qreal(row) / imageRows); int originRow = translationStep * imageRows; effectiveRow = row - originRow; tileWrappingTranslation.ry() = translationStep * ir.height(); } KisTextureTile *tile = d->openGLImageTextures->getTextureTileCR(effectiveCol, effectiveRow); if (!tile) { warnUI << "OpenGL: Trying to paint texture tile but it has not been created yet."; continue; } /* * We create a float rect here to workaround Qt's * "history reasons" in calculation of right() * and bottom() coordinates of integer rects. */ QRectF textureRect(tile->tileRectInTexturePixels()); QRectF modelRect(tile->tileRectInImagePixels().translated(tileWrappingTranslation.x(), tileWrappingTranslation.y())); //Setup the geometry for rendering if (KisOpenGL::hasOpenGL3()) { rectToVertices(d->vertices, modelRect); d->quadBuffers[0].bind(); d->quadBuffers[0].write(0, d->vertices, 3 * 6 * sizeof(float)); rectToTexCoords(d->texCoords, textureRect); d->quadBuffers[1].bind(); d->quadBuffers[1].write(0, d->texCoords, 2 * 6 * sizeof(float)); } else { rectToVertices(d->vertices, modelRect); d->displayShader->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE); d->displayShader->setAttributeArray(PROGRAM_VERTEX_ATTRIBUTE, d->vertices); rectToTexCoords(d->texCoords, textureRect); d->displayShader->enableAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE); d->displayShader->setAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE, d->texCoords); } if (d->displayFilter) { glActiveTexture(GL_TEXTURE0 + 1); glBindTexture(GL_TEXTURE_3D, d->displayFilter->lutTexture()); d->displayShader->setUniformValue(d->displayShader->location(Uniform::Texture1), 1); } int currentLodPlane = tile->currentLodPlane(); if (d->displayShader->location(Uniform::FixedLodLevel) >= 0) { d->displayShader->setUniformValue(d->displayShader->location(Uniform::FixedLodLevel), (GLfloat) currentLodPlane); } glActiveTexture(GL_TEXTURE0); tile->bindToActiveTexture(); if (currentLodPlane > 0) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST); } else if (SCALE_MORE_OR_EQUAL_TO(scaleX, scaleY, 2.0)) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); } else { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); switch(d->filterMode) { case KisOpenGL::NearestFilterMode: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); break; case KisOpenGL::BilinearFilterMode: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); break; case KisOpenGL::TrilinearFilterMode: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); break; case KisOpenGL::HighQualityFiltering: if (SCALE_LESS_THAN(scaleX, scaleY, 0.5)) { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST); } else { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); } break; } } glDrawArrays(GL_TRIANGLES, 0, 6); } } glBindTexture(GL_TEXTURE_2D, 0); d->displayShader->release(); glBindBuffer(GL_ARRAY_BUFFER, 0); glDisable(GL_BLEND); } void KisOpenGLCanvas2::slotConfigChanged() { KisConfig cfg; d->checkSizeScale = KisOpenGLImageTextures::BACKGROUND_TEXTURE_CHECK_SIZE / static_cast(cfg.checkSize()); d->scrollCheckers = cfg.scrollCheckers(); d->openGLImageTextures->generateCheckerTexture(createCheckersImage(cfg.checkSize())); d->openGLImageTextures->updateConfig(cfg.useOpenGLTextureBuffer(), cfg.numMipmapLevels()); d->filterMode = (KisOpenGL::FilterMode) cfg.openGLFilteringMode(); d->pixelGridDrawingThreshold = cfg.getPixelGridDrawingThreshold(); d->pixelGridEnabled = cfg.pixelGridEnabled(); d->gridColor = cfg.getPixelGridColor(); notifyConfigChanged(); } QVariant KisOpenGLCanvas2::inputMethodQuery(Qt::InputMethodQuery query) const { return processInputMethodQuery(query); } void KisOpenGLCanvas2::inputMethodEvent(QInputMethodEvent *event) { processInputMethodEvent(event); } void KisOpenGLCanvas2::renderCanvasGL() { // Draw the border (that is, clear the whole widget to the border color) QColor widgetBackgroundColor = borderColor(); glClearColor(widgetBackgroundColor.redF(), widgetBackgroundColor.greenF(), widgetBackgroundColor.blueF(), 1.0); glClear(GL_COLOR_BUFFER_BIT); if ((d->displayFilter && d->displayFilter->updateShader()) || (bool(d->displayFilter) != d->displayShaderCompiledWithDisplayFilterSupport)) { KIS_SAFE_ASSERT_RECOVER_NOOP(d->canvasInitialized); d->canvasInitialized = false; // TODO: check if actually needed? initializeDisplayShader(); d->canvasInitialized = true; } if (KisOpenGL::hasOpenGL3()) { d->quadVAO.bind(); } drawCheckers(); drawImage(); if ((coordinatesConverter()->effectiveZoom() > d->pixelGridDrawingThreshold - 0.00001) && d->pixelGridEnabled) { drawGrid(); } if (KisOpenGL::hasOpenGL3()) { d->quadVAO.release(); } } void KisOpenGLCanvas2::renderDecorations(QPainter *painter) { QRect boundingRect = coordinatesConverter()->imageRectInWidgetPixels().toAlignedRect(); drawDecorations(*painter, boundingRect); } void KisOpenGLCanvas2::setDisplayProfile(KisDisplayColorConverter *colorConverter) { d->openGLImageTextures->setMonitorProfile(colorConverter->monitorProfile(), colorConverter->renderingIntent(), colorConverter->conversionFlags()); } void KisOpenGLCanvas2::channelSelectionChanged(const QBitArray &channelFlags) { d->openGLImageTextures->setChannelFlags(channelFlags); } void KisOpenGLCanvas2::finishResizingImage(qint32 w, qint32 h) { if (d->canvasInitialized) { d->openGLImageTextures->slotImageSizeChanged(w, h); } } KisUpdateInfoSP KisOpenGLCanvas2::startUpdateCanvasProjection(const QRect & rc, const QBitArray &channelFlags) { d->openGLImageTextures->setChannelFlags(channelFlags); if (canvas()->proofingConfigUpdated()) { d->openGLImageTextures->setProofingConfig(canvas()->proofingConfiguration()); canvas()->setProofingConfigUpdated(false); } return d->openGLImageTextures->updateCache(rc, d->openGLImageTextures->image()); } QRect KisOpenGLCanvas2::updateCanvasProjection(KisUpdateInfoSP info) { // See KisQPainterCanvas::updateCanvasProjection for more info bool isOpenGLUpdateInfo = dynamic_cast(info.data()); if (isOpenGLUpdateInfo) { d->openGLImageTextures->recalculateCache(info); } #ifdef Q_OS_OSX /** * There is a bug on OSX: if we issue frame redraw before the tiles finished * uploading, the tiles will become corrupted. Depending on the GPU/driver * version either the tile itself, or its mipmaps will become totally * transparent. */ glFinish(); #endif return QRect(); // FIXME: Implement dirty rect for OpenGL } bool KisOpenGLCanvas2::callFocusNextPrevChild(bool next) { return focusNextPrevChild(next); } KisOpenGLImageTexturesSP KisOpenGLCanvas2::openGLImageTextures() const { return d->openGLImageTextures; } diff --git a/libs/ui/opengl/kis_opengl_canvas2.h b/libs/ui/opengl/kis_opengl_canvas2.h index f39c8853ec..a9eec1bbf3 100644 --- a/libs/ui/opengl/kis_opengl_canvas2.h +++ b/libs/ui/opengl/kis_opengl_canvas2.h @@ -1,127 +1,125 @@ /* * Copyright (C) Boudewijn Rempt , (C) 2006 * Copyright (C) Michael Abrahams , (C) 2015 * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_OPENGL_CANVAS_2_H #define KIS_OPENGL_CANVAS_2_H #include #ifndef Q_OS_OSX #include #else #include #endif #include "canvas/kis_canvas_widget_base.h" #include "opengl/kis_opengl_image_textures.h" #include "kritaui_export.h" #include "kis_ui_types.h" class KisCanvas2; class KisDisplayColorConverter; class QOpenGLShaderProgram; class QPainterPath; #ifndef Q_MOC_RUN #ifndef Q_OS_OSX #define GLFunctions QOpenGLFunctions #else #define GLFunctions QOpenGLFunctions_3_2_Core #endif #endif /** * KisOpenGLCanvas is the widget that shows the actual image using OpenGL * * NOTE: if you change something in the event handling here, also change it * in the qpainter canvas. * */ class KRITAUI_EXPORT KisOpenGLCanvas2 : public QOpenGLWidget #ifndef Q_MOC_RUN , protected GLFunctions #endif , public KisCanvasWidgetBase { Q_OBJECT public: KisOpenGLCanvas2(KisCanvas2 *canvas, KisCoordinatesConverter *coordinatesConverter, QWidget *parent, KisImageWSP image, KisDisplayColorConverter *colorConverter); ~KisOpenGLCanvas2() override; public: // QOpenGLWidget void resizeGL(int width, int height) override; void initializeGL() override; void paintGL() override; QVariant inputMethodQuery(Qt::InputMethodQuery query) const override; void inputMethodEvent(QInputMethodEvent *event) override; public: void renderCanvasGL(); void renderDecorations(QPainter *painter); void paintToolOutline(const QPainterPath &path); - bool needsFpsDebugging() const; - public: // Implement kis_abstract_canvas_widget interface void setDisplayFilter(QSharedPointer displayFilter) override; void setWrapAroundViewingMode(bool value) override; void channelSelectionChanged(const QBitArray &channelFlags) override; void setDisplayProfile(KisDisplayColorConverter *colorConverter) override; void finishResizingImage(qint32 w, qint32 h) override; KisUpdateInfoSP startUpdateCanvasProjection(const QRect & rc, const QBitArray &channelFlags) override; QRect updateCanvasProjection(KisUpdateInfoSP info) override; QWidget *widget() override { return this; } bool isBusy() const override; void setDisplayFilterImpl(QSharedPointer displayFilter, bool initializing); KisOpenGLImageTexturesSP openGLImageTextures() const; public Q_SLOTS: void slotConfigChanged(); protected: // KisCanvasWidgetBase bool callFocusNextPrevChild(bool next) override; private: void initializeShaders(); void initializeDisplayShader(); void reportFailedShaderCompilation(const QString &context); void drawImage(); void drawCheckers(); void drawGrid(); private: struct Private; Private * const d; }; #endif // KIS_OPENGL_CANVAS_2_H diff --git a/libs/ui/opengl/kis_opengl_canvas_debugger.cpp b/libs/ui/opengl/kis_opengl_canvas_debugger.cpp index 3e13bb3842..86df5a0bf9 100644 --- a/libs/ui/opengl/kis_opengl_canvas_debugger.cpp +++ b/libs/ui/opengl/kis_opengl_canvas_debugger.cpp @@ -1,115 +1,121 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_opengl_canvas_debugger.h" #include #include #include #include "kis_config.h" - +#include struct KisOpenglCanvasDebugger::Private { Private() : fpsCounter(0), fpsSum(0), syncFlaggedCounter(0), syncFlaggedSum(0), isEnabled(true) {} QElapsedTimer time; int fpsCounter; int fpsSum; int syncFlaggedCounter; int syncFlaggedSum; bool isEnabled; }; Q_GLOBAL_STATIC(KisOpenglCanvasDebugger, s_instance) KisOpenglCanvasDebugger::KisOpenglCanvasDebugger() : m_d(new Private) { - KisConfig cfg; - m_d->isEnabled = cfg.enableOpenGLFramerateLogging(); - - if (m_d->isEnabled) { - m_d->time.start(); - } + connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); + slotConfigChanged(); } KisOpenglCanvasDebugger::~KisOpenglCanvasDebugger() { } KisOpenglCanvasDebugger* KisOpenglCanvasDebugger::instance() { return s_instance; } bool KisOpenglCanvasDebugger::showFpsOnCanvas() const { return m_d->isEnabled; } qreal KisOpenglCanvasDebugger::accumulatedFps() { qreal value = 0; if (m_d->fpsSum > 0) { value = qreal(m_d->fpsCounter) / m_d->fpsSum * 1000.0; } return value; } +void KisOpenglCanvasDebugger::slotConfigChanged() +{ + KisConfig cfg; + m_d->isEnabled = cfg.enableOpenGLFramerateLogging(); + + if (m_d->isEnabled) { + m_d->time.start(); + } +} + void KisOpenglCanvasDebugger::nofityPaintRequested() { if (!m_d->isEnabled) return; m_d->fpsSum += m_d->time.restart(); m_d->fpsCounter++; if (m_d->fpsCounter > 100 && m_d->fpsSum > 0) { qDebug() << "Requested FPS:" << qreal(m_d->fpsCounter) / m_d->fpsSum * 1000.0; m_d->fpsSum = 0; m_d->fpsCounter = 0; } } void KisOpenglCanvasDebugger::nofitySyncStatus(bool isBusy) { if (!m_d->isEnabled) return; m_d->syncFlaggedSum += isBusy; m_d->syncFlaggedCounter++; if (m_d->syncFlaggedCounter > 500 && m_d->syncFlaggedSum > 0) { qDebug() << "glSync effectiveness:" << qreal(m_d->syncFlaggedSum) / m_d->syncFlaggedCounter; m_d->syncFlaggedSum = 0; m_d->syncFlaggedCounter = 0; } } diff --git a/libs/ui/opengl/kis_opengl_canvas_debugger.h b/libs/ui/opengl/kis_opengl_canvas_debugger.h index 8d7e605e96..293128e860 100644 --- a/libs/ui/opengl/kis_opengl_canvas_debugger.h +++ b/libs/ui/opengl/kis_opengl_canvas_debugger.h @@ -1,45 +1,49 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_OPENGL_CANVAS_DEBUGGER_H #define __KIS_OPENGL_CANVAS_DEBUGGER_H #include +#include - -class KisOpenglCanvasDebugger +class KisOpenglCanvasDebugger : public QObject { + Q_OBJECT public: KisOpenglCanvasDebugger(); ~KisOpenglCanvasDebugger(); static KisOpenglCanvasDebugger* instance(); bool showFpsOnCanvas() const; void nofityPaintRequested(); void nofitySyncStatus(bool value); qreal accumulatedFps(); +private Q_SLOTS: + void slotConfigChanged(); + private: struct Private; const QScopedPointer m_d; }; #endif /* __KIS_OPENGL_CANVAS_DEBUGGER_H */ diff --git a/libs/ui/tests/CMakeLists.txt b/libs/ui/tests/CMakeLists.txt index 40eef8169e..cc9d4629bc 100644 --- a/libs/ui/tests/CMakeLists.txt +++ b/libs/ui/tests/CMakeLists.txt @@ -1,177 +1,182 @@ set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} ) #add_subdirectory(scratchpad) include_directories(${CMAKE_SOURCE_DIR}/libs/image/metadata ${CMAKE_SOURCE_DIR}/sdk/tests ) include(ECMAddTests) macro_add_unittest_definitions() ecm_add_tests( kis_image_view_converter_test.cpp squeezedcombobox_test.cpp kis_shape_selection_test.cpp kis_recorded_action_editor_test.cpp kis_doc2_test.cpp kis_coordinates_converter_test.cpp kis_grid_config_test.cpp kis_stabilized_events_sampler_test.cpp kis_derived_resources_test.cpp kis_brush_hud_properties_config_test.cpp kis_shape_commands_test.cpp kis_stop_gradient_editor_test.cpp NAME_PREFIX "krita-ui-" LINK_LIBRARIES kritaui Qt5::Test ) ecm_add_tests( kis_file_layer_test.cpp kis_multinode_property_test.cpp NAME_PREFIX "krita-ui-" LINK_LIBRARIES kritaui kritaimage Qt5::Test ) ecm_add_test( kis_selection_decoration_test.cpp ../../../sdk/tests/stroke_testing_utils.cpp TEST_NAME krita-ui-KisSelectionDecorationTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) ecm_add_test( kis_node_dummies_graph_test.cpp ../../../sdk/tests/testutil.cpp TEST_NAME krita-ui-KisNodeDummiesGraphTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) ecm_add_test( kis_node_shapes_graph_test.cpp ../../../sdk/tests/testutil.cpp TEST_NAME krita-ui-KisNodeShapesGraphTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) ecm_add_test( kis_model_index_converter_test.cpp ../../../sdk/tests/testutil.cpp TEST_NAME krita-ui-KisModelIndexConverterTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) ecm_add_test( kis_categorized_list_model_test.cpp modeltest.cpp TEST_NAME krita-ui-KisCategorizedListModelTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) ecm_add_test( kis_resource_server_provider_test.cpp modeltest.cpp TEST_NAME krita-ui-KisResourceServerProviderTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) ecm_add_test( kis_node_juggler_compressed_test.cpp ../../../sdk/tests/testutil.cpp TEST_NAME krita-image-BaseNodeTest LINK_LIBRARIES kritaimage kritaui Qt5::Test) ecm_add_test( kis_animation_exporter_test.cpp TEST_NAME kritaui-animation_exporter_test LINK_LIBRARIES kritaui kritaimage Qt5::Test) set(kis_node_view_test_SRCS kis_node_view_test.cpp ../../../sdk/tests/testutil.cpp) qt5_add_resources(kis_node_view_test_SRCS ${krita_QRCS}) ecm_add_test(${kis_node_view_test_SRCS} TEST_NAME krita-image-kis_node_view_test LINK_LIBRARIES kritaimage kritaui Qt5::Test) ##### Tests that currently fail and should be fixed ##### include(KritaAddBrokenUnitTest) krita_add_broken_unit_test( kis_node_model_test.cpp modeltest.cpp TEST_NAME krita-ui-kis_node_model_test LINK_LIBRARIES kritaui Qt5::Test) krita_add_broken_unit_test( kis_shape_controller_test.cpp kis_dummies_facade_base_test.cpp TEST_NAME krita-ui-kis_shape_controller_test LINK_LIBRARIES kritaimage kritaui Qt5::Test) krita_add_broken_unit_test( kis_prescaled_projection_test.cpp TEST_NAME krita-ui-kis_prescaled_projection_test LINK_LIBRARIES kritaui Qt5::Test) krita_add_broken_unit_test( kis_exiv2_test.cpp TEST_NAME krita-ui-KisExiv2Test LINK_LIBRARIES kritaimage kritaui Qt5::Test) krita_add_broken_unit_test( kis_clipboard_test.cpp TEST_NAME krita-ui-KisClipboardTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) krita_add_broken_unit_test( freehand_stroke_test.cpp ${CMAKE_SOURCE_DIR}/sdk/tests/stroke_testing_utils.cpp TEST_NAME krita-ui-FreehandStrokeTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) +krita_add_broken_unit_test( + FreehandStrokeBenchmark.cpp ${CMAKE_SOURCE_DIR}/sdk/tests/stroke_testing_utils.cpp + TEST_NAME krita-ui-FreehandStrokeBenchmark + LINK_LIBRARIES kritaui kritaimage Qt5::Test) + krita_add_broken_unit_test( fill_processing_visitor_test.cpp ${CMAKE_SOURCE_DIR}/sdk/tests/stroke_testing_utils.cpp TEST_NAME krita-ui-FillProcessingVisitorTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) krita_add_broken_unit_test( filter_stroke_test.cpp ../../../sdk/tests/stroke_testing_utils.cpp TEST_NAME krita-ui-FilterStrokeTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) krita_add_broken_unit_test( kis_selection_manager_test.cpp TEST_NAME krita-ui-KisSelectionManagerTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) #set_tests_properties(krita-ui-KisSelectionManagerTest PROPERTIES TIMEOUT 300) krita_add_broken_unit_test( kis_node_manager_test.cpp TEST_NAME krita-ui-KisNodeManagerTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) krita_add_broken_unit_test( kis_dummies_facade_test.cpp kis_dummies_facade_base_test.cpp ../../../sdk/tests/testutil.cpp TEST_NAME krita-ui-KisDummiesFacadeTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) krita_add_broken_unit_test( kis_zoom_and_pan_test.cpp ../../../sdk/tests/testutil.cpp TEST_NAME krita-ui-KisZoomAndPanTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) #set_tests_properties(krita-ui-KisZoomAndPanTest PROPERTIES TIMEOUT 300) krita_add_broken_unit_test( kis_action_manager_test.cpp TEST_NAME krita-ui-KisActionManagerTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) krita_add_broken_unit_test( kis_categories_mapper_test.cpp testing_categories_mapper.cpp TEST_NAME krita-ui-KisCategoriesMapperTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) krita_add_broken_unit_test( kis_asl_layer_style_serializer_test.cpp TEST_NAME krita-ui-KisAslLayerStyleSerializerTest LINK_LIBRARIES kritaui kritaimage Qt5::Test) krita_add_broken_unit_test( kis_animation_importer_test.cpp TEST_NAME kritaui-animation_importer_test LINK_LIBRARIES kritaui kritaimage Qt5::Test) krita_add_broken_unit_test( kis_animation_frame_cache_test.cpp TEST_NAME kritaui-animation_frame_cache_test LINK_LIBRARIES kritaui kritaimage Qt5::Test) krita_add_broken_unit_test( ResourceBundleTest.cpp TEST_NAME krita-resourcemanager-ResourceBundleTest LINK_LIBRARIES kritaui kritalibbrush kritalibpaintop Qt5::Test) # FIXME this test doesn't compile #ecm_add_test( # kis_input_manager_test.cpp ../../../sdk/tests/testutil.cpp # TEST_NAME krita-ui-KisInputManagerTest # LINK_LIBRARIES kritaui kritaimage Qt5::Test) diff --git a/libs/ui/tests/FreehandStrokeBenchmark.cpp b/libs/ui/tests/FreehandStrokeBenchmark.cpp new file mode 100644 index 0000000000..6e6a65305d --- /dev/null +++ b/libs/ui/tests/FreehandStrokeBenchmark.cpp @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "FreehandStrokeBenchmark.h" + +#include +#include +#include +#include "stroke_testing_utils.h" +#include "strokes/freehand_stroke.h" +#include "kis_resources_snapshot.h" +#include "kis_image.h" +#include + +class FreehandStrokeBenchmarkTester : public utils::StrokeTester +{ +public: + FreehandStrokeBenchmarkTester(const QString &presetFilename) + : StrokeTester("freehand_benchmark", QSize(5000, 5000), presetFilename) + { + setBaseFuzziness(3); + } + + void setCpuCoresLimit(int value) { + m_cpuCoresLimit = value; + } + +protected: + using utils::StrokeTester::initImage; + void initImage(KisImageWSP image, KisNodeSP activeNode) override { + Q_UNUSED(activeNode); + + if (m_cpuCoresLimit > 0) { + image->setWorkingThreadsLimit(m_cpuCoresLimit); + } + } + + KisStrokeStrategy* createStroke(bool indirectPainting, + KisResourcesSnapshotSP resources, + KisImageWSP image) override { + Q_UNUSED(image); + + FreehandStrokeStrategy::PainterInfo *painterInfo = + new FreehandStrokeStrategy::PainterInfo(); + + QScopedPointer stroke( + new FreehandStrokeStrategy(indirectPainting, COMPOSITE_ALPHA_DARKEN, resources, painterInfo, kundo2_noi18n("Freehand Stroke"))); + + return stroke.take(); + } + + void addPaintingJobs(KisImageWSP image, + KisResourcesSnapshotSP resources) override + { + addPaintingJobs(image, resources, 0); + } + + void addPaintingJobs(KisImageWSP image, KisResourcesSnapshotSP resources, int iteration) override { + Q_UNUSED(iteration); + Q_UNUSED(resources); + + for (int y = 100; y < 4900; y += 300) { + KisPaintInformation pi1; + KisPaintInformation pi2; + + pi1 = KisPaintInformation(QPointF(100, y), 0.5); + pi2 = KisPaintInformation(QPointF(4900, y + 100), 1.0); + + QScopedPointer data( + new FreehandStrokeStrategy::Data(0, pi1, pi2)); + + image->addJob(strokeId(), data.take()); + } + + image->addJob(strokeId(), new FreehandStrokeStrategy::UpdateData(true)); + } + +private: + FreehandStrokeStrategy::PainterInfo *m_painterInfo; + int m_cpuCoresLimit = -1; +}; + +void benchmarkBrush(const QString &presetName) +{ + FreehandStrokeBenchmarkTester tester(presetName); + + for (int i = 1; i <= QThread::idealThreadCount(); i++) { + tester.setCpuCoresLimit(i); + tester.benchmark(); + + qDebug() << qPrintable(QString("Cores: %1 Time: %2 (ms)").arg(i).arg(tester.lastStrokeTime())); + } +} + +#include + +void FreehandStrokeBenchmark::initTestCase() +{ + KoResourcePaths::addResourceType("kis_brushes", "data", FILES_DATA_DIR); +} + +void FreehandStrokeBenchmark::testDefaultTip() +{ + benchmarkBrush("testing_1000px_auto_deafult.kpp"); +} + +void FreehandStrokeBenchmark::testSoftTip() +{ + benchmarkBrush("testing_1000px_auto_soft.kpp"); +} + +void FreehandStrokeBenchmark::testGaussianTip() +{ + benchmarkBrush("testing_1000px_auto_gaussian.kpp"); +} + +void FreehandStrokeBenchmark::testStampTip() +{ + benchmarkBrush("testing_1000px_stamp_450_rotated.kpp"); +} + +void FreehandStrokeBenchmark::testColorsmudgeDefaultTip() +{ + benchmarkBrush("testing_200px_colorsmudge_default.kpp"); +} + +QTEST_MAIN(FreehandStrokeBenchmark) diff --git a/libs/image/kis_projection_updates_filter.cpp b/libs/ui/tests/FreehandStrokeBenchmark.h similarity index 62% copy from libs/image/kis_projection_updates_filter.cpp copy to libs/ui/tests/FreehandStrokeBenchmark.h index e1493ec367..943b611d9f 100644 --- a/libs/image/kis_projection_updates_filter.cpp +++ b/libs/ui/tests/FreehandStrokeBenchmark.h @@ -1,36 +1,38 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#ifndef FREEHANDSTROKEBENCHMARK_H +#define FREEHANDSTROKEBENCHMARK_H +#include -#include -#include - -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +class FreehandStrokeBenchmark : public QObject { -} + Q_OBJECT +private Q_SLOTS: + void initTestCase(); -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} + void testDefaultTip(); + void testSoftTip(); + void testGaussianTip(); + void testStampTip(); + + void testColorsmudgeDefaultTip(); +}; + +#endif // FREEHANDSTROKEBENCHMARK_H diff --git a/libs/ui/tests/data/3_texture.png b/libs/ui/tests/data/3_texture.png new file mode 100644 index 0000000000..85fa56d26d Binary files /dev/null and b/libs/ui/tests/data/3_texture.png differ diff --git a/libs/ui/tests/data/testing_1000px_auto_deafult.kpp b/libs/ui/tests/data/testing_1000px_auto_deafult.kpp new file mode 100644 index 0000000000..b3bddc8217 Binary files /dev/null and b/libs/ui/tests/data/testing_1000px_auto_deafult.kpp differ diff --git a/libs/ui/tests/data/testing_1000px_auto_gaussian.kpp b/libs/ui/tests/data/testing_1000px_auto_gaussian.kpp new file mode 100644 index 0000000000..cd03fe18d9 Binary files /dev/null and b/libs/ui/tests/data/testing_1000px_auto_gaussian.kpp differ diff --git a/libs/ui/tests/data/testing_1000px_auto_soft.kpp b/libs/ui/tests/data/testing_1000px_auto_soft.kpp new file mode 100644 index 0000000000..705bcee1e8 Binary files /dev/null and b/libs/ui/tests/data/testing_1000px_auto_soft.kpp differ diff --git a/libs/ui/tests/data/testing_1000px_stamp_450_rotated.kpp b/libs/ui/tests/data/testing_1000px_stamp_450_rotated.kpp new file mode 100644 index 0000000000..90a8a13e4c Binary files /dev/null and b/libs/ui/tests/data/testing_1000px_stamp_450_rotated.kpp differ diff --git a/libs/ui/tests/data/testing_200px_colorsmudge_default.kpp b/libs/ui/tests/data/testing_200px_colorsmudge_default.kpp new file mode 100644 index 0000000000..fd45d050d8 Binary files /dev/null and b/libs/ui/tests/data/testing_200px_colorsmudge_default.kpp differ diff --git a/libs/ui/tests/freehand_stroke_test.cpp b/libs/ui/tests/freehand_stroke_test.cpp index 5d1f618c1c..041cc3acd3 100644 --- a/libs/ui/tests/freehand_stroke_test.cpp +++ b/libs/ui/tests/freehand_stroke_test.cpp @@ -1,187 +1,190 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "freehand_stroke_test.h" #include #include #include #include "stroke_testing_utils.h" #include "strokes/freehand_stroke.h" #include "kis_resources_snapshot.h" #include "kis_image.h" #include "kis_painter.h" #include class FreehandStrokeTester : public utils::StrokeTester { public: FreehandStrokeTester(const QString &presetFilename, bool useLod = false) : StrokeTester(useLod ? "freehand-lod" : "freehand", QSize(500, 500), presetFilename), m_useLod(useLod), m_flipLineDirection(false) { + setBaseFuzziness(3); } void setFlipLineDirection(bool value) { m_flipLineDirection = value; setNumIterations(2); } void setPaintColor(const QColor &color) { m_paintColor.reset(new QColor(color)); } protected: using utils::StrokeTester::initImage; void initImage(KisImageWSP image, KisNodeSP activeNode) override { Q_UNUSED(activeNode); if (m_useLod) { image->setDesiredLevelOfDetail(1); } } void beforeCheckingResult(KisImageWSP image, KisNodeSP activeNode) override { Q_UNUSED(image) Q_UNUSED(activeNode); if (m_useLod) { //image->testingSetLevelOfDetailsEnabled(true); } } void modifyResourceManager(KoCanvasResourceManager *manager, KisImageWSP image) override { modifyResourceManager(manager, image, 0); } void modifyResourceManager(KoCanvasResourceManager *manager, KisImageWSP image, int iteration) override { if (m_paintColor && iteration > 0) { QVariant i; i.setValue(KoColor(*m_paintColor, image->colorSpace())); manager->setResource(KoCanvasResourceManager::ForegroundColor, i); } } KisStrokeStrategy* createStroke(bool indirectPainting, KisResourcesSnapshotSP resources, KisImageWSP image) override { Q_UNUSED(image); FreehandStrokeStrategy::PainterInfo *painterInfo = new FreehandStrokeStrategy::PainterInfo(); QScopedPointer stroke( new FreehandStrokeStrategy(indirectPainting, COMPOSITE_ALPHA_DARKEN, resources, painterInfo, kundo2_noi18n("Freehand Stroke"))); return stroke.take(); } void addPaintingJobs(KisImageWSP image, KisResourcesSnapshotSP resources) override { addPaintingJobs(image, resources, 0); } void addPaintingJobs(KisImageWSP image, KisResourcesSnapshotSP resources, int iteration) override { + Q_UNUSED(resources); + KisPaintInformation pi1; KisPaintInformation pi2; if (!iteration) { pi1 = KisPaintInformation(QPointF(200, 200)); pi2 = KisPaintInformation(QPointF(300, 300)); } else { pi1 = KisPaintInformation(QPointF(200, 300)); pi2 = KisPaintInformation(QPointF(300, 200)); } QScopedPointer data( - new FreehandStrokeStrategy::Data(resources->currentNode(), - 0, pi1, pi2)); + new FreehandStrokeStrategy::Data(0, pi1, pi2)); image->addJob(strokeId(), data.take()); + image->addJob(strokeId(), new FreehandStrokeStrategy::UpdateData(true)); } private: FreehandStrokeStrategy::PainterInfo *m_painterInfo; bool m_useLod; bool m_flipLineDirection; QScopedPointer m_paintColor; }; void FreehandStrokeTest::testAutoBrushStroke() { FreehandStrokeTester tester("autobrush_300px.kpp"); tester.test(); } void FreehandStrokeTest::testHatchingStroke() { FreehandStrokeTester tester("hatching_30px.kpp"); tester.test(); } void FreehandStrokeTest::testColorSmudgeStroke() { FreehandStrokeTester tester("colorsmudge_predefined.kpp"); tester.test(); } void FreehandStrokeTest::testAutoTextured17() { FreehandStrokeTester tester("auto_textured_17.kpp"); tester.test(); } void FreehandStrokeTest::testAutoTextured38() { FreehandStrokeTester tester("auto_textured_38.kpp"); tester.test(); } void FreehandStrokeTest::testMixDullCompositioning() { FreehandStrokeTester tester("Mix_dull.kpp"); tester.setFlipLineDirection(true); tester.setPaintColor(Qt::red); tester.test(); } void FreehandStrokeTest::testAutoBrushStrokeLod() { FreehandStrokeTester tester("Basic_tip_default.kpp", true); tester.testSimpleStroke(); } void FreehandStrokeTest::testPredefinedBrushStrokeLod() { qsrand(QTime::currentTime().msec()); FreehandStrokeTester tester("testing_predefined_lod_spc13.kpp", true); //FreehandStrokeTester tester("testing_predefined_lod.kpp", true); tester.testSimpleStroke(); } QTEST_MAIN(FreehandStrokeTest) diff --git a/libs/ui/tool/KisStrokeSpeedMonitor.cpp b/libs/ui/tool/KisStrokeSpeedMonitor.cpp new file mode 100644 index 0000000000..4dccf85d83 --- /dev/null +++ b/libs/ui/tool/KisStrokeSpeedMonitor.cpp @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisStrokeSpeedMonitor.h" + +#include +#include +#include + +#include +#include "kis_paintop_preset.h" +#include "kis_paintop_settings.h" + +#include "kis_config.h" +#include "kis_config_notifier.h" +#include "KisUpdateSchedulerConfigNotifier.h" + + +Q_GLOBAL_STATIC(KisStrokeSpeedMonitor, s_instance) + + +struct KisStrokeSpeedMonitor::Private +{ + static const int averageWindow = 10; + + Private() + : avgCursorSpeed(averageWindow), + avgRenderingSpeed(averageWindow), + avgFps(averageWindow) + { + } + + KisRollingMeanAccumulatorWrapper avgCursorSpeed; + KisRollingMeanAccumulatorWrapper avgRenderingSpeed; + KisRollingMeanAccumulatorWrapper avgFps; + + qreal cachedAvgCursorSpeed = 0; + qreal cachedAvgRenderingSpeed = 0; + qreal cachedAvgFps = 0; + + qreal lastCursorSpeed = 0; + qreal lastRenderingSpeed = 0; + qreal lastFps = 0; + bool lastStrokeSaturated = false; + + QByteArray lastPresetMd5; + QString lastPresetName; + qreal lastPresetSize = 0; + + bool haveStrokeSpeedMeasurement = true; + + QMutex mutex; +}; + +KisStrokeSpeedMonitor::KisStrokeSpeedMonitor() + : m_d(new Private()) +{ + connect(KisUpdateSchedulerConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(resetAccumulatedValues())); + connect(KisUpdateSchedulerConfigNotifier::instance(), SIGNAL(configChanged()), SIGNAL(sigStatsUpdated())); + connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); + + slotConfigChanged(); +} + +KisStrokeSpeedMonitor::~KisStrokeSpeedMonitor() +{ +} + +KisStrokeSpeedMonitor *KisStrokeSpeedMonitor::instance() +{ + return s_instance; +} + +bool KisStrokeSpeedMonitor::haveStrokeSpeedMeasurement() const +{ + return m_d->haveStrokeSpeedMeasurement; +} + +void KisStrokeSpeedMonitor::setHaveStrokeSpeedMeasurement(bool value) +{ + m_d->haveStrokeSpeedMeasurement = value; +} + +void KisStrokeSpeedMonitor::resetAccumulatedValues() +{ + m_d->avgCursorSpeed.reset(m_d->averageWindow); + m_d->avgRenderingSpeed.reset(m_d->averageWindow); + m_d->avgFps.reset(m_d->averageWindow); +} + +void KisStrokeSpeedMonitor::slotConfigChanged() +{ + KisConfig cfg; + m_d->haveStrokeSpeedMeasurement = cfg.enableBrushSpeedLogging(); + resetAccumulatedValues(); + emit sigStatsUpdated(); +} + +void KisStrokeSpeedMonitor::notifyStrokeFinished(qreal cursorSpeed, qreal renderingSpeed, qreal fps, KisPaintOpPresetSP preset) +{ + if (qFuzzyCompare(cursorSpeed, 0.0) || qFuzzyCompare(renderingSpeed, 0.0)) return; + + QMutexLocker locker(&m_d->mutex); + + const bool isSamePreset = + m_d->lastPresetName == preset->name() && + qFuzzyCompare(m_d->lastPresetSize, preset->settings()->paintOpSize()); + + ENTER_FUNCTION() << ppVar(isSamePreset); + + if (!isSamePreset) { + resetAccumulatedValues(); + m_d->lastPresetName = preset->name(); + m_d->lastPresetSize = preset->settings()->paintOpSize(); + } + + m_d->lastCursorSpeed = cursorSpeed; + m_d->lastRenderingSpeed = renderingSpeed; + m_d->lastFps = fps; + + + static const qreal saturationSpeedThreshold = 0.30; // cursor speed should be at least 30% higher + m_d->lastStrokeSaturated = cursorSpeed / renderingSpeed > (1.0 + saturationSpeedThreshold); + + + if (m_d->lastStrokeSaturated) { + m_d->avgCursorSpeed(cursorSpeed); + m_d->avgRenderingSpeed(renderingSpeed); + m_d->avgFps(fps); + + m_d->cachedAvgCursorSpeed = m_d->avgCursorSpeed.rollingMean(); + m_d->cachedAvgRenderingSpeed = m_d->avgRenderingSpeed.rollingMean(); + m_d->cachedAvgFps = m_d->avgFps.rollingMean(); + } + + emit sigStatsUpdated(); + + + ENTER_FUNCTION() << + QString(" CS: %1 RS: %2 FPS: %3 %4") + .arg(m_d->lastCursorSpeed, 5) + .arg(m_d->lastRenderingSpeed, 5) + .arg(m_d->lastFps, 5) + .arg(m_d->lastStrokeSaturated ? "(saturated)" : ""); + ENTER_FUNCTION() << + QString("ACS: %1 ARS: %2 AFPS: %3") + .arg(m_d->cachedAvgCursorSpeed, 5) + .arg(m_d->cachedAvgRenderingSpeed, 5) + .arg(m_d->cachedAvgFps, 5); +} + +QString KisStrokeSpeedMonitor::lastPresetName() const +{ + return m_d->lastPresetName; +} + +qreal KisStrokeSpeedMonitor::lastPresetSize() const +{ + return m_d->lastPresetSize; +} + +qreal KisStrokeSpeedMonitor::lastCursorSpeed() const +{ + return m_d->lastCursorSpeed; +} + +qreal KisStrokeSpeedMonitor::lastRenderingSpeed() const +{ + return m_d->lastRenderingSpeed; +} + +qreal KisStrokeSpeedMonitor::lastFps() const +{ + return m_d->lastFps; +} + +bool KisStrokeSpeedMonitor::lastStrokeSaturated() const +{ + return m_d->lastStrokeSaturated; +} + +qreal KisStrokeSpeedMonitor::avgCursorSpeed() const +{ + return m_d->cachedAvgCursorSpeed; +} + +qreal KisStrokeSpeedMonitor::avgRenderingSpeed() const +{ + return m_d->cachedAvgRenderingSpeed; +} + +qreal KisStrokeSpeedMonitor::avgFps() const +{ + return m_d->cachedAvgFps; +} diff --git a/libs/ui/tool/KisStrokeSpeedMonitor.h b/libs/ui/tool/KisStrokeSpeedMonitor.h new file mode 100644 index 0000000000..51536ed1b6 --- /dev/null +++ b/libs/ui/tool/KisStrokeSpeedMonitor.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KISSTROKESPEEDMONITOR_H +#define KISSTROKESPEEDMONITOR_H + +#include + +#include "kis_types.h" +#include "kritaui_export.h" + +class KRITAUI_EXPORT KisStrokeSpeedMonitor : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString lastPresetName READ lastPresetName NOTIFY sigStatsUpdated) + Q_PROPERTY(qreal lastPresetSize READ lastPresetSize NOTIFY sigStatsUpdated) + + Q_PROPERTY(qreal lastCursorSpeed READ lastCursorSpeed NOTIFY sigStatsUpdated) + Q_PROPERTY(qreal lastRenderingSpeed READ lastRenderingSpeed NOTIFY sigStatsUpdated) + Q_PROPERTY(qreal lastFps READ lastFps NOTIFY sigStatsUpdated) + + Q_PROPERTY(bool lastStrokeSaturated READ lastCursorSpeed NOTIFY sigStatsUpdated) + + Q_PROPERTY(qreal avgCursorSpeed READ avgCursorSpeed NOTIFY sigStatsUpdated) + Q_PROPERTY(qreal avgRenderingSpeed READ avgRenderingSpeed NOTIFY sigStatsUpdated) + Q_PROPERTY(qreal avgFps READ avgFps NOTIFY sigStatsUpdated) + +public: + KisStrokeSpeedMonitor(); + ~KisStrokeSpeedMonitor(); + + static KisStrokeSpeedMonitor* instance(); + + bool haveStrokeSpeedMeasurement() const; + + void notifyStrokeFinished(qreal cursorSpeed, qreal renderingSpeed, qreal fps, KisPaintOpPresetSP preset); + + + QString lastPresetName() const; + qreal lastPresetSize() const; + + qreal lastCursorSpeed() const; + qreal lastRenderingSpeed() const; + qreal lastFps() const; + bool lastStrokeSaturated() const; + + qreal avgCursorSpeed() const; + qreal avgRenderingSpeed() const; + qreal avgFps() const; + + +Q_SIGNALS: + void sigStatsUpdated(); + +public Q_SLOTS: + void setHaveStrokeSpeedMeasurement(bool value); + +private Q_SLOTS: + void resetAccumulatedValues(); + void slotConfigChanged(); + +private: + struct Private; + const QScopedPointer m_d; +}; + +#endif // KISSTROKESPEEDMONITOR_H diff --git a/libs/ui/tool/kis_figure_painting_tool_helper.cpp b/libs/ui/tool/kis_figure_painting_tool_helper.cpp index 96880b689f..549c8db0a9 100644 --- a/libs/ui/tool/kis_figure_painting_tool_helper.cpp +++ b/libs/ui/tool/kis_figure_painting_tool_helper.cpp @@ -1,151 +1,145 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_figure_painting_tool_helper.h" #include #include "kis_resources_snapshot.h" #include #include "kis_image.h" #include "kis_painter.h" KisFigurePaintingToolHelper::KisFigurePaintingToolHelper(const KUndo2MagicString &name, KisImageWSP image, KisNodeSP currentNode, KoCanvasResourceManager *resourceManager, KisPainter::StrokeStyle strokeStyle, KisPainter::FillStyle fillStyle) { m_strokesFacade = image.data(); m_resources = new KisResourcesSnapshot(image, currentNode, resourceManager); m_resources->setStrokeStyle(strokeStyle); m_resources->setFillStyle(fillStyle); PainterInfo *painterInfo = new PainterInfo(); KisStrokeStrategy *stroke = new FreehandStrokeStrategy(m_resources->needsIndirectPainting(), m_resources->indirectPaintingCompositeOp(), m_resources, painterInfo, name); m_strokeId = m_strokesFacade->startStroke(stroke); } KisFigurePaintingToolHelper::~KisFigurePaintingToolHelper() { + m_strokesFacade->addJob(m_strokeId, + new FreehandStrokeStrategy::UpdateData(true)); m_strokesFacade->endStroke(m_strokeId); } void KisFigurePaintingToolHelper::paintLine(const KisPaintInformation &pi0, const KisPaintInformation &pi1) { m_strokesFacade->addJob(m_strokeId, - new FreehandStrokeStrategy::Data(m_resources->currentNode(), - 0, + new FreehandStrokeStrategy::Data(0, pi0, pi1)); } void KisFigurePaintingToolHelper::paintPolyline(const vQPointF &points) { m_strokesFacade->addJob(m_strokeId, - new FreehandStrokeStrategy::Data(m_resources->currentNode(), - 0, + new FreehandStrokeStrategy::Data(0, FreehandStrokeStrategy::Data::POLYLINE, points)); } void KisFigurePaintingToolHelper::paintPolygon(const vQPointF &points) { m_strokesFacade->addJob(m_strokeId, - new FreehandStrokeStrategy::Data(m_resources->currentNode(), - 0, + new FreehandStrokeStrategy::Data(0, FreehandStrokeStrategy::Data::POLYGON, points)); } void KisFigurePaintingToolHelper::paintRect(const QRectF &rect) { m_strokesFacade->addJob(m_strokeId, - new FreehandStrokeStrategy::Data(m_resources->currentNode(), - 0, + new FreehandStrokeStrategy::Data(0, FreehandStrokeStrategy::Data::RECT, rect)); } void KisFigurePaintingToolHelper::paintEllipse(const QRectF &rect) { m_strokesFacade->addJob(m_strokeId, - new FreehandStrokeStrategy::Data(m_resources->currentNode(), - 0, + new FreehandStrokeStrategy::Data(0, FreehandStrokeStrategy::Data::ELLIPSE, rect)); } void KisFigurePaintingToolHelper::paintPainterPath(const QPainterPath &path) { m_strokesFacade->addJob(m_strokeId, - new FreehandStrokeStrategy::Data(m_resources->currentNode(), - 0, + new FreehandStrokeStrategy::Data(0, FreehandStrokeStrategy::Data::PAINTER_PATH, path)); } void KisFigurePaintingToolHelper::setFGColorOverride(const KoColor &color) { m_resources->setFGColorOverride(color); } void KisFigurePaintingToolHelper::setBGColorOverride(const KoColor &color) { m_resources->setBGColorOverride(color); } void KisFigurePaintingToolHelper::setSelectionOverride(KisSelectionSP m_selection) { m_resources->setSelectionOverride(m_selection); } void KisFigurePaintingToolHelper::setBrush(const KisPaintOpPresetSP &brush) { m_resources->setBrush(brush); } void KisFigurePaintingToolHelper::paintPainterPathQPen(const QPainterPath path, const QPen &pen, const KoColor &color) { m_strokesFacade->addJob(m_strokeId, - new FreehandStrokeStrategy::Data(m_resources->currentNode(), - 0, + new FreehandStrokeStrategy::Data(0, FreehandStrokeStrategy::Data::QPAINTER_PATH, path, pen, color)); } void KisFigurePaintingToolHelper::paintPainterPathQPenFill(const QPainterPath path, const QPen &pen, const KoColor &color) { m_strokesFacade->addJob(m_strokeId, - new FreehandStrokeStrategy::Data(m_resources->currentNode(), - 0, + new FreehandStrokeStrategy::Data(0, FreehandStrokeStrategy::Data::QPAINTER_PATH_FILL, path, pen, color)); } diff --git a/libs/ui/tool/kis_resources_snapshot.cpp b/libs/ui/tool/kis_resources_snapshot.cpp index dc20e63113..1e4f87d28e 100644 --- a/libs/ui/tool/kis_resources_snapshot.cpp +++ b/libs/ui/tool/kis_resources_snapshot.cpp @@ -1,394 +1,399 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_resources_snapshot.h" #include #include #include #include #include #include #include #include #include "kis_canvas_resource_provider.h" #include "filter/kis_filter_configuration.h" #include "kis_image.h" #include "kis_paint_device.h" #include "kis_paint_layer.h" #include "recorder/kis_recorded_paint_action.h" #include "kis_selection.h" #include "kis_selection_mask.h" #include "kis_algebra_2d.h" struct KisResourcesSnapshot::Private { Private() : currentPattern(0) , currentGradient(0) , currentGenerator(0) , compositeOp(0) { } KisImageSP image; KisDefaultBoundsBaseSP bounds; KoColor currentFgColor; KoColor currentBgColor; - KoPattern *currentPattern; + KoPattern *currentPattern = 0; KoAbstractGradient *currentGradient; KisPaintOpPresetSP currentPaintOpPreset; KisNodeSP currentNode; qreal currentExposure; KisFilterConfigurationSP currentGenerator; QPointF axesCenter; - bool mirrorMaskHorizontal; - bool mirrorMaskVertical; + bool mirrorMaskHorizontal = false; + bool mirrorMaskVertical = false; - quint8 opacity; - QString compositeOpId; + quint8 opacity = OPACITY_OPAQUE_U8; + QString compositeOpId = COMPOSITE_OVER; const KoCompositeOp *compositeOp; - KisPainter::StrokeStyle strokeStyle; - KisPainter::FillStyle fillStyle; + KisPainter::StrokeStyle strokeStyle = KisPainter::StrokeStyleBrush; + KisPainter::FillStyle fillStyle = KisPainter::FillStyleForegroundColor; - bool globalAlphaLock; - qreal effectiveZoom; - bool presetAllowsLod; + bool globalAlphaLock = false; + qreal effectiveZoom = 1.0; + bool presetAllowsLod = false; KisSelectionSP selectionOverride; }; KisResourcesSnapshot::KisResourcesSnapshot(KisImageSP image, KisNodeSP currentNode, KoCanvasResourceManager *resourceManager, KisDefaultBoundsBaseSP bounds) : m_d(new Private()) { m_d->image = image; if (!bounds) { bounds = new KisDefaultBounds(m_d->image); } m_d->bounds = bounds; m_d->currentFgColor = resourceManager->resource(KoCanvasResourceManager::ForegroundColor).value(); m_d->currentBgColor = resourceManager->resource(KoCanvasResourceManager::BackgroundColor).value(); m_d->currentPattern = resourceManager->resource(KisCanvasResourceProvider::CurrentPattern).value(); m_d->currentGradient = resourceManager->resource(KisCanvasResourceProvider::CurrentGradient).value(); /** * We should deep-copy the preset, so that long-runnign actions * will have correct brush parameters. Theoretically this cloniong * can be expensive, but according to measurements, it takes * something like 0.1 ms for an average preset. */ m_d->currentPaintOpPreset = resourceManager->resource(KisCanvasResourceProvider::CurrentPaintOpPreset).value()->clone(); #ifdef HAVE_THREADED_TEXT_RENDERING_WORKAROUND KisPaintOpRegistry::instance()->preinitializePaintOpIfNeeded(m_d->currentPaintOpPreset); #endif /* HAVE_THREADED_TEXT_RENDERING_WORKAROUND */ m_d->currentExposure = resourceManager->resource(KisCanvasResourceProvider::HdrExposure).toDouble(); m_d->currentGenerator = resourceManager->resource(KisCanvasResourceProvider::CurrentGeneratorConfiguration).value(); QPointF relativeAxesCenter(0.5, 0.5); if (m_d->image) { relativeAxesCenter = m_d->image->mirrorAxesCenter(); } m_d->axesCenter = KisAlgebra2D::relativeToAbsolute(relativeAxesCenter, m_d->bounds->bounds()); m_d->mirrorMaskHorizontal = resourceManager->resource(KisCanvasResourceProvider::MirrorHorizontal).toBool(); m_d->mirrorMaskVertical = resourceManager->resource(KisCanvasResourceProvider::MirrorVertical).toBool(); qreal normOpacity = resourceManager->resource(KisCanvasResourceProvider::Opacity).toDouble(); m_d->opacity = quint8(normOpacity * OPACITY_OPAQUE_U8); m_d->compositeOpId = resourceManager->resource(KisCanvasResourceProvider::CurrentEffectiveCompositeOp).toString(); setCurrentNode(currentNode); /** * Fill and Stroke styles are not a part of the resource manager * so the tools should set them manually * TODO: port stroke and fill styles to be a part * of the resource manager */ m_d->strokeStyle = KisPainter::StrokeStyleBrush; m_d->fillStyle = KisPainter::FillStyleNone; m_d->globalAlphaLock = resourceManager->resource(KisCanvasResourceProvider::GlobalAlphaLock).toBool(); m_d->effectiveZoom = resourceManager->resource(KisCanvasResourceProvider::EffectiveZoom).toDouble(); m_d->presetAllowsLod = resourceManager->resource(KisCanvasResourceProvider::PresetAllowsLod).toBool(); } KisResourcesSnapshot::KisResourcesSnapshot(KisImageSP image, KisNodeSP currentNode, KisDefaultBoundsBaseSP bounds) : m_d(new Private()) { m_d->image = image; if (!bounds) { bounds = new KisDefaultBounds(m_d->image); } m_d->bounds = bounds; #ifdef HAVE_THREADED_TEXT_RENDERING_WORKAROUND KisPaintOpRegistry::instance()->preinitializePaintOpIfNeeded(m_d->currentPaintOpPreset); #endif /* HAVE_THREADED_TEXT_RENDERING_WORKAROUND */ QPointF relativeAxesCenter(0.5, 0.5); if (m_d->image) { relativeAxesCenter = m_d->image->mirrorAxesCenter(); } m_d->axesCenter = KisAlgebra2D::relativeToAbsolute(relativeAxesCenter, m_d->bounds->bounds()); m_d->opacity = OPACITY_OPAQUE_U8; setCurrentNode(currentNode); /** * Fill and Stroke styles are not a part of the resource manager * so the tools should set them manually * TODO: port stroke and fill styles to be a part * of the resource manager */ m_d->strokeStyle = KisPainter::StrokeStyleBrush; m_d->fillStyle = KisPainter::FillStyleNone; } KisResourcesSnapshot::~KisResourcesSnapshot() { delete m_d; } void KisResourcesSnapshot::setupPainter(KisPainter* painter) { painter->setPaintColor(m_d->currentFgColor); painter->setBackgroundColor(m_d->currentBgColor); painter->setGenerator(m_d->currentGenerator); painter->setPattern(m_d->currentPattern); painter->setGradient(m_d->currentGradient); QBitArray lockflags = channelLockFlags(); if (lockflags.size() > 0) { painter->setChannelFlags(lockflags); } painter->setOpacity(m_d->opacity); painter->setCompositeOp(m_d->compositeOp); painter->setMirrorInformation(m_d->axesCenter, m_d->mirrorMaskHorizontal, m_d->mirrorMaskVertical); painter->setStrokeStyle(m_d->strokeStyle); painter->setFillStyle(m_d->fillStyle); /** * The paintOp should be initialized the last, because it may * ask the painter for some options while initialization */ painter->setPaintOpPreset(m_d->currentPaintOpPreset, m_d->currentNode, m_d->image); } void KisResourcesSnapshot::setupPaintAction(KisRecordedPaintAction *action) { action->setPaintOpPreset(m_d->currentPaintOpPreset); action->setPaintIncremental(!needsIndirectPainting()); action->setPaintColor(m_d->currentFgColor); action->setBackgroundColor(m_d->currentBgColor); action->setGenerator(m_d->currentGenerator); action->setGradient(m_d->currentGradient); action->setPattern(m_d->currentPattern); action->setOpacity(m_d->opacity / qreal(OPACITY_OPAQUE_U8)); action->setCompositeOp(m_d->compositeOp->id()); action->setStrokeStyle(m_d->strokeStyle); action->setFillStyle(m_d->fillStyle); } KisPostExecutionUndoAdapter* KisResourcesSnapshot::postExecutionUndoAdapter() const { return m_d->image ? m_d->image->postExecutionUndoAdapter() : 0; } void KisResourcesSnapshot::setCurrentNode(KisNodeSP node) { m_d->currentNode = node; KisPaintDeviceSP device; if(m_d->currentNode && (device = m_d->currentNode->paintDevice())) { m_d->compositeOp = device->colorSpace()->compositeOp(m_d->compositeOpId); if(!m_d->compositeOp) { m_d->compositeOp = device->colorSpace()->compositeOp(COMPOSITE_OVER); } } } void KisResourcesSnapshot::setStrokeStyle(KisPainter::StrokeStyle strokeStyle) { m_d->strokeStyle = strokeStyle; } void KisResourcesSnapshot::setFillStyle(KisPainter::FillStyle fillStyle) { m_d->fillStyle = fillStyle; } KisNodeSP KisResourcesSnapshot::currentNode() const { return m_d->currentNode; } KisImageSP KisResourcesSnapshot::image() const { return m_d->image; } bool KisResourcesSnapshot::needsIndirectPainting() const { return !m_d->currentPaintOpPreset->settings()->paintIncremental(); } QString KisResourcesSnapshot::indirectPaintingCompositeOp() const { return m_d->currentPaintOpPreset->settings()->indirectPaintingCompositeOp(); } KisSelectionSP KisResourcesSnapshot::activeSelection() const { /** * It is possible to have/use the snapshot without the image. Such * usecase is present for example in the scratchpad. */ if (m_d->selectionOverride) { return m_d->selectionOverride; } KisSelectionSP selection = m_d->image ? m_d->image->globalSelection() : 0; KisLayerSP layer = qobject_cast(m_d->currentNode.data()); KisSelectionMaskSP mask; if((layer = qobject_cast(m_d->currentNode.data()))) { selection = layer->selection(); } else if ((mask = dynamic_cast(m_d->currentNode.data())) && mask->selection() == selection) { selection = 0; } return selection; } bool KisResourcesSnapshot::needsAirbrushing() const { return m_d->currentPaintOpPreset->settings()->isAirbrushing(); } qreal KisResourcesSnapshot::airbrushingInterval() const { return m_d->currentPaintOpPreset->settings()->airbrushInterval(); } bool KisResourcesSnapshot::needsSpacingUpdates() const { return m_d->currentPaintOpPreset->settings()->useSpacingUpdates(); } void KisResourcesSnapshot::setOpacity(qreal opacity) { m_d->opacity = opacity * OPACITY_OPAQUE_U8; } quint8 KisResourcesSnapshot::opacity() const { return m_d->opacity; } const KoCompositeOp* KisResourcesSnapshot::compositeOp() const { return m_d->compositeOp; } QString KisResourcesSnapshot::compositeOpId() const { return m_d->compositeOpId; } KoPattern* KisResourcesSnapshot::currentPattern() const { return m_d->currentPattern; } KoColor KisResourcesSnapshot::currentFgColor() const { return m_d->currentFgColor; } KoColor KisResourcesSnapshot::currentBgColor() const { return m_d->currentBgColor; } KisPaintOpPresetSP KisResourcesSnapshot::currentPaintOpPreset() const { return m_d->currentPaintOpPreset; } QBitArray KisResourcesSnapshot::channelLockFlags() const { QBitArray channelFlags; KisPaintLayer *paintLayer; if ((paintLayer = dynamic_cast(m_d->currentNode.data()))) { channelFlags = paintLayer->channelLockFlags(); if (m_d->globalAlphaLock) { if (channelFlags.isEmpty()) { channelFlags = paintLayer->colorSpace()->channelFlags(true, true); } channelFlags &= paintLayer->colorSpace()->channelFlags(true, false); } } return channelFlags; } qreal KisResourcesSnapshot::effectiveZoom() const { return m_d->effectiveZoom; } bool KisResourcesSnapshot::presetAllowsLod() const { return m_d->presetAllowsLod; } +bool KisResourcesSnapshot::presetNeedsAsynchronousUpdates() const +{ + return m_d->currentPaintOpPreset && m_d->currentPaintOpPreset->settings()->needsAsynchronousUpdates(); +} + void KisResourcesSnapshot::setFGColorOverride(const KoColor &color) { m_d->currentFgColor = color; } void KisResourcesSnapshot::setBGColorOverride(const KoColor &color) { m_d->currentBgColor = color; } void KisResourcesSnapshot::setSelectionOverride(KisSelectionSP selection) { m_d->selectionOverride = selection; } void KisResourcesSnapshot::setBrush(const KisPaintOpPresetSP &brush) { m_d->currentPaintOpPreset = brush; } diff --git a/libs/ui/tool/kis_resources_snapshot.h b/libs/ui/tool/kis_resources_snapshot.h index dede9ba147..b223f401c1 100644 --- a/libs/ui/tool/kis_resources_snapshot.h +++ b/libs/ui/tool/kis_resources_snapshot.h @@ -1,106 +1,107 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_RESOURCES_SNAPSHOT_H #define __KIS_RESOURCES_SNAPSHOT_H #include "kis_shared.h" #include "kis_shared_ptr.h" #include "kis_types.h" #include "kritaui_export.h" #include "kis_painter.h" #include "kis_default_bounds.h" class KoCanvasResourceManager; class KoCompositeOp; class KisPainter; class KisPostExecutionUndoAdapter; class KisRecordedPaintAction; class KoPattern; /** * @brief The KisResourcesSnapshot class takes a snapshot of the various resources * like colors and settings used at the begin of a stroke or a recording so subsequent * changes don't impact the running stroke. The main reason for the snapshot is that the * user can *change* the options while the stroke is being executed in the background. */ class KRITAUI_EXPORT KisResourcesSnapshot : public KisShared { public: KisResourcesSnapshot(KisImageSP image, KisNodeSP currentNode, KoCanvasResourceManager *resourceManager, KisDefaultBoundsBaseSP bounds = 0); KisResourcesSnapshot(KisImageSP image, KisNodeSP currentNode, KisDefaultBoundsBaseSP bounds = 0); ~KisResourcesSnapshot(); void setupPainter(KisPainter *painter); // XXX: This was marked as KDE_DEPRECATED, but no althernative was // given in the apidox. void setupPaintAction(KisRecordedPaintAction *action); KisPostExecutionUndoAdapter* postExecutionUndoAdapter() const; void setCurrentNode(KisNodeSP node); void setStrokeStyle(KisPainter::StrokeStyle strokeStyle); void setFillStyle(KisPainter::FillStyle fillStyle); KisNodeSP currentNode() const; KisImageSP image() const; bool needsIndirectPainting() const; QString indirectPaintingCompositeOp() const; /** * \return currently active selection. Note that it will return * null if current node *is* the current selection. This * is done to avoid recursive selection application when * painting on selectgion masks. */ KisSelectionSP activeSelection() const; bool needsAirbrushing() const; qreal airbrushingInterval() const; bool needsSpacingUpdates() const; void setOpacity(qreal opacity); quint8 opacity() const; const KoCompositeOp* compositeOp() const; QString compositeOpId() const; KoPattern* currentPattern() const; KoColor currentFgColor() const; KoColor currentBgColor() const; KisPaintOpPresetSP currentPaintOpPreset() const; /// @return the channel lock flags of the current node with the global override applied QBitArray channelLockFlags() const; qreal effectiveZoom() const; bool presetAllowsLod() const; + bool presetNeedsAsynchronousUpdates() const; void setFGColorOverride(const KoColor &color); void setBGColorOverride(const KoColor &color); void setSelectionOverride(KisSelectionSP selection); void setBrush(const KisPaintOpPresetSP &brush); private: struct Private; Private * const m_d; }; typedef KisSharedPtr KisResourcesSnapshotSP; #endif /* __KIS_RESOURCES_SNAPSHOT_H */ diff --git a/libs/ui/tool/kis_tool_freehand_helper.cpp b/libs/ui/tool/kis_tool_freehand_helper.cpp index 5e74ba16d1..eb4490709f 100644 --- a/libs/ui/tool/kis_tool_freehand_helper.cpp +++ b/libs/ui/tool/kis_tool_freehand_helper.cpp @@ -1,982 +1,997 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_tool_freehand_helper.h" #include #include #include #include #include #include "kis_algebra_2d.h" #include "kis_distance_information.h" #include "kis_painting_information_builder.h" #include "kis_recording_adapter.h" #include "kis_image.h" #include "kis_painter.h" #include #include #include "kis_update_time_monitor.h" #include "kis_stabilized_events_sampler.h" #include "KisStabilizerDelayedPaintHelper.h" #include "kis_config.h" #include //#define DEBUG_BEZIER_CURVES // Factor by which to scale the airbrush timer's interval, relative to the actual airbrushing rate. // Setting this less than 1 makes the timer-generated pseudo-events happen faster than the desired // airbrush rate, which can improve responsiveness. const qreal AIRBRUSH_INTERVAL_FACTOR = 0.5; // The amount of time, in milliseconds, to allow between updates of the spacing information. Only // used when spacing updates between dabs are enabled. const qreal SPACING_UPDATE_INTERVAL = 50.0; // The amount of time, in milliseconds, to allow between updates of the timing information. Only // used when airbrushing. const qreal TIMING_UPDATE_INTERVAL = 50.0; struct KisToolFreehandHelper::Private { KisPaintingInformationBuilder *infoBuilder; KisRecordingAdapter *recordingAdapter; KisStrokesFacade *strokesFacade; KUndo2MagicString transactionText; bool haveTangent; QPointF previousTangent; bool hasPaintAtLeastOnce; QTime strokeTime; QTimer strokeTimeoutTimer; QVector painterInfos; KisResourcesSnapshotSP resources; KisStrokeId strokeId; KisPaintInformation previousPaintInformation; KisPaintInformation olderPaintInformation; KisSmoothingOptionsSP smoothingOptions; // Timer used to generate paint updates periodically even without input events. This is only // used for paintops that depend on timely updates even when the cursor is not moving, e.g. for // airbrushing effects. QTimer airbrushingTimer; QList history; QList distanceHistory; // Keeps track of past cursor positions. This is used to determine the drawing angle when // drawing the brush outline or starting a stroke. KisPaintOpUtils::PositionHistory lastCursorPos; // Stabilizer data QQueue stabilizerDeque; QTimer stabilizerPollTimer; KisStabilizedEventsSampler stabilizedSampler; KisStabilizerDelayedPaintHelper stabilizerDelayedPaintHelper; + QTimer asynchronousUpdatesThresholdTimer; + int canvasRotation; bool canvasMirroredH; qreal effectiveSmoothnessDistance() const; }; KisToolFreehandHelper::KisToolFreehandHelper(KisPaintingInformationBuilder *infoBuilder, const KUndo2MagicString &transactionText, KisRecordingAdapter *recordingAdapter, KisSmoothingOptions *smoothingOptions) : m_d(new Private()) { m_d->infoBuilder = infoBuilder; m_d->recordingAdapter = recordingAdapter; m_d->transactionText = transactionText; m_d->smoothingOptions = KisSmoothingOptionsSP( smoothingOptions ? smoothingOptions : new KisSmoothingOptions()); m_d->canvasRotation = 0; m_d->strokeTimeoutTimer.setSingleShot(true); connect(&m_d->strokeTimeoutTimer, SIGNAL(timeout()), SLOT(finishStroke())); connect(&m_d->airbrushingTimer, SIGNAL(timeout()), SLOT(doAirbrushing())); + connect(&m_d->asynchronousUpdatesThresholdTimer, SIGNAL(timeout()), SLOT(doAsynchronousUpdate())); connect(&m_d->stabilizerPollTimer, SIGNAL(timeout()), SLOT(stabilizerPollAndPaint())); m_d->stabilizerDelayedPaintHelper.setPaintLineCallback( [this](const KisPaintInformation &pi1, const KisPaintInformation &pi2) { paintLine(pi1, pi2); }); m_d->stabilizerDelayedPaintHelper.setUpdateOutlineCallback( [this]() { emit requestExplicitUpdateOutline(); }); } KisToolFreehandHelper::~KisToolFreehandHelper() { delete m_d; } void KisToolFreehandHelper::setSmoothness(KisSmoothingOptionsSP smoothingOptions) { m_d->smoothingOptions = smoothingOptions; } KisSmoothingOptionsSP KisToolFreehandHelper::smoothingOptions() const { return m_d->smoothingOptions; } QPainterPath KisToolFreehandHelper::paintOpOutline(const QPointF &savedCursorPos, const KoPointerEvent *event, const KisPaintOpSettingsSP globalSettings, KisPaintOpSettings::OutlineMode mode) const { KisPaintOpSettingsSP settings = globalSettings; KisPaintInformation info = m_d->infoBuilder->hover(savedCursorPos, event); QPointF prevPoint = m_d->lastCursorPos.pushThroughHistory(savedCursorPos); qreal startAngle = KisAlgebra2D::directionBetweenPoints(prevPoint, savedCursorPos, 0); info.setCanvasRotation(m_d->canvasRotation); info.setCanvasHorizontalMirrorState( m_d->canvasMirroredH ); - KisDistanceInformation distanceInfo(prevPoint, 0, startAngle); + KisDistanceInformation distanceInfo(prevPoint, startAngle); if (!m_d->painterInfos.isEmpty()) { settings = m_d->resources->currentPaintOpPreset()->settings(); if (m_d->stabilizerDelayedPaintHelper.running() && m_d->stabilizerDelayedPaintHelper.hasLastPaintInformation()) { info = m_d->stabilizerDelayedPaintHelper.lastPaintInformation(); } else { info = m_d->previousPaintInformation; } /** * When LoD mode is active it may happen that the helper has * already started a stroke, but it painted noting, because * all the work is being calculated by the scaled-down LodN * stroke. So at first we try to fetch the data from the lodN * stroke ("buddy") and then check if there is at least * something has been painted with this distance information * object. */ KisDistanceInformation *buddyDistance = m_d->painterInfos.first()->buddyDragDistance(); if (buddyDistance) { /** * Tiny hack alert: here we fetch the distance information * directly from the LodN stroke. Ideally, we should * upscale its data, but here we just override it with our * local copy of the coordinates. */ distanceInfo = *buddyDistance; - distanceInfo.overrideLastValues(prevPoint, 0, startAngle); + distanceInfo.overrideLastValues(prevPoint, startAngle); } else if (m_d->painterInfos.first()->dragDistance->isStarted()) { distanceInfo = *m_d->painterInfos.first()->dragDistance; } } KisPaintInformation::DistanceInformationRegistrar registrar = info.registerDistanceInformation(&distanceInfo); QPainterPath outline = settings->brushOutline(info, mode); if (m_d->resources && m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::STABILIZER && m_d->smoothingOptions->useDelayDistance()) { const qreal R = m_d->smoothingOptions->delayDistance() / m_d->resources->effectiveZoom(); outline.addEllipse(info.pos(), R, R); } return outline; } void KisToolFreehandHelper::cursorMoved(const QPointF &cursorPos) { m_d->lastCursorPos.pushThroughHistory(cursorPos); } void KisToolFreehandHelper::initPaint(KoPointerEvent *event, const QPointF &pixelCoords, KoCanvasResourceManager *resourceManager, KisImageWSP image, KisNodeSP currentNode, KisStrokesFacade *strokesFacade, KisNodeSP overrideNode, KisDefaultBoundsBaseSP bounds) { QPointF prevPoint = m_d->lastCursorPos.pushThroughHistory(pixelCoords); m_d->strokeTime.start(); KisPaintInformation pi = m_d->infoBuilder->startStroke(event, elapsedStrokeTime(), resourceManager); qreal startAngle = KisAlgebra2D::directionBetweenPoints(prevPoint, pixelCoords, 0.0); initPaintImpl(startAngle, pi, resourceManager, image, currentNode, strokesFacade, overrideNode, bounds); } bool KisToolFreehandHelper::isRunning() const { return m_d->strokeId; } void KisToolFreehandHelper::initPaintImpl(qreal startAngle, const KisPaintInformation &pi, KoCanvasResourceManager *resourceManager, KisImageWSP image, KisNodeSP currentNode, KisStrokesFacade *strokesFacade, KisNodeSP overrideNode, KisDefaultBoundsBaseSP bounds) { m_d->strokesFacade = strokesFacade; m_d->haveTangent = false; m_d->previousTangent = QPointF(); m_d->hasPaintAtLeastOnce = false; m_d->previousPaintInformation = pi; m_d->resources = new KisResourcesSnapshot(image, currentNode, resourceManager, bounds); if(overrideNode) { m_d->resources->setCurrentNode(overrideNode); } const bool airbrushing = m_d->resources->needsAirbrushing(); const bool useSpacingUpdates = m_d->resources->needsSpacingUpdates(); KisDistanceInitInfo startDistInfo(m_d->previousPaintInformation.pos(), - m_d->previousPaintInformation.currentTime(), startAngle, useSpacingUpdates ? SPACING_UPDATE_INTERVAL : LONG_TIME, - airbrushing ? TIMING_UPDATE_INTERVAL : LONG_TIME); + airbrushing ? TIMING_UPDATE_INTERVAL : LONG_TIME, + 0); KisDistanceInformation startDist = startDistInfo.makeDistInfo(); createPainters(m_d->painterInfos, startDist); if(m_d->recordingAdapter) { m_d->recordingAdapter->startStroke(image, m_d->resources, startDistInfo); } KisStrokeStrategy *stroke = new FreehandStrokeStrategy(m_d->resources->needsIndirectPainting(), m_d->resources->indirectPaintingCompositeOp(), m_d->resources, m_d->painterInfos, m_d->transactionText); m_d->strokeId = m_d->strokesFacade->startStroke(stroke); m_d->history.clear(); m_d->distanceHistory.clear(); - if(airbrushing) { + if (airbrushing) { m_d->airbrushingTimer.setInterval(computeAirbrushTimerInterval()); m_d->airbrushingTimer.start(); + } else if (m_d->resources->presetNeedsAsynchronousUpdates()) { + m_d->asynchronousUpdatesThresholdTimer.setInterval(80 /* msec */); + m_d->asynchronousUpdatesThresholdTimer.start(); } if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::STABILIZER) { stabilizerStart(m_d->previousPaintInformation); } // If airbrushing, paint an initial dab immediately. This is a workaround for an issue where // some paintops (Dyna, Particle, Sketch) might never initialize their spacing/timing // information until paintAt is called. if (airbrushing) { paintAt(pi); } } void KisToolFreehandHelper::paintBezierSegment(KisPaintInformation pi1, KisPaintInformation pi2, QPointF tangent1, QPointF tangent2) { if (tangent1.isNull() || tangent2.isNull()) return; const qreal maxSanePoint = 1e6; QPointF controlTarget1; QPointF controlTarget2; // Shows the direction in which control points go QPointF controlDirection1 = pi1.pos() + tangent1; QPointF controlDirection2 = pi2.pos() - tangent2; // Lines in the direction of the control points QLineF line1(pi1.pos(), controlDirection1); QLineF line2(pi2.pos(), controlDirection2); // Lines to check whether the control points lay on the opposite // side of the line QLineF line3(controlDirection1, controlDirection2); QLineF line4(pi1.pos(), pi2.pos()); QPointF intersection; if (line3.intersect(line4, &intersection) == QLineF::BoundedIntersection) { qreal controlLength = line4.length() / 2; line1.setLength(controlLength); line2.setLength(controlLength); controlTarget1 = line1.p2(); controlTarget2 = line2.p2(); } else { QLineF::IntersectType type = line1.intersect(line2, &intersection); if (type == QLineF::NoIntersection || intersection.manhattanLength() > maxSanePoint) { intersection = 0.5 * (pi1.pos() + pi2.pos()); // dbgKrita << "WARINING: there is no intersection point " // << "in the basic smoothing algoriths"; } controlTarget1 = intersection; controlTarget2 = intersection; } // shows how near to the controlTarget the value raises qreal coeff = 0.8; qreal velocity1 = QLineF(QPointF(), tangent1).length(); qreal velocity2 = QLineF(QPointF(), tangent2).length(); if (velocity1 == 0.0 || velocity2 == 0.0) { velocity1 = 1e-6; velocity2 = 1e-6; warnKrita << "WARNING: Basic Smoothing: Velocity is Zero! Please report a bug:" << ppVar(velocity1) << ppVar(velocity2); } qreal similarity = qMin(velocity1/velocity2, velocity2/velocity1); // the controls should not differ more than 50% similarity = qMax(similarity, qreal(0.5)); // when the controls are symmetric, their size should be smaller // to avoid corner-like curves coeff *= 1 - qMax(qreal(0.0), similarity - qreal(0.8)); Q_ASSERT(coeff > 0); QPointF control1; QPointF control2; if (velocity1 > velocity2) { control1 = pi1.pos() * (1.0 - coeff) + coeff * controlTarget1; coeff *= similarity; control2 = pi2.pos() * (1.0 - coeff) + coeff * controlTarget2; } else { control2 = pi2.pos() * (1.0 - coeff) + coeff * controlTarget2; coeff *= similarity; control1 = pi1.pos() * (1.0 - coeff) + coeff * controlTarget1; } paintBezierCurve(pi1, control1, control2, pi2); } qreal KisToolFreehandHelper::Private::effectiveSmoothnessDistance() const { const qreal effectiveSmoothnessDistance = !smoothingOptions->useScalableDistance() ? smoothingOptions->smoothnessDistance() : smoothingOptions->smoothnessDistance() / resources->effectiveZoom(); return effectiveSmoothnessDistance; } void KisToolFreehandHelper::paintEvent(KoPointerEvent *event) { KisPaintInformation info = m_d->infoBuilder->continueStroke(event, elapsedStrokeTime()); info.setCanvasRotation( m_d->canvasRotation ); info.setCanvasHorizontalMirrorState( m_d->canvasMirroredH ); KisUpdateTimeMonitor::instance()->reportMouseMove(info.pos()); paint(info); } void KisToolFreehandHelper::paint(KisPaintInformation &info) { /** * Smooth the coordinates out using the history and the * distance. This is a heavily modified version of an algo used in * Gimp and described in https://bugs.kde.org/show_bug.cgi?id=281267 and * http://www24.atwiki.jp/sigetch_2007/pages/17.html. The main * differences are: * * 1) It uses 'distance' instead of 'velocity', since time * measurements are too unstable in realworld environment * * 2) There is no 'Quality' parameter, since the number of samples * is calculated automatically * * 3) 'Tail Aggressiveness' is used for controling the end of the * stroke * * 4) The formila is a little bit different: 'Distance' parameter * stands for $3 \Sigma$ */ if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::WEIGHTED_SMOOTHING && m_d->smoothingOptions->smoothnessDistance() > 0.0) { { // initialize current distance QPointF prevPos; if (!m_d->history.isEmpty()) { const KisPaintInformation &prevPi = m_d->history.last(); prevPos = prevPi.pos(); } else { prevPos = m_d->previousPaintInformation.pos(); } qreal currentDistance = QVector2D(info.pos() - prevPos).length(); m_d->distanceHistory.append(currentDistance); } m_d->history.append(info); qreal x = 0.0; qreal y = 0.0; if (m_d->history.size() > 3) { const qreal sigma = m_d->effectiveSmoothnessDistance() / 3.0; // '3.0' for (3 * sigma) range qreal gaussianWeight = 1 / (sqrt(2 * M_PI) * sigma); qreal gaussianWeight2 = sigma * sigma; qreal distanceSum = 0.0; qreal scaleSum = 0.0; qreal pressure = 0.0; qreal baseRate = 0.0; Q_ASSERT(m_d->history.size() == m_d->distanceHistory.size()); for (int i = m_d->history.size() - 1; i >= 0; i--) { qreal rate = 0.0; const KisPaintInformation nextInfo = m_d->history.at(i); double distance = m_d->distanceHistory.at(i); Q_ASSERT(distance >= 0.0); qreal pressureGrad = 0.0; if (i < m_d->history.size() - 1) { pressureGrad = nextInfo.pressure() - m_d->history.at(i + 1).pressure(); const qreal tailAgressiveness = 40.0 * m_d->smoothingOptions->tailAggressiveness(); if (pressureGrad > 0.0 ) { pressureGrad *= tailAgressiveness * (1.0 - nextInfo.pressure()); distance += pressureGrad * 3.0 * sigma; // (3 * sigma) --- holds > 90% of the region } } if (gaussianWeight2 != 0.0) { distanceSum += distance; rate = gaussianWeight * exp(-distanceSum * distanceSum / (2 * gaussianWeight2)); } if (m_d->history.size() - i == 1) { baseRate = rate; } else if (baseRate / rate > 100) { break; } scaleSum += rate; x += rate * nextInfo.pos().x(); y += rate * nextInfo.pos().y(); if (m_d->smoothingOptions->smoothPressure()) { pressure += rate * nextInfo.pressure(); } } if (scaleSum != 0.0) { x /= scaleSum; y /= scaleSum; if (m_d->smoothingOptions->smoothPressure()) { pressure /= scaleSum; } } if ((x != 0.0 && y != 0.0) || (x == info.pos().x() && y == info.pos().y())) { info.setPos(QPointF(x, y)); if (m_d->smoothingOptions->smoothPressure()) { info.setPressure(pressure); } m_d->history.last() = info; } } } if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::SIMPLE_SMOOTHING || m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::WEIGHTED_SMOOTHING) { // Now paint between the coordinates, using the bezier curve interpolation if (!m_d->haveTangent) { m_d->haveTangent = true; m_d->previousTangent = (info.pos() - m_d->previousPaintInformation.pos()) / qMax(qreal(1.0), info.currentTime() - m_d->previousPaintInformation.currentTime()); } else { QPointF newTangent = (info.pos() - m_d->olderPaintInformation.pos()) / qMax(qreal(1.0), info.currentTime() - m_d->olderPaintInformation.currentTime()); if (newTangent.isNull() || m_d->previousTangent.isNull()) { paintLine(m_d->previousPaintInformation, info); } else { paintBezierSegment(m_d->olderPaintInformation, m_d->previousPaintInformation, m_d->previousTangent, newTangent); } m_d->previousTangent = newTangent; } m_d->olderPaintInformation = m_d->previousPaintInformation; // Enable stroke timeout only when not airbrushing. if (!m_d->airbrushingTimer.isActive()) { m_d->strokeTimeoutTimer.start(100); } } else if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::NO_SMOOTHING){ paintLine(m_d->previousPaintInformation, info); } if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::STABILIZER) { m_d->stabilizedSampler.addEvent(info); if (m_d->stabilizerDelayedPaintHelper.running()) { // Paint here so we don't have to rely on the timer // This is just a tricky source for a relatively stable 7ms "timer" m_d->stabilizerDelayedPaintHelper.paintSome(); } } else { m_d->previousPaintInformation = info; } if(m_d->airbrushingTimer.isActive()) { m_d->airbrushingTimer.start(); } } void KisToolFreehandHelper::endPaint() { if (!m_d->hasPaintAtLeastOnce) { paintAt(m_d->previousPaintInformation); } else if (m_d->smoothingOptions->smoothingType() != KisSmoothingOptions::NO_SMOOTHING) { finishStroke(); } m_d->strokeTimeoutTimer.stop(); if(m_d->airbrushingTimer.isActive()) { m_d->airbrushingTimer.stop(); } + if (m_d->asynchronousUpdatesThresholdTimer.isActive()) { + m_d->asynchronousUpdatesThresholdTimer.stop(); + } + if (m_d->smoothingOptions->smoothingType() == KisSmoothingOptions::STABILIZER) { stabilizerEnd(); } /** * There might be some timer events still pending, so * we should cancel them. Use this flag for the purpose. * Please note that we are not in MT here, so no mutex * is needed */ m_d->painterInfos.clear(); + // last update to complete rendering if there is still something pending + doAsynchronousUpdate(true); + m_d->strokesFacade->endStroke(m_d->strokeId); m_d->strokeId.clear(); if(m_d->recordingAdapter) { m_d->recordingAdapter->endStroke(); } } void KisToolFreehandHelper::cancelPaint() { if (!m_d->strokeId) return; m_d->strokeTimeoutTimer.stop(); if (m_d->airbrushingTimer.isActive()) { m_d->airbrushingTimer.stop(); } + if (m_d->asynchronousUpdatesThresholdTimer.isActive()) { + m_d->asynchronousUpdatesThresholdTimer.stop(); + } + if (m_d->stabilizerPollTimer.isActive()) { m_d->stabilizerPollTimer.stop(); } if (m_d->stabilizerDelayedPaintHelper.running()) { m_d->stabilizerDelayedPaintHelper.cancel(); } // see a comment in endPaint() m_d->painterInfos.clear(); m_d->strokesFacade->cancelStroke(m_d->strokeId); m_d->strokeId.clear(); if(m_d->recordingAdapter) { //FIXME: not implemented //m_d->recordingAdapter->cancelStroke(); } } int KisToolFreehandHelper::elapsedStrokeTime() const { return m_d->strokeTime.elapsed(); } void KisToolFreehandHelper::stabilizerStart(KisPaintInformation firstPaintInfo) { // FIXME: Ugly hack, this is no a "distance" in any way int sampleSize = qRound(m_d->effectiveSmoothnessDistance()); sampleSize = qMax(3, sampleSize); // Fill the deque with the current value repeated until filling the sample m_d->stabilizerDeque.clear(); for (int i = sampleSize; i > 0; i--) { m_d->stabilizerDeque.enqueue(firstPaintInfo); } // Poll and draw regularly KisConfig cfg; int stabilizerSampleSize = cfg.stabilizerSampleSize(); m_d->stabilizerPollTimer.setInterval(stabilizerSampleSize); m_d->stabilizerPollTimer.start(); bool delayedPaintEnabled = cfg.stabilizerDelayedPaint(); if (delayedPaintEnabled) { m_d->stabilizerDelayedPaintHelper.start(firstPaintInfo); } m_d->stabilizedSampler.clear(); m_d->stabilizedSampler.addEvent(firstPaintInfo); } KisPaintInformation KisToolFreehandHelper::getStabilizedPaintInfo(const QQueue &queue, const KisPaintInformation &lastPaintInfo) { KisPaintInformation result(lastPaintInfo.pos(), lastPaintInfo.pressure(), lastPaintInfo.xTilt(), lastPaintInfo.yTilt(), lastPaintInfo.rotation(), lastPaintInfo.tangentialPressure(), lastPaintInfo.perspective(), elapsedStrokeTime(), lastPaintInfo.drawingSpeed()); if (queue.size() > 1) { QQueue::const_iterator it = queue.constBegin(); QQueue::const_iterator end = queue.constEnd(); /** * The first point is going to be overridden by lastPaintInfo, skip it. */ it++; int i = 2; if (m_d->smoothingOptions->stabilizeSensors()) { while (it != end) { qreal k = qreal(i - 1) / i; // coeff for uniform averaging result.KisPaintInformation::mixOtherWithoutTime(k, *it); it++; i++; } } else{ while (it != end) { qreal k = qreal(i - 1) / i; // coeff for uniform averaging result.KisPaintInformation::mixOtherOnlyPosition(k, *it); it++; i++; } } } return result; } void KisToolFreehandHelper::stabilizerPollAndPaint() { KisStabilizedEventsSampler::iterator it; KisStabilizedEventsSampler::iterator end; std::tie(it, end) = m_d->stabilizedSampler.range(); QVector delayedPaintTodoItems; for (; it != end; ++it) { KisPaintInformation sampledInfo = *it; bool canPaint = true; if (m_d->smoothingOptions->useDelayDistance()) { const qreal R = m_d->smoothingOptions->delayDistance() / m_d->resources->effectiveZoom(); QPointF diff = sampledInfo.pos() - m_d->previousPaintInformation.pos(); qreal dx = sqrt(pow2(diff.x()) + pow2(diff.y())); if (!(dx > R)) { if (m_d->resources->needsAirbrushing()) { sampledInfo.setPos(m_d->previousPaintInformation.pos()); } else { canPaint = false; } } } if (canPaint) { KisPaintInformation newInfo = getStabilizedPaintInfo(m_d->stabilizerDeque, sampledInfo); if (m_d->stabilizerDelayedPaintHelper.running()) { delayedPaintTodoItems.append(newInfo); } else { paintLine(m_d->previousPaintInformation, newInfo); } m_d->previousPaintInformation = newInfo; // Push the new entry through the queue m_d->stabilizerDeque.dequeue(); m_d->stabilizerDeque.enqueue(sampledInfo); } else if (m_d->stabilizerDeque.head().pos() != m_d->previousPaintInformation.pos()) { QQueue::iterator it = m_d->stabilizerDeque.begin(); QQueue::iterator end = m_d->stabilizerDeque.end(); while (it != end) { *it = m_d->previousPaintInformation; ++it; } } } m_d->stabilizedSampler.clear(); if (m_d->stabilizerDelayedPaintHelper.running()) { m_d->stabilizerDelayedPaintHelper.update(delayedPaintTodoItems); } else { emit requestExplicitUpdateOutline(); } } void KisToolFreehandHelper::stabilizerEnd() { // Stop the timer m_d->stabilizerPollTimer.stop(); // Finish the line if (m_d->smoothingOptions->finishStabilizedCurve()) { // Process all the existing events first stabilizerPollAndPaint(); // Draw the finish line with pending events and a time override m_d->stabilizedSampler.addFinishingEvent(m_d->stabilizerDeque.size()); stabilizerPollAndPaint(); } if (m_d->stabilizerDelayedPaintHelper.running()) { m_d->stabilizerDelayedPaintHelper.end(); } } -const KisPaintOp* KisToolFreehandHelper::currentPaintOp() const -{ - return !m_d->painterInfos.isEmpty() ? m_d->painterInfos.first()->painter->paintOp() : 0; -} - void KisToolFreehandHelper::finishStroke() { if (m_d->haveTangent) { m_d->haveTangent = false; QPointF newTangent = (m_d->previousPaintInformation.pos() - m_d->olderPaintInformation.pos()) / (m_d->previousPaintInformation.currentTime() - m_d->olderPaintInformation.currentTime()); paintBezierSegment(m_d->olderPaintInformation, m_d->previousPaintInformation, m_d->previousTangent, newTangent); } } void KisToolFreehandHelper::doAirbrushing() { // Check that the stroke hasn't ended. if (!m_d->painterInfos.isEmpty()) { // Add a new painting update at a point identical to the previous one, except for the time // and speed information. const KisPaintInformation &prevPaint = m_d->previousPaintInformation; KisPaintInformation nextPaint(prevPaint.pos(), prevPaint.pressure(), prevPaint.xTilt(), prevPaint.yTilt(), prevPaint.rotation(), prevPaint.tangentialPressure(), prevPaint.perspective(), elapsedStrokeTime(), 0.0); paint(nextPaint); } } +void KisToolFreehandHelper::doAsynchronousUpdate(bool forceUpdate) +{ + m_d->strokesFacade->addJob(m_d->strokeId, + new FreehandStrokeStrategy::UpdateData(forceUpdate)); +} + int KisToolFreehandHelper::computeAirbrushTimerInterval() const { qreal realInterval = m_d->resources->airbrushingInterval() * AIRBRUSH_INTERVAL_FACTOR; return qMax(1, qFloor(realInterval)); } void KisToolFreehandHelper::paintAt(int painterInfoId, const KisPaintInformation &pi) { m_d->hasPaintAtLeastOnce = true; m_d->strokesFacade->addJob(m_d->strokeId, - new FreehandStrokeStrategy::Data(m_d->resources->currentNode(), - painterInfoId, pi)); + new FreehandStrokeStrategy::Data(painterInfoId, pi)); if(m_d->recordingAdapter) { m_d->recordingAdapter->addPoint(pi); } } void KisToolFreehandHelper::paintLine(int painterInfoId, const KisPaintInformation &pi1, const KisPaintInformation &pi2) { m_d->hasPaintAtLeastOnce = true; m_d->strokesFacade->addJob(m_d->strokeId, - new FreehandStrokeStrategy::Data(m_d->resources->currentNode(), - painterInfoId, pi1, pi2)); + new FreehandStrokeStrategy::Data(painterInfoId, pi1, pi2)); if(m_d->recordingAdapter) { m_d->recordingAdapter->addLine(pi1, pi2); } } void KisToolFreehandHelper::paintBezierCurve(int painterInfoId, const KisPaintInformation &pi1, const QPointF &control1, const QPointF &control2, const KisPaintInformation &pi2) { #ifdef DEBUG_BEZIER_CURVES KisPaintInformation tpi1; KisPaintInformation tpi2; tpi1 = pi1; tpi2 = pi2; tpi1.setPressure(0.3); tpi2.setPressure(0.3); paintLine(tpi1, tpi2); tpi1.setPressure(0.6); tpi2.setPressure(0.3); tpi1.setPos(pi1.pos()); tpi2.setPos(control1); paintLine(tpi1, tpi2); tpi1.setPos(pi2.pos()); tpi2.setPos(control2); paintLine(tpi1, tpi2); #endif m_d->hasPaintAtLeastOnce = true; m_d->strokesFacade->addJob(m_d->strokeId, - new FreehandStrokeStrategy::Data(m_d->resources->currentNode(), - painterInfoId, + new FreehandStrokeStrategy::Data(painterInfoId, pi1, control1, control2, pi2)); if(m_d->recordingAdapter) { m_d->recordingAdapter->addCurve(pi1, control1, control2, pi2); } } void KisToolFreehandHelper::createPainters(QVector &painterInfos, const KisDistanceInformation &startDist) { painterInfos << new PainterInfo(startDist); } void KisToolFreehandHelper::paintAt(const KisPaintInformation &pi) { paintAt(0, pi); } void KisToolFreehandHelper::paintLine(const KisPaintInformation &pi1, const KisPaintInformation &pi2) { paintLine(0, pi1, pi2); } void KisToolFreehandHelper::paintBezierCurve(const KisPaintInformation &pi1, const QPointF &control1, const QPointF &control2, const KisPaintInformation &pi2) { paintBezierCurve(0, pi1, control1, control2, pi2); } int KisToolFreehandHelper::canvasRotation() { return m_d->canvasRotation; } void KisToolFreehandHelper::setCanvasRotation(int rotation) { m_d->canvasRotation = rotation; } bool KisToolFreehandHelper::canvasMirroredH() { return m_d->canvasMirroredH; } void KisToolFreehandHelper::setCanvasHorizontalMirrorState(bool mirrored) { m_d->canvasMirroredH = mirrored; } diff --git a/libs/ui/tool/kis_tool_freehand_helper.h b/libs/ui/tool/kis_tool_freehand_helper.h index dcf07292b5..389d955d28 100644 --- a/libs/ui/tool/kis_tool_freehand_helper.h +++ b/libs/ui/tool/kis_tool_freehand_helper.h @@ -1,162 +1,162 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_TOOL_FREEHAND_HELPER_H #define __KIS_TOOL_FREEHAND_HELPER_H #include #include "kis_types.h" #include "kritaui_export.h" #include #include "kis_default_bounds.h" #include #include "kis_smoothing_options.h" #include "strokes/freehand_stroke.h" class KoPointerEvent; class KoCanvasResourceManager; class KisPaintingInformationBuilder; class KisRecordingAdapter; class KisStrokesFacade; class KisPostExecutionUndoAdapter; class KisPaintOp; class KRITAUI_EXPORT KisToolFreehandHelper : public QObject { Q_OBJECT protected: typedef FreehandStrokeStrategy::PainterInfo PainterInfo; public: KisToolFreehandHelper(KisPaintingInformationBuilder *infoBuilder, const KUndo2MagicString &transactionText = KUndo2MagicString(), KisRecordingAdapter *recordingAdapter = 0, KisSmoothingOptions *smoothingOptions = 0); ~KisToolFreehandHelper() override; void setSmoothness(KisSmoothingOptionsSP smoothingOptions); KisSmoothingOptionsSP smoothingOptions() const; bool isRunning() const; void cursorMoved(const QPointF &cursorPos); /** * @param pixelCoords - The position of the KoPointerEvent, in pixel coordinates. */ void initPaint(KoPointerEvent *event, const QPointF &pixelCoords, KoCanvasResourceManager *resourceManager, KisImageWSP image, KisNodeSP currentNode, KisStrokesFacade *strokesFacade, KisNodeSP overrideNode = 0, KisDefaultBoundsBaseSP bounds = 0); void paintEvent(KoPointerEvent *event); void endPaint(); - const KisPaintOp* currentPaintOp() const; QPainterPath paintOpOutline(const QPointF &savedCursorPos, const KoPointerEvent *event, const KisPaintOpSettingsSP globalSettings, KisPaintOpSettings::OutlineMode mode) const; int canvasRotation(); void setCanvasRotation(int rotation = 0); bool canvasMirroredH(); void setCanvasHorizontalMirrorState (bool mirrored = false); Q_SIGNALS: /** * The signal is emitted when the outline should be updated * explicitly by the tool. Used by Stabilizer option, because it * paints on internal timer events instead of the on every paint() * event */ void requestExplicitUpdateOutline(); protected: void cancelPaint(); int elapsedStrokeTime() const; void initPaintImpl(qreal startAngle, const KisPaintInformation &pi, KoCanvasResourceManager *resourceManager, KisImageWSP image, KisNodeSP node, KisStrokesFacade *strokesFacade, KisNodeSP overrideNode = 0, KisDefaultBoundsBaseSP bounds = 0); protected: virtual void createPainters(QVector &painterInfos, const KisDistanceInformation &startDist); // lo-level methods for painting primitives void paintAt(int painterInfoId, const KisPaintInformation &pi); void paintLine(int painterInfoId, const KisPaintInformation &pi1, const KisPaintInformation &pi2); void paintBezierCurve(int painterInfoId, const KisPaintInformation &pi1, const QPointF &control1, const QPointF &control2, const KisPaintInformation &pi2); // hi-level methods for painting primitives virtual void paintAt(const KisPaintInformation &pi); virtual void paintLine(const KisPaintInformation &pi1, const KisPaintInformation &pi2); virtual void paintBezierCurve(const KisPaintInformation &pi1, const QPointF &control1, const QPointF &control2, const KisPaintInformation &pi2); private: void paint(KisPaintInformation &info); void paintBezierSegment(KisPaintInformation pi1, KisPaintInformation pi2, QPointF tangent1, QPointF tangent2); void stabilizerStart(KisPaintInformation firstPaintInfo); void stabilizerEnd(); KisPaintInformation getStabilizedPaintInfo(const QQueue &queue, const KisPaintInformation &lastPaintInfo); int computeAirbrushTimerInterval() const; private Q_SLOTS: void finishStroke(); void doAirbrushing(); + void doAsynchronousUpdate(bool forceUpdate = false); void stabilizerPollAndPaint(); private: struct Private; Private * const m_d; }; #endif /* __KIS_TOOL_FREEHAND_HELPER_H */ diff --git a/libs/ui/tool/strokes/FreehandStrokeRunnableJobDataWithUpdate.h b/libs/ui/tool/strokes/FreehandStrokeRunnableJobDataWithUpdate.h new file mode 100644 index 0000000000..e268c4e0a3 --- /dev/null +++ b/libs/ui/tool/strokes/FreehandStrokeRunnableJobDataWithUpdate.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef FREEHANDSTROKERUNNABLEJOBDATAWITHUPDATE_H +#define FREEHANDSTROKERUNNABLEJOBDATAWITHUPDATE_H + +#include "KisRunnableStrokeJobData.h" + + +class FreehandStrokeRunnableJobDataWithUpdate : public KisRunnableStrokeJobData +{ +public: + FreehandStrokeRunnableJobDataWithUpdate(QRunnable *runnable, KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL, + KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL) + : KisRunnableStrokeJobData(runnable, sequentiality, exclusivity) + { + } + + FreehandStrokeRunnableJobDataWithUpdate(std::function func, KisStrokeJobData::Sequentiality sequentiality = KisStrokeJobData::SEQUENTIAL, + KisStrokeJobData::Exclusivity exclusivity = KisStrokeJobData::NORMAL) + : KisRunnableStrokeJobData(func, sequentiality, exclusivity) + { + } +}; + +#endif // FREEHANDSTROKERUNNABLEJOBDATAWITHUPDATE_H diff --git a/libs/ui/tool/strokes/KisStrokeEfficiencyMeasurer.cpp b/libs/ui/tool/strokes/KisStrokeEfficiencyMeasurer.cpp new file mode 100644 index 0000000000..fc41abd846 --- /dev/null +++ b/libs/ui/tool/strokes/KisStrokeEfficiencyMeasurer.cpp @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisStrokeEfficiencyMeasurer.h" + +#include +#include +#include + +#include + +#include "kis_global.h" + +struct KisStrokeEfficiencyMeasurer::Private +{ + boost::optional lastSamplePos; + qreal distance = 0; + + QElapsedTimer strokeTimeSource; + bool isEnabled = true; + + int renderingStartTime = 0; + int renderingTime = 0; + + int cursorMoveStartTime = 0; + int cursorMoveTime = 0; + + int framesCount = 0; + +}; + +KisStrokeEfficiencyMeasurer::KisStrokeEfficiencyMeasurer() + : m_d(new Private()) +{ + m_d->strokeTimeSource.start(); +} + +KisStrokeEfficiencyMeasurer::~KisStrokeEfficiencyMeasurer() +{ +} + +void KisStrokeEfficiencyMeasurer::setEnabled(bool value) +{ + m_d->isEnabled = value; +} + +bool KisStrokeEfficiencyMeasurer::isEnabled() const +{ + return m_d->isEnabled; +} + +void KisStrokeEfficiencyMeasurer::addSample(const QPointF &pt) +{ + if (!m_d->isEnabled) return; + + if (!m_d->lastSamplePos) { + m_d->lastSamplePos = pt; + } else { + m_d->distance += kisDistance(pt, *m_d->lastSamplePos); + *m_d->lastSamplePos = pt; + } +} + +void KisStrokeEfficiencyMeasurer::addSamples(const QVector &points) +{ + if (!m_d->isEnabled) return; + + Q_FOREACH (const QPointF &pt, points) { + addSample(pt); + } +} + +void KisStrokeEfficiencyMeasurer::notifyRenderingStarted() +{ + m_d->renderingStartTime = m_d->strokeTimeSource.elapsed(); +} + +void KisStrokeEfficiencyMeasurer::notifyRenderingFinished() +{ + m_d->renderingTime = m_d->strokeTimeSource.elapsed() - m_d->renderingStartTime; +} + +void KisStrokeEfficiencyMeasurer::notifyCursorMoveStarted() +{ + m_d->cursorMoveStartTime = m_d->strokeTimeSource.elapsed(); +} + +void KisStrokeEfficiencyMeasurer::notifyCursorMoveFinished() +{ + m_d->cursorMoveTime = m_d->strokeTimeSource.elapsed() - m_d->cursorMoveStartTime; +} + +void KisStrokeEfficiencyMeasurer::notifyFrameRenderingStarted() +{ + m_d->framesCount++; +} + +qreal KisStrokeEfficiencyMeasurer::averageCursorSpeed() const +{ + return m_d->cursorMoveTime ? m_d->distance / m_d->cursorMoveTime : 0.0; +} + +qreal KisStrokeEfficiencyMeasurer::averageRenderingSpeed() const +{ + return m_d->renderingTime ? m_d->distance / m_d->renderingTime : 0.0; +} + +qreal KisStrokeEfficiencyMeasurer::averageFps() const +{ + return m_d->renderingTime ? m_d->framesCount * 1000.0 / m_d->renderingTime : 0.0; +} + + diff --git a/libs/ui/opengl/kis_opengl_canvas_debugger.h b/libs/ui/tool/strokes/KisStrokeEfficiencyMeasurer.h similarity index 50% copy from libs/ui/opengl/kis_opengl_canvas_debugger.h copy to libs/ui/tool/strokes/KisStrokeEfficiencyMeasurer.h index 8d7e605e96..4fa26e5082 100644 --- a/libs/ui/opengl/kis_opengl_canvas_debugger.h +++ b/libs/ui/tool/strokes/KisStrokeEfficiencyMeasurer.h @@ -1,45 +1,60 @@ /* - * Copyright (c) 2015 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_OPENGL_CANVAS_DEBUGGER_H -#define __KIS_OPENGL_CANVAS_DEBUGGER_H +#ifndef KISSTROKEEFFICIENCYMEASURER_H +#define KISSTROKEEFFICIENCYMEASURER_H +#include "kritaui_export.h" #include +#include +class QPointF; -class KisOpenglCanvasDebugger +class KRITAUI_EXPORT KisStrokeEfficiencyMeasurer { public: - KisOpenglCanvasDebugger(); - ~KisOpenglCanvasDebugger(); + KisStrokeEfficiencyMeasurer(); + ~KisStrokeEfficiencyMeasurer(); - static KisOpenglCanvasDebugger* instance(); + void setEnabled(bool value); + bool isEnabled() const; - bool showFpsOnCanvas() const; + void addSample(const QPointF &pt); + void addSamples(const QVector &points); - void nofityPaintRequested(); - void nofitySyncStatus(bool value); - qreal accumulatedFps(); + qreal averageCursorSpeed() const; + qreal averageRenderingSpeed() const; + qreal averageFps() const; + + void notifyRenderingStarted(); + void notifyRenderingFinished(); + + void notifyCursorMoveStarted(); + void notifyCursorMoveFinished(); + + void notifyFrameRenderingStarted(); + + void reset(); private: struct Private; const QScopedPointer m_d; }; -#endif /* __KIS_OPENGL_CANVAS_DEBUGGER_H */ +#endif // KISSTROKEEFFICIENCYMEASURER_H diff --git a/libs/ui/tool/strokes/freehand_stroke.cpp b/libs/ui/tool/strokes/freehand_stroke.cpp index 2ca96405bc..1b35c3b2c1 100644 --- a/libs/ui/tool/strokes/freehand_stroke.cpp +++ b/libs/ui/tool/strokes/freehand_stroke.cpp @@ -1,149 +1,300 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "freehand_stroke.h" +#include + #include "kis_canvas_resource_provider.h" #include #include #include "kis_painter.h" +#include "kis_paintop.h" #include "kis_update_time_monitor.h" #include +#include +#include "FreehandStrokeRunnableJobDataWithUpdate.h" +#include +#include "KisStrokeEfficiencyMeasurer.h" +#include struct FreehandStrokeStrategy::Private { - Private(KisResourcesSnapshotSP _resources) : resources(_resources) {} + Private(KisResourcesSnapshotSP _resources) + : resources(_resources), + needsAsynchronousUpdates(_resources->presetNeedsAsynchronousUpdates()) + { + if (needsAsynchronousUpdates) { + timeSinceLastUpdate.start(); + } + } + + Private(const Private &rhs) + : randomSource(rhs.randomSource), + resources(rhs.resources), + needsAsynchronousUpdates(rhs.needsAsynchronousUpdates) + { + if (needsAsynchronousUpdates) { + timeSinceLastUpdate.start(); + } + } KisStrokeRandomSource randomSource; KisResourcesSnapshotSP resources; + + KisStrokeEfficiencyMeasurer efficiencyMeasurer; + + QElapsedTimer timeSinceLastUpdate; + int currentUpdatePeriod = 40; + + const bool needsAsynchronousUpdates = false; + std::mutex updateEntryMutex; }; FreehandStrokeStrategy::FreehandStrokeStrategy(bool needsIndirectPainting, const QString &indirectPaintingCompositeOp, KisResourcesSnapshotSP resources, PainterInfo *painterInfo, const KUndo2MagicString &name) : KisPainterBasedStrokeStrategy("FREEHAND_STROKE", name, resources, painterInfo), m_d(new Private(resources)) { init(needsIndirectPainting, indirectPaintingCompositeOp); } FreehandStrokeStrategy::FreehandStrokeStrategy(bool needsIndirectPainting, const QString &indirectPaintingCompositeOp, KisResourcesSnapshotSP resources, QVector painterInfos, const KUndo2MagicString &name) : KisPainterBasedStrokeStrategy("FREEHAND_STROKE", name, resources, painterInfos), m_d(new Private(resources)) { init(needsIndirectPainting, indirectPaintingCompositeOp); } FreehandStrokeStrategy::FreehandStrokeStrategy(const FreehandStrokeStrategy &rhs, int levelOfDetail) : KisPainterBasedStrokeStrategy(rhs, levelOfDetail), m_d(new Private(*rhs.m_d)) { m_d->randomSource.setLevelOfDetail(levelOfDetail); } FreehandStrokeStrategy::~FreehandStrokeStrategy() { + KisStrokeSpeedMonitor::instance()->notifyStrokeFinished(m_d->efficiencyMeasurer.averageCursorSpeed(), + m_d->efficiencyMeasurer.averageRenderingSpeed(), + m_d->efficiencyMeasurer.averageFps(), + m_d->resources->currentPaintOpPreset()); + KisUpdateTimeMonitor::instance()->endStrokeMeasure(); } void FreehandStrokeStrategy::init(bool needsIndirectPainting, const QString &indirectPaintingCompositeOp) { setNeedsIndirectPainting(needsIndirectPainting); setIndirectPaintingCompositeOp(indirectPaintingCompositeOp); setSupportsWrapAroundMode(true); enableJob(KisSimpleStrokeStrategy::JOB_DOSTROKE); + if (m_d->needsAsynchronousUpdates) { + /** + * In case the paintop uses asynchronous updates, we should set priority to it, + * because FPS is controlled separately, not by the queue's merging algorithm. + */ + setBalancingRatioOverride(0.01); // set priority to updates + } + KisUpdateTimeMonitor::instance()->startStrokeMeasure(); + m_d->efficiencyMeasurer.setEnabled(KisStrokeSpeedMonitor::instance()->haveStrokeSpeedMeasurement()); +} + +void FreehandStrokeStrategy::initStrokeCallback() +{ + KisPainterBasedStrokeStrategy::initStrokeCallback(); + m_d->efficiencyMeasurer.notifyRenderingStarted(); +} + +void FreehandStrokeStrategy::finishStrokeCallback() +{ + m_d->efficiencyMeasurer.notifyRenderingFinished(); + KisPainterBasedStrokeStrategy::finishStrokeCallback(); } + void FreehandStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) { - Data *d = dynamic_cast(data); - PainterInfo *info = painterInfos()[d->painterInfoId]; - - KisUpdateTimeMonitor::instance()->reportPaintOpPreset(info->painter->preset()); - KisRandomSourceSP rnd = m_d->randomSource.source(); - - switch(d->type) { - case Data::POINT: - d->pi1.setRandomSource(rnd); - info->painter->paintAt(d->pi1, info->dragDistance); - break; - case Data::LINE: - d->pi1.setRandomSource(rnd); - d->pi2.setRandomSource(rnd); - info->painter->paintLine(d->pi1, d->pi2, info->dragDistance); - break; - case Data::CURVE: - d->pi1.setRandomSource(rnd); - d->pi2.setRandomSource(rnd); - info->painter->paintBezierCurve(d->pi1, - d->control1, - d->control2, - d->pi2, - info->dragDistance); - break; - case Data::POLYLINE: - info->painter->paintPolyline(d->points, 0, d->points.size()); - break; - case Data::POLYGON: - info->painter->paintPolygon(d->points); - break; - case Data::RECT: - info->painter->paintRect(d->rect); - break; - case Data::ELLIPSE: - info->painter->paintEllipse(d->rect); - break; - case Data::PAINTER_PATH: - info->painter->paintPainterPath(d->path); - break; - case Data::QPAINTER_PATH: - info->painter->drawPainterPath(d->path, d->pen); - break; - case Data::QPAINTER_PATH_FILL: { - info->painter->setBackgroundColor(d->customColor); - info->painter->fillPainterPath(d->path);} - info->painter->drawPainterPath(d->path, d->pen); - break; - }; - - QVector dirtyRects = info->painter->takeDirtyRegion(); - KisUpdateTimeMonitor::instance()->reportJobFinished(data, dirtyRects); - d->node->setDirty(dirtyRects); + PainterInfo *info = 0; + + if (UpdateData *d = dynamic_cast(data)) { + // this job is lod-clonable in contrast to FreehandStrokeRunnableJobDataWithUpdate! + tryDoUpdate(d->forceUpdate); + + } else if (Data *d = dynamic_cast(data)) { + info = painterInfos()[d->painterInfoId]; + + KisUpdateTimeMonitor::instance()->reportPaintOpPreset(info->painter->preset()); + KisRandomSourceSP rnd = m_d->randomSource.source(); + KisPerStrokeRandomSourceSP strokeRnd = m_d->randomSource.perStrokeSource(); + + switch(d->type) { + case Data::POINT: + d->pi1.setRandomSource(rnd); + d->pi1.setPerStrokeRandomSource(strokeRnd); + info->painter->paintAt(d->pi1, info->dragDistance); + m_d->efficiencyMeasurer.addSample(d->pi1.pos()); + break; + case Data::LINE: + d->pi1.setRandomSource(rnd); + d->pi2.setRandomSource(rnd); + d->pi1.setPerStrokeRandomSource(strokeRnd); + d->pi2.setPerStrokeRandomSource(strokeRnd); + info->painter->paintLine(d->pi1, d->pi2, info->dragDistance); + m_d->efficiencyMeasurer.addSample(d->pi2.pos()); + break; + case Data::CURVE: + d->pi1.setRandomSource(rnd); + d->pi2.setRandomSource(rnd); + d->pi1.setPerStrokeRandomSource(strokeRnd); + d->pi2.setPerStrokeRandomSource(strokeRnd); + info->painter->paintBezierCurve(d->pi1, + d->control1, + d->control2, + d->pi2, + info->dragDistance); + m_d->efficiencyMeasurer.addSample(d->pi2.pos()); + break; + case Data::POLYLINE: + info->painter->paintPolyline(d->points, 0, d->points.size()); + m_d->efficiencyMeasurer.addSamples(d->points); + break; + case Data::POLYGON: + info->painter->paintPolygon(d->points); + m_d->efficiencyMeasurer.addSamples(d->points); + break; + case Data::RECT: + info->painter->paintRect(d->rect); + m_d->efficiencyMeasurer.addSample(d->rect.topLeft()); + m_d->efficiencyMeasurer.addSample(d->rect.topRight()); + m_d->efficiencyMeasurer.addSample(d->rect.bottomRight()); + m_d->efficiencyMeasurer.addSample(d->rect.bottomLeft()); + break; + case Data::ELLIPSE: + info->painter->paintEllipse(d->rect); + // TODO: add speed measures + break; + case Data::PAINTER_PATH: + info->painter->paintPainterPath(d->path); + // TODO: add speed measures + break; + case Data::QPAINTER_PATH: + info->painter->drawPainterPath(d->path, d->pen); + break; + case Data::QPAINTER_PATH_FILL: { + info->painter->setBackgroundColor(d->customColor); + info->painter->fillPainterPath(d->path);} + info->painter->drawPainterPath(d->path, d->pen); + break; + }; + + tryDoUpdate(); + } else { + KisPainterBasedStrokeStrategy::doStrokeCallback(data); + + FreehandStrokeRunnableJobDataWithUpdate *dataWithUpdate = + dynamic_cast(data); + + if (dataWithUpdate) { + tryDoUpdate(); + } + } +} + +void FreehandStrokeStrategy::tryDoUpdate(bool forceEnd) +{ + // we should enter this function only once! + std::unique_lock entryLock(m_d->updateEntryMutex, std::try_to_lock); + if (!entryLock.owns_lock()) return; + + if (m_d->needsAsynchronousUpdates) { + if (forceEnd || m_d->timeSinceLastUpdate.elapsed() > m_d->currentUpdatePeriod) { + m_d->timeSinceLastUpdate.restart(); + + Q_FOREACH (PainterInfo *info, painterInfos()) { + KisPaintOp *paintop = info->painter->paintOp(); + KIS_SAFE_ASSERT_RECOVER_RETURN(paintop); + + // TODO: well, we should count all N simultaneous painters for FPS rate! + QVector jobs; + m_d->currentUpdatePeriod = paintop->doAsyncronousUpdate(jobs); + + if (!jobs.isEmpty()) { + jobs.append(new KisRunnableStrokeJobData( + [this] () { + this->issueSetDirtySignals(); + }, + KisStrokeJobData::SEQUENTIAL)); + + runnableJobsInterface()->addRunnableJobs(jobs); + m_d->efficiencyMeasurer.notifyFrameRenderingStarted(); + } + + } + } + } else { + issueSetDirtySignals(); + } + + +} + +void FreehandStrokeStrategy::issueSetDirtySignals() +{ + QVector dirtyRects; + + Q_FOREACH (PainterInfo *info, painterInfos()) { + dirtyRects.append(info->painter->takeDirtyRegion()); + } + + //KisUpdateTimeMonitor::instance()->reportJobFinished(data, dirtyRects); + targetNode()->setDirty(dirtyRects); } KisStrokeStrategy* FreehandStrokeStrategy::createLodClone(int levelOfDetail) { if (!m_d->resources->presetAllowsLod()) return 0; FreehandStrokeStrategy *clone = new FreehandStrokeStrategy(*this, levelOfDetail); return clone; } + +void FreehandStrokeStrategy::notifyUserStartedStroke() +{ + m_d->efficiencyMeasurer.notifyCursorMoveStarted(); +} + +void FreehandStrokeStrategy::notifyUserEndedStroke() +{ + m_d->efficiencyMeasurer.notifyCursorMoveFinished(); +} diff --git a/libs/ui/tool/strokes/freehand_stroke.h b/libs/ui/tool/strokes/freehand_stroke.h index 09c316b000..3ee13e65b6 100644 --- a/libs/ui/tool/strokes/freehand_stroke.h +++ b/libs/ui/tool/strokes/freehand_stroke.h @@ -1,204 +1,241 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __FREEHAND_STROKE_H #define __FREEHAND_STROKE_H #include "kritaui_export.h" #include "kis_types.h" #include "kis_node.h" #include "kis_painter_based_stroke_strategy.h" #include #include #include "kis_lod_transform.h" #include "KoColor.h" class KRITAUI_EXPORT FreehandStrokeStrategy : public KisPainterBasedStrokeStrategy { public: class Data : public KisStrokeJobData { public: enum DabType { POINT, LINE, CURVE, POLYLINE, POLYGON, RECT, ELLIPSE, PAINTER_PATH, QPAINTER_PATH, QPAINTER_PATH_FILL }; - Data(KisNodeSP _node, int _painterInfoId, + Data(int _painterInfoId, const KisPaintInformation &_pi) - : node(_node), painterInfoId(_painterInfoId), + : KisStrokeJobData(KisStrokeJobData::UNIQUELY_CONCURRENT), + painterInfoId(_painterInfoId), type(POINT), pi1(_pi) {} - Data(KisNodeSP _node, int _painterInfoId, + Data(int _painterInfoId, const KisPaintInformation &_pi1, const KisPaintInformation &_pi2) - : node(_node), painterInfoId(_painterInfoId), + : KisStrokeJobData(KisStrokeJobData::UNIQUELY_CONCURRENT), + painterInfoId(_painterInfoId), type(LINE), pi1(_pi1), pi2(_pi2) {} - Data(KisNodeSP _node, int _painterInfoId, + Data(int _painterInfoId, const KisPaintInformation &_pi1, const QPointF &_control1, const QPointF &_control2, const KisPaintInformation &_pi2) - : node(_node), painterInfoId(_painterInfoId), + : KisStrokeJobData(KisStrokeJobData::UNIQUELY_CONCURRENT), + painterInfoId(_painterInfoId), type(CURVE), pi1(_pi1), pi2(_pi2), control1(_control1), control2(_control2) {} - Data(KisNodeSP _node, int _painterInfoId, + Data(int _painterInfoId, DabType _type, const vQPointF &_points) - : node(_node), painterInfoId(_painterInfoId), + : KisStrokeJobData(KisStrokeJobData::UNIQUELY_CONCURRENT), + painterInfoId(_painterInfoId), type(_type), points(_points) {} - Data(KisNodeSP _node, int _painterInfoId, + Data(int _painterInfoId, DabType _type, const QRectF &_rect) - : node(_node), painterInfoId(_painterInfoId), + : KisStrokeJobData(KisStrokeJobData::UNIQUELY_CONCURRENT), + painterInfoId(_painterInfoId), type(_type), rect(_rect) {} - Data(KisNodeSP _node, int _painterInfoId, + Data(int _painterInfoId, DabType _type, const QPainterPath &_path) - : node(_node), painterInfoId(_painterInfoId), + : KisStrokeJobData(KisStrokeJobData::UNIQUELY_CONCURRENT), + painterInfoId(_painterInfoId), type(_type), path(_path) {} - Data(KisNodeSP _node, int _painterInfoId, + Data(int _painterInfoId, DabType _type, const QPainterPath &_path, const QPen &_pen, const KoColor &_customColor) - : node(_node), painterInfoId(_painterInfoId), + : KisStrokeJobData(KisStrokeJobData::UNIQUELY_CONCURRENT), + painterInfoId(_painterInfoId), type(_type), path(_path), pen(_pen), customColor(_customColor) {} KisStrokeJobData* createLodClone(int levelOfDetail) override { return new Data(*this, levelOfDetail); } private: Data(const Data &rhs, int levelOfDetail) : KisStrokeJobData(rhs), - node(rhs.node), painterInfoId(rhs.painterInfoId), type(rhs.type) { KisLodTransform t(levelOfDetail); switch(type) { case Data::POINT: pi1 = t.map(rhs.pi1); break; case Data::LINE: pi1 = t.map(rhs.pi1); pi2 = t.map(rhs.pi2); break; case Data::CURVE: pi1 = t.map(rhs.pi1); pi2 = t.map(rhs.pi2); control1 = t.map(rhs.control1); control2 = t.map(rhs.control2); break; case Data::POLYLINE: points = t.map(rhs.points); break; case Data::POLYGON: points = t.map(rhs.points); break; case Data::RECT: rect = t.map(rhs.rect); break; case Data::ELLIPSE: rect = t.map(rhs.rect); break; case Data::PAINTER_PATH: path = t.map(rhs.path); break; case Data::QPAINTER_PATH: path = t.map(rhs.path); pen = rhs.pen; break; case Data::QPAINTER_PATH_FILL: path = t.map(rhs.path); pen = rhs.pen; customColor = rhs.customColor; break; }; } public: - KisNodeSP node; int painterInfoId; DabType type; KisPaintInformation pi1; KisPaintInformation pi2; QPointF control1; QPointF control2; vQPointF points; QRectF rect; QPainterPath path; QPen pen; KoColor customColor; }; + class UpdateData : public KisStrokeJobData { + public: + UpdateData(bool _forceUpdate) + : KisStrokeJobData(KisStrokeJobData::SEQUENTIAL), + forceUpdate(_forceUpdate) + {} + + + KisStrokeJobData* createLodClone(int levelOfDetail) override { + return new UpdateData(*this, levelOfDetail); + } + + private: + UpdateData(const UpdateData &rhs, int levelOfDetail) + : KisStrokeJobData(rhs), + forceUpdate(rhs.forceUpdate) + { + Q_UNUSED(levelOfDetail); + } + public: + bool forceUpdate = false; + }; + public: FreehandStrokeStrategy(bool needsIndirectPainting, const QString &indirectPaintingCompositeOp, KisResourcesSnapshotSP resources, PainterInfo *painterInfo, const KUndo2MagicString &name); FreehandStrokeStrategy(bool needsIndirectPainting, const QString &indirectPaintingCompositeOp, KisResourcesSnapshotSP resources, QVector painterInfos, const KUndo2MagicString &name); ~FreehandStrokeStrategy() override; + void initStrokeCallback() override; + void finishStrokeCallback() override; + void doStrokeCallback(KisStrokeJobData *data) override; KisStrokeStrategy* createLodClone(int levelOfDetail) override; + void notifyUserStartedStroke() override; + void notifyUserEndedStroke() override; + protected: FreehandStrokeStrategy(const FreehandStrokeStrategy &rhs, int levelOfDetail); private: void init(bool needsIndirectPainting, const QString &indirectPaintingCompositeOp); + void tryDoUpdate(bool forceEnd = false); + void issueSetDirtySignals(); + private: struct Private; const QScopedPointer m_d; }; #endif /* __FREEHAND_STROKE_H */ diff --git a/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.cpp b/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.cpp index ba003cb9a4..6291dde590 100644 --- a/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.cpp +++ b/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.cpp @@ -1,316 +1,322 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_painter_based_stroke_strategy.h" #include #include #include #include "kis_painter.h" #include "kis_paint_device.h" #include "kis_paint_layer.h" #include "kis_transaction.h" #include "kis_image.h" #include #include "kis_undo_stores.h" KisPainterBasedStrokeStrategy::PainterInfo::PainterInfo() : painter(new KisPainter()), dragDistance(new KisDistanceInformation()), m_parentPainterInfo(0), m_childPainterInfo(0) { } KisPainterBasedStrokeStrategy::PainterInfo::PainterInfo(const KisDistanceInformation &startDist) : painter(new KisPainter()), dragDistance(new KisDistanceInformation(startDist)), m_parentPainterInfo(0), m_childPainterInfo(0) { } KisPainterBasedStrokeStrategy::PainterInfo::PainterInfo(PainterInfo *rhs, int levelOfDetail) : painter(new KisPainter()), dragDistance(new KisDistanceInformation(*rhs->dragDistance, levelOfDetail)), m_parentPainterInfo(rhs) { rhs->m_childPainterInfo = this; } KisPainterBasedStrokeStrategy::PainterInfo::~PainterInfo() { if (m_parentPainterInfo) { m_parentPainterInfo->m_childPainterInfo = 0; } delete(painter); delete(dragDistance); } KisDistanceInformation* KisPainterBasedStrokeStrategy::PainterInfo::buddyDragDistance() { return m_childPainterInfo ? m_childPainterInfo->dragDistance : 0; } KisPainterBasedStrokeStrategy::KisPainterBasedStrokeStrategy(const QString &id, const KUndo2MagicString &name, KisResourcesSnapshotSP resources, QVector painterInfos,bool useMergeID) - : KisSimpleStrokeStrategy(id, name), + : KisRunnableBasedStrokeStrategy(id, name), m_resources(resources), m_painterInfos(painterInfos), m_transaction(0), m_useMergeID(useMergeID) { init(); } KisPainterBasedStrokeStrategy::KisPainterBasedStrokeStrategy(const QString &id, const KUndo2MagicString &name, KisResourcesSnapshotSP resources, PainterInfo *painterInfo,bool useMergeID) - : KisSimpleStrokeStrategy(id, name), + : KisRunnableBasedStrokeStrategy(id, name), m_resources(resources), m_painterInfos(QVector() << painterInfo), m_transaction(0), m_useMergeID(useMergeID) { init(); } void KisPainterBasedStrokeStrategy::init() { enableJob(KisSimpleStrokeStrategy::JOB_INIT); enableJob(KisSimpleStrokeStrategy::JOB_FINISH); enableJob(KisSimpleStrokeStrategy::JOB_CANCEL, true, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); enableJob(KisSimpleStrokeStrategy::JOB_SUSPEND); enableJob(KisSimpleStrokeStrategy::JOB_RESUME); } KisPainterBasedStrokeStrategy::KisPainterBasedStrokeStrategy(const KisPainterBasedStrokeStrategy &rhs, int levelOfDetail) - : KisSimpleStrokeStrategy(rhs), + : KisRunnableBasedStrokeStrategy(rhs), m_resources(rhs.m_resources), m_transaction(rhs.m_transaction), m_useMergeID(rhs.m_useMergeID) { Q_FOREACH (PainterInfo *info, rhs.m_painterInfos) { m_painterInfos.append(new PainterInfo(info, levelOfDetail)); } KIS_ASSERT_RECOVER_NOOP( !rhs.m_transaction && !rhs.m_targetDevice && !rhs.m_activeSelection && "After the stroke has been started, no copying must happen"); } KisPaintDeviceSP KisPainterBasedStrokeStrategy::targetDevice() const { return m_targetDevice; } KisSelectionSP KisPainterBasedStrokeStrategy::activeSelection() const { return m_activeSelection; } const QVector KisPainterBasedStrokeStrategy::painterInfos() const { return m_painterInfos; } void KisPainterBasedStrokeStrategy::initPainters(KisPaintDeviceSP targetDevice, KisSelectionSP selection, bool hasIndirectPainting, const QString &indirectPaintingCompositeOp) { Q_FOREACH (PainterInfo *info, m_painterInfos) { KisPainter *painter = info->painter; painter->begin(targetDevice, !hasIndirectPainting ? selection : 0); + painter->setRunnableStrokeJobsInterface(runnableJobsInterface()); m_resources->setupPainter(painter); if(hasIndirectPainting) { painter->setCompositeOp(targetDevice->colorSpace()->compositeOp(indirectPaintingCompositeOp)); painter->setOpacity(OPACITY_OPAQUE_U8); painter->setChannelFlags(QBitArray()); } } } void KisPainterBasedStrokeStrategy::deletePainters() { Q_FOREACH (PainterInfo *info, m_painterInfos) { delete info; } m_painterInfos.clear(); } void KisPainterBasedStrokeStrategy::initStrokeCallback() { KisNodeSP node = m_resources->currentNode(); KisPaintDeviceSP paintDevice = node->paintDevice(); KisPaintDeviceSP targetDevice = paintDevice; bool hasIndirectPainting = needsIndirectPainting(); KisSelectionSP selection = m_resources->activeSelection(); if (hasIndirectPainting) { KisIndirectPaintingSupport *indirect = dynamic_cast(node.data()); if (indirect) { targetDevice = paintDevice->createCompositionSourceDevice(); targetDevice->setParentNode(node); indirect->setCurrentColor(m_resources->currentFgColor()); indirect->setTemporaryTarget(targetDevice); indirect->setTemporaryCompositeOp(m_resources->compositeOpId()); indirect->setTemporaryOpacity(m_resources->opacity()); indirect->setTemporarySelection(selection); QBitArray channelLockFlags = m_resources->channelLockFlags(); indirect->setTemporaryChannelFlags(channelLockFlags); } else { hasIndirectPainting = false; } } if(m_useMergeID){ m_transaction = new KisTransaction(name(), targetDevice,0,timedID(this->id())); } else{ m_transaction = new KisTransaction(name(), targetDevice); } initPainters(targetDevice, selection, hasIndirectPainting, indirectPaintingCompositeOp()); m_targetDevice = targetDevice; m_activeSelection = selection; // sanity check: selection should be applied only once if (selection && !m_painterInfos.isEmpty()) { KisIndirectPaintingSupport *indirect = dynamic_cast(node.data()); KIS_ASSERT_RECOVER_RETURN(hasIndirectPainting || m_painterInfos.first()->painter->selection()); KIS_ASSERT_RECOVER_RETURN(!hasIndirectPainting || !indirect->temporarySelection() || !m_painterInfos.first()->painter->selection()); } } void KisPainterBasedStrokeStrategy::finishStrokeCallback() { KisNodeSP node = m_resources->currentNode(); KisIndirectPaintingSupport *indirect = dynamic_cast(node.data()); KisPostExecutionUndoAdapter *undoAdapter = m_resources->postExecutionUndoAdapter(); QScopedPointer dumbUndoAdapter; QScopedPointer dumbUndoStore; if (!undoAdapter) { dumbUndoStore.reset(new KisDumbUndoStore()); dumbUndoAdapter.reset(new KisPostExecutionUndoAdapter(dumbUndoStore.data(), 0)); undoAdapter = dumbUndoAdapter.data(); } if (indirect && indirect->hasTemporaryTarget()) { KUndo2MagicString transactionText = m_transaction->text(); m_transaction->end(); if(m_useMergeID){ indirect->mergeToLayer(node, undoAdapter, transactionText,timedID(this->id())); } else{ indirect->mergeToLayer(node, undoAdapter, transactionText); } } else { m_transaction->commit(undoAdapter); } delete m_transaction; deletePainters(); } void KisPainterBasedStrokeStrategy::cancelStrokeCallback() { KisNodeSP node = m_resources->currentNode(); KisIndirectPaintingSupport *indirect = dynamic_cast(node.data()); bool revert = true; if (indirect) { KisPaintDeviceSP t = indirect->temporaryTarget(); if (t) { delete m_transaction; deletePainters(); QRegion region = t->region(); indirect->setTemporaryTarget(0); node->setDirty(region); revert = false; } } if (revert) { m_transaction->revert(); delete m_transaction; deletePainters(); } } void KisPainterBasedStrokeStrategy::suspendStrokeCallback() { KisNodeSP node = m_resources->currentNode(); KisIndirectPaintingSupport *indirect = dynamic_cast(node.data()); if(indirect && indirect->hasTemporaryTarget()) { indirect->setTemporaryTarget(0); } } void KisPainterBasedStrokeStrategy::resumeStrokeCallback() { KisNodeSP node = m_resources->currentNode(); KisIndirectPaintingSupport *indirect = dynamic_cast(node.data()); if(indirect) { // todo: don't ask about paint device if (node->paintDevice() != m_targetDevice) { indirect->setTemporaryTarget(m_targetDevice); indirect->setTemporaryCompositeOp(m_resources->compositeOpId()); indirect->setTemporaryOpacity(m_resources->opacity()); indirect->setTemporarySelection(m_activeSelection); } } } + +KisNodeSP KisPainterBasedStrokeStrategy::targetNode() const +{ + return m_resources->currentNode(); +} diff --git a/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.h b/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.h index 040559ca91..e772029bbb 100644 --- a/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.h +++ b/libs/ui/tool/strokes/kis_painter_based_stroke_strategy.h @@ -1,110 +1,111 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_PAINTER_BASED_STROKE_STRATEGY_H #define __KIS_PAINTER_BASED_STROKE_STRATEGY_H -#include "kis_simple_stroke_strategy.h" +#include "KisRunnableBasedStrokeStrategy.h" #include "kis_resources_snapshot.h" #include "kis_selection.h" class KisPainter; class KisDistanceInformation; class KisTransaction; -class KRITAUI_EXPORT KisPainterBasedStrokeStrategy : public KisSimpleStrokeStrategy +class KRITAUI_EXPORT KisPainterBasedStrokeStrategy : public KisRunnableBasedStrokeStrategy { public: /** * The distance information should be associated with each * painter individually, so we strore and manipulate with * them together using the structure PainterInfo */ class KRITAUI_EXPORT PainterInfo { public: PainterInfo(); PainterInfo(const KisDistanceInformation &startDist); PainterInfo(PainterInfo *rhs, int levelOfDetail); ~PainterInfo(); KisPainter *painter; KisDistanceInformation *dragDistance; /** * The distance inforametion of the associated LodN * stroke. Returns zero if LodN stroke has already finished * execution or does not exist. */ KisDistanceInformation* buddyDragDistance(); private: PainterInfo *m_parentPainterInfo; PainterInfo *m_childPainterInfo; }; public: KisPainterBasedStrokeStrategy(const QString &id, const KUndo2MagicString &name, KisResourcesSnapshotSP resources, QVector painterInfos,bool useMergeID = false); KisPainterBasedStrokeStrategy(const QString &id, const KUndo2MagicString &name, KisResourcesSnapshotSP resources, PainterInfo *painterInfo,bool useMergeID = false); void initStrokeCallback() override; void finishStrokeCallback() override; void cancelStrokeCallback() override; void suspendStrokeCallback() override; void resumeStrokeCallback() override; protected: + KisNodeSP targetNode() const; KisPaintDeviceSP targetDevice() const; KisSelectionSP activeSelection() const; const QVector painterInfos() const; void setUndoEnabled(bool value); protected: KisPainterBasedStrokeStrategy(const KisPainterBasedStrokeStrategy &rhs, int levelOfDetail); private: void init(); void initPainters(KisPaintDeviceSP targetDevice, KisSelectionSP selection, bool hasIndirectPainting, const QString &indirectPaintingCompositeOp); void deletePainters(); inline int timedID(const QString &id){ return int(qHash(id)); } private: KisResourcesSnapshotSP m_resources; QVector m_painterInfos; KisTransaction *m_transaction; KisPaintDeviceSP m_targetDevice; KisSelectionSP m_activeSelection; bool m_useMergeID; }; #endif /* __KIS_PAINTER_BASED_STROKE_STRATEGY_H */ diff --git a/libs/ui/widgets/kis_preset_chooser.cpp b/libs/ui/widgets/kis_preset_chooser.cpp index 9fa4e3e3b7..b3c1b9e81f 100644 --- a/libs/ui/widgets/kis_preset_chooser.cpp +++ b/libs/ui/widgets/kis_preset_chooser.cpp @@ -1,343 +1,355 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2009 Sven Langkamp * Copyright (C) 2011 Silvio Heinrich * Copyright (C) 2011 Srikanth Tiyyagura * Copyright (c) 2011 José Luis Vergara * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_preset_chooser.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "KoResourceItemView.h" #include #include #include "kis_resource_server_provider.h" #include "kis_global.h" #include "kis_slider_spin_box.h" #include "kis_config.h" #include "kis_config_notifier.h" #include /// The resource item delegate for rendering the resource preview class KisPresetDelegate : public QAbstractItemDelegate { public: KisPresetDelegate(QObject * parent = 0) : QAbstractItemDelegate(parent), m_showText(false), m_useDirtyPresets(false) {} ~KisPresetDelegate() override {} /// reimplemented void paint(QPainter *, const QStyleOptionViewItem &, const QModelIndex &) const override; /// reimplemented QSize sizeHint(const QStyleOptionViewItem & option, const QModelIndex &) const override { return option.decorationSize; } void setShowText(bool showText) { m_showText = showText; } void setUseDirtyPresets(bool value) { m_useDirtyPresets = value; } private: bool m_showText; bool m_useDirtyPresets; }; void KisPresetDelegate::paint(QPainter * painter, const QStyleOptionViewItem & option, const QModelIndex & index) const { painter->save(); painter->setRenderHint(QPainter::SmoothPixmapTransform, true); if (! index.isValid()) return; KisPaintOpPreset* preset = static_cast(index.internalPointer()); QImage preview = preset->image(); if(preview.isNull()) { return; } QRect paintRect = option.rect.adjusted(1, 1, -1, -1); if (!m_showText) { painter->drawImage(paintRect.x(), paintRect.y(), preview.scaled(paintRect.size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); } else { QSize pixSize(paintRect.height(), paintRect.height()); painter->drawImage(paintRect.x(), paintRect.y(), preview.scaled(pixSize, Qt::KeepAspectRatio, Qt::SmoothTransformation)); // Put an asterisk after the preset if it is dirty. This will help in case the pixmap icon is too small QString dirtyPresetIndicator = QString(""); if (m_useDirtyPresets && preset->isPresetDirty()) { dirtyPresetIndicator = QString("*"); } - painter->drawText(pixSize.width() + 10, option.rect.y() + option.rect.height() - 10, preset->name().append(dirtyPresetIndicator)); + qreal brushSize = preset->settings()->paintOpSize(); + QString brushSizeText; + + // Disable displayed decimal precision beyond a certain brush size + if (brushSize < 100) { + brushSizeText = QString::number(brushSize, 'g', 3); + } else { + brushSizeText = QString::number(brushSize, 'f', 0); + } + + painter->drawText(pixSize.width() + 10, option.rect.y() + option.rect.height() - 10, brushSizeText); + painter->drawText(pixSize.width() + 40, option.rect.y() + option.rect.height() - 10, preset->name().append(dirtyPresetIndicator)); + } if (m_useDirtyPresets && preset->isPresetDirty()) { const QIcon icon = KisIconUtils::loadIcon(koIconName("dirty-preset")); QPixmap pixmap = icon.pixmap(QSize(15,15)); painter->drawPixmap(paintRect.x() + 3, paintRect.y() + 3, pixmap); } if (!preset->settings() || !preset->settings()->isValid()) { const QIcon icon = KisIconUtils::loadIcon("broken-preset"); icon.paint(painter, QRect(paintRect.x() + paintRect.height() - 25, paintRect.y() + paintRect.height() - 25, 25, 25)); } if (option.state & QStyle::State_Selected) { painter->setCompositionMode(QPainter::CompositionMode_HardLight); painter->setOpacity(1.0); painter->fillRect(option.rect, option.palette.highlight()); // highlight is not strong enough to pick out preset. draw border around it. painter->setCompositionMode(QPainter::CompositionMode_SourceOver); painter->setPen(QPen(option.palette.highlight(), 4, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin)); QRect selectedBorder = option.rect.adjusted(2 , 2, -2, -2); // constrict the rectangle so it doesn't bleed into other presets painter->drawRect(selectedBorder); } painter->restore(); } class KisPresetProxyAdapter : public KisPaintOpPresetResourceServerAdapter { public: KisPresetProxyAdapter(KisPaintOpPresetResourceServer* resourceServer) : KisPaintOpPresetResourceServerAdapter(resourceServer) { setSortingEnabled(true); } ~KisPresetProxyAdapter() override {} QList< KoResource* > resources() override { QList serverResources = KisPaintOpPresetResourceServerAdapter::resources(); if (m_paintopID.isEmpty()) { return serverResources; } QList resources; Q_FOREACH (KoResource *resource, serverResources) { KisPaintOpPreset *preset = dynamic_cast(resource); if (preset && preset->paintOp().id() == m_paintopID) { resources.append(preset); } } return resources; } ///Set id for paintop to be accept by the proxy model, if not filter is set all ///presets will be shown. void setPresetFilter(const QString& paintOpId) { m_paintopID = paintOpId; invalidate(); } ///Resets the model connected to the adapter void invalidate() { emitRemovingResource(0); } QString currentPaintOpId() const { return m_paintopID; } private: QString m_paintopID; }; KisPresetChooser::KisPresetChooser(QWidget *parent, const char *name) : QWidget(parent) { setObjectName(name); QVBoxLayout * layout = new QVBoxLayout(this); layout->setMargin(0); KisPaintOpPresetResourceServer * rserver = KisResourceServerProvider::instance()->paintOpPresetServer(false); m_adapter = QSharedPointer(new KisPresetProxyAdapter(rserver)); m_chooser = new KoResourceItemChooser(m_adapter, this); m_chooser->setObjectName("ResourceChooser"); m_chooser->setColumnCount(10); m_chooser->setRowHeight(50); m_delegate = new KisPresetDelegate(this); m_chooser->setItemDelegate(m_delegate); m_chooser->setSynced(true); layout->addWidget(m_chooser); connect(m_chooser, SIGNAL(resourceSelected(KoResource*)), this, SIGNAL(resourceSelected(KoResource*))); connect(m_chooser, SIGNAL(resourceClicked(KoResource*)), this, SIGNAL(resourceClicked(KoResource*))); m_mode = THUMBNAIL; connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(notifyConfigChanged())); notifyConfigChanged(); } KisPresetChooser::~KisPresetChooser() { } void KisPresetChooser::showButtons(bool show) { m_chooser->showButtons(show); } void KisPresetChooser::setViewMode(KisPresetChooser::ViewMode mode) { m_mode = mode; updateViewSettings(); } void KisPresetChooser::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); updateViewSettings(); } void KisPresetChooser::notifyConfigChanged() { KisConfig cfg; m_delegate->setUseDirtyPresets(cfg.useDirtyPresets()); setIconSize(cfg.presetIconSize()); updateViewSettings(); } void KisPresetChooser::updateViewSettings() { if (m_mode == THUMBNAIL) { m_chooser->setSynced(true); m_delegate->setShowText(false); } else if (m_mode == DETAIL) { m_chooser->setSynced(false); m_chooser->setColumnCount(1); m_chooser->setColumnWidth(m_chooser->width()); KoResourceItemChooserSync* chooserSync = KoResourceItemChooserSync::instance(); m_chooser->setRowHeight(chooserSync->baseLength()); m_delegate->setShowText(true); } else if (m_mode == STRIP) { m_chooser->setSynced(false); m_chooser->setRowCount(1); m_chooser->itemView()->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_chooser->itemView()->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // An offset of 7 keeps the cell exactly square, TODO: use constants, not hardcoded numbers m_chooser->setColumnWidth(m_chooser->viewSize().height() - 7); m_delegate->setShowText(false); } } void KisPresetChooser::setCurrentResource(KoResource *resource) { /** * HACK ALERT: here we use a direct call to an adapter to notify the view * that the preset might have changed its dirty state. This state * doesn't affect the filtering so the server's cache must not be * invalidated! * * Ideally, we should call some method of KoResourceServer instead, * but ut seems like a bit too much effort for such a small fix. */ if (resource == currentResource()) { KisPresetProxyAdapter *adapter = static_cast(m_adapter.data()); KisPaintOpPreset *preset = dynamic_cast(resource); if (preset) { adapter->resourceChangedNoCacheInvalidation(preset); } } m_chooser->setCurrentResource(resource); } KoResource* KisPresetChooser::currentResource() const { return m_chooser->currentResource(); } void KisPresetChooser::showTaggingBar(bool show) { m_chooser->showTaggingBar(show); } KoResourceItemChooser *KisPresetChooser::itemChooser() { return m_chooser; } void KisPresetChooser::setPresetFilter(const QString& paintOpId) { KisPresetProxyAdapter *adapter = static_cast(m_adapter.data()); if (adapter->currentPaintOpId() != paintOpId) { adapter->setPresetFilter(paintOpId); updateViewSettings(); } } void KisPresetChooser::setIconSize(int newSize) { KoResourceItemChooserSync* chooserSync = KoResourceItemChooserSync::instance(); chooserSync->setBaseLength(newSize); updateViewSettings(); } int KisPresetChooser::iconSize() { KoResourceItemChooserSync* chooserSync = KoResourceItemChooserSync::instance(); return chooserSync->baseLength(); } void KisPresetChooser::saveIconSize() { // save icon size KisConfig cfg; cfg.setPresetIconSize(iconSize()); } diff --git a/libs/ui/widgets/kis_preset_live_preview_view.cpp b/libs/ui/widgets/kis_preset_live_preview_view.cpp index b3fe1208b3..e194f71818 100644 --- a/libs/ui/widgets/kis_preset_live_preview_view.cpp +++ b/libs/ui/widgets/kis_preset_live_preview_view.cpp @@ -1,246 +1,265 @@ /* * Copyright (c) 2017 Scott Petrovic * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include "kis_paintop_settings.h" +#include KisPresetLivePreviewView::KisPresetLivePreviewView(QWidget *parent): QGraphicsView(parent) { } KisPresetLivePreviewView::~KisPresetLivePreviewView() { - delete m_brushPreviewPainter; delete m_noPreviewText; delete m_brushPreviewScene; } void KisPresetLivePreviewView::setup() { // initializing to 0 helps check later if they actually have something in them m_noPreviewText = 0; m_sceneImageItem = 0; setHorizontalScrollBarPolicy ( Qt::ScrollBarAlwaysOff ); setVerticalScrollBarPolicy ( Qt::ScrollBarAlwaysOff ); // layer image needs to be big enough to get an entire stroke of data m_canvasSize.setWidth(this->width()); m_canvasSize.setHeight(this->height()); m_canvasCenterPoint.setX(m_canvasSize.width()*0.5); m_canvasCenterPoint.setY(m_canvasSize.height()*0.5); m_colorSpace = KoColorSpaceRegistry::instance()->rgb8(); m_image = new KisImage(0, m_canvasSize.width(), m_canvasSize.height(), m_colorSpace, "stroke sample image"); m_layer = new KisPaintLayer(m_image, "livePreviewStrokeSample", OPACITY_OPAQUE_U8, m_colorSpace); - m_brushPreviewPainter = new KisPainter(m_layer->paintDevice()); // set scene for the view m_brushPreviewScene = new QGraphicsScene(); setScene(m_brushPreviewScene); } void KisPresetLivePreviewView::setCurrentPreset(KisPaintOpPresetSP preset) { m_currentPreset = preset; } void KisPresetLivePreviewView::updateStroke() { paintBackground(); // do not paint a stroke if we are any of these engines (they have some issue currently) if (m_currentPreset->paintOp().id() == "roundmarker" || m_currentPreset->paintOp().id() == "experimentbrush" || m_currentPreset->paintOp().id() == "duplicate") { return; } setupAndPaintStroke(); // crop the layer so a brush stroke won't go outside of the area m_layer->paintDevice()->crop(0,0, m_layer->image()->width(), m_layer->image()->height()); QImage m_temp_image; m_temp_image = m_layer->paintDevice()->convertToQImage(0); // only add the object once...then just update the pixmap so we can move the preview around if (!m_sceneImageItem) { m_sceneImageItem = m_brushPreviewScene->addPixmap(QPixmap::fromImage(m_temp_image)); } else { m_sceneImageItem->setPixmap(QPixmap::fromImage(m_temp_image)); } } void KisPresetLivePreviewView::paintBackground() { // clean up "no preview" text object if it exists. we will add it later if we need it if (m_noPreviewText) { this->scene()->removeItem(m_noPreviewText); m_noPreviewText = 0; } if (m_currentPreset->paintOp().id() == "colorsmudge" || m_currentPreset->paintOp().id() == "deformbrush" || m_currentPreset->paintOp().id() == "filter") { // easier to see deformations and smudging with alternating stripes in the background // paint the whole background with alternating stripes // filter engine may or may not show things depending on the filter...but it is better than nothing int grayStrips = 20; for (int i=0; i < grayStrips; i++ ) { float sectionPercent = 1.0 / (float)grayStrips; bool isAlternating = i % 2; - KoColor fillColor; + KoColor fillColor(m_layer->paintDevice()->colorSpace()); if (isAlternating) { fillColor.fromQColor(QColor(80,80,80)); } else { fillColor.fromQColor(QColor(140,140,140)); } - m_brushPreviewPainter->fill(m_layer->image()->width()*sectionPercent*i, - 0, - m_layer->image()->width()*(sectionPercent*i +sectionPercent), - m_layer->image()->height(), - fillColor); - + const QRect fillRect(m_layer->image()->width()*sectionPercent*i, + 0, + m_layer->image()->width()*(sectionPercent*i +sectionPercent), + m_layer->image()->height()); + m_layer->paintDevice()->fill(fillRect, fillColor); } - m_brushPreviewPainter->setPaintColor(KoColor(Qt::white, m_colorSpace)); + m_paintColor = KoColor(Qt::white, m_colorSpace); } else if (m_currentPreset->paintOp().id() == "roundmarker" || m_currentPreset->paintOp().id() == "experimentbrush" || m_currentPreset->paintOp().id() == "duplicate" ) { // cases where we will not show a preview for now // roundbrush (quick) -- this isn't showing anything, disable showing preview // experimentbrush -- this creates artifacts that carry over to other previews and messes up their display // duplicate (clone) brush doesn't have a preview as it doesn't show anything) if(m_sceneImageItem) { this->scene()->removeItem(m_sceneImageItem); m_sceneImageItem = 0; } QFont font; font.setPixelSize(14); font.setBold(false); m_noPreviewText = this->scene()->addText(i18n("No Preview for this engine"),font); m_noPreviewText->setPos(50, this->height()/4); return; } else { // fill with gray first to clear out what existed from previous preview - m_brushPreviewPainter->fill(0,0, - m_layer->image()->width(), - m_layer->image()->height(), - KoColor(palette().color(QPalette::Background) , m_colorSpace)); - - m_brushPreviewPainter->setPaintColor(KoColor(palette().color(QPalette::Text), m_colorSpace)); + m_layer->paintDevice()->fill(m_image->bounds(), KoColor(palette().color(QPalette::Background) , m_colorSpace)); + m_paintColor = KoColor(palette().color(QPalette::Text), m_colorSpace); } } void KisPresetLivePreviewView::setupAndPaintStroke() { // limit the brush stroke size. larger brush strokes just don't look good and are CPU intensive // we are making a proxy preset and setting it to the painter...otherwise setting the brush size of the original preset // will fire off signals that make this run in an infinite loop qreal originalPresetSize = m_currentPreset->settings()->paintOpSize(); qreal previewSize = qBound(3.0, m_currentPreset->settings()->paintOpSize(), 25.0 ); // constrain live preview brush size KisPaintOpPresetSP proxy_preset = m_currentPreset->clone(); proxy_preset->settings()->setPaintOpSize(previewSize); - m_brushPreviewPainter->setPaintOpPreset(proxy_preset, m_layer, m_image); + + + KisResourcesSnapshotSP resources = + new KisResourcesSnapshot(m_image, + m_layer); + + resources->setBrush(proxy_preset); + resources->setFGColorOverride(m_paintColor); + FreehandStrokeStrategy::PainterInfo *painterInfo = new FreehandStrokeStrategy::PainterInfo(); + + KisStrokeStrategy *stroke = + new FreehandStrokeStrategy(resources->needsIndirectPainting(), + resources->indirectPaintingCompositeOp(), + resources, painterInfo, kundo2_noi18n("temp_stroke")); + + KisStrokeId strokeId = m_image->startStroke(stroke); + + + + //m_brushPreviewPainter->setPaintOpPreset(proxy_preset, m_layer, m_image); // paint the stroke. The sketchbrush gets a differnet shape than the others to show how it works if (m_currentPreset->paintOp().id() == "sketchbrush") { KisPaintInformation pointOne; pointOne.setPressure(0.0); pointOne.setPos(QPointF(m_canvasCenterPoint.x() - (this->width() * 0.4), m_canvasCenterPoint.y() - (this->height()*0.2) )); KisPaintInformation pointTwo; pointTwo.setPressure(1.0); pointTwo.setPos(QPointF(m_canvasCenterPoint.x() + (this->width() * 0.4), m_canvasCenterPoint.y() + (this->height()*0.2) )); - m_brushPreviewPainter->paintBezierCurve(pointOne, - QPointF(m_canvasCenterPoint.x() + this->width(), - m_canvasCenterPoint.y() - (this->height()*0.2) ), - QPointF(m_canvasCenterPoint.x() - this->width(), - m_canvasCenterPoint.y() + (this->height()*0.2) ), - pointTwo, &m_currentDistance); - + m_image->addJob(strokeId, + new FreehandStrokeStrategy::Data(0, + pointOne, + QPointF(m_canvasCenterPoint.x() + this->width(), + m_canvasCenterPoint.y() - (this->height()*0.2) ), + QPointF(m_canvasCenterPoint.x() - this->width(), + m_canvasCenterPoint.y() + (this->height()*0.2) ), + pointTwo)); } else { // paint an S curve m_curvePointPI1.setPos(QPointF(m_canvasCenterPoint.x() - (this->width()*0.45), m_canvasCenterPoint.y() + (this->height()*0.2))); m_curvePointPI1.setPressure(0.0); m_curvePointPI2.setPos(QPointF(m_canvasCenterPoint.x() + (this->width()*0.4), m_canvasCenterPoint.y() - (this->height()*0.2) )); m_curvePointPI2.setPressure(1.0); - m_brushPreviewPainter->paintBezierCurve(m_curvePointPI1, - QPointF(m_canvasCenterPoint.x(), - m_canvasCenterPoint.y()-this->height()), - QPointF(m_canvasCenterPoint.x(), - m_canvasCenterPoint.y()+this->height()), - m_curvePointPI2, &m_currentDistance); + m_image->addJob(strokeId, + new FreehandStrokeStrategy::Data(0, + m_curvePointPI1, + QPointF(m_canvasCenterPoint.x(), + m_canvasCenterPoint.y()-this->height()), + QPointF(m_canvasCenterPoint.x(), + m_canvasCenterPoint.y()+this->height()), + m_curvePointPI2)); } + m_image->addJob(strokeId, new FreehandStrokeStrategy::UpdateData(true)); + m_image->endStroke(strokeId); + m_image->waitForDone(); // even though the brush is cloned, the proxy_preset still has some connection to the original preset which will mess brush sizing // we need to return brush size to normal.The normal brush sends out a lot of extra signals, so keeping the proxy for now proxy_preset->settings()->setPaintOpSize(originalPresetSize); } diff --git a/libs/ui/widgets/kis_preset_live_preview_view.h b/libs/ui/widgets/kis_preset_live_preview_view.h index 634be42793..a09d1c366d 100644 --- a/libs/ui/widgets/kis_preset_live_preview_view.h +++ b/libs/ui/widgets/kis_preset_live_preview_view.h @@ -1,149 +1,150 @@ /* * Copyright (c) 2017 Scott Petrovic * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_PRESET_LIVE_PREVIEW_ #define _KIS_PRESET_LIVE_PREVIEW_ #include #include #include #include #include #include "kis_paintop_preset.h" #include "KoColorSpaceRegistry.h" #include "kis_paint_layer.h" #include "kis_painter.h" #include "kis_distance_information.h" #include "kis_painting_information_builder.h" #include #include +#include /** * Widget for displaying a live brush preview of your * selected brush. It listens for signalsetting changes * that the brush preset outputs and updates the preview * accordingly. This class can be added to a UI file * similar to how a QGraphicsView is added */ class KisPresetLivePreviewView : public QGraphicsView { Q_OBJECT public: KisPresetLivePreviewView(QWidget *parent); ~KisPresetLivePreviewView(); /** * @brief one time setup for initialization of many variables. * This live preview might be in a UI file, so make sure to * call this before starting to use it */ void setup(); /** * @brief set the current preset from resource manager for the live preview to use. * Good to call this every stroke update in case the preset has changed * @param the current preset from the resource manager */ void setCurrentPreset(KisPaintOpPresetSP preset); void updateStroke(); private: /// internally sets the image area for brush preview KisImageSP m_image; /// internally sets the layer area for brush preview KisLayerSP m_layer; /// internally sets the color space for brush preview const KoColorSpace *m_colorSpace; - /// painter that actually paints the stroke - KisPainter *m_brushPreviewPainter; + /// the color which is used for rendering the stroke + KoColor m_paintColor; /// the scene that can add items like text and the brush stroke image QGraphicsScene *m_brushPreviewScene; /// holds the preview brush stroke data QGraphicsPixmapItem *m_sceneImageItem; /// holds the 'no preview available' text object QGraphicsTextItem *m_noPreviewText; /// holds the width and height of the image of the brush preview /// Probably can later add set functions to make this customizable /// It is hard-coded to 1200 x 400 for right now for image size QRect m_canvasSize; /// convenience variable used internally when positioning the objects /// and points in the scene QPointF m_canvasCenterPoint; /// internal variables for constructing the stroke start and end shape /// there are two points that construct the "S" curve with this KisDistanceInformation m_currentDistance; QPainterPath m_curvedLine; KisPaintInformation m_curvePointPI1; KisPaintInformation m_curvePointPI2; /// internally stores the current preset. /// See setCurrentPreset(KisPaintOpPresetSP preset) /// for setting this externally KisPaintOpPresetSP m_currentPreset; /// holds the current zoom(scale) level of scene float m_scaleFactor; /// internal reference for internal brush size /// used to check if our brush size has changed /// do zooming and other things internall if it has changed float m_currentBrushSize = 1.0; /// the range of brush sizes that will control zooming in/out const float m_minBrushVal = 10.0; const float m_maxBrushVal = 100.0; /// range of scale values. 1.0 == 100% const qreal m_minScale = 1.0; const qreal m_maxScale = 0.3; /// multiplier that is used for lengthening the brush stroke points const float m_minStrokeScale = 0.4; // for smaller brush stroke const float m_maxStrokeScale = 1.0; // for larger brush stroke /** * @brief works as both clearing the previous stroke, providing * striped backgrounds for smudging brushes, and text if there is no preview */ void paintBackground(); /** * @brief creates and performs the actual stroke that goes on top of the background * this is internally and should always be called after the paintBackground() */ void setupAndPaintStroke(); }; #endif diff --git a/libs/ui/widgets/kis_scratch_pad.cpp b/libs/ui/widgets/kis_scratch_pad.cpp index dc62ec1fc7..46ecce2397 100644 --- a/libs/ui/widgets/kis_scratch_pad.cpp +++ b/libs/ui/widgets/kis_scratch_pad.cpp @@ -1,509 +1,512 @@ /* This file is part of the KDE project * Copyright 2010 (C) Boudewijn Rempt * Copyright 2011 (C) Dmitry Kazakov * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kis_scratch_pad.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_config.h" #include "kis_image.h" #include "kis_undo_stores.h" #include "kis_update_scheduler.h" #include "kis_post_execution_undo_adapter.h" #include "kis_scratch_pad_event_filter.h" #include "kis_painting_information_builder.h" #include "kis_tool_freehand_helper.h" #include "kis_image_patch.h" #include "kis_canvas_widget_base.h" #include "kis_layer_projection_plane.h" #include "kis_node_graph_listener.h" class KisScratchPadNodeListener : public KisNodeGraphListener { public: KisScratchPadNodeListener(KisScratchPad *scratchPad) : m_scratchPad(scratchPad) { } - void requestProjectionUpdate(KisNode *node, const QRect& rect, bool resetAnimationCache) override { - KisNodeGraphListener::requestProjectionUpdate(node, rect, resetAnimationCache); + void requestProjectionUpdate(KisNode *node, const QVector &rects, bool resetAnimationCache) override { + KisNodeGraphListener::requestProjectionUpdate(node, rects, resetAnimationCache); QMutexLocker locker(&m_lock); - m_scratchPad->imageUpdated(rect); + + Q_FOREACH (const QRect &rc, rects) { + m_scratchPad->imageUpdated(rc); + } } private: KisScratchPad *m_scratchPad; QMutex m_lock; }; class KisScratchPadDefaultBounds : public KisDefaultBounds { public: KisScratchPadDefaultBounds(KisScratchPad *scratchPad) : m_scratchPad(scratchPad) { } ~KisScratchPadDefaultBounds() override {} QRect bounds() const override { return m_scratchPad->imageBounds(); } private: Q_DISABLE_COPY(KisScratchPadDefaultBounds) KisScratchPad *m_scratchPad; }; KisScratchPad::KisScratchPad(QWidget *parent) : QWidget(parent) , m_toolMode(HOVERING) , m_paintLayer(0) , m_displayProfile(0) , m_resourceProvider(0) { setAutoFillBackground(false); setMouseTracking(true); m_cursor = KisCursor::load("tool_freehand_cursor.png", 5, 5); setCursor(m_cursor); KisConfig cfg; QImage checkImage = KisCanvasWidgetBase::createCheckersImage(cfg.checkSize()); m_checkBrush = QBrush(checkImage); // We are not supposed to use updates here, // so just set the listener to null m_updateScheduler = new KisUpdateScheduler(0); m_undoStore = new KisSurrogateUndoStore(); m_undoAdapter = new KisPostExecutionUndoAdapter(m_undoStore, m_updateScheduler); m_nodeListener = new KisScratchPadNodeListener(this); connect(this, SIGNAL(sigUpdateCanvas(QRect)), SLOT(slotUpdateCanvas(QRect)), Qt::QueuedConnection); // filter will be deleted by the QObject hierarchy m_eventFilter = new KisScratchPadEventFilter(this); m_infoBuilder = new KisPaintingInformationBuilder(); m_helper = new KisToolFreehandHelper(m_infoBuilder); m_scaleBorderWidth = 1; } KisScratchPad::~KisScratchPad() { delete m_helper; delete m_infoBuilder; delete m_undoAdapter; delete m_undoStore; delete m_updateScheduler; delete m_nodeListener; } KisScratchPad::Mode KisScratchPad::modeFromButton(Qt::MouseButton button) const { return button == Qt::NoButton ? HOVERING : button == Qt::MidButton ? PANNING : button == Qt::RightButton ? PICKING : PAINTING; } void KisScratchPad::pointerPress(KoPointerEvent *event) { if (m_toolMode != HOVERING) return; m_toolMode = modeFromButton(event->button()); if (m_toolMode == PAINTING) { beginStroke(event); event->accept(); } else if (m_toolMode == PANNING) { beginPan(event); event->accept(); } else if (m_toolMode == PICKING) { pick(event); event->accept(); } } void KisScratchPad::pointerRelease(KoPointerEvent *event) { if (modeFromButton(event->button()) != m_toolMode) return; if (m_toolMode == PAINTING) { endStroke(event); m_toolMode = HOVERING; event->accept(); } else if (m_toolMode == PANNING) { endPan(event); m_toolMode = HOVERING; event->accept(); } else if (m_toolMode == PICKING) { event->accept(); m_toolMode = HOVERING; } } void KisScratchPad::pointerMove(KoPointerEvent *event) { m_helper->cursorMoved(documentToWidget().map(event->point)); if (m_toolMode == PAINTING) { doStroke(event); event->accept(); } else if (m_toolMode == PANNING) { doPan(event); event->accept(); } else if (m_toolMode == PICKING) { pick(event); event->accept(); } } void KisScratchPad::beginStroke(KoPointerEvent *event) { KoCanvasResourceManager *resourceManager = m_resourceProvider->resourceManager(); m_helper->initPaint(event, documentToWidget().map(event->point), resourceManager, 0, 0, m_updateScheduler, m_paintLayer, m_paintLayer->paintDevice()->defaultBounds()); } void KisScratchPad::doStroke(KoPointerEvent *event) { m_helper->paintEvent(event); } void KisScratchPad::endStroke(KoPointerEvent *event) { Q_UNUSED(event); m_helper->endPaint(); } void KisScratchPad::beginPan(KoPointerEvent *event) { setCursor(QCursor(Qt::ClosedHandCursor)); m_panDocPoint = event->point; } void KisScratchPad::doPan(KoPointerEvent *event) { QPointF docOffset = event->point - m_panDocPoint; m_translateTransform.translate(-docOffset.x(), -docOffset.y()); updateTransformations(); update(); } void KisScratchPad::endPan(KoPointerEvent *event) { Q_UNUSED(event); setCursor(m_cursor); } void KisScratchPad::pick(KoPointerEvent *event) { KoColor color; if (KisToolUtils::pick(m_paintLayer->projection(), event->point.toPoint(), &color)) { emit colorSelected(color); } } void KisScratchPad::setOnScreenResolution(qreal scaleX, qreal scaleY) { m_scaleBorderWidth = BORDER_SIZE(qMax(scaleX, scaleY)); m_scaleTransform = QTransform::fromScale(scaleX, scaleY); updateTransformations(); update(); } QTransform KisScratchPad::documentToWidget() const { return m_translateTransform.inverted() * m_scaleTransform; } QTransform KisScratchPad::widgetToDocument() const { return m_scaleTransform.inverted() * m_translateTransform; } void KisScratchPad::updateTransformations() { m_eventFilter->setWidgetToDocumentTransform(widgetToDocument()); } QRect KisScratchPad::imageBounds() const { return widgetToDocument().mapRect(rect()); } void KisScratchPad::imageUpdated(const QRect &rect) { emit sigUpdateCanvas(documentToWidget().mapRect(QRectF(rect)).toAlignedRect()); } void KisScratchPad::slotUpdateCanvas(const QRect &rect) { update(rect); } void KisScratchPad::paintEvent ( QPaintEvent * event ) { if(!m_paintLayer) return; QRectF imageRect = widgetToDocument().mapRect(QRectF(event->rect())); QRect alignedImageRect = imageRect.adjusted(-m_scaleBorderWidth, -m_scaleBorderWidth, m_scaleBorderWidth, m_scaleBorderWidth).toAlignedRect(); QPointF offset = alignedImageRect.topLeft(); m_paintLayer->projectionPlane()->recalculate(alignedImageRect, m_paintLayer); KisPaintDeviceSP projection = m_paintLayer->projection(); QImage image = projection->convertToQImage(m_displayProfile, alignedImageRect.x(), alignedImageRect.y(), alignedImageRect.width(), alignedImageRect.height(), KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); QPainter gc(this); gc.fillRect(event->rect(), m_checkBrush); gc.setRenderHints(QPainter::SmoothPixmapTransform); gc.drawImage(QRectF(event->rect()), image, imageRect.translated(-offset)); QBrush brush(Qt::lightGray); QPen pen(brush, 1, Qt::DotLine); gc.setPen(pen); if (m_cutoutOverlay.isValid()) { gc.drawRect(m_cutoutOverlay); } if(!isEnabled()) { QColor color(Qt::lightGray); color.setAlphaF(0.5); QBrush disabledBrush(color); gc.fillRect(event->rect(), disabledBrush); } gc.end(); } void KisScratchPad::setupScratchPad(KisCanvasResourceProvider* resourceProvider, const QColor &defaultColor) { m_resourceProvider = resourceProvider; KisConfig cfg; setDisplayProfile(cfg.displayProfile(QApplication::desktop()->screenNumber(this))); connect(m_resourceProvider, SIGNAL(sigDisplayProfileChanged(const KoColorProfile*)), SLOT(setDisplayProfile(const KoColorProfile*))); connect(m_resourceProvider, SIGNAL(sigOnScreenResolutionChanged(qreal,qreal)), SLOT(setOnScreenResolution(qreal,qreal))); connect(this, SIGNAL(colorSelected(const KoColor&)), m_resourceProvider, SLOT(slotSetFGColor(const KoColor&))); m_defaultColor = KoColor(defaultColor, KoColorSpaceRegistry::instance()->rgb8()); KisPaintDeviceSP paintDevice = new KisPaintDevice(m_defaultColor.colorSpace(), "scratchpad"); m_paintLayer = new KisPaintLayer(0, "ScratchPad", OPACITY_OPAQUE_U8, paintDevice); m_paintLayer->setGraphListener(m_nodeListener); m_paintLayer->paintDevice()->setDefaultBounds(new KisScratchPadDefaultBounds(this)); fillDefault(); } void KisScratchPad::setCutoutOverlayRect(const QRect& rc) { m_cutoutOverlay = rc; } QImage KisScratchPad::cutoutOverlay() const { if(!m_paintLayer) return QImage(); KisPaintDeviceSP paintDevice = m_paintLayer->paintDevice(); QRect rc = widgetToDocument().mapRect(m_cutoutOverlay); QImage rawImage = paintDevice->convertToQImage(0, rc.x(), rc.y(), rc.width(), rc.height(), KoColorConversionTransformation::internalRenderingIntent(), KoColorConversionTransformation::internalConversionFlags()); QImage scaledImage = rawImage.scaled(m_cutoutOverlay.size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); return scaledImage; } void KisScratchPad::setPresetImage(const QImage& image) { m_presetImage = image; } void KisScratchPad::paintCustomImage(const QImage& loadedImage) { // this is 99% copied from the normal paintPresetImage() // we dont' want to save over the preset image, so we don't // want to store it in the m_presetImage if(!m_paintLayer) return; KisPaintDeviceSP paintDevice = m_paintLayer->paintDevice(); QRect overlayRect = widgetToDocument().mapRect(m_cutoutOverlay); QRect imageRect(QPoint(), overlayRect.size()); QImage scaledImage = loadedImage.scaled(overlayRect.size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); KisPaintDeviceSP device = new KisPaintDevice(paintDevice->colorSpace()); device->convertFromQImage(scaledImage, 0); KisPainter painter(paintDevice); painter.bitBlt(overlayRect.topLeft(), device, imageRect); update(); } void KisScratchPad::paintPresetImage() { if(!m_paintLayer) return; KisPaintDeviceSP paintDevice = m_paintLayer->paintDevice(); QRect overlayRect = widgetToDocument().mapRect(m_cutoutOverlay); QRect imageRect(QPoint(), overlayRect.size()); QImage scaledImage = m_presetImage.scaled(overlayRect.size(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation); KisPaintDeviceSP device = new KisPaintDevice(paintDevice->colorSpace()); device->convertFromQImage(scaledImage, 0); KisPainter painter(paintDevice); painter.bitBlt(overlayRect.topLeft(), device, imageRect); update(); } void KisScratchPad::setDisplayProfile(const KoColorProfile *colorProfile) { if (colorProfile) { m_displayProfile = colorProfile; QWidget::update(); } } void KisScratchPad::fillDefault() { if(!m_paintLayer) return; KisPaintDeviceSP paintDevice = m_paintLayer->paintDevice(); paintDevice->setDefaultPixel(m_defaultColor); paintDevice->clear(); update(); } void KisScratchPad::fillTransparent() { if(!m_paintLayer) return; KisPaintDeviceSP paintDevice = m_paintLayer->paintDevice(); QColor transQColor(0,0,0,0); KoColor transparentColor(transQColor, KoColorSpaceRegistry::instance()->rgb8()); transparentColor.setOpacity(0.0); paintDevice->setDefaultPixel(transparentColor); paintDevice->clear(); update(); } void KisScratchPad::fillGradient() { if(!m_paintLayer) return; KisPaintDeviceSP paintDevice = m_paintLayer->paintDevice(); KoAbstractGradient* gradient = m_resourceProvider->currentGradient(); QRect gradientRect = widgetToDocument().mapRect(rect()); paintDevice->clear(); KisGradientPainter painter(paintDevice); painter.setGradient(gradient); painter.setGradientShape(KisGradientPainter::GradientShapeLinear); painter.paintGradient(gradientRect.topLeft(), gradientRect.bottomRight(), KisGradientPainter::GradientRepeatNone, 0.2, false, gradientRect.left(), gradientRect.top(), gradientRect.width(), gradientRect.height()); update(); } void KisScratchPad::fillBackground() { if(!m_paintLayer) return; KisPaintDeviceSP paintDevice = m_paintLayer->paintDevice(); paintDevice->setDefaultPixel(m_resourceProvider->bgColor()); paintDevice->clear(); update(); } void KisScratchPad::fillLayer() { // TODO } diff --git a/libs/widgetutils/KoResourcePaths.cpp b/libs/widgetutils/KoResourcePaths.cpp index 6e53a16c52..1ea1d30d27 100644 --- a/libs/widgetutils/KoResourcePaths.cpp +++ b/libs/widgetutils/KoResourcePaths.cpp @@ -1,564 +1,564 @@ /* * Copyright (c) 2015 Boudewijn Rempt * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #include "KoResourcePaths.h" #include #include #include #include #include #include #include #include #include #include #include "kis_debug.h" #include "WidgetUtilsDebug.h" Q_GLOBAL_STATIC(KoResourcePaths, s_instance); static QString cleanup(const QString &path) { return QDir::cleanPath(path); } static QStringList cleanup(const QStringList &pathList) { QStringList cleanedPathList; Q_FOREACH(const QString &path, pathList) { cleanedPathList << cleanup(path); } return cleanedPathList; } static QString cleanupDirs(const QString &path) { return QDir::cleanPath(path) + QDir::separator(); } static QStringList cleanupDirs(const QStringList &pathList) { QStringList cleanedPathList; Q_FOREACH(const QString &path, pathList) { cleanedPathList << cleanupDirs(path); } return cleanedPathList; } void appendResources(QStringList *dst, const QStringList &src, bool eliminateDuplicates) { Q_FOREACH (const QString &resource, src) { QString realPath = QDir::cleanPath(resource); if (!eliminateDuplicates || !dst->contains(realPath)) { *dst << realPath; } } } #ifdef Q_OS_WIN static const Qt::CaseSensitivity cs = Qt::CaseInsensitive; #else static const Qt::CaseSensitivity cs = Qt::CaseSensitive; #endif #ifdef Q_OS_OSX #include #include #include #endif QString getInstallationPrefix() { #ifdef Q_OS_OSX QString appPath = qApp->applicationDirPath(); debugWidgetUtils << "1" << appPath; appPath.chop(QString("MacOS/").length()); debugWidgetUtils << "2" << appPath; bool makeInstall = QDir(appPath + "/../../../share/kritaplugins").exists(); bool inBundle = QDir(appPath + "/Resources/kritaplugins").exists(); debugWidgetUtils << "3. After make install" << makeInstall; debugWidgetUtils << "4. In Bundle" << inBundle; QString bundlePath; if (inBundle) { bundlePath = appPath + "/"; } else if (makeInstall) { appPath.chop(QString("Contents/").length()); bundlePath = appPath + "/../../"; } else { qFatal("Cannot calculate the bundle path from the app path"); } debugWidgetUtils << ">>>>>>>>>>>" << bundlePath; return bundlePath; #else #ifdef Q_OS_QWIN QDir appdir(qApp->applicationDirPath()); // Corrects for mismatched case errors in path (qtdeclarative fails to load) wchar_t buffer[1024]; QString absolute = appdir.absolutePath(); DWORD rv = ::GetShortPathName((wchar_t*)absolute.utf16(), buffer, 1024); rv = ::GetLongPathName(buffer, buffer, 1024); QString correctedPath((QChar *)buffer); appdir.setPath(correctedPath); appdir.cdUp(); return appdir.canonicalPath(); #else return qApp->applicationDirPath() + "/../"; #endif #endif } class Q_DECL_HIDDEN KoResourcePaths::Private { public: QMap absolutes; // For each resource type, the list of absolute paths, from most local (most priority) to most global QMap relatives; // Same with relative paths QMutex relativesMutex; QMutex absolutesMutex; bool ready = false; // Paths have been initialized QStringList aliases(const QString &type) { QStringList r; QStringList a; relativesMutex.lock(); if (relatives.contains(type)) { r += relatives[type]; } relativesMutex.unlock(); debugWidgetUtils << "\trelatives" << r; absolutesMutex.lock(); if (absolutes.contains(type)) { a += absolutes[type]; } debugWidgetUtils << "\tabsolutes" << a; absolutesMutex.unlock(); return r + a; } QStandardPaths::StandardLocation mapTypeToQStandardPaths(const QString &type) { if (type == "tmp") { return QStandardPaths::TempLocation; } else if (type == "appdata") { return QStandardPaths::AppDataLocation; } else if (type == "data") { return QStandardPaths::AppDataLocation; } else if (type == "cache") { return QStandardPaths::CacheLocation; } else if (type == "locale") { return QStandardPaths::AppDataLocation; } else { return QStandardPaths::AppDataLocation; } } }; KoResourcePaths::KoResourcePaths() : d(new Private) { } KoResourcePaths::~KoResourcePaths() { } QString KoResourcePaths::getApplicationRoot() { return getInstallationPrefix(); } void KoResourcePaths::addResourceType(const char *type, const char *basetype, const QString &relativeName, bool priority) { s_instance->addResourceTypeInternal(QString::fromLatin1(type), QString::fromLatin1(basetype), relativeName, priority); } void KoResourcePaths::addResourceDir(const char *type, const QString &dir, bool priority) { s_instance->addResourceDirInternal(QString::fromLatin1(type), dir, priority); } QString KoResourcePaths::findResource(const char *type, const QString &fileName) { return cleanup(s_instance->findResourceInternal(QString::fromLatin1(type), fileName)); } QStringList KoResourcePaths::findDirs(const char *type) { return cleanupDirs(s_instance->findDirsInternal(QString::fromLatin1(type))); } QStringList KoResourcePaths::findAllResources(const char *type, const QString &filter, SearchOptions options) { return cleanup(s_instance->findAllResourcesInternal(QString::fromLatin1(type), filter, options)); } QStringList KoResourcePaths::resourceDirs(const char *type) { return cleanupDirs(s_instance->resourceDirsInternal(QString::fromLatin1(type))); } QString KoResourcePaths::saveLocation(const char *type, const QString &suffix, bool create) { return cleanupDirs(s_instance->saveLocationInternal(QString::fromLatin1(type), suffix, create)); } QString KoResourcePaths::locate(const char *type, const QString &filename) { return cleanup(s_instance->locateInternal(QString::fromLatin1(type), filename)); } QString KoResourcePaths::locateLocal(const char *type, const QString &filename, bool createDir) { return cleanup(s_instance->locateLocalInternal(QString::fromLatin1(type), filename, createDir)); } void KoResourcePaths::addResourceTypeInternal(const QString &type, const QString &basetype, const QString &relativename, bool priority) { Q_UNUSED(basetype); if (relativename.isEmpty()) return; QString copy = relativename; Q_ASSERT(basetype == "data"); if (!copy.endsWith(QLatin1Char('/'))) { copy += QLatin1Char('/'); } d->relativesMutex.lock(); QStringList &rels = d->relatives[type]; // find or insert if (!rels.contains(copy, cs)) { if (priority) { rels.prepend(copy); } else { rels.append(copy); } } d->relativesMutex.unlock(); debugWidgetUtils << "addResourceType: type" << type << "basetype" << basetype << "relativename" << relativename << "priority" << priority << d->relatives[type]; } void KoResourcePaths::addResourceDirInternal(const QString &type, const QString &absdir, bool priority) { if (absdir.isEmpty() || type.isEmpty()) return; // find or insert entry in the map QString copy = absdir; if (copy.at(copy.length() - 1) != QLatin1Char('/')) { copy += QLatin1Char('/'); } d->absolutesMutex.lock(); QStringList &paths = d->absolutes[type]; if (!paths.contains(copy, cs)) { if (priority) { paths.prepend(copy); } else { paths.append(copy); } } d->absolutesMutex.unlock(); debugWidgetUtils << "addResourceDir: type" << type << "absdir" << absdir << "priority" << priority << d->absolutes[type]; } QString KoResourcePaths::findResourceInternal(const QString &type, const QString &fileName) { QStringList aliases = d->aliases(type); debugWidgetUtils << "aliases" << aliases << getApplicationRoot(); QString resource = QStandardPaths::locate(QStandardPaths::AppDataLocation, fileName, QStandardPaths::LocateFile); if (resource.isEmpty()) { Q_FOREACH (const QString &alias, aliases) { resource = QStandardPaths::locate(d->mapTypeToQStandardPaths(type), alias + '/' + fileName, QStandardPaths::LocateFile); debugWidgetUtils << "\t1" << resource; if (QFile::exists(resource)) { continue; } } } if (resource.isEmpty() || !QFile::exists(resource)) { QString approot = getApplicationRoot(); Q_FOREACH (const QString &alias, aliases) { resource = approot + "/share/" + alias + '/' + fileName; debugWidgetUtils << "\t1" << resource; if (QFile::exists(resource)) { continue; } } } if (resource.isEmpty() || !QFile::exists(resource)) { QString approot = getApplicationRoot(); Q_FOREACH (const QString &alias, aliases) { resource = approot + "/share/krita/" + alias + '/' + fileName; debugWidgetUtils << "\t1" << resource; if (QFile::exists(resource)) { continue; } } } debugWidgetUtils << "findResource: type" << type << "filename" << fileName << "resource" << resource; Q_ASSERT(!resource.isEmpty()); return resource; } QStringList KoResourcePaths::findDirsInternal(const QString &type) { QStringList aliases = d->aliases(type); debugWidgetUtils << type << aliases << d->mapTypeToQStandardPaths(type); QStringList dirs; QStringList standardDirs = QStandardPaths::locateAll(d->mapTypeToQStandardPaths(type), "", QStandardPaths::LocateDirectory); appendResources(&dirs, standardDirs, true); Q_FOREACH (const QString &alias, aliases) { QStringList aliasDirs = QStandardPaths::locateAll(d->mapTypeToQStandardPaths(type), alias + '/', QStandardPaths::LocateDirectory); appendResources(&dirs, aliasDirs, true); #ifdef Q_OS_OSX debugWidgetUtils << "MAC:" << getApplicationRoot(); QStringList bundlePaths; bundlePaths << getApplicationRoot() + "/share/krita/" + alias; bundlePaths << getApplicationRoot() + "/../share/krita/" + alias; debugWidgetUtils << "bundlePaths" << bundlePaths; appendResources(&dirs, bundlePaths, true); Q_ASSERT(!dirs.isEmpty()); #endif QStringList fallbackPaths; fallbackPaths << getApplicationRoot() + "/share/" + alias; fallbackPaths << getApplicationRoot() + "/share/krita/" + alias; appendResources(&dirs, fallbackPaths, true); } debugWidgetUtils << "findDirs: type" << type << "resource" << dirs; return dirs; } QStringList filesInDir(const QString &startdir, const QString & filter, bool recursive) { debugWidgetUtils << "filesInDir: startdir" << startdir << "filter" << filter << "recursive" << recursive; QStringList result; // First the entries in this path QStringList nameFilters; nameFilters << filter; const QStringList fileNames = QDir(startdir).entryList(nameFilters, QDir::Files | QDir::CaseSensitive, QDir::Name); debugWidgetUtils << "\tFound:" << fileNames.size() << ":" << fileNames; Q_FOREACH (const QString &fileName, fileNames) { QString file = startdir + '/' + fileName; result << file; } // And then everything underneath, if recursive is specified if (recursive) { const QStringList entries = QDir(startdir).entryList(QDir::Dirs | QDir::NoDotAndDotDot); Q_FOREACH (const QString &subdir, entries) { debugWidgetUtils << "\tGoing to look in subdir" << subdir << "of" << startdir; result << filesInDir(startdir + '/' + subdir, filter, recursive); } } return result; } QStringList KoResourcePaths::findAllResourcesInternal(const QString &type, const QString &_filter, SearchOptions options) const { debugWidgetUtils << "====================================================="; debugWidgetUtils << type << _filter << QStandardPaths::standardLocations(d->mapTypeToQStandardPaths(type)); bool recursive = options & KoResourcePaths::Recursive; debugWidgetUtils << "findAllResources: type" << type << "filter" << _filter << "recursive" << recursive; QStringList aliases = d->aliases(type); QString filter = _filter; // In cases where the filter is like "color-schemes/*.colors" instead of "*.kpp", used with unregistered resource types if (filter.indexOf('*') > 0) { aliases << filter.split('*').first(); filter = '*' + filter.split('*')[1]; debugWidgetUtils << "Split up alias" << aliases << "filter" << filter; } QStringList resources; if (aliases.isEmpty()) { QStringList standardResources = QStandardPaths::locateAll(d->mapTypeToQStandardPaths(type), filter, QStandardPaths::LocateFile); appendResources(&resources, standardResources, true); } debugWidgetUtils << "\tresources from qstandardpaths:" << resources.size(); Q_FOREACH (const QString &alias, aliases) { debugWidgetUtils << "\t\talias:" << alias; QStringList dirs; QFileInfo dirInfo(alias); if (dirInfo.exists() && dirInfo.isDir() && dirInfo.isAbsolute()) { dirs << alias; } else { dirs << QStandardPaths::locateAll(d->mapTypeToQStandardPaths(type), alias, QStandardPaths::LocateDirectory) << getInstallationPrefix() + "share/" + alias + "/" << getInstallationPrefix() + "share/krita/" + alias + "/"; } Q_FOREACH (const QString &dir, dirs) { appendResources(&resources, filesInDir(dir, filter, recursive), true); } } debugWidgetUtils << "\tresources also from aliases:" << resources.size(); // if the original filter is "input/*", we only want share/input/* and share/krita/input/* here, but not // share/*. therefore, use _filter here instead of filter which was split into alias and "*". QFileInfo fi(_filter); QStringList prefixResources; prefixResources << filesInDir(getInstallationPrefix() + "share/" + fi.path(), fi.fileName(), false); prefixResources << filesInDir(getInstallationPrefix() + "share/krita/" + fi.path(), fi.fileName(), false); appendResources(&resources, prefixResources, true); debugWidgetUtils << "\tresources from installation:" << resources.size(); debugWidgetUtils << "====================================================="; return resources; } QStringList KoResourcePaths::resourceDirsInternal(const QString &type) { QStringList resourceDirs; QStringList aliases = d->aliases(type); Q_FOREACH (const QString &alias, aliases) { QStringList aliasDirs; aliasDirs << QStandardPaths::locateAll(d->mapTypeToQStandardPaths(type), alias, QStandardPaths::LocateDirectory); aliasDirs << getInstallationPrefix() + "share/" + alias + "/" << QStandardPaths::locateAll(d->mapTypeToQStandardPaths(type), alias, QStandardPaths::LocateDirectory); aliasDirs << getInstallationPrefix() + "share/krita/" + alias + "/" << QStandardPaths::locateAll(d->mapTypeToQStandardPaths(type), alias, QStandardPaths::LocateDirectory); appendResources(&resourceDirs, aliasDirs, true); } debugWidgetUtils << "resourceDirs: type" << type << resourceDirs; return resourceDirs; } QString KoResourcePaths::saveLocationInternal(const QString &type, const QString &suffix, bool create) { QStringList aliases = d->aliases(type); QString path; if (aliases.size() > 0) { path = QStandardPaths::writableLocation(d->mapTypeToQStandardPaths(type)) + '/' + aliases.first(); } else { path = QStandardPaths::writableLocation(d->mapTypeToQStandardPaths(type)); if (!path.endsWith("krita")) { path += "/krita"; } if (!suffix.isEmpty()) { path += "/" + suffix; } } QDir d(path); if (!d.exists() && create) { d.mkpath(path); } debugWidgetUtils << "saveLocation: type" << type << "suffix" << suffix << "create" << create << "path" << path; return path; } QString KoResourcePaths::locateInternal(const QString &type, const QString &filename) { QStringList aliases = d->aliases(type); QStringList locations; if (aliases.isEmpty()) { locations << QStandardPaths::locate(d->mapTypeToQStandardPaths(type), filename, QStandardPaths::LocateFile); } Q_FOREACH (const QString &alias, aliases) { locations << QStandardPaths::locate(d->mapTypeToQStandardPaths(type), (alias.endsWith('/') ? alias : alias + '/') + filename, QStandardPaths::LocateFile); } debugWidgetUtils << "locate: type" << type << "filename" << filename << "locations" << locations; if (locations.size() > 0) { return locations.first(); } else { return ""; } } QString KoResourcePaths::locateLocalInternal(const QString &type, const QString &filename, bool createDir) { QString path = saveLocationInternal(type, "", createDir); debugWidgetUtils << "locateLocal: type" << type << "filename" << filename << "CreateDir" << createDir << "path" << path; return path + '/' + filename; } void KoResourcePaths::setReady() { s_instance->d->ready = true; } -void KoResourcePaths::assertReady() +bool KoResourcePaths::isReady() { - KIS_ASSERT_X(s_instance->d->ready, "KoResourcePaths::assertReady", "Resource paths are not ready yet."); + return s_instance->d->ready; } diff --git a/libs/widgetutils/KoResourcePaths.h b/libs/widgetutils/KoResourcePaths.h index 0d9c5bb787..853ad89bbc 100644 --- a/libs/widgetutils/KoResourcePaths.h +++ b/libs/widgetutils/KoResourcePaths.h @@ -1,262 +1,263 @@ /* * Copyright (c) 2015 Boudewijn Rempt * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ #ifndef KORESOURCEPATHS_H #define KORESOURCEPATHS_H #include #include #include #include /** * DEBUGGING KoResourcePaths: * * The usual place to look for resources is Qt's AppDataLocation. * This corresponds to XDG_DATA_DIRS on Linux. To ensure your installation and * path are configured correctly, ensure your files are located in the directories * contained in this variable: * * QStandardPaths::standardLocations(QStandardPaths::AppDataLocation); * * There are many debug lines that can be uncommented for more specific installation * checks. In the future these should be converted to qloggingcategory to enable * convenient enable/disable functionality. */ class KRITAWIDGETUTILS_EXPORT KoResourcePaths { public: KoResourcePaths(); virtual ~KoResourcePaths(); enum SearchOption { NoSearchOptions = 0, Recursive = 1, IgnoreExecBit = 4 }; Q_DECLARE_FLAGS(SearchOptions, SearchOption) static QString getApplicationRoot(); /** * Adds suffixes for types. * * You may add as many as you need, but it is advised that there * is exactly one to make writing definite. * * The later a suffix is added, the higher its priority. Note, that the * suffix should end with / but doesn't have to start with one (as prefixes * should end with one). So adding a suffix for app_pics would look * like KoStandardPaths::addResourceType("app_pics", "data", "app/pics"); * * @param type Specifies a short descriptive string to access * files of this type. * @param basetype Specifies an already known type, or 0 if none * @param relativename Specifies a directory relative to the basetype * @param priority if true, the directory is added before any other, * otherwise after */ static void addResourceType(const char *type, const char *basetype, const QString &relativeName, bool priority = true); /** * Adds absolute path at the beginning of the search path for * particular types (for example in case of icons where * the user specifies extra paths). * * You shouldn't need this function in 99% of all cases besides * adding user-given paths. * * @param type Specifies a short descriptive string to access files * of this type. * @param absdir Points to directory where to look for this specific * type. Non-existent directories may be saved but pruned. * @param priority if true, the directory is added before any other, * otherwise after */ static void addResourceDir(const char *type, const QString &dir, bool priority = true); /** * Tries to find a resource in the following order: * @li All PREFIX/\ paths (most recent first). * @li All absolute paths (most recent first). * * The filename should be a filename relative to the base dir * for resources. So is a way to get the path to libkdecore.la * to findResource("lib", "libkdecore.la"). KStandardDirs will * then look into the subdir lib of all elements of all prefixes * ($KDEDIRS) for a file libkdecore.la and return the path to * the first one it finds (e.g. /opt/kde/lib/libkdecore.la). * * Example: * @code * QString iconfilename = KStandardPaths::findResource("icon",QString("oxygen/22x22/apps/ktip.png")); * @endcode * * @param type The type of the wanted resource * @param filename A relative filename of the resource. * * @return A full path to the filename specified in the second * argument, or QString() if not found. */ static QString findResource(const char *type, const QString &fileName); /** * Tries to find all directories whose names consist of the * specified type and a relative path. So * findDirs("xdgdata-apps", "Settings") would return * @li /home/joe/.local/share/applications/Settings/ * @li /usr/share/applications/Settings/ * * (from the most local to the most global) * * Note that it appends @c / to the end of the directories, * so you can use this right away as directory names. * * @param type The type of the base directory. * @param reldir Relative directory. * * @return A list of matching directories, or an empty * list if the resource specified is not found. */ static QStringList findDirs(const char *type); /** * Tries to find all resources with the specified type. * * The function will look into all specified directories * and return all filenames in these directories. * * The "most local" files are returned before the "more global" files. * * @param type The type of resource to locate directories for. * @param filter Only accept filenames that fit to filter. The filter * may consist of an optional directory and a QRegExp * wildcard expression. E.g. "images\*.jpg". * Use QString() if you do not want a filter. * @param options if the flags passed include Recursive, subdirectories * will also be search. * * @return List of all the files whose filename matches the * specified filter. */ static QStringList findAllResources(const char *type, const QString &filter = QString(), SearchOptions options = NoSearchOptions); /** * @param type The type of resource * @return The list of possible directories for the specified @p type. * The function updates the cache if possible. If the resource * type specified is unknown, it will return an empty list. * Note, that the directories are assured to exist beside the save * location, which may not exist, but is returned anyway. */ static QStringList resourceDirs(const char *type); /** * Finds a location to save files into for the given type * in the user's home directory. * * @param type The type of location to return. * @param suffix A subdirectory name. * Makes it easier for you to create subdirectories. * You can't pass filenames here, you _have_ to pass * directory names only and add possible filename in * that directory yourself. A directory name always has a * trailing slash ('/'). * @param create If set, saveLocation() will create the directories * needed (including those given by @p suffix). * * @return A path where resources of the specified type should be * saved, or QString() if the resource type is unknown. */ static QString saveLocation(const char *type, const QString &suffix = QString(), bool create = true); /** * This function is just for convenience. It simply calls * KoResourcePaths::findResource((type, filename). * * @param type The type of the wanted resource, see KStandardDirs * @param filename A relative filename of the resource * * @return A full path to the filename specified in the second * argument, or QString() if not found **/ static QString locate(const char *type, const QString &filename); /** * This function is much like locate. However it returns a * filename suitable for writing to. No check is made if the * specified @p filename actually exists. Missing directories * are created. If @p filename is only a directory, without a * specific file, @p filename must have a trailing slash. * * @param type The type of the wanted resource, see KStandardDirs * @param filename A relative filename of the resource * * @return A full path to the filename specified in the second * argument, or QString() if not found **/ static QString locateLocal(const char *type, const QString &filename, bool createDir = false); /** * Indicate that resource paths have been initialized and users * of this class may expect to load resources from the proper paths. */ static void setReady(); /** - * Assert that all resource paths have been initialized. + * Return if resource paths have been initialized and users + * of this class may expect to load resources from the proper paths. */ - static void assertReady(); + static bool isReady(); private: void addResourceTypeInternal(const QString &type, const QString &basetype, const QString &relativeName, bool priority); void addResourceDirInternal(const QString &type, const QString &absdir, bool priority); QString findResourceInternal(const QString &type, const QString &fileName); QStringList findDirsInternal(const QString &type); QStringList findAllResourcesInternal(const QString &type, const QString &filter = QString(), SearchOptions options = NoSearchOptions) const; QStringList resourceDirsInternal(const QString &type); QString saveLocationInternal(const QString &type, const QString &suffix = QString(), bool create = true); QString locateInternal(const QString &type, const QString &filename); QString locateLocalInternal(const QString &type, const QString &filename, bool createDir = false); class Private; QScopedPointer d; }; Q_DECLARE_OPERATORS_FOR_FLAGS(KoResourcePaths::SearchOptions) #endif // KORESOURCEPATHS_H diff --git a/libs/widgetutils/kis_icon_utils.cpp b/libs/widgetutils/kis_icon_utils.cpp index fb6dfb0e6b..f47ed236cf 100644 --- a/libs/widgetutils/kis_icon_utils.cpp +++ b/libs/widgetutils/kis_icon_utils.cpp @@ -1,202 +1,213 @@ /* * Copyright (c) 2015 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_icon_utils.h" #include #include #include #include #include #include #include #include #include - +#if QT_VERSION >= 0x050900 +#define CACHE_ICONS +#endif namespace KisIconUtils { +#if defined CACHE_ICONS static QMap s_cache; +#endif static QMap s_icons; QIcon loadIcon(const QString &name) { +#if defined CACHE_ICONS QMap::const_iterator cached = s_cache.constFind(name); if (cached != s_cache.constEnd()) { return cached.value(); } - +#endif // try load themed icon const char * const prefix = useDarkIcons() ? "dark_" : "light_"; QString realName = QLatin1String(prefix) + name; // Dark and light, no size specified const QStringList names = { ":/pics/" + realName + ".png", ":/pics/" + realName + ".svg", ":/pics/" + realName + ".svgz", ":/pics/" + name + ".png", ":/pics/" + name + ".svg", ":/pics/" + name + ".svz", ":/" + realName + ".png", ":/" + realName + ".svg", ":/" + realName + ".svz", ":/" + name, ":/" + name + ".png", ":/" + name + ".svg", ":/" + name + ".svgz"}; for (const QString &resname : names) { if (QFile(resname).exists()) { QIcon icon(resname); s_icons.insert(icon.cacheKey(), name); +#if defined CACHE_ICONS s_cache.insert(name, icon); +#endif return icon; } } // Now check for icons with sizes QStringList sizes = QStringList() << "16_" << "22_" << "32_" << "48_" << "64_" << "128_" << "256_" << "512_" << "1048_"; QVector > icons; Q_FOREACH (const QString &size, sizes) { const QStringList names = { ":/pics/" + size + realName + ".png", ":/pics/" + size + realName + ".svg", ":/pics/" + size + realName + ".svgz", ":/pics/" + size + name + ".png", ":/pics/" + size + name + ".svg", ":/pics/" + size + name + ".svz", ":/" + size + realName + ".png", ":/" + size + realName + ".svg", ":/" + size + realName + ".svz", ":/" + size + name, ":/" + size + name + ".png", ":/" + size + name + ".svg", ":/" + size + name + ".svgz"}; for (const QString &resname : names) { if (QFile(resname).exists()) { icons << qMakePair(size, resname); } } } if (!icons.isEmpty()) { QIcon icon; Q_FOREACH (auto p, icons) { QString sz = p.first; sz.chop(1); int size = sz.toInt(); icon.addFile(p.second, QSize(size, size)); } s_icons.insert(icon.cacheKey(), name); +#if defined CACHE_ICONS s_cache.insert(name, icon); +#endif return icon; } QIcon icon = QIcon::fromTheme(name); qWarning() << "\tfalling back on QIcon::FromTheme:" << name; s_icons.insert(icon.cacheKey(), name); +#if defined CACHE_ICONS s_cache.insert(name, icon); +#endif return icon; } bool useDarkIcons() { QColor background = qApp->palette().background().color(); return background.value() > 100; } bool adjustIcon(QIcon *icon) { bool result = false; QString iconName = icon->name(); if (iconName.isNull()) { if (s_icons.contains(icon->cacheKey())) { iconName = s_icons.take(icon->cacheKey()); } } QString realIconName = iconName; if (iconName.startsWith("dark_")) { realIconName = iconName.mid(5); } if (iconName.startsWith("light_")) { realIconName = iconName.mid(6); } if (!realIconName.isNull()) { *icon = loadIcon(realIconName); result = !icon->isNull(); s_icons.insert(icon->cacheKey(), iconName); } return result; } void updateIconCommon(QObject *object) { QAbstractButton* button = qobject_cast(object); if (button) { updateIcon(button); } QComboBox* comboBox = qobject_cast(object); if (comboBox) { updateIcon(comboBox); } QAction* action = qobject_cast(object); if (action) { updateIcon(action); } } void updateIcon(QAbstractButton *button) { QIcon icon = button->icon(); if (adjustIcon(&icon)) { button->setIcon(icon); } } void updateIcon(QComboBox *comboBox) { for (int i = 0; i < comboBox->count(); i++) { QIcon icon = comboBox->itemIcon(i); if (adjustIcon(&icon)) { comboBox->setItemIcon(i, icon); } } } void updateIcon(QAction *action) { QIcon icon = action->icon(); if (adjustIcon(&icon)) { action->setIcon(icon); } } } diff --git a/plugins/color/lcms2engine/LcmsEnginePlugin.cpp b/plugins/color/lcms2engine/LcmsEnginePlugin.cpp index c3d5364939..9078645234 100644 --- a/plugins/color/lcms2engine/LcmsEnginePlugin.cpp +++ b/plugins/color/lcms2engine/LcmsEnginePlugin.cpp @@ -1,297 +1,303 @@ /* * Copyright (c) 2003 Patrick Julien * Copyright (c) 2004,2010 Cyrille Berger * Copyright (c) 2011 Srikanth Tiyyagura * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "LcmsEnginePlugin.h" #include #include #include #include #include #include #include +#include #include "kis_assert.h" #include #include #include #include #include "IccColorSpaceEngine.h" #include "colorprofiles/LcmsColorProfileContainer.h" #include "colorspaces/cmyk_u8/CmykU8ColorSpace.h" #include "colorspaces/cmyk_u16/CmykU16ColorSpace.h" #include "colorspaces/cmyk_f32/CmykF32ColorSpace.h" #include "colorspaces/gray_u8/GrayU8ColorSpace.h" #include "colorspaces/gray_u16/GrayU16ColorSpace.h" #include "colorspaces/gray_f32/GrayF32ColorSpace.h" #include "colorspaces/lab_u8/LabU8ColorSpace.h" #include "colorspaces/lab_u16/LabColorSpace.h" #include "colorspaces/lab_f32/LabF32ColorSpace.h" #include "colorspaces/xyz_u8/XyzU8ColorSpace.h" #include "colorspaces/xyz_u16/XyzU16ColorSpace.h" #include "colorspaces/xyz_f32/XyzF32ColorSpace.h" #include "colorspaces/rgb_u8/RgbU8ColorSpace.h" #include "colorspaces/rgb_u16/RgbU16ColorSpace.h" #include "colorspaces/rgb_f32/RgbF32ColorSpace.h" #include "colorspaces/ycbcr_u8/YCbCrU8ColorSpace.h" #include "colorspaces/ycbcr_u16/YCbCrU16ColorSpace.h" #include "colorspaces/ycbcr_f32/YCbCrF32ColorSpace.h" #include #ifdef HAVE_OPENEXR #include #ifdef HAVE_LCMS24 #include "colorspaces/gray_f16/GrayF16ColorSpace.h" #include "colorspaces/xyz_f16/XyzF16ColorSpace.h" #include "colorspaces/rgb_f16/RgbF16ColorSpace.h" #endif #endif void lcms2LogErrorHandlerFunction(cmsContext /*ContextID*/, cmsUInt32Number ErrorCode, const char *Text) { qCritical() << "Lcms2 error: " << ErrorCode << Text; } K_PLUGIN_FACTORY_WITH_JSON(PluginFactory, "kolcmsengine.json", registerPlugin();) LcmsEnginePlugin::LcmsEnginePlugin(QObject *parent, const QVariantList &) : QObject(parent) { // We need all resource paths to be properly initialized via KisApplication, otherwise we will // initialize this instance with lacking color profiles which will cause lookup errors later on. - KoResourcePaths::assertReady(); + + KIS_ASSERT_X(KoResourcePaths::isReady() || + (QApplication::instance()->applicationName() != "krita" && + QApplication::instance()->applicationName() != "krita.exe"), + "LcmsEnginePlugin::LcmsEnginePlugin", "Resource paths are not ready yet."); + // Set the lmcs error reporting function cmsSetLogErrorHandler(&lcms2LogErrorHandlerFunction); KoColorSpaceRegistry *registry = KoColorSpaceRegistry::instance(); // Initialise color engine KoColorSpaceEngineRegistry::instance()->add(new IccColorSpaceEngine); QStringList profileFilenames; profileFilenames += KoResourcePaths::findAllResources("icc_profiles", "*.icm", KoResourcePaths::Recursive); profileFilenames += KoResourcePaths::findAllResources("icc_profiles", "*.ICM", KoResourcePaths::Recursive); profileFilenames += KoResourcePaths::findAllResources("icc_profiles", "*.ICC", KoResourcePaths::Recursive); profileFilenames += KoResourcePaths::findAllResources("icc_profiles", "*.icc", KoResourcePaths::Recursive); // Load the profiles if (!profileFilenames.empty()) { KoColorProfile *profile = 0; for (QStringList::Iterator it = profileFilenames.begin(); it != profileFilenames.end(); ++it) { profile = new IccColorProfile(*it); Q_CHECK_PTR(profile); profile->load(); if (profile->valid()) { //qDebug() << "Valid profile : " << profile->fileName() << profile->name(); registry->addProfileToMap(profile); } else { qDebug() << "Invalid profile : " << profile->fileName() << profile->name(); delete profile; } } } // ------------------- LAB --------------------------------- KoColorProfile *labProfile = LcmsColorProfileContainer::createFromLcmsProfile(cmsCreateLab2Profile(0)); registry->addProfile(labProfile); registry->add(new LabU8ColorSpaceFactory()); registry->add(new LabU16ColorSpaceFactory()); registry->add(new LabF32ColorSpaceFactory()); KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("LABAU8HISTO", i18n("L*a*b* Histogram")), LABAColorModelID.id(), Integer8BitsColorDepthID.id())); KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("LABAU16HISTO", i18n("L*a*b* Histogram")), LABAColorModelID.id(), Integer16BitsColorDepthID.id())); KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("LABAF32HISTO", i18n("L*a*b* Histogram")), LABAColorModelID.id(), Float32BitsColorDepthID.id())); // ------------------- RGB --------------------------------- KoColorProfile *rgbProfile = LcmsColorProfileContainer::createFromLcmsProfile(cmsCreate_sRGBProfile()); registry->addProfile(rgbProfile); registry->add(new RgbU8ColorSpaceFactory()); registry->add(new RgbU16ColorSpaceFactory()); #ifdef HAVE_LCMS24 #ifdef HAVE_OPENEXR registry->add(new RgbF16ColorSpaceFactory()); #endif #endif registry->add(new RgbF32ColorSpaceFactory()); KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("RGBU8HISTO", i18n("RGB8 Histogram")), RGBAColorModelID.id(), Integer8BitsColorDepthID.id())); KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("RGBU16HISTO", i18n("RGB16 Histogram")), RGBAColorModelID.id(), Integer16BitsColorDepthID.id())); #ifdef HAVE_LCMS24 #ifdef HAVE_OPENEXR KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("RGBF16HISTO", i18n("RGBF16 Histogram")), RGBAColorModelID.id(), Float16BitsColorDepthID.id())); #endif #endif KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("RGF328HISTO", i18n("RGBF32 Histogram")), RGBAColorModelID.id(), Float32BitsColorDepthID.id())); // ------------------- GRAY --------------------------------- cmsToneCurve *Gamma = cmsBuildGamma(0, 2.2); cmsHPROFILE hProfile = cmsCreateGrayProfile(cmsD50_xyY(), Gamma); cmsFreeToneCurve(Gamma); KoColorProfile *defProfile = LcmsColorProfileContainer::createFromLcmsProfile(hProfile); registry->addProfile(defProfile); registry->add(new GrayAU8ColorSpaceFactory()); registry->add(new GrayAU16ColorSpaceFactory()); #ifdef HAVE_LCMS24 #ifdef HAVE_OPENEXR registry->add(new GrayF16ColorSpaceFactory()); #endif #endif registry->add(new GrayF32ColorSpaceFactory()); KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("GRAYA8HISTO", i18n("GRAY/Alpha8 Histogram")), GrayAColorModelID.id(), Integer8BitsColorDepthID.id())); KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("GRAYA16HISTO", i18n("GRAY/Alpha16 Histogram")), GrayAColorModelID.id(), Integer16BitsColorDepthID.id())); #ifdef HAVE_LCMS24 #ifdef HAVE_OPENEXR KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("GRAYF16HISTO", i18n("GRAYF16 Histogram")), GrayAColorModelID.id(), Float16BitsColorDepthID.id())); #endif #endif KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("GRAYAF32HISTO", i18n("GRAY/Alpha 32 float Histogram")), GrayAColorModelID.id(), Float32BitsColorDepthID.id())); // ------------------- CMYK --------------------------------- registry->add(new CmykU8ColorSpaceFactory()); registry->add(new CmykU16ColorSpaceFactory()); registry->add(new CmykF32ColorSpaceFactory()); KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("CMYK8HISTO", i18n("CMYK8 Histogram")), CMYKAColorModelID.id(), Integer8BitsColorDepthID.id())); KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("CMYK16HISTO", i18n("CMYK16 Histogram")), CMYKAColorModelID.id(), Integer16BitsColorDepthID.id())); KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("CMYKF32HISTO", i18n("CMYK F32 Histogram")), CMYKAColorModelID.id(), Float32BitsColorDepthID.id())); // ------------------- XYZ --------------------------------- KoColorProfile *xyzProfile = LcmsColorProfileContainer::createFromLcmsProfile(cmsCreateXYZProfile()); registry->addProfile(xyzProfile); registry->add(new XyzU8ColorSpaceFactory()); registry->add(new XyzU16ColorSpaceFactory()); #ifdef HAVE_LCMS24 #ifdef HAVE_OPENEXR registry->add(new XyzF16ColorSpaceFactory()); #endif #endif registry->add(new XyzF32ColorSpaceFactory()); KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("XYZ8HISTO", i18n("XYZ8 Histogram")), XYZAColorModelID.id(), Integer8BitsColorDepthID.id())); KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("XYZ16HISTO", i18n("XYZ16 Histogram")), XYZAColorModelID.id(), Integer16BitsColorDepthID.id())); #ifdef HAVE_LCMS24 #ifdef HAVE_OPENEXR KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("XYZF16HISTO", i18n("XYZF16 Histogram")), XYZAColorModelID.id(), Float16BitsColorDepthID.id())); #endif #endif KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("XYZF32HISTO", i18n("XYZF32 Histogram")), XYZAColorModelID.id(), Float32BitsColorDepthID.id())); // ------------------- YCBCR --------------------------------- // KoColorProfile *yCbCrProfile = LcmsColorProfileContainer::createFromLcmsProfile(cmsCreateYCBCRProfile()); // registry->addProfile(yCbCrProfile); registry->add(new YCbCrU8ColorSpaceFactory()); registry->add(new YCbCrU16ColorSpaceFactory()); registry->add(new YCbCrF32ColorSpaceFactory()); KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("YCBCR8HISTO", i18n("YCBCR8 Histogram")), YCbCrAColorModelID.id(), Integer8BitsColorDepthID.id())); KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("YCBCR16HISTO", i18n("YCBCR16 Histogram")), YCbCrAColorModelID.id(), Integer16BitsColorDepthID.id())); KoHistogramProducerFactoryRegistry::instance()->add( new KoBasicHistogramProducerFactory (KoID("YCBCRF32HISTO", i18n("YCBCRF32 Histogram")), YCbCrAColorModelID.id(), Float32BitsColorDepthID.id())); // Add profile alias for default profile from lcms1 registry->addProfileAlias("sRGB built-in - (lcms internal)", "sRGB built-in"); registry->addProfileAlias("gray built-in - (lcms internal)", "gray built-in"); registry->addProfileAlias("Lab identity built-in - (lcms internal)", "Lab identity built-in"); registry->addProfileAlias("XYZ built-in - (lcms internal)", "XYZ identity built-in"); } #include diff --git a/plugins/dockers/advancedcolorselector/wdg_color_selector_settings.ui b/plugins/dockers/advancedcolorselector/wdg_color_selector_settings.ui index b63d5ed75c..2eff486a4d 100644 --- a/plugins/dockers/advancedcolorselector/wdg_color_selector_settings.ui +++ b/plugins/dockers/advancedcolorselector/wdg_color_selector_settings.ui @@ -1,1557 +1,1557 @@ KisColorSelectorSettings 0 0 612 973 0 0 Color Selector Settings Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop 6 20 Docker: 0 0 0 30 0 0 0 280 - 4 + 0 Color Selector 0 0 Qt::Vertical QSizePolicy::MinimumExpanding 20 5 6 12 6 6 0 0 Qt::LeftToRight Color &Model Type: Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter colorSelectorConfiguration 0 0 0 0 Type Description goes here Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true 0 0 Luma Coefficients 0 0 6 0 0 Red': Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 50 0 4 0 0 Green': 0 0 Blue': 0 0 50 0 4 0 0 50 0 4 <html><head/><body><p>This sets the gamma value that the linearised HSY Luminosity is crunched with. 1 makes the selector fully linear, 2.2 is a practical default value.</p></body></html> 1 -3.000000000000000 3.000000000000000 0.100000000000000 2.200000000000000 Gamma: 0 0 Color Selector Uses Different Color Space than Ima&ge true false 0 0 Qt::Vertical QSizePolicy::MinimumExpanding 20 5 Behavior 0 0 Qt::LeftToRight When Docker Resizes: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Show Zoom Selector UI: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Zoom Selector Size: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 0 0 px 100 1000 10 260 true 0 0 Hide Popup on click. Qt::Vertical QSizePolicy::MinimumExpanding 20 228 Shade Selector Type: Color model: Qt::Horizontal 20 20 0 0 0 Update Selector When: Right clicking on shade selector Left clicking on shade selector this doesn't include a color change by the shade selector Foreground color changes false this doesn't include a color change by the shade selector Background color change true 0 0 Minimal Shade Selector 0 0 Display: &Gradient Colo&r Patches 0 0 Qt::Horizontal 59 20 Line Count: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 1 10 3 Line Height: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter px 8 99 16 Patches Per Line: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter Qt::Vertical QSizePolicy::MinimumExpanding 20 10 true 0 0 Color History 0 0 Show Color Histor&y true Patch Options Height: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter px 16 Width: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter px 16 Max Patches: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 1 30 Qt::Horizontal 40 20 Layout 0 &Vertical true Colu&mns: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter lastUsedColorsNumCols 1 20 2 Hori&zontal &Rows: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter lastUsedColorsNumRows 1 20 3 Allow scrolling true Qt::Vertical QSizePolicy::MinimumExpanding 20 5 Colors from Image true Show Colors from the ima&ge true Patch Options Height: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter px 16 Width: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter px 16 Max Patches: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 1 30 Qt::Horizontal 40 20 Layout 6 0 &Vertical Colu&mns: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter commonColorsNumCols 1 20 2 Hori&zontal true &Rows: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter commonColorsNumRows 1 20 3 this can be slow on big images Update after every stroke Allow scrolling Qt::Vertical QSizePolicy::MinimumExpanding 20 10 HSV Sliders to Show Hue Saturation Value HSL Sliders to Show Hue Saturation Lightness HSI Sliders to Show Hue Saturation Intensity HSY' Sliders to Show Hue Saturation Luma Lightness, Saturation and Hue hotkey steps Lightness: steps Qt::Horizontal 40 20 Saturation: steps Qt::Horizontal 40 20 Hue: steps YUV Redder/Greener/Bluer/Yellower hotkey steps Redder/Greener: steps Qt::Horizontal 40 20 Bluer/Yellower: steps Qt::Vertical 20 10 KisColorSpaceSelector QWidget
widgets/kis_color_space_selector.h
1
+ + KisIntParseSpinBox + QSpinBox +
kis_int_parse_spin_box.h
+
KisColorSelectorComboBox QComboBox
kis_color_selector_combo_box.h
KisShadeSelectorLinesSettings QComboBox
kis_shade_selector_lines_settings.h
- - KisIntParseSpinBox - QSpinBox -
kis_int_parse_spin_box.h
-
KisDoubleParseSpinBox QDoubleSpinBox
kis_double_parse_spin_box.h
minimalShadeSelectorAsColorPatches toggled(bool) minimalShadeSelectorPatchesPerLine setEnabled(bool) 194 393 520 463
diff --git a/plugins/dockers/throttle/Throttle.cpp b/plugins/dockers/throttle/Throttle.cpp index e8b8d08874..6ddf806a09 100644 --- a/plugins/dockers/throttle/Throttle.cpp +++ b/plugins/dockers/throttle/Throttle.cpp @@ -1,82 +1,95 @@ /* * Copyright (c) 2017 Boudewijn Rempt * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "Throttle.h" #include #include #include #include #include #include #include #include #include #include #include #include #include +#include "kis_signal_compressor.h" + +#include "KisUpdateSchedulerConfigNotifier.h" + ThreadManager::ThreadManager(QObject *parent) - : QObject(parent) -{} + : QObject(parent), + m_configUpdateCompressor(new KisSignalCompressor(500, KisSignalCompressor::POSTPONE, this)) +{ + connect(m_configUpdateCompressor, SIGNAL(timeout()), SLOT(slotDoUpdateConfig())); +} ThreadManager::~ThreadManager() { } void ThreadManager::setThreadCount(int threadCount) { - threadCount = qMin(maxThreadCount(), int(qreal(threadCount) * (maxThreadCount() / 100.0))); + threadCount = 1 + qreal(threadCount) * (maxThreadCount() - 1) / 100.0; if (m_threadCount != threadCount) { m_threadCount = threadCount; + m_configUpdateCompressor->start(); emit threadCountChanged(); - KisImageConfig().setMaxNumberOfThreads(m_threadCount); - KisImageConfig().setFrameRenderingClones(qCeil(m_threadCount * 0.5)); - // XXX: also set for the brush threads } } int ThreadManager::threadCount() const { return m_threadCount; } int ThreadManager::maxThreadCount() const { return QThread::idealThreadCount(); } +void ThreadManager::slotDoUpdateConfig() +{ + KisImageConfig cfg; + cfg.setMaxNumberOfThreads(m_threadCount); + cfg.setFrameRenderingClones(qCeil(m_threadCount * 0.5)); + KisUpdateSchedulerConfigNotifier::instance()->notifyConfigChanged(); +} + Throttle::Throttle(QWidget *parent) : QQuickWidget(parent) { m_threadManager = new ThreadManager(); // In % of available cores... engine()->rootContext()->setContextProperty("ThreadManager", m_threadManager); m_threadManager->setThreadCount(100); setSource(QUrl("qrc:/slider.qml")); setResizeMode(SizeRootObjectToView); } Throttle::~Throttle() { setSource(QUrl()); } diff --git a/plugins/dockers/throttle/Throttle.h b/plugins/dockers/throttle/Throttle.h index b23db516d0..84e7a0a9b9 100644 --- a/plugins/dockers/throttle/Throttle.h +++ b/plugins/dockers/throttle/Throttle.h @@ -1,55 +1,61 @@ /* * Copyright (c) 2017 Boudewijn Rempt * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef THROTTLE_H #define THROTTLE_H #include + class KisCanvas2; +class KisSignalCompressor; class ThreadManager : public QObject { Q_OBJECT Q_PROPERTY(int threadCount READ threadCount WRITE setThreadCount NOTIFY threadCountChanged) Q_PROPERTY(int maxThreadCount READ maxThreadCount) public: ThreadManager(QObject *parent = 0); ~ThreadManager() override; void setThreadCount(int threadCount); int threadCount() const; int maxThreadCount() const; +private Q_SLOTS: + void slotDoUpdateConfig(); + Q_SIGNALS: void threadCountChanged(); private: - int m_threadCount {0}; + int m_threadCount = 0; + KisSignalCompressor *m_configUpdateCompressor; }; class Throttle : public QQuickWidget { Q_OBJECT public: Throttle(QWidget *parent); ~Throttle() override; private: ThreadManager *m_threadManager {0}; }; #endif diff --git a/plugins/filters/edgedetection/kis_edge_detection_filter.cpp b/plugins/filters/edgedetection/kis_edge_detection_filter.cpp index 79b594a016..5775d1d166 100644 --- a/plugins/filters/edgedetection/kis_edge_detection_filter.cpp +++ b/plugins/filters/edgedetection/kis_edge_detection_filter.cpp @@ -1,157 +1,158 @@ /* * Copyright (c) 2017 Wolthera van Hövell tot Westerflier * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_edge_detection_filter.h" #include "kis_wdg_edge_detection.h" #include #include #include #include #include #include #include #include #include #include "kis_lod_transform.h" #include #include #include K_PLUGIN_FACTORY_WITH_JSON(KritaEdgeDetectionFilterFactory, "kritaedgedetection.json", registerPlugin();) KritaEdgeDetectionFilter::KritaEdgeDetectionFilter(QObject *parent, const QVariantList &) : QObject(parent) { KisFilterRegistry::instance()->add(KisFilterSP(new KisEdgeDetectionFilter())); } KritaEdgeDetectionFilter::~KritaEdgeDetectionFilter() { } KisEdgeDetectionFilter::KisEdgeDetectionFilter(): KisFilter(id(), categoryEdgeDetection(), i18n("&Edge Detection...")) { setSupportsPainting(true); setSupportsAdjustmentLayers(true); setSupportsLevelOfDetail(true); setColorSpaceIndependence(FULLY_INDEPENDENT); setShowConfigurationWidget(true); } void KisEdgeDetectionFilter::processImpl(KisPaintDeviceSP device, const QRect &rect, const KisFilterConfigurationSP config, KoUpdater *progressUpdater) const { Q_ASSERT(device != 0); KisFilterConfigurationSP configuration = config ? config : new KisFilterConfiguration(id().id(), 1); KisLodTransformScalar t(device); QVariant value; configuration->getProperty("horizRadius", value); float horizontalRadius = t.scale(value.toFloat()); configuration->getProperty("vertRadius", value); float verticalRadius = t.scale(value.toFloat()); QBitArray channelFlags; if (configuration) { channelFlags = configuration->channelFlags(); } if (channelFlags.isEmpty() || !configuration) { channelFlags = device->colorSpace()->channelFlags(); } KisEdgeDetectionKernel::FilterType type = KisEdgeDetectionKernel::SobolVector; if (config->getString("type") == "prewitt") { type = KisEdgeDetectionKernel::Prewit; } else if (config->getString("type") == "simple") { type = KisEdgeDetectionKernel::Simple; } KisEdgeDetectionKernel::FilterOutput output = KisEdgeDetectionKernel::pythagorean; if (config->getString("output") == "xGrowth") { output = KisEdgeDetectionKernel::xGrowth; } else if (config->getString("output") == "xFall") { output = KisEdgeDetectionKernel::xFall; } else if (config->getString("output") == "yGrowth") { output = KisEdgeDetectionKernel::yGrowth; } else if (config->getString("output") == "yFall") { output = KisEdgeDetectionKernel::yFall; } else if (config->getString("output") == "radian") { output = KisEdgeDetectionKernel::radian; } KisEdgeDetectionKernel::applyEdgeDetection(device, rect, horizontalRadius, verticalRadius, type, channelFlags, progressUpdater, output, config->getBool("transparency", false)); } KisFilterConfigurationSP KisEdgeDetectionFilter::factoryConfiguration() const { KisFilterConfigurationSP config = new KisFilterConfiguration(id().id(), 1); config->setProperty("horizRadius", 1); config->setProperty("vertRadius", 1); config->setProperty("type", "prewitt"); config->setProperty("output", "pythagorean"); config->setProperty("lockAspect", true); config->setProperty("transparency", false); return config; } KisConfigWidget *KisEdgeDetectionFilter::createConfigurationWidget(QWidget *parent, const KisPaintDeviceSP dev) const { + Q_UNUSED(dev); return new KisWdgEdgeDetection(parent); } QRect KisEdgeDetectionFilter::neededRect(const QRect &rect, const KisFilterConfigurationSP _config, int lod) const { KisLodTransformScalar t(lod); QVariant value; /** * NOTE: integer devision by two is done on purpose, * because the kernel size is always odd */ const int halfWidth = _config->getProperty("horizRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; const int halfHeight = _config->getProperty("vertRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; return rect.adjusted(-halfWidth * 2, -halfHeight * 2, halfWidth * 2, halfHeight * 2); } QRect KisEdgeDetectionFilter::changedRect(const QRect &rect, const KisFilterConfigurationSP _config, int lod) const { KisLodTransformScalar t(lod); QVariant value; const int halfWidth = _config->getProperty("horizRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; const int halfHeight = _config->getProperty("vertRadius", value) ? KisEdgeDetectionKernel::kernelSizeFromRadius(t.scale(value.toFloat())) / 2 : 5; return rect.adjusted( -halfWidth, -halfHeight, halfWidth, halfHeight); } #include "kis_edge_detection_filter.moc" diff --git a/plugins/paintops/defaultpaintops/CMakeLists.txt b/plugins/paintops/defaultpaintops/CMakeLists.txt index 0e06523a94..68ac300940 100644 --- a/plugins/paintops/defaultpaintops/CMakeLists.txt +++ b/plugins/paintops/defaultpaintops/CMakeLists.txt @@ -1,28 +1,35 @@ add_subdirectory(brush/tests) -include_directories(brush) -include_directories(duplicate) - +include_directories(brush + duplicate + ${CMAKE_CURRENT_BINARY_DIR}) set(kritadefaultpaintops_SOURCES defaultpaintops_plugin.cc brush/kis_brushop.cpp + brush/KisBrushOpResources.cpp + brush/KisBrushOpSettings.cpp brush/kis_brushop_settings_widget.cpp - duplicate/kis_duplicateop.cpp + brush/KisDabRenderingQueue.cpp + brush/KisDabRenderingQueueCache.cpp + brush/KisDabRenderingJob.cpp + brush/KisDabRenderingExecutor.cpp + duplicate/kis_duplicateop.cpp duplicate/kis_duplicateop_settings.cpp duplicate/kis_duplicateop_settings_widget.cpp duplicate/kis_duplicateop_option.cpp ) ki18n_wrap_ui(kritadefaultpaintops_SOURCES duplicate/wdgduplicateop.ui ) add_library(kritadefaultpaintops MODULE ${kritadefaultpaintops_SOURCES}) +generate_export_header(kritadefaultpaintops BASE_NAME kritadefaultpaintops EXPORT_MACRO_NAME KRITADEFAULTPAINTOPS_EXPORT) target_link_libraries(kritadefaultpaintops kritalibpaintop) install(TARGETS kritadefaultpaintops DESTINATION ${KRITA_PLUGIN_INSTALL_DIR}) install( FILES krita-paintbrush.png krita-eraser.png krita-duplicate.png DESTINATION ${DATA_INSTALL_DIR}/krita/images) diff --git a/plugins/paintops/defaultpaintops/brush/KisBrushOpResources.cpp b/plugins/paintops/defaultpaintops/brush/KisBrushOpResources.cpp new file mode 100644 index 0000000000..5da1c8ea48 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisBrushOpResources.cpp @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisBrushOpResources.h" + +#include +#include + +#include "kis_color_source.h" +#include "kis_pressure_mix_option.h" +#include "kis_pressure_darken_option.h" +#include "kis_pressure_hsv_option.h" +#include "kis_color_source_option.h" +#include "kis_pressure_sharpness_option.h" +#include "kis_texture_option.h" +#include "kis_painter.h" +#include "kis_paintop_settings.h" + +struct KisBrushOpResources::Private +{ + QList hsvOptions; + KoColorTransformation *hsvTransformation = 0; + KisPressureMixOption mixOption; + KisPressureDarkenOption darkenOption; +}; + + +KisBrushOpResources::KisBrushOpResources(const KisPaintOpSettingsSP settings, KisPainter *painter) + : m_d(new Private) +{ + KisColorSourceOption colorSourceOption; + colorSourceOption.readOptionSetting(settings); + colorSource.reset(colorSourceOption.createColorSource(painter)); + + sharpnessOption.reset(new KisPressureSharpnessOption()); + sharpnessOption->readOptionSetting(settings); + sharpnessOption->resetAllSensors(); + + textureOption.reset(new KisTextureProperties(painter->device()->defaultBounds()->currentLevelOfDetail())); + textureOption->fillProperties(settings); + + m_d->hsvOptions.append(KisPressureHSVOption::createHueOption()); + m_d->hsvOptions.append(KisPressureHSVOption::createSaturationOption()); + m_d->hsvOptions.append(KisPressureHSVOption::createValueOption()); + + Q_FOREACH (KisPressureHSVOption * option, m_d->hsvOptions) { + option->readOptionSetting(settings); + option->resetAllSensors(); + if (option->isChecked() && !m_d->hsvTransformation) { + m_d->hsvTransformation = painter->backgroundColor().colorSpace()->createColorTransformation("hsv_adjustment", QHash()); + } + } + + m_d->darkenOption.readOptionSetting(settings); + m_d->mixOption.readOptionSetting(settings); + + m_d->darkenOption.resetAllSensors(); + m_d->mixOption.resetAllSensors(); +} + +KisBrushOpResources::~KisBrushOpResources() +{ + qDeleteAll(m_d->hsvOptions); + delete m_d->hsvTransformation; +} + +void KisBrushOpResources::syncResourcesToSeqNo(int seqNo, const KisPaintInformation &info) +{ + colorSource->selectColor(m_d->mixOption.apply(info), info); + m_d->darkenOption.apply(colorSource.data(), info); + + if (m_d->hsvTransformation) { + Q_FOREACH (KisPressureHSVOption * option, m_d->hsvOptions) { + option->apply(m_d->hsvTransformation, info); + } + colorSource->applyColorTransformation(m_d->hsvTransformation); + } + + KisDabCacheUtils::DabRenderingResources::syncResourcesToSeqNo(seqNo, info); +} diff --git a/libs/ui/opengl/kis_opengl_canvas_debugger.h b/plugins/paintops/defaultpaintops/brush/KisBrushOpResources.h similarity index 62% copy from libs/ui/opengl/kis_opengl_canvas_debugger.h copy to plugins/paintops/defaultpaintops/brush/KisBrushOpResources.h index 8d7e605e96..dcd610bf0e 100644 --- a/libs/ui/opengl/kis_opengl_canvas_debugger.h +++ b/plugins/paintops/defaultpaintops/brush/KisBrushOpResources.h @@ -1,45 +1,43 @@ /* - * Copyright (c) 2015 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_OPENGL_CANVAS_DEBUGGER_H -#define __KIS_OPENGL_CANVAS_DEBUGGER_H +#ifndef KISBRUSHOPRESOURCES_H +#define KISBRUSHOPRESOURCES_H + +#include "KisDabCacheUtils.h" #include +class KisPainter; +class KisPaintInformation; -class KisOpenglCanvasDebugger +class KisBrushOpResources : public KisDabCacheUtils::DabRenderingResources { public: - KisOpenglCanvasDebugger(); - ~KisOpenglCanvasDebugger(); - - static KisOpenglCanvasDebugger* instance(); - - bool showFpsOnCanvas() const; + KisBrushOpResources(const KisPaintOpSettingsSP settings, KisPainter *painter); + ~KisBrushOpResources() override; - void nofityPaintRequested(); - void nofitySyncStatus(bool value); - qreal accumulatedFps(); + void syncResourcesToSeqNo(int seqNo, const KisPaintInformation &info) override; private: struct Private; const QScopedPointer m_d; }; -#endif /* __KIS_OPENGL_CANVAS_DEBUGGER_H */ +#endif // KISBRUSHOPRESOURCES_H diff --git a/libs/image/kis_projection_updates_filter.cpp b/plugins/paintops/defaultpaintops/brush/KisBrushOpSettings.cpp similarity index 64% copy from libs/image/kis_projection_updates_filter.cpp copy to plugins/paintops/defaultpaintops/brush/KisBrushOpSettings.cpp index e1493ec367..f7573c09a5 100644 --- a/libs/image/kis_projection_updates_filter.cpp +++ b/plugins/paintops/defaultpaintops/brush/KisBrushOpSettings.cpp @@ -1,36 +1,25 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#include "KisBrushOpSettings.h" -#include -#include - -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() -{ -} - -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) +bool KisBrushOpSettings::needsAsynchronousUpdates() const { - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); return true; } diff --git a/libs/image/kis_projection_updates_filter.cpp b/plugins/paintops/defaultpaintops/brush/KisBrushOpSettings.h similarity index 62% copy from libs/image/kis_projection_updates_filter.cpp copy to plugins/paintops/defaultpaintops/brush/KisBrushOpSettings.h index e1493ec367..f4dcd7ed66 100644 --- a/libs/image/kis_projection_updates_filter.cpp +++ b/plugins/paintops/defaultpaintops/brush/KisBrushOpSettings.h @@ -1,36 +1,31 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#ifndef KISBRUSHOPSETTINGS_H +#define KISBRUSHOPSETTINGS_H +#include "kis_brush_based_paintop_settings.h" -#include -#include -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +class KisBrushOpSettings : public KisBrushBasedPaintOpSettings { -} +public: + bool needsAsynchronousUpdates() const; +}; -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} +#endif // KISBRUSHOPSETTINGS_H diff --git a/plugins/paintops/defaultpaintops/brush/KisDabRenderingExecutor.cpp b/plugins/paintops/defaultpaintops/brush/KisDabRenderingExecutor.cpp new file mode 100644 index 0000000000..6ede283678 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisDabRenderingExecutor.cpp @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisDabRenderingExecutor.h" + +#include "KisDabRenderingQueue.h" +#include "KisDabRenderingQueueCache.h" +#include "KisDabRenderingJob.h" +#include "KisRenderedDab.h" +#include "KisRunnableStrokeJobsInterface.h" +#include "KisRunnableStrokeJobData.h" +#include + +struct KisDabRenderingExecutor::Private +{ + QScopedPointer renderingQueue; + KisRunnableStrokeJobsInterface *runnableJobsInterface; +}; + +KisDabRenderingExecutor::KisDabRenderingExecutor(const KoColorSpace *cs, + KisDabCacheUtils::ResourcesFactory resourcesFactory, + KisRunnableStrokeJobsInterface *runnableJobsInterface, + KisPressureMirrorOption *mirrorOption, + KisPrecisionOption *precisionOption) + : m_d(new Private) +{ + m_d->runnableJobsInterface = runnableJobsInterface; + + m_d->renderingQueue.reset( + new KisDabRenderingQueue(cs, resourcesFactory)); + + KisDabRenderingQueueCache *cache = new KisDabRenderingQueueCache(); + cache->setMirrorPostprocessing(mirrorOption); + cache->setPrecisionOption(precisionOption); + + m_d->renderingQueue->setCacheInterface(cache); +} + +KisDabRenderingExecutor::~KisDabRenderingExecutor() +{ +} + +void KisDabRenderingExecutor::addDab(const KisDabCacheUtils::DabRequestInfo &request, + qreal opacity, qreal flow) +{ + KisDabRenderingJobSP job = m_d->renderingQueue->addDab(request, opacity, flow); + if (job) { + m_d->runnableJobsInterface->addRunnableJob( + new FreehandStrokeRunnableJobDataWithUpdate( + new KisDabRenderingJobRunner(job, m_d->renderingQueue.data(), m_d->runnableJobsInterface), + KisStrokeJobData::CONCURRENT)); + } +} + +QList KisDabRenderingExecutor::takeReadyDabs(bool returnMutableDabs) +{ + return m_d->renderingQueue->takeReadyDabs(returnMutableDabs); +} + +bool KisDabRenderingExecutor::hasPreparedDabs() const +{ + return m_d->renderingQueue->hasPreparedDabs(); +} + +int KisDabRenderingExecutor::averageDabRenderingTime() const +{ + return m_d->renderingQueue->averageExecutionTime(); +} + +int KisDabRenderingExecutor::averageDabSize() const +{ + return m_d->renderingQueue->averageDabSize(); +} + +void KisDabRenderingExecutor::recyclePaintDevicesForCache(const QVector devices) +{ + m_d->renderingQueue->recyclePaintDevicesForCache(devices); +} + diff --git a/plugins/paintops/defaultpaintops/brush/KisDabRenderingExecutor.h b/plugins/paintops/defaultpaintops/brush/KisDabRenderingExecutor.h new file mode 100644 index 0000000000..95b8317be1 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisDabRenderingExecutor.h @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KISDABRENDERINGEXECUTOR_H +#define KISDABRENDERINGEXECUTOR_H + +#include "kritadefaultpaintops_export.h" + +#include + +#include +class KisRenderedDab; + +#include "KisDabCacheUtils.h" + +class KisPressureMirrorOption; +class KisPrecisionOption; +class KisRunnableStrokeJobsInterface; + + +class KRITADEFAULTPAINTOPS_EXPORT KisDabRenderingExecutor +{ +public: + KisDabRenderingExecutor(const KoColorSpace *cs, + KisDabCacheUtils::ResourcesFactory resourcesFactory, + KisRunnableStrokeJobsInterface *runnableJobsInterface, + KisPressureMirrorOption *mirrorOption = 0, + KisPrecisionOption *precisionOption = 0); + ~KisDabRenderingExecutor(); + + void addDab(const KisDabCacheUtils::DabRequestInfo &request, + qreal opacity, qreal flow); + + QList takeReadyDabs(bool returnMutableDabs = false); + + bool hasPreparedDabs() const; + + int averageDabRenderingTime() const; // usecs + int averageDabSize() const; + + void recyclePaintDevicesForCache(const QVector devices); + +private: + KisDabRenderingExecutor(const KisDabRenderingExecutor &rhs) = delete; + + struct Private; + const QScopedPointer m_d; +}; + +#endif // KISDABRENDERINGEXECUTOR_H diff --git a/plugins/paintops/defaultpaintops/brush/KisDabRenderingJob.cpp b/plugins/paintops/defaultpaintops/brush/KisDabRenderingJob.cpp new file mode 100644 index 0000000000..586dbdee2f --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisDabRenderingJob.cpp @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisDabRenderingJob.h" + +#include + +#include +#include + +#include "KisDabCacheUtils.h" +#include "KisDabRenderingQueue.h" + +#include + + +KisDabRenderingJob::KisDabRenderingJob() +{ +} + +KisDabRenderingJob::KisDabRenderingJob(int _seqNo, KisDabCacheUtils::DabGenerationInfo _generationInfo, KisDabRenderingJob::JobType _type) + : seqNo(_seqNo), + generationInfo(_generationInfo), + type(_type) +{ +} + +KisDabRenderingJob::KisDabRenderingJob(const KisDabRenderingJob &rhs) + : seqNo(rhs.seqNo), + generationInfo(rhs.generationInfo), + type(rhs.type), + originalDevice(rhs.originalDevice), + postprocessedDevice(rhs.postprocessedDevice), + status(rhs.status), + opacity(rhs.opacity), + flow(rhs.flow) +{ +} + +KisDabRenderingJob &KisDabRenderingJob::operator=(const KisDabRenderingJob &rhs) +{ + seqNo = rhs.seqNo; + generationInfo = rhs.generationInfo; + type = rhs.type; + originalDevice = rhs.originalDevice; + postprocessedDevice = rhs.postprocessedDevice; + status = rhs.status; + opacity = rhs.opacity; + flow = rhs.flow; + + return *this; +} + +QPoint KisDabRenderingJob::dstDabOffset() const +{ + return generationInfo.dstDabRect.topLeft(); +} + + + +KisDabRenderingJobRunner::KisDabRenderingJobRunner(KisDabRenderingJobSP job, + KisDabRenderingQueue *parentQueue, + KisRunnableStrokeJobsInterface *runnableJobsInterface) + : m_job(job), + m_parentQueue(parentQueue), + m_runnableJobsInterface(runnableJobsInterface) +{ +} + +KisDabRenderingJobRunner::~KisDabRenderingJobRunner() +{ +} + +int KisDabRenderingJobRunner::executeOneJob(KisDabRenderingJob *job, + KisDabCacheUtils::DabRenderingResources *resources, + KisDabRenderingQueue *parentQueue) +{ + using namespace KisDabCacheUtils; + + KIS_SAFE_ASSERT_RECOVER_NOOP(job->type == KisDabRenderingJob::Dab || + job->type == KisDabRenderingJob::Postprocess); + + QElapsedTimer executionTime; + executionTime.start(); + + resources->syncResourcesToSeqNo(job->seqNo, job->generationInfo.info); + + if (job->type == KisDabRenderingJob::Dab) { + // TODO: thing about better interface for the reverse queue link + job->originalDevice = parentQueue->fetchCachedPaintDevce(); + + generateDab(job->generationInfo, resources, &job->originalDevice); + } + + // by now the original device should be already prepared + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(job->originalDevice, 0); + + if (job->type == KisDabRenderingJob::Dab || + job->type == KisDabRenderingJob::Postprocess) { + + if (job->generationInfo.needsPostprocessing) { + // TODO: cache postprocessed device + + if (!job->postprocessedDevice || + *job->originalDevice->colorSpace() != *job->postprocessedDevice->colorSpace()) { + + job->postprocessedDevice = parentQueue->fetchCachedPaintDevce(); + *job->postprocessedDevice = *job->originalDevice; + } else { + *job->postprocessedDevice = *job->originalDevice; + } + + postProcessDab(job->postprocessedDevice, + job->generationInfo.dstDabRect.topLeft(), + job->generationInfo.info, + resources); + } else { + job->postprocessedDevice = job->originalDevice; + } + } + + return executionTime.nsecsElapsed() / 1000; +} + +void KisDabRenderingJobRunner::run() +{ + int executionTime = 0; + + KisDabCacheUtils::DabRenderingResources *resources = m_parentQueue->fetchResourcesFromCache(); + + executionTime = executeOneJob(m_job.data(), resources, m_parentQueue); + QList jobs = m_parentQueue->notifyJobFinished(m_job->seqNo, executionTime); + + while (!jobs.isEmpty()) { + QVector dataList; + + // start all-but-the-first jobs asynchronously + for (int i = 1; i < jobs.size(); i++) { + dataList.append(new FreehandStrokeRunnableJobDataWithUpdate( + new KisDabRenderingJobRunner(jobs[i], m_parentQueue, m_runnableJobsInterface), + KisStrokeJobData::CONCURRENT)); + } + + m_runnableJobsInterface->addRunnableJobs(dataList); + + + // execute the first job in the current thread + KisDabRenderingJobSP job = jobs.first(); + executionTime = executeOneJob(job.data(), resources, m_parentQueue); + jobs = m_parentQueue->notifyJobFinished(job->seqNo, executionTime); + } + + m_parentQueue->putResourcesToCache(resources); +} diff --git a/plugins/paintops/defaultpaintops/brush/KisDabRenderingJob.h b/plugins/paintops/defaultpaintops/brush/KisDabRenderingJob.h new file mode 100644 index 0000000000..83955a8c3a --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisDabRenderingJob.h @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KISDABRENDERINGJOB_H +#define KISDABRENDERINGJOB_H + +#include +#include +#include +#include +#include "kritadefaultpaintops_export.h" + +class KisDabRenderingQueue; +class KisRunnableStrokeJobsInterface; + +class KRITADEFAULTPAINTOPS_EXPORT KisDabRenderingJob +{ +public: + enum JobType { + Dab, + Postprocess, + Copy + }; + + enum Status { + New, + Running, + Completed + }; + +public: + KisDabRenderingJob(); + KisDabRenderingJob(int _seqNo, + KisDabCacheUtils::DabGenerationInfo _generationInfo, + JobType _type); + KisDabRenderingJob(const KisDabRenderingJob &rhs); + KisDabRenderingJob& operator=(const KisDabRenderingJob &rhs); + + QPoint dstDabOffset() const; + + int seqNo = -1; + KisDabCacheUtils::DabGenerationInfo generationInfo; + JobType type = Dab; + KisFixedPaintDeviceSP originalDevice; + KisFixedPaintDeviceSP postprocessedDevice; + + // high-level members, not directly related to job execution itself + Status status = New; + + qreal opacity = OPACITY_OPAQUE_F; + qreal flow = OPACITY_OPAQUE_F; +}; + +#include +typedef QSharedPointer KisDabRenderingJobSP; + +class KRITADEFAULTPAINTOPS_EXPORT KisDabRenderingJobRunner : public QRunnable +{ +public: + KisDabRenderingJobRunner(KisDabRenderingJobSP job, + KisDabRenderingQueue *parentQueue, + KisRunnableStrokeJobsInterface *runnableJobsInterface); + ~KisDabRenderingJobRunner(); + + void run() override; + + static int executeOneJob(KisDabRenderingJob *job, KisDabCacheUtils::DabRenderingResources *resources, KisDabRenderingQueue *parentQueue); + +private: + KisDabRenderingJobSP m_job; + KisDabRenderingQueue *m_parentQueue = 0; + KisRunnableStrokeJobsInterface *m_runnableJobsInterface = 0; +}; + + +#endif // KISDABRENDERINGJOB_H diff --git a/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueue.cpp b/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueue.cpp new file mode 100644 index 0000000000..26d1d01383 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueue.cpp @@ -0,0 +1,459 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisDabRenderingQueue.h" + +#include "KisDabRenderingJob.h" +#include "KisRenderedDab.h" +#include "kis_painter.h" + +#include +#include +#include +#include + +#include "kis_algebra_2d.h" + +struct KisDabRenderingQueue::Private +{ + struct DumbCacheInterface : public CacheInterface { + void getDabType(bool hasDabInCache, + KisDabCacheUtils::DabRenderingResources *resources, + const KisDabCacheUtils::DabRequestInfo &request, + /* out */ + KisDabCacheUtils::DabGenerationInfo *di, + bool *shouldUseCache) override + { + Q_UNUSED(hasDabInCache); + Q_UNUSED(resources); + Q_UNUSED(request); + + di->needsPostprocessing = false; + *shouldUseCache = false; + } + + bool hasSeparateOriginal(KisDabCacheUtils::DabRenderingResources *resources) const override + { + Q_UNUSED(resources); + return false; + } + + }; + + Private(const KoColorSpace *_colorSpace, + KisDabCacheUtils::ResourcesFactory _resourcesFactory) + : cacheInterface(new DumbCacheInterface), + colorSpace(_colorSpace), + resourcesFactory(_resourcesFactory), + avgExecutionTime(50), + avgDabSize(50) + { + KIS_SAFE_ASSERT_RECOVER_NOOP(resourcesFactory); + } + + ~Private() { + qDeleteAll(cachedResources); + cachedResources.clear(); + } + + QList jobs; + int nextSeqNoToUse = 0; + int lastPaintedJob = -1; + int lastDabJobInQueue = -1; + QScopedPointer cacheInterface; + const KoColorSpace *colorSpace; + qreal averageOpacity = 0.0; + + KisDabCacheUtils::ResourcesFactory resourcesFactory; + + QList cachedResources; + QSet cachedPaintDevices; + + QMutex mutex; + + KisRollingMeanAccumulatorWrapper avgExecutionTime; + KisRollingMeanAccumulatorWrapper avgDabSize; + + int calculateLastDabJobIndex(int startSearchIndex); + void cleanPaintedDabs(); + bool dabsHaveSeparateOriginal(); + + KisDabCacheUtils::DabRenderingResources* fetchResourcesFromCache(); + void putResourcesToCache(KisDabCacheUtils::DabRenderingResources *resources); +}; + + +KisDabRenderingQueue::KisDabRenderingQueue(const KoColorSpace *cs, + KisDabCacheUtils::ResourcesFactory resourcesFactory) + : m_d(new Private(cs, resourcesFactory)) +{ +} + +KisDabRenderingQueue::~KisDabRenderingQueue() +{ +} + +int KisDabRenderingQueue::Private::calculateLastDabJobIndex(int startSearchIndex) +{ + if (startSearchIndex < 0) { + startSearchIndex = jobs.size() - 1; + } + + // try to use cached value + if (startSearchIndex >= lastDabJobInQueue) { + return lastDabJobInQueue; + } + + // if we are below the cached value, just iterate through the dabs + // (which is extremely(!) slow) + for (int i = startSearchIndex; i >= 0; i--) { + if (jobs[i]->type == KisDabRenderingJob::Dab) { + return i; + } + } + + return -1; +} + +KisDabRenderingJobSP KisDabRenderingQueue::addDab(const KisDabCacheUtils::DabRequestInfo &request, + qreal opacity, qreal flow) +{ + QMutexLocker l(&m_d->mutex); + + const int seqNo = m_d->nextSeqNoToUse++; + + KisDabCacheUtils::DabRenderingResources *resources = m_d->fetchResourcesFromCache(); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(resources, KisDabRenderingJobSP()); + + // We should sync the cached brush into the current seqNo + resources->syncResourcesToSeqNo(seqNo, request.info); + + const int lastDabJobIndex = m_d->lastDabJobInQueue; + + KisDabRenderingJobSP job(new KisDabRenderingJob()); + + bool shouldUseCache = false; + m_d->cacheInterface->getDabType(lastDabJobIndex >= 0, + resources, + request, + &job->generationInfo, + &shouldUseCache); + + m_d->putResourcesToCache(resources); + resources = 0; + + // TODO: initialize via c-tor + job->seqNo = seqNo; + job->type = + !shouldUseCache ? KisDabRenderingJob::Dab : + job->generationInfo.needsPostprocessing ? KisDabRenderingJob::Postprocess : + KisDabRenderingJob::Copy; + job->opacity = opacity; + job->flow = flow; + + + if (job->type == KisDabRenderingJob::Dab) { + job->status = KisDabRenderingJob::Running; + } else if (job->type == KisDabRenderingJob::Postprocess || + job->type == KisDabRenderingJob::Copy) { + + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(lastDabJobIndex >= 0, KisDabRenderingJobSP()); + + if (m_d->jobs[lastDabJobIndex]->status == KisDabRenderingJob::Completed) { + if (job->type == KisDabRenderingJob::Postprocess) { + job->status = KisDabRenderingJob::Running; + job->originalDevice = m_d->jobs[lastDabJobIndex]->originalDevice; + } else if (job->type == KisDabRenderingJob::Copy) { + job->status = KisDabRenderingJob::Completed; + job->originalDevice = m_d->jobs[lastDabJobIndex]->originalDevice; + job->postprocessedDevice = m_d->jobs[lastDabJobIndex]->postprocessedDevice; + } + } + } + + m_d->jobs.append(job); + + KisDabRenderingJobSP jobToRun; + if (job->status == KisDabRenderingJob::Running) { + jobToRun = job; + } + + if (job->type == KisDabRenderingJob::Dab) { + m_d->lastDabJobInQueue = m_d->jobs.size() - 1; + m_d->cleanPaintedDabs(); + } + + // collect some statistics about the dab + m_d->avgDabSize(KisAlgebra2D::maxDimension(job->generationInfo.dstDabRect)); + + return jobToRun; +} + +QList KisDabRenderingQueue::notifyJobFinished(int seqNo, int usecsTime) +{ + QMutexLocker l(&m_d->mutex); + + QList dependentJobs; + + /** + * Here we use binary search for locating the necessary original dab + */ + auto finishedJobIt = + std::lower_bound(m_d->jobs.begin(), m_d->jobs.end(), seqNo, + [] (KisDabRenderingJobSP job, int seqNo) { + return job->seqNo < seqNo; + }); + + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(finishedJobIt != m_d->jobs.end(), dependentJobs); + KisDabRenderingJobSP finishedJob = *finishedJobIt; + + KIS_SAFE_ASSERT_RECOVER_NOOP(finishedJob->status == KisDabRenderingJob::Running); + KIS_SAFE_ASSERT_RECOVER_NOOP(finishedJob->seqNo == seqNo); + KIS_SAFE_ASSERT_RECOVER_NOOP(finishedJob->originalDevice); + KIS_SAFE_ASSERT_RECOVER_NOOP(finishedJob->postprocessedDevice); + + finishedJob->status = KisDabRenderingJob::Completed; + + if (finishedJob->type == KisDabRenderingJob::Dab) { + for (auto it = finishedJobIt + 1; it != m_d->jobs.end(); ++it) { + KisDabRenderingJobSP j = *it; + + // next dab job closes the chain + if (j->type == KisDabRenderingJob::Dab) break; + + // the non 'dab'-type job couldn't have + // been started before the source ob was completed + KIS_SAFE_ASSERT_RECOVER_BREAK(j->status == KisDabRenderingJob::New); + + if (j->type == KisDabRenderingJob::Copy) { + + j->originalDevice = finishedJob->originalDevice; + j->postprocessedDevice = finishedJob->postprocessedDevice; + j->status = KisDabRenderingJob::Completed; + + } else if (j->type == KisDabRenderingJob::Postprocess) { + + j->originalDevice = finishedJob->originalDevice; + j->status = KisDabRenderingJob::Running; + dependentJobs << j; + } + } + } + + if (usecsTime >= 0) { + m_d->avgExecutionTime(usecsTime); + } + + return dependentJobs; +} + +void KisDabRenderingQueue::Private::cleanPaintedDabs() +{ + const int nextToBePainted = lastPaintedJob + 1; + const int lastSourceJob = calculateLastDabJobIndex(qMin(nextToBePainted, jobs.size() - 1)); + + if (lastPaintedJob >= 0) { + int numRemovedJobs = 0; + int numRemovedJobsBeforeLastSource = 0; + + auto it = jobs.begin(); + for (int i = 0; i <= lastPaintedJob; i++) { + KisDabRenderingJobSP job = *it; + + if (i < lastSourceJob || job->type != KisDabRenderingJob::Dab){ + + // cache unique 'original' devices + if (job->type == KisDabRenderingJob::Dab && + job->postprocessedDevice != job->originalDevice) { + cachedPaintDevices << job->originalDevice; + job->originalDevice = 0; + } + + it = jobs.erase(it); + numRemovedJobs++; + if (i < lastSourceJob) { + numRemovedJobsBeforeLastSource++; + } + + } else { + ++it; + } + } + + KIS_SAFE_ASSERT_RECOVER_RETURN(jobs.size() > 0); + + lastPaintedJob -= numRemovedJobs; + lastDabJobInQueue -= numRemovedJobsBeforeLastSource; + } +} + +QList KisDabRenderingQueue::takeReadyDabs(bool returnMutableDabs) +{ + QMutexLocker l(&m_d->mutex); + + QList renderedDabs; + if (m_d->jobs.isEmpty()) return renderedDabs; + + KIS_SAFE_ASSERT_RECOVER_NOOP( + m_d->jobs.isEmpty() || + m_d->jobs.first()->type == KisDabRenderingJob::Dab); + + const int copyJobAfterInclusive = + returnMutableDabs && !m_d->dabsHaveSeparateOriginal() ? + m_d->lastDabJobInQueue : + std::numeric_limits::max(); + + for (int i = 0; i < m_d->jobs.size(); i++) { + KisDabRenderingJobSP j = m_d->jobs[i]; + + if (j->status != KisDabRenderingJob::Completed) break; + + if (i <= m_d->lastPaintedJob) continue; + + KisRenderedDab dab; + KisFixedPaintDeviceSP resultDevice = j->postprocessedDevice; + + if (i >= copyJobAfterInclusive) { + resultDevice = new KisFixedPaintDevice(*resultDevice); + } + + dab.device = resultDevice; + dab.offset = j->dstDabOffset(); + dab.opacity = j->opacity; + dab.flow = j->flow; + + m_d->averageOpacity = KisPainter::blendAverageOpacity(j->opacity, m_d->averageOpacity); + dab.averageOpacity = m_d->averageOpacity; + + + renderedDabs.append(dab); + + m_d->lastPaintedJob = i; + } + + m_d->cleanPaintedDabs(); + return renderedDabs; +} + +bool KisDabRenderingQueue::hasPreparedDabs() const +{ + QMutexLocker l(&m_d->mutex); + + const int nextToBePainted = m_d->lastPaintedJob + 1; + + return + nextToBePainted >= 0 && + nextToBePainted < m_d->jobs.size() && + m_d->jobs[nextToBePainted]->status == KisDabRenderingJob::Completed; +} + +void KisDabRenderingQueue::setCacheInterface(KisDabRenderingQueue::CacheInterface *interface) +{ + m_d->cacheInterface.reset(interface); +} + +KisFixedPaintDeviceSP KisDabRenderingQueue::fetchCachedPaintDevce() +{ + QMutexLocker l(&m_d->mutex); + + KisFixedPaintDeviceSP result; + + if (m_d->cachedPaintDevices.isEmpty()) { + result = new KisFixedPaintDevice(m_d->colorSpace); + } else { + // there is no difference from which side to take elements from QSet + auto it = m_d->cachedPaintDevices.begin(); + result = *it; + m_d->cachedPaintDevices.erase(it); + } + + return result; +} + +void KisDabRenderingQueue::recyclePaintDevicesForCache(const QVector devices) +{ + QMutexLocker l(&m_d->mutex); + + Q_FOREACH (KisFixedPaintDeviceSP device, devices) { + // the set automatically checks if the device is unique in the set + m_d->cachedPaintDevices << device; + } +} + +int KisDabRenderingQueue::averageExecutionTime() const +{ + QMutexLocker l(&m_d->mutex); + return qRound(m_d->avgExecutionTime.rollingMean()); +} + +int KisDabRenderingQueue::averageDabSize() const +{ + QMutexLocker l(&m_d->mutex); + return qRound(m_d->avgDabSize.rollingMean()); +} + +bool KisDabRenderingQueue::Private::dabsHaveSeparateOriginal() +{ + KisDabCacheUtils::DabRenderingResources *resources = fetchResourcesFromCache(); + + const bool result = cacheInterface->hasSeparateOriginal(resources); + + putResourcesToCache(resources); + + return result; +} + +KisDabCacheUtils::DabRenderingResources *KisDabRenderingQueue::Private::fetchResourcesFromCache() +{ + KisDabCacheUtils::DabRenderingResources *resources = 0; + + // fetch/create a temporary resources object + if (!cachedResources.isEmpty()) { + resources = cachedResources.takeLast(); + } else { + resources = resourcesFactory(); + } + + return resources; +} + +void KisDabRenderingQueue::Private::putResourcesToCache(KisDabCacheUtils::DabRenderingResources *resources) +{ + cachedResources << resources; +} + +KisDabCacheUtils::DabRenderingResources *KisDabRenderingQueue::fetchResourcesFromCache() +{ + // TODO: make a separate lock for that + QMutexLocker l(&m_d->mutex); + return m_d->fetchResourcesFromCache(); +} + +void KisDabRenderingQueue::putResourcesToCache(KisDabCacheUtils::DabRenderingResources *resources) +{ + QMutexLocker l(&m_d->mutex); + m_d->putResourcesToCache(resources); +} + +int KisDabRenderingQueue::testingGetQueueSize() const +{ + QMutexLocker l(&m_d->mutex); + + return m_d->jobs.size(); +} + diff --git a/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueue.h b/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueue.h new file mode 100644 index 0000000000..a9933396d1 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueue.h @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KISDABRENDERINGQUEUE_H +#define KISDABRENDERINGQUEUE_H + +#include + +#include "kritadefaultpaintops_export.h" + +#include +class KisDabRenderingJob; +class KisRenderedDab; + +#include "KisDabCacheUtils.h" + +class KRITADEFAULTPAINTOPS_EXPORT KisDabRenderingQueue +{ +public: + struct CacheInterface { + virtual ~CacheInterface() {} + virtual void getDabType(bool hasDabInCache, + KisDabCacheUtils::DabRenderingResources *resources, + const KisDabCacheUtils::DabRequestInfo &request, + /* out */ + KisDabCacheUtils::DabGenerationInfo *di, + bool *shouldUseCache) = 0; + + virtual bool hasSeparateOriginal(KisDabCacheUtils::DabRenderingResources *resources) const = 0; + }; + + +public: + KisDabRenderingQueue(const KoColorSpace *cs, KisDabCacheUtils::ResourcesFactory resourcesFactory); + ~KisDabRenderingQueue(); + + KisDabRenderingJobSP addDab(const KisDabCacheUtils::DabRequestInfo &request, + qreal opacity, qreal flow); + + QList notifyJobFinished(int seqNo, int usecsTime = -1); + + QList takeReadyDabs(bool returnMutableDabs = false); + + bool hasPreparedDabs() const; + + void setCacheInterface(CacheInterface *interface); + + KisFixedPaintDeviceSP fetchCachedPaintDevce(); + void recyclePaintDevicesForCache(const QVector devices); + + void putResourcesToCache(KisDabCacheUtils::DabRenderingResources *resources); + KisDabCacheUtils::DabRenderingResources* fetchResourcesFromCache(); + + int averageExecutionTime() const; + int averageDabSize() const; + + int testingGetQueueSize() const; + +private: + struct Private; + const QScopedPointer m_d; +}; + +#endif // KISDABRENDERINGQUEUE_H diff --git a/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueueCache.cpp b/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueueCache.cpp new file mode 100644 index 0000000000..d000938171 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueueCache.cpp @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisDabRenderingQueueCache.h" + +struct KisDabRenderingQueueCache::Private +{ + Private() + { + } +}; + +KisDabRenderingQueueCache::KisDabRenderingQueueCache() + : m_d(new Private()) +{ +} + +KisDabRenderingQueueCache::~KisDabRenderingQueueCache() +{ +} + +void KisDabRenderingQueueCache::getDabType(bool hasDabInCache, KisDabCacheUtils::DabRenderingResources *resources, const KisDabCacheUtils::DabRequestInfo &request, KisDabCacheUtils::DabGenerationInfo *di, bool *shouldUseCache) +{ + fetchDabGenerationInfo(hasDabInCache, resources, request, di, shouldUseCache); +} + +bool KisDabRenderingQueueCache::hasSeparateOriginal(KisDabCacheUtils::DabRenderingResources *resources) const +{ + return needSeparateOriginal(resources->textureOption.data(), resources->sharpnessOption.data()); +} diff --git a/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueueCache.h b/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueueCache.h new file mode 100644 index 0000000000..151a0eb096 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/KisDabRenderingQueueCache.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KISDABRENDERINGQUEUECACHE_H +#define KISDABRENDERINGQUEUECACHE_H + +#include "KisDabRenderingQueue.h" +#include "kis_dab_cache_base.h" + +#include "kritadefaultpaintops_export.h" + +class KisPressureMirrorOption; +class KisPrecisionOption; +class KisPressureSharpnessOption; + +class KRITADEFAULTPAINTOPS_EXPORT KisDabRenderingQueueCache : public KisDabRenderingQueue::CacheInterface, public KisDabCacheBase +{ +public: + +public: + KisDabRenderingQueueCache(); + ~KisDabRenderingQueueCache(); + + void getDabType(bool hasDabInCache, + KisDabCacheUtils::DabRenderingResources *resources, + const KisDabCacheUtils::DabRequestInfo &request, + /* out */ + KisDabCacheUtils::DabGenerationInfo *di, + bool *shouldUseCache) override; + + bool hasSeparateOriginal(KisDabCacheUtils::DabRenderingResources *resources) const override; + +private: + struct Private; + QScopedPointer m_d; +}; + +#endif // KISDABRENDERINGQUEUECACHE_H diff --git a/plugins/paintops/defaultpaintops/brush/kis_brushop.cpp b/plugins/paintops/defaultpaintops/brush/kis_brushop.cpp index dbf10dac83..0e400706e2 100644 --- a/plugins/paintops/defaultpaintops/brush/kis_brushop.cpp +++ b/plugins/paintops/defaultpaintops/brush/kis_brushop.cpp @@ -1,209 +1,353 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2004-2008 Boudewijn Rempt * Copyright (c) 2004 Clarence Dang * Copyright (c) 2004 Adrian Page * Copyright (c) 2004 Cyrille Berger * Copyright (c) 2010 Lukáš Tvrdý * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_brushop.h" #include #include #include #include #include #include #include #include #include #include #include -#include -#include -#include #include #include +#include "krita_utils.h" +#include +#include "kis_algebra_2d.h" +#include +#include +#include +#include "KisBrushOpResources.h" +#include +#include + +#include +#include +#include "kis_image_config.h" KisBrushOp::KisBrushOp(const KisPaintOpSettingsSP settings, KisPainter *painter, KisNodeSP node, KisImageSP image) : KisBrushBasedPaintOp(settings, painter) , m_opacityOption(node) - , m_hsvTransformation(0) + , m_avgSpacing(50) + , m_avgNumDabs(50) + , m_idealNumRects(KisImageConfig().maxNumberOfThreads()) { Q_UNUSED(image); Q_ASSERT(settings); - KisColorSourceOption colorSourceOption; - colorSourceOption.readOptionSetting(settings); - m_colorSource = colorSourceOption.createColorSource(painter); - - m_hsvOptions.append(KisPressureHSVOption::createHueOption()); - m_hsvOptions.append(KisPressureHSVOption::createSaturationOption()); - m_hsvOptions.append(KisPressureHSVOption::createValueOption()); - - Q_FOREACH (KisPressureHSVOption * option, m_hsvOptions) { - option->readOptionSetting(settings); - option->resetAllSensors(); - if (option->isChecked() && !m_hsvTransformation) { - m_hsvTransformation = painter->backgroundColor().colorSpace()->createColorTransformation("hsv_adjustment", QHash()); - } - } + /** + * We do our own threading here, so we need to forbid the brushes + * to do threading internally + */ + m_brush->setThreadingAllowed(false); m_airbrushOption.readOptionSetting(settings); m_opacityOption.readOptionSetting(settings); m_flowOption.readOptionSetting(settings); m_sizeOption.readOptionSetting(settings); m_ratioOption.readOptionSetting(settings); m_spacingOption.readOptionSetting(settings); m_rateOption.readOptionSetting(settings); m_softnessOption.readOptionSetting(settings); - m_sharpnessOption.readOptionSetting(settings); - m_darkenOption.readOptionSetting(settings); m_rotationOption.readOptionSetting(settings); - m_mixOption.readOptionSetting(settings); m_scatterOption.readOptionSetting(settings); + m_sharpnessOption.readOptionSetting(settings); m_opacityOption.resetAllSensors(); m_flowOption.resetAllSensors(); m_sizeOption.resetAllSensors(); m_ratioOption.resetAllSensors(); m_rateOption.resetAllSensors(); m_softnessOption.resetAllSensors(); m_sharpnessOption.resetAllSensors(); - m_darkenOption.resetAllSensors(); m_rotationOption.resetAllSensors(); m_scatterOption.resetAllSensors(); + m_sharpnessOption.resetAllSensors(); - m_dabCache->setSharpnessPostprocessing(&m_sharpnessOption); m_rotationOption.applyFanCornersInfo(this); + + KisBrushSP baseBrush = m_brush; + auto resourcesFactory = + [baseBrush, settings, painter] () { + KisDabCacheUtils::DabRenderingResources *resources = + new KisBrushOpResources(settings, painter); + resources->brush = baseBrush->clone(); + + return resources; + }; + + + m_dabExecutor.reset( + new KisDabRenderingExecutor( + painter->device()->compositionSourceColorSpace(), + resourcesFactory, + painter->runnableStrokeJobsInterface(), + &m_mirrorOption, + &m_precisionOption)); } KisBrushOp::~KisBrushOp() { - qDeleteAll(m_hsvOptions); - delete m_colorSource; - delete m_hsvTransformation; } KisSpacingInformation KisBrushOp::paintAt(const KisPaintInformation& info) { if (!painter()->device()) return KisSpacingInformation(1.0); KisBrushSP brush = m_brush; Q_ASSERT(brush); if (!brush) return KisSpacingInformation(1.0); if (!brush->canPaintFor(info)) return KisSpacingInformation(1.0); qreal scale = m_sizeOption.apply(info); scale *= KisLodTransform::lodToScale(painter()->device()); if (checkSizeTooSmall(scale)) return KisSpacingInformation(); qreal rotation = m_rotationOption.apply(info); qreal ratio = m_ratioOption.apply(info); - KisPaintDeviceSP device = painter()->device(); - - KisDabShape shape(scale, ratio, rotation); QPointF cursorPos = m_scatterOption.apply(info, brush->maskWidth(shape, 0, 0, info), brush->maskHeight(shape, 0, 0, info)); - quint8 origOpacity = painter()->opacity(); - m_opacityOption.setFlow(m_flowOption.apply(info)); - m_opacityOption.apply(painter(), info); - m_colorSource->selectColor(m_mixOption.apply(info), info); - m_darkenOption.apply(m_colorSource, info); - if (m_hsvTransformation) { - Q_FOREACH (KisPressureHSVOption * option, m_hsvOptions) { - option->apply(m_hsvTransformation, info); - } - m_colorSource->applyColorTransformation(m_hsvTransformation); + quint8 dabOpacity = OPACITY_OPAQUE_U8; + quint8 dabFlow = OPACITY_OPAQUE_U8; + + m_opacityOption.apply(info, &dabOpacity, &dabFlow); + + KisDabCacheUtils::DabRequestInfo request(painter()->paintColor(), + cursorPos, + shape, + info, + m_softnessOption.apply(info)); + + m_dabExecutor->addDab(request, qreal(dabOpacity) / 255.0, qreal(dabFlow) / 255.0); + + + KisSpacingInformation spacingInfo = + effectiveSpacing(scale, rotation, &m_airbrushOption, &m_spacingOption, info); + + // gather statistics about dabs + m_avgSpacing(spacingInfo.scalarApprox()); + + return spacingInfo; +} + +struct KisBrushOp::UpdateSharedState +{ + // rendering data + KisPainter *painter = 0; + QList dabsQueue; + + // speed metrics + QVector dabPoints; + QElapsedTimer dabRenderingTimer; + + // final report + QVector allDirtyRects; +}; + +void KisBrushOp::addMirroringJobs(Qt::Orientation direction, + QVector &rects, + UpdateSharedStateSP state, + QVector &jobs) +{ + jobs.append(new KisRunnableStrokeJobData(0, KisStrokeJobData::SEQUENTIAL)); + + for (KisRenderedDab &dab : state->dabsQueue) { + jobs.append( + new KisRunnableStrokeJobData( + [state, &dab, direction] () { + state->painter->mirrorDab(direction, &dab); + }, + KisStrokeJobData::CONCURRENT)); } - QRect dabRect; - KisFixedPaintDeviceSP dab = m_dabCache->fetchDab(device->compositionSourceColorSpace(), - m_colorSource, - cursorPos, - shape, - info, - m_softnessOption.apply(info), - &dabRect); - - // sanity check for the size calculation code - if (dab->bounds().size() != dabRect.size()) { - warnKrita << "KisBrushOp: dab bounds is not dab rect. See bug 327156" << dab->bounds().size() << dabRect.size(); + jobs.append(new KisRunnableStrokeJobData(0, KisStrokeJobData::SEQUENTIAL)); + + for (QRect &rc : rects) { + state->painter->mirrorRect(direction, &rc); + + jobs.append( + new KisRunnableStrokeJobData( + [rc, state] () { + state->painter->bltFixed(rc, state->dabsQueue); + }, + KisStrokeJobData::CONCURRENT)); } - painter()->bltFixed(dabRect.topLeft(), dab, dab->bounds()); + state->allDirtyRects.append(rects); +} - painter()->renderMirrorMaskSafe(dabRect, - dab, - !m_dabCache->needSeparateOriginal()); - painter()->setOpacity(origOpacity); +int KisBrushOp::doAsyncronousUpdate(QVector &jobs) +{ + if (!m_updateSharedState && m_dabExecutor->hasPreparedDabs()) { - return effectiveSpacing(scale, rotation, &m_airbrushOption, &m_spacingOption, info); + m_updateSharedState = toQShared(new UpdateSharedState()); + UpdateSharedStateSP state = m_updateSharedState; + + state->painter = painter(); + + state->dabsQueue = m_dabExecutor->takeReadyDabs(painter()->hasMirroring()); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!state->dabsQueue.isEmpty(), + m_currentUpdatePeriod); + + const int diameter = m_dabExecutor->averageDabSize(); + const qreal spacing = m_avgSpacing.rollingMean(); + + const int idealNumRects = m_idealNumRects; + QVector rects = + KisPaintOpUtils::splitDabsIntoRects(state->dabsQueue, + idealNumRects, diameter, spacing); + + state->allDirtyRects = rects; + + Q_FOREACH (const KisRenderedDab &dab, state->dabsQueue) { + state->dabPoints.append(dab.realBounds().center()); + } + + state->dabRenderingTimer.start(); + + Q_FOREACH (const QRect &rc, rects) { + jobs.append( + new KisRunnableStrokeJobData( + [rc, state] () { + state->painter->bltFixed(rc, state->dabsQueue); + }, + KisStrokeJobData::CONCURRENT)); + } + + /** + * After the dab has been rendered once, we should mirror it either one + * (h __or__ v) or three (h __and__ v) times. This sequence of 'if's achives + * the goal without any extra copying. Please note that it has __no__ 'else' + * branches, which is done intentionally! + */ + if (state->painter->hasHorizontalMirroring()) { + addMirroringJobs(Qt::Horizontal, rects, state, jobs); + } + + if (state->painter->hasVerticalMirroring()) { + addMirroringJobs(Qt::Vertical, rects, state, jobs); + } + + if (state->painter->hasHorizontalMirroring() && state->painter->hasVerticalMirroring()) { + addMirroringJobs(Qt::Horizontal, rects, state, jobs); + } + + jobs.append( + new KisRunnableStrokeJobData( + [state, this] () { + Q_FOREACH(const QRect &rc, state->allDirtyRects) { + state->painter->addDirtyRect(rc); + } + + state->painter->setAverageOpacity(state->dabsQueue.last().averageOpacity); + + const int updateRenderingTime = state->dabRenderingTimer.elapsed(); + const int dabRenderingTime = m_dabExecutor->averageDabRenderingTime() / 1000; + m_avgNumDabs(state->dabsQueue.size()); + + QVector recycledDevices; + for (auto it = state->dabsQueue.begin(); it != state->dabsQueue.end(); ++it) { + // we don't need to check for uniqueness, it is done by the queue + recycledDevices << it->device; + it->device.clear(); + } + m_dabExecutor->recyclePaintDevicesForCache(recycledDevices); + + + + const int approxDabRenderingTime = qreal(dabRenderingTime) / m_idealNumRects * m_avgNumDabs.rollingMean(); + + m_currentUpdatePeriod = qBound(20, int(1.5 * (approxDabRenderingTime + updateRenderingTime)), 100); + + + { // debug chunk +// const int updateRenderingTime = state->dabRenderingTimer.nsecsElapsed() / 1000; +// const int dabRenderingTime = m_dabExecutor->averageDabRenderingTime(); +// ENTER_FUNCTION() << ppVar(state->allDirtyRects.size()) << ppVar(state->dabsQueue.size()) << ppVar(dabRenderingTime) << ppVar(updateRenderingTime); +// ENTER_FUNCTION() << ppVar(m_currentUpdatePeriod); + } + + m_updateSharedState.clear(); + }, + KisStrokeJobData::SEQUENTIAL)); + } + + return m_currentUpdatePeriod; } KisSpacingInformation KisBrushOp::updateSpacingImpl(const KisPaintInformation &info) const { const qreal scale = m_sizeOption.apply(info) * KisLodTransform::lodToScale(painter()->device()); qreal rotation = m_rotationOption.apply(info); return effectiveSpacing(scale, rotation, &m_airbrushOption, &m_spacingOption, info); } KisTimingInformation KisBrushOp::updateTimingImpl(const KisPaintInformation &info) const { return KisPaintOpPluginUtils::effectiveTiming(&m_airbrushOption, &m_rateOption, info); } void KisBrushOp::paintLine(const KisPaintInformation& pi1, const KisPaintInformation& pi2, KisDistanceInformation *currentDistance) { if (m_sharpnessOption.isChecked() && m_brush && (m_brush->width() == 1) && (m_brush->height() == 1)) { if (!m_lineCacheDevice) { m_lineCacheDevice = source()->createCompositionSourceDevice(); } else { m_lineCacheDevice->clear(); } KisPainter p(m_lineCacheDevice); p.setPaintColor(painter()->paintColor()); p.drawDDALine(pi1.pos(), pi2.pos()); QRect rc = m_lineCacheDevice->extent(); painter()->bitBlt(rc.x(), rc.y(), m_lineCacheDevice, rc.x(), rc.y(), rc.width(), rc.height()); //fixes Bug 338011 painter()->renderMirrorMask(rc, m_lineCacheDevice); } else { KisPaintOp::paintLine(pi1, pi2, currentDistance); } } diff --git a/plugins/paintops/defaultpaintops/brush/kis_brushop.h b/plugins/paintops/defaultpaintops/brush/kis_brushop.h index 51e7b52fe0..390f84e4cc 100644 --- a/plugins/paintops/defaultpaintops/brush/kis_brushop.h +++ b/plugins/paintops/defaultpaintops/brush/kis_brushop.h @@ -1,87 +1,103 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2004-2008 Boudewijn Rempt * Copyright (c) 2004 Clarence Dang * Copyright (c) 2004 Adrian Page * Copyright (c) 2004 Cyrille Berger * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_BRUSHOP_H_ #define KIS_BRUSHOP_H_ #include "kis_brush_based_paintop.h" #include -#include #include #include #include #include #include -#include -#include #include #include #include -#include #include #include #include +#include + +#include + class KisPainter; class KisColorSource; - +class KisDabRenderingExecutor; +class KisRenderedDab; +class KisRunnableStrokeJobData; class KisBrushOp : public KisBrushBasedPaintOp { public: KisBrushOp(const KisPaintOpSettingsSP settings, KisPainter * painter, KisNodeSP node, KisImageSP image); ~KisBrushOp() override; void paintLine(const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance) override; + int doAsyncronousUpdate(QVector &jobs) override; + protected: KisSpacingInformation paintAt(const KisPaintInformation& info) override; KisSpacingInformation updateSpacingImpl(const KisPaintInformation &info) const override; KisTimingInformation updateTimingImpl(const KisPaintInformation &info) const override; + struct UpdateSharedState; + typedef QSharedPointer UpdateSharedStateSP; + + void addMirroringJobs(Qt::Orientation direction, + QVector &rects, + UpdateSharedStateSP state, + QVector &jobs); + + UpdateSharedStateSP m_updateSharedState; + + private: - KisColorSource *m_colorSource; KisAirbrushOption m_airbrushOption; KisPressureSizeOption m_sizeOption; KisPressureRatioOption m_ratioOption; KisPressureSpacingOption m_spacingOption; KisPressureRateOption m_rateOption; KisPressureFlowOption m_flowOption; KisFlowOpacityOption m_opacityOption; KisPressureSoftnessOption m_softnessOption; KisPressureSharpnessOption m_sharpnessOption; - KisPressureDarkenOption m_darkenOption; KisPressureRotationOption m_rotationOption; - KisPressureMixOption m_mixOption; KisPressureScatterOption m_scatterOption; - QList m_hsvOptions; - KoColorTransformation *m_hsvTransformation; KisPaintDeviceSP m_lineCacheDevice; - KisPaintDeviceSP m_colorSourceDevice; + + QScopedPointer m_dabExecutor; + qreal m_currentUpdatePeriod = 20.0; + KisRollingMeanAccumulatorWrapper m_avgSpacing; + KisRollingMeanAccumulatorWrapper m_avgNumDabs; + + const int m_idealNumRects; }; #endif // KIS_BRUSHOP_H_ diff --git a/plugins/paintops/defaultpaintops/brush/kis_brushop_settings_widget.cpp b/plugins/paintops/defaultpaintops/brush/kis_brushop_settings_widget.cpp index bbe667d467..e71b11a68c 100644 --- a/plugins/paintops/defaultpaintops/brush/kis_brushop_settings_widget.cpp +++ b/plugins/paintops/defaultpaintops/brush/kis_brushop_settings_widget.cpp @@ -1,99 +1,99 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2004-2008 Boudewijn Rempt * Copyright (c) 2004 Clarence Dang * Copyright (c) 2004 Adrian Page * Copyright (c) 2004 Cyrille Berger * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_brushop_settings_widget.h" -#include +#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_texture_option.h" #include "kis_curve_option_widget.h" #include #include "kis_pressure_texture_strength_option.h" KisBrushOpSettingsWidget::KisBrushOpSettingsWidget(QWidget* parent) : KisBrushBasedPaintopOptionWidget(parent) { setObjectName("brush option widget"); setPrecisionEnabled(true); // Brush tip options addPaintOpOption(new KisCompositeOpOption(true), i18n("Blending Mode")); addPaintOpOption(new KisFlowOpacityOptionWidget(), i18n("Opacity")); addPaintOpOption(new KisCurveOptionWidget(new KisPressureFlowOption(), i18n("0%"), i18n("100%")), i18n("Flow")); addPaintOpOption(new KisCurveOptionWidget(new KisPressureSizeOption(), i18n("0%"), i18n("100%")), i18n("Size")); addPaintOpOption(new KisCurveOptionWidget(new KisPressureRatioOption(), i18n("0%"), i18n("100%")), i18n("Ratio")); addPaintOpOption(new KisPressureSpacingOptionWidget(), i18n("Spacing")); addPaintOpOption(new KisPressureMirrorOptionWidget(), i18n("Mirror")); addPaintOpOption(new KisCurveOptionWidget(new KisPressureSoftnessOption(), i18n("Soft"), i18n("Hard")), i18n("Softness")); addPaintOpOption(new KisPressureSharpnessOptionWidget(), i18n("Sharpness")); addPaintOpOption(new KisCurveOptionWidget(new KisPressureRotationOption(), i18n("-180°"), i18n("180°")), i18n("Rotation")); addPaintOpOption(new KisPressureScatterOptionWidget(), i18n("Scatter")); // Colors options addPaintOpOption(new KisColorSourceOptionWidget(), i18n("Source")); addPaintOpOption(new KisCurveOptionWidget(new KisPressureDarkenOption(), i18n("0.0"), i18n("1.0")), i18n("Darken")); addPaintOpOption(new KisCurveOptionWidget(new KisPressureMixOption(), i18n("Foreground"), i18n("Background")), i18n("Mix")); addPaintOpOption(new KisCurveOptionWidget(KisPressureHSVOption::createHueOption(), KisPressureHSVOption::hueMinLabel(), KisPressureHSVOption::huemaxLabel()), i18n("Hue")); addPaintOpOption(new KisCurveOptionWidget(KisPressureHSVOption::createSaturationOption(), KisPressureHSVOption::saturationMinLabel(), KisPressureHSVOption::saturationmaxLabel()), i18n("Saturation")); addPaintOpOption(new KisCurveOptionWidget(KisPressureHSVOption::createValueOption(), KisPressureHSVOption::valueMinLabel(), KisPressureHSVOption::valuemaxLabel()), i18n("Value")); addPaintOpOption(new KisAirbrushOption(false), i18n("Airbrush")); addPaintOpOption(new KisCurveOptionWidget(new KisPressureRateOption(), i18n("0%"), i18n("100%")), i18n("Rate")); addPaintOpOption(new KisPaintActionTypeOption(), i18n("Painting Mode")); addPaintOpOption(new KisTextureOption(), i18n("Pattern")); addPaintOpOption(new KisCurveOptionWidget(new KisPressureTextureStrengthOption(), i18n("Weak"), i18n("Strong")), i18n("Strength")); } KisBrushOpSettingsWidget::~KisBrushOpSettingsWidget() { } KisPropertiesConfigurationSP KisBrushOpSettingsWidget::configuration() const { - KisBrushBasedPaintOpSettingsSP config = new KisBrushBasedPaintOpSettings(); + KisBrushBasedPaintOpSettingsSP config = new KisBrushOpSettings(); config->setOptionsWidget(const_cast(this)); config->setProperty("paintop", "paintbrush"); // XXX: make this a const id string writeConfiguration(config); return config; } diff --git a/plugins/paintops/defaultpaintops/brush/tests/CMakeLists.txt b/plugins/paintops/defaultpaintops/brush/tests/CMakeLists.txt index ed22adf8e7..d2d4500c5e 100644 --- a/plugins/paintops/defaultpaintops/brush/tests/CMakeLists.txt +++ b/plugins/paintops/defaultpaintops/brush/tests/CMakeLists.txt @@ -1,14 +1,20 @@ set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} ) include_directories( ${CMAKE_SOURCE_DIR}/sdk/tests ) include(KritaAddBrokenUnitTest) macro_add_unittest_definitions() include(ECMAddTests) +ecm_add_test(KisDabRenderingQueueTest.cpp + TEST_NAME KisDabRenderingQueueTest + LINK_LIBRARIES kritadefaultpaintops kritalibpaintop kritaimage Qt5::Test) + + + krita_add_broken_unit_test(kis_brushop_test.cpp ../../../../../sdk/tests/stroke_testing_utils.cpp TEST_NAME krita-plugins-KisBrushOpTest LINK_LIBRARIES kritaimage kritaui kritalibpaintop Qt5::Test) diff --git a/plugins/paintops/defaultpaintops/brush/tests/KisDabRenderingQueueTest.cpp b/plugins/paintops/defaultpaintops/brush/tests/KisDabRenderingQueueTest.cpp new file mode 100644 index 0000000000..c72f7603b4 --- /dev/null +++ b/plugins/paintops/defaultpaintops/brush/tests/KisDabRenderingQueueTest.cpp @@ -0,0 +1,541 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisDabRenderingQueueTest.h" + +#include +#include +#include + +#include <../KisDabRenderingQueue.h> +#include <../KisRenderedDab.h> +#include <../KisDabRenderingJob.h> + +struct SurrogateCacheInterface : public KisDabRenderingQueue::CacheInterface +{ + void getDabType(bool hasDabInCache, + KisDabCacheUtils::DabRenderingResources *resources, + const KisDabCacheUtils::DabRequestInfo &request, + /* out */ + KisDabCacheUtils::DabGenerationInfo *di, + bool *shouldUseCache) override + { + Q_UNUSED(resources); + Q_UNUSED(request); + + if (!hasDabInCache || typeOverride == KisDabRenderingJob::Dab) { + di->needsPostprocessing = false; + *shouldUseCache = false; + } else if (typeOverride == KisDabRenderingJob::Copy) { + di->needsPostprocessing = false; + *shouldUseCache = true; + } else if (typeOverride == KisDabRenderingJob::Postprocess) { + di->needsPostprocessing = true; + *shouldUseCache = true; + } + + di->info = request.info; + } + + bool hasSeparateOriginal(KisDabCacheUtils::DabRenderingResources *resources) const override { + Q_UNUSED(resources); + return typeOverride == KisDabRenderingJob::Postprocess; + } + + KisDabRenderingJob::JobType typeOverride = KisDabRenderingJob::Dab; +}; + +#include +#include "kis_auto_brush.h" + +KisDabCacheUtils::DabRenderingResources *testResourcesFactory() +{ + KisDabCacheUtils::DabRenderingResources *resources = + new KisDabCacheUtils::DabRenderingResources(); + + KisCircleMaskGenerator* circle = new KisCircleMaskGenerator(10, 1.0, 1.0, 1.0, 2, false); + KisBrushSP brush = new KisAutoBrush(circle, 0.0, 0.0); + resources->brush = brush; + + return resources; +} + +void KisDabRenderingQueueTest::testCachedDabs() +{ + const KoColorSpace *cs = KoColorSpaceRegistry::instance()->rgb8(); + + SurrogateCacheInterface *cacheInterface = new SurrogateCacheInterface(); + + KisDabRenderingQueue queue(cs, testResourcesFactory); + queue.setCacheInterface(cacheInterface); + + KoColor color; + QPointF pos1(10,10); + QPointF pos2(20,20); + KisDabShape shape; + KisPaintInformation pi1(pos1); + KisPaintInformation pi2(pos2); + + KisDabCacheUtils::DabRequestInfo request1(color, pos1, shape, pi1, 1.0); + KisDabCacheUtils::DabRequestInfo request2(color, pos2, shape, pi2, 1.0); + + cacheInterface->typeOverride = KisDabRenderingJob::Dab; + KisDabRenderingJobSP job0 = queue.addDab(request1, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + + QVERIFY(job0); + QCOMPARE(job0->seqNo, 0); + QCOMPARE(job0->generationInfo.info.pos(), request1.info.pos()); + QCOMPARE(job0->type, KisDabRenderingJob::Dab); + QVERIFY(!job0->originalDevice); + QVERIFY(!job0->postprocessedDevice); + + cacheInterface->typeOverride = KisDabRenderingJob::Dab; + KisDabRenderingJobSP job1 = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + + QVERIFY(job1); + QCOMPARE(job1->seqNo, 1); + QCOMPARE(job1->generationInfo.info.pos(), request2.info.pos()); + QCOMPARE(job1->type, KisDabRenderingJob::Dab); + QVERIFY(!job1->originalDevice); + QVERIFY(!job1->postprocessedDevice); + + cacheInterface->typeOverride = KisDabRenderingJob::Copy; + KisDabRenderingJobSP job2 = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + QVERIFY(!job2); + + cacheInterface->typeOverride = KisDabRenderingJob::Copy; + KisDabRenderingJobSP job3 = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + QVERIFY(!job3); + + // we only added the dabs, but we haven't completed them yet + QVERIFY(!queue.hasPreparedDabs()); + QCOMPARE(queue.testingGetQueueSize(), 4); + + QList jobs; + QList renderedDabs; + + + { + // we've completed job0 + job0->originalDevice = new KisFixedPaintDevice(cs); + job0->postprocessedDevice = job0->originalDevice; + + jobs = queue.notifyJobFinished(job0->seqNo); + QVERIFY(jobs.isEmpty()); + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 1); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + QCOMPARE(queue.testingGetQueueSize(), 3); + } + + { + // we've completed job1 + job1->originalDevice = new KisFixedPaintDevice(cs); + job1->postprocessedDevice = job1->originalDevice; + + jobs = queue.notifyJobFinished(job1->seqNo); + QVERIFY(jobs.isEmpty()); + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 3); + + // since they are copies, they should be the same + QCOMPARE(renderedDabs[1].device, renderedDabs[0].device); + QCOMPARE(renderedDabs[2].device, renderedDabs[0].device); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + + // we delete all the painted jobs except the latest 'dab' job + QCOMPARE(queue.testingGetQueueSize(), 1); + } + + { + // add one more cached job and take it + cacheInterface->typeOverride = KisDabRenderingJob::Copy; + KisDabRenderingJobSP job = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + QVERIFY(!job); + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 1); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + + // we delete all the painted jobs except the latest 'dab' job + QCOMPARE(queue.testingGetQueueSize(), 1); + } + + { + // add a 'dab' job and complete it + + cacheInterface->typeOverride = KisDabRenderingJob::Dab; + KisDabRenderingJobSP job = queue.addDab(request1, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + + QVERIFY(job); + QCOMPARE(job->seqNo, 5); + QCOMPARE(job->generationInfo.info.pos(), request1.info.pos()); + QCOMPARE(job->type, KisDabRenderingJob::Dab); + QVERIFY(!job->originalDevice); + QVERIFY(!job->postprocessedDevice); + + // now the queue can be cleared from the completed dabs! + QCOMPARE(queue.testingGetQueueSize(), 1); + + job->originalDevice = new KisFixedPaintDevice(cs); + job->postprocessedDevice = job->originalDevice; + + jobs = queue.notifyJobFinished(job->seqNo); + QVERIFY(jobs.isEmpty()); + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 1); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + + // we do not delete the queue of jobs until the next 'dab' + // job arrives + QCOMPARE(queue.testingGetQueueSize(), 1); + } + +} + +void KisDabRenderingQueueTest::testPostprocessedDabs() +{ + const KoColorSpace *cs = KoColorSpaceRegistry::instance()->rgb8(); + + SurrogateCacheInterface *cacheInterface = new SurrogateCacheInterface(); + + KisDabRenderingQueue queue(cs, testResourcesFactory); + queue.setCacheInterface(cacheInterface); + + KoColor color; + QPointF pos1(10,10); + QPointF pos2(20,20); + KisDabShape shape; + KisPaintInformation pi1(pos1); + KisPaintInformation pi2(pos2); + + KisDabCacheUtils::DabRequestInfo request1(color, pos1, shape, pi1, 1.0); + KisDabCacheUtils::DabRequestInfo request2(color, pos2, shape, pi2, 1.0); + + cacheInterface->typeOverride = KisDabRenderingJob::Dab; + KisDabRenderingJobSP job0 = queue.addDab(request1, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + + QVERIFY(job0); + QCOMPARE(job0->seqNo, 0); + QCOMPARE(job0->generationInfo.info.pos(), request1.info.pos()); + QCOMPARE(job0->type, KisDabRenderingJob::Dab); + QVERIFY(!job0->originalDevice); + QVERIFY(!job0->postprocessedDevice); + + cacheInterface->typeOverride = KisDabRenderingJob::Dab; + KisDabRenderingJobSP job1 = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + + QVERIFY(job1); + QCOMPARE(job1->seqNo, 1); + QCOMPARE(job1->generationInfo.info.pos(), request2.info.pos()); + QCOMPARE(job1->type, KisDabRenderingJob::Dab); + QVERIFY(!job1->originalDevice); + QVERIFY(!job1->postprocessedDevice); + + cacheInterface->typeOverride = KisDabRenderingJob::Postprocess; + KisDabRenderingJobSP job2 = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + QVERIFY(!job2); + + cacheInterface->typeOverride = KisDabRenderingJob::Postprocess; + KisDabRenderingJobSP job3 = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + QVERIFY(!job3); + + // we only added the dabs, but we haven't completed them yet + QVERIFY(!queue.hasPreparedDabs()); + QCOMPARE(queue.testingGetQueueSize(), 4); + + QList jobs; + QList renderedDabs; + + + { + // we've completed job0 + job0->originalDevice = new KisFixedPaintDevice(cs); + job0->postprocessedDevice = job0->originalDevice; + + jobs = queue.notifyJobFinished(job0->seqNo); + QVERIFY(jobs.isEmpty()); + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 1); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + QCOMPARE(queue.testingGetQueueSize(), 3); + } + + { + // we've completed job1 + job1->originalDevice = new KisFixedPaintDevice(cs); + job1->postprocessedDevice = job1->originalDevice; + + jobs = queue.notifyJobFinished(job1->seqNo); + QCOMPARE(jobs.size(), 2); + + QCOMPARE(jobs[0]->seqNo, 2); + QCOMPARE(jobs[1]->seqNo, 3); + + QVERIFY(jobs[0]->originalDevice); + QVERIFY(!jobs[0]->postprocessedDevice); + + QVERIFY(jobs[1]->originalDevice); + QVERIFY(!jobs[1]->postprocessedDevice); + + // pretend we have created a postprocessed device + jobs[0]->postprocessedDevice = new KisFixedPaintDevice(cs); + jobs[1]->postprocessedDevice = new KisFixedPaintDevice(cs); + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 1); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + + + // return back two postprocessed dabs + QList emptyJobs; + emptyJobs = queue.notifyJobFinished(jobs[0]->seqNo); + QVERIFY(emptyJobs.isEmpty()); + + emptyJobs = queue.notifyJobFinished(jobs[1]->seqNo); + QVERIFY(emptyJobs.isEmpty()); + + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 2); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + + // we delete all the painted jobs except the latest 'dab' job + QCOMPARE(queue.testingGetQueueSize(), 1); + } + + { + // add one more postprocessed job and take it + cacheInterface->typeOverride = KisDabRenderingJob::Postprocess; + KisDabRenderingJobSP job = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + + QVERIFY(job); + QCOMPARE(job->seqNo, 4); + QCOMPARE(job->generationInfo.info.pos(), request2.info.pos()); + ENTER_FUNCTION() << ppVar(job->type); + + QCOMPARE(job->type, KisDabRenderingJob::Postprocess); + QVERIFY(job->originalDevice); + QVERIFY(!job->postprocessedDevice); + + // the list should still be empty + QVERIFY(!queue.hasPreparedDabs()); + + // pretend we have created a postprocessed device + job->postprocessedDevice = new KisFixedPaintDevice(cs); + + // return back the postprocessed dab + QList emptyJobs; + emptyJobs = queue.notifyJobFinished(job->seqNo); + QVERIFY(emptyJobs.isEmpty()); + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 1); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + + // we delete all the painted jobs except the latest 'dab' job + QCOMPARE(queue.testingGetQueueSize(), 1); + } + + { + // add a 'dab' job and complete it. That will clear the queue! + + cacheInterface->typeOverride = KisDabRenderingJob::Dab; + KisDabRenderingJobSP job = queue.addDab(request1, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + + QVERIFY(job); + QCOMPARE(job->seqNo, 5); + QCOMPARE(job->generationInfo.info.pos(), request1.info.pos()); + QCOMPARE(job->type, KisDabRenderingJob::Dab); + QVERIFY(!job->originalDevice); + QVERIFY(!job->postprocessedDevice); + + // now the queue can be cleared from the completed dabs! + QCOMPARE(queue.testingGetQueueSize(), 1); + + job->originalDevice = new KisFixedPaintDevice(cs); + job->postprocessedDevice = job->originalDevice; + + jobs = queue.notifyJobFinished(job->seqNo); + QVERIFY(jobs.isEmpty()); + + // now we should have at least one job in prepared state + QVERIFY(queue.hasPreparedDabs()); + + // take the prepared dabs + renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 1); + + // the list should be empty again + QVERIFY(!queue.hasPreparedDabs()); + + // we do not delete the queue of jobs until the next 'dab' + // job arrives + QCOMPARE(queue.testingGetQueueSize(), 1); + } + +} + +#include <../KisDabRenderingQueueCache.h> + +void KisDabRenderingQueueTest::testRunningJobs() +{ + const KoColorSpace *cs = KoColorSpaceRegistry::instance()->rgb8(); + + KisDabRenderingQueueCache *cacheInterface = new KisDabRenderingQueueCache(); + // we do *not* initialize any options yet! + + KisDabRenderingQueue queue(cs, testResourcesFactory); + queue.setCacheInterface(cacheInterface); + + + KoColor color(Qt::red, cs); + QPointF pos1(10,10); + QPointF pos2(20,20); + KisDabShape shape; + KisPaintInformation pi1(pos1); + KisPaintInformation pi2(pos2); + + KisDabCacheUtils::DabRequestInfo request1(color, pos1, shape, pi1, 1.0); + KisDabCacheUtils::DabRequestInfo request2(color, pos2, shape, pi2, 1.0); + + KisDabRenderingJobSP job0 = queue.addDab(request1, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + + QVERIFY(job0); + QCOMPARE(job0->seqNo, 0); + QCOMPARE(job0->generationInfo.info.pos(), request1.info.pos()); + QCOMPARE(job0->type, KisDabRenderingJob::Dab); + + QVERIFY(!job0->originalDevice); + QVERIFY(!job0->postprocessedDevice); + + KisDabRenderingJobRunner runner(job0, &queue, 0); + runner.run(); + + QVERIFY(job0->originalDevice); + QVERIFY(job0->postprocessedDevice); + QCOMPARE(job0->originalDevice, job0->postprocessedDevice); + + QVERIFY(!job0->originalDevice->bounds().isEmpty()); + + KisDabRenderingJobSP job1 = queue.addDab(request2, OPACITY_OPAQUE_F, OPACITY_OPAQUE_F); + QVERIFY(!job1); + + QList renderedDabs = queue.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 2); + + // we did the caching + QVERIFY(renderedDabs[0].device == renderedDabs[1].device); + + QCOMPARE(renderedDabs[0].offset, QPoint(5,5)); + QCOMPARE(renderedDabs[1].offset, QPoint(15,15)); +} + +#include "../KisDabRenderingExecutor.h" +#include "KisFakeRunnableStrokeJobsExecutor.h" + +void KisDabRenderingQueueTest::testExecutor() +{ + const KoColorSpace *cs = KoColorSpaceRegistry::instance()->rgb8(); + + QScopedPointer runner(new KisFakeRunnableStrokeJobsExecutor()); + + KisDabRenderingExecutor executor(cs, testResourcesFactory, runner.data()); + + KoColor color(Qt::red, cs); + QPointF pos1(10,10); + QPointF pos2(20,20); + KisDabShape shape; + KisPaintInformation pi1(pos1); + KisPaintInformation pi2(pos2); + + KisDabCacheUtils::DabRequestInfo request1(color, pos1, shape, pi1, 1.0); + KisDabCacheUtils::DabRequestInfo request2(color, pos2, shape, pi2, 1.0); + + executor.addDab(request1, 0.5, 0.25); + executor.addDab(request2, 0.125, 1.0); + + QList renderedDabs = executor.takeReadyDabs(); + QCOMPARE(renderedDabs.size(), 2); + + // we did the caching + QVERIFY(renderedDabs[0].device == renderedDabs[1].device); + + QCOMPARE(renderedDabs[0].offset, QPoint(5,5)); + QCOMPARE(renderedDabs[1].offset, QPoint(15,15)); + + QCOMPARE(renderedDabs[0].opacity, 0.5); + QCOMPARE(renderedDabs[0].flow, 0.25); + QCOMPARE(renderedDabs[1].opacity, 0.125); + QCOMPARE(renderedDabs[1].flow, 1.0); + +} + +QTEST_MAIN(KisDabRenderingQueueTest) diff --git a/libs/image/kis_projection_updates_filter.cpp b/plugins/paintops/defaultpaintops/brush/tests/KisDabRenderingQueueTest.h similarity index 62% copy from libs/image/kis_projection_updates_filter.cpp copy to plugins/paintops/defaultpaintops/brush/tests/KisDabRenderingQueueTest.h index e1493ec367..fa3f66d076 100644 --- a/libs/image/kis_projection_updates_filter.cpp +++ b/plugins/paintops/defaultpaintops/brush/tests/KisDabRenderingQueueTest.h @@ -1,36 +1,35 @@ /* - * Copyright (c) 2014 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_projection_updates_filter.h" +#ifndef KISDABRENDERINGQUEUETEST_H +#define KISDABRENDERINGQUEUETEST_H +#include -#include -#include - -KisProjectionUpdatesFilter::~KisProjectionUpdatesFilter() +class KisDabRenderingQueueTest : public QObject { -} + Q_OBJECT +private Q_SLOTS: + void testCachedDabs(); + void testPostprocessedDabs(); + void testRunningJobs(); -bool KisDropAllProjectionUpdatesFilter::filter(KisImage *image, KisNode *node, const QRect& rect, bool resetAnimationCache) -{ - Q_UNUSED(image); - Q_UNUSED(node); - Q_UNUSED(rect); - Q_UNUSED(resetAnimationCache); - return true; -} + void testExecutor(); +}; + +#endif // KISDABRENDERINGQUEUETEST_H diff --git a/plugins/paintops/defaultpaintops/defaultpaintops_plugin.cc b/plugins/paintops/defaultpaintops/defaultpaintops_plugin.cc index b50be936f0..03d5bde99d 100644 --- a/plugins/paintops/defaultpaintops/defaultpaintops_plugin.cc +++ b/plugins/paintops/defaultpaintops/defaultpaintops_plugin.cc @@ -1,57 +1,57 @@ /* * defaultpaintops_plugin.cc -- Part of Krita * * Copyright (c) 2004 Boudewijn Rempt (boud@valdyas.org) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "defaultpaintops_plugin.h" #include #include #include #include #include "kis_simple_paintop_factory.h" #include "kis_brushop.h" #include "kis_brushop_settings_widget.h" #include "kis_duplicateop.h" #include "kis_duplicateop_settings.h" #include "kis_global.h" #include -#include "kis_brush_based_paintop_settings.h" +#include "KisBrushOpSettings.h" #include "kis_brush_server.h" #include "kis_duplicateop_settings_widget.h" K_PLUGIN_FACTORY_WITH_JSON(DefaultPaintOpsPluginFactory, "kritadefaultpaintops.json", registerPlugin();) DefaultPaintOpsPlugin::DefaultPaintOpsPlugin(QObject *parent, const QVariantList &) : QObject(parent) { KisPaintOpRegistry *r = KisPaintOpRegistry::instance(); - r->add(new KisSimplePaintOpFactory("paintbrush", i18nc("Pixel paintbrush", "Pixel"), KisPaintOpFactory::categoryStable(), "krita-paintbrush.png", QString(), QStringList(), 1)); + r->add(new KisSimplePaintOpFactory("paintbrush", i18nc("Pixel paintbrush", "Pixel"), KisPaintOpFactory::categoryStable(), "krita-paintbrush.png", QString(), QStringList(), 1)); r->add(new KisSimplePaintOpFactory("duplicate", i18nc("clone paintbrush (previously \"Duplicate\")", "Clone"), KisPaintOpFactory::categoryStable(), "krita-duplicate.png", QString(), QStringList(COMPOSITE_COPY), 15)); KisBrushServer::instance(); } DefaultPaintOpsPlugin::~DefaultPaintOpsPlugin() { } #include "defaultpaintops_plugin.moc" diff --git a/plugins/paintops/deform/deform_brush.cpp b/plugins/paintops/deform/deform_brush.cpp index 569c720df9..c1b4f1951a 100644 --- a/plugins/paintops/deform/deform_brush.cpp +++ b/plugins/paintops/deform/deform_brush.cpp @@ -1,294 +1,291 @@ /* * Copyright (c) 2008,2010 Lukáš Tvrdý * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "deform_brush.h" #include "kis_painter.h" #include "kis_fixed_paint_device.h" #include #include #include #include #include #include #include #include #include const qreal degToRad = M_PI / 180.0; DeformBrush::DeformBrush() { m_firstPaint = false; m_counter = 1; m_deformAction = 0; } DeformBrush::~DeformBrush() { delete m_deformAction; } void DeformBrush::initDeformAction() { DeformModes mode = DeformModes(m_properties->deform_action - 1); switch (mode) { case GROW: case SHRINK: { m_deformAction = new DeformScale(); break; } case SWIRL_CW: case SWIRL_CCW: { m_deformAction = new DeformRotation(); break; } case MOVE: { m_deformAction = new DeformMove(); static_cast(m_deformAction)->setFactor(m_properties->deform_amount); break; } case LENS_IN: case LENS_OUT: { m_deformAction = new DeformLens(); static_cast(m_deformAction)->setLensFactor(m_properties->deform_amount, 0.0); static_cast(m_deformAction)->setMode(mode == LENS_OUT); break; } case DEFORM_COLOR: { m_deformAction = new DeformColor(); static_cast(m_deformAction)->setFactor(m_properties->deform_amount); break; } default: { m_deformAction = new DeformBase(); break; } } } bool DeformBrush::setupAction( DeformModes mode, const QPointF& pos, QTransform const& rotation) { switch (mode) { case GROW: case SHRINK: { // grow or shrink, the sign decide qreal sign = (mode == GROW) ? 1.0 : -1.0; qreal factor; if (m_properties->deform_use_counter) { factor = (1.0 + sign * (m_counter * m_counter / 100.0)); } else { factor = (1.0 + sign * (m_properties->deform_amount)); } dynamic_cast(m_deformAction)->setFactor(factor); break; } case SWIRL_CW: case SWIRL_CCW: { // CW or CCW, the sign decide qreal sign = (mode == SWIRL_CW) ? 1.0 : -1.0; qreal factor; if (m_properties->deform_use_counter) { factor = m_counter * sign * degToRad; } else { factor = (360 * m_properties->deform_amount * 0.5) * sign * degToRad; } dynamic_cast(m_deformAction)->setAlpha(factor); break; } case MOVE: { if (m_firstPaint == false) { m_prevX = pos.x(); m_prevY = pos.y(); static_cast(m_deformAction)->setDistance(0.0, 0.0); m_firstPaint = true; return false; } else { qreal xDistance = pos.x() - m_prevX; qreal yDistance = pos.y() - m_prevY; rotation.map(xDistance, yDistance, &xDistance, &yDistance); static_cast(m_deformAction)->setDistance(xDistance, yDistance); m_prevX = pos.x(); m_prevY = pos.y(); } break; } case LENS_IN: case LENS_OUT: { static_cast(m_deformAction)->setMaxDistance(m_sizeProperties->brush_diameter * 0.5, m_sizeProperties->brush_diameter * 0.5); break; } case DEFORM_COLOR: { // no run-time setup break; } default: { break; } } return true; } KisFixedPaintDeviceSP DeformBrush::paintMask(KisFixedPaintDeviceSP dab, KisPaintDeviceSP layer, qreal scale, qreal rotation, QPointF pos, qreal subPixelX, qreal subPixelY, int dabX, int dabY) { KisFixedPaintDeviceSP mask = new KisFixedPaintDevice(KoColorSpaceRegistry::instance()->alpha8()); KisCrossDeviceColorPicker colorPicker(layer, dab); qreal fWidth = maskWidth(scale); qreal fHeight = maskHeight(scale); int dstWidth = qRound(m_maskRect.width()); int dstHeight = qRound(m_maskRect.height()); // clear if (dab->bounds().width() != dstWidth || dab->bounds().height() != dstHeight) { dab->setRect(m_maskRect.toRect()); - dab->initialize(); - } - else { - dab->clear(m_maskRect.toRect()); + dab->lazyGrowBufferWithoutInitialization(); } qreal const centerX = dstWidth * 0.5 + subPixelX; qreal const centerY = dstHeight * 0.5 + subPixelY; qreal const majorAxis = 2.0 / fWidth; qreal const minorAxis = 2.0 / fHeight; qreal distance; QTransform forwardRotationMatrix; forwardRotationMatrix.rotateRadians(-rotation); QTransform reverseRotationMatrix; reverseRotationMatrix.rotateRadians(rotation); // if can't paint, stop if (!setupAction(DeformModes(m_properties->deform_action - 1), pos, forwardRotationMatrix)) { return 0; } mask->setRect(dab->bounds()); - mask->initialize(); + mask->lazyGrowBufferWithoutInitialization(); quint8* maskPointer = mask->data(); qint8 maskPixelSize = mask->pixelSize(); quint8* dabPointer = dab->data(); int dabPixelSize = dab->colorSpace()->pixelSize(); for (int y = 0; y < dstHeight; y++) { for (int x = 0; x < dstWidth; x++) { qreal maskX = x - centerX; qreal maskY = y - centerY; forwardRotationMatrix.map(maskX, maskY, &maskX, &maskY); distance = norme(maskX * majorAxis, maskY * minorAxis); if (distance > 1.0) { // leave there OPACITY TRANSPARENT pixel (default pixel) colorPicker.pickOldColor(x + dabX, y + dabY, dabPointer); dabPointer += dabPixelSize; *maskPointer = OPACITY_TRANSPARENT_U8; maskPointer += maskPixelSize; continue; } if (m_sizeProperties->brush_density != 1.0) { if (m_sizeProperties->brush_density < drand48()) { dabPointer += dabPixelSize; *maskPointer = OPACITY_TRANSPARENT_U8; maskPointer += maskPixelSize; continue; } } m_deformAction->transform(&maskX, &maskY, distance); reverseRotationMatrix.map(maskX, maskY, &maskX, &maskY); maskX += pos.x(); maskY += pos.y(); if (!m_properties->deform_use_bilinear) { maskX = qRound(maskX); maskY = qRound(maskY); } if (m_properties->deform_use_old_data) { colorPicker.pickOldColor(maskX, maskY, dabPointer); } else { colorPicker.pickColor(maskX, maskY, dabPointer); } dabPointer += dabPixelSize; *maskPointer = OPACITY_OPAQUE_U8; maskPointer += maskPixelSize; } } m_counter++; return mask; } void DeformBrush::debugColor(const quint8* data, KoColorSpace * cs) { QColor rgbcolor; cs->toQColor(data, &rgbcolor); dbgPlugins << "RGBA: (" << rgbcolor.red() << ", " << rgbcolor.green() << ", " << rgbcolor.blue() << ", " << rgbcolor.alpha() << ")"; } QPointF DeformBrush::hotSpot(qreal scale, qreal rotation) { qreal fWidth = maskWidth(scale); qreal fHeight = maskHeight(scale); QTransform m; m.reset(); m.rotateRadians(rotation); m_maskRect = QRect(0, 0, fWidth, fHeight); m_maskRect.translate(-m_maskRect.center()); m_maskRect = m.mapRect(m_maskRect); m_maskRect.translate(-m_maskRect.topLeft()); return m_maskRect.center(); } diff --git a/plugins/paintops/libpaintop/CMakeLists.txt b/plugins/paintops/libpaintop/CMakeLists.txt index 4743cc32bd..ea0af1519b 100644 --- a/plugins/paintops/libpaintop/CMakeLists.txt +++ b/plugins/paintops/libpaintop/CMakeLists.txt @@ -1,102 +1,104 @@ set(kritalibpaintop_LIB_SRCS kis_airbrush_option.cpp kis_auto_brush_widget.cpp kis_spacing_selection_widget.cpp kis_bidirectional_mixing_option.cpp kis_bidirectional_mixing_option_widget.cpp kis_brush_based_paintop.cpp kis_brush_chooser.cpp kis_brush_option_widget.cpp kis_brush_option.cpp kis_brush_selection_widget.cpp kis_color_option.cpp kis_color_source.cpp kis_color_source_option.cpp kis_color_source_option_widget.cpp kis_curve_option.cpp kis_curve_option_widget.cpp kis_curve_option_uniform_property.cpp kis_custom_brush_widget.cpp kis_clipboard_brush_widget.cpp kis_dynamic_sensor.cc + KisDabCacheUtils.cpp + kis_dab_cache_base.cpp kis_dab_cache.cpp kis_filter_option.cpp kis_multi_sensors_model_p.cpp kis_multi_sensors_selector.cpp kis_paint_action_type_option.cpp kis_precision_option.cpp kis_pressure_darken_option.cpp kis_pressure_hsv_option.cpp kis_pressure_opacity_option.cpp kis_pressure_flow_option.cpp kis_pressure_mirror_option.cpp kis_pressure_scatter_option.cpp kis_pressure_scatter_option_widget.cpp kis_pressure_sharpness_option.cpp kis_pressure_sharpness_option_widget.cpp kis_pressure_mirror_option_widget.cpp kis_pressure_rotation_option.cpp kis_pressure_size_option.cpp kis_pressure_spacing_option.cpp kis_pressure_rate_option.cpp kis_pressure_softness_option.cpp kis_pressure_mix_option.cpp kis_pressure_gradient_option.cpp kis_pressure_flow_opacity_option.cpp kis_pressure_flow_opacity_option_widget.cpp kis_pressure_spacing_option_widget.cpp kis_pressure_ratio_option.cpp kis_current_outline_fetcher.cpp kis_text_brush_chooser.cpp kis_brush_based_paintop_options_widget.cpp kis_brush_based_paintop_settings.cpp kis_compositeop_option.cpp kis_texture_option.cpp kis_texture_chooser.cpp kis_pressure_texture_strength_option.cpp kis_embedded_pattern_manager.cpp sensors/kis_dynamic_sensors.cc sensors/kis_dynamic_sensor_drawing_angle.cpp sensors/kis_dynamic_sensor_distance.cc sensors/kis_dynamic_sensor_time.cc sensors/kis_dynamic_sensor_fade.cpp sensors/kis_dynamic_sensor_fuzzy.cpp ) ki18n_wrap_ui(kritalibpaintop_LIB_SRCS forms/wdgautobrush.ui forms/wdgBrushSizeOptions.ui forms/wdgcurveoption.ui forms/wdgcustombrush.ui forms/wdgclipboardbrush.ui forms/wdgtextbrush.ui forms/wdgincremental.ui forms/wdgmultisensorsselector.ui forms/wdgairbrush.ui forms/wdgfilteroption.ui forms/wdgcoloroptions.ui forms/wdgbrushchooser.ui forms/wdgpredefinedbrushchooser.ui forms/wdgtexturechooser.ui forms/wdgCompositeOpOption.ui forms/wdgflowopacityoption.ui sensors/SensorDistanceConfiguration.ui sensors/SensorTimeConfiguration.ui sensors/SensorFadeConfiguration.ui ) add_library(kritalibpaintop SHARED ${kritalibpaintop_LIB_SRCS} ) generate_export_header(kritalibpaintop BASE_NAME kritapaintop EXPORT_MACRO_NAME PAINTOP_EXPORT) target_link_libraries(kritalibpaintop kritaui kritalibbrush kritawidgetutils) target_link_libraries(kritalibpaintop LINK_INTERFACE_LIBRARIES kritaui kritalibbrush) set_target_properties(kritalibpaintop PROPERTIES VERSION ${GENERIC_KRITA_LIB_VERSION} SOVERSION ${GENERIC_KRITA_LIB_SOVERSION} ) install(TARGETS kritalibpaintop ${INSTALL_TARGETS_DEFAULT_ARGS}) add_subdirectory(tests) diff --git a/plugins/paintops/libpaintop/KisDabCacheUtils.cpp b/plugins/paintops/libpaintop/KisDabCacheUtils.cpp new file mode 100644 index 0000000000..a17bec115f --- /dev/null +++ b/plugins/paintops/libpaintop/KisDabCacheUtils.cpp @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KisDabCacheUtils.h" + +#include "kis_brush.h" +#include "kis_paint_device.h" +#include "kis_fixed_paint_device.h" +#include "kis_color_source.h" + +#include +#include + +#include + +namespace KisDabCacheUtils +{ + +DabRenderingResources::DabRenderingResources() +{ +} + +DabRenderingResources::~DabRenderingResources() +{ +} + +void DabRenderingResources::syncResourcesToSeqNo(int seqNo, const KisPaintInformation &info) +{ + brush->prepareForSeqNo(info, seqNo); +} + +QRect correctDabRectWhenFetchedFromCache(const QRect &dabRect, + const QSize &realDabSize) +{ + int diffX = (realDabSize.width() - dabRect.width()) / 2; + int diffY = (realDabSize.height() - dabRect.height()) / 2; + + return QRect(dabRect.x() - diffX, dabRect.y() - diffY, + realDabSize.width() , realDabSize.height()); +} + + +void generateDab(const DabGenerationInfo &di, DabRenderingResources *resources, KisFixedPaintDeviceSP *dab) +{ + KIS_SAFE_ASSERT_RECOVER_RETURN(*dab); + const KoColorSpace *cs = (*dab)->colorSpace(); + + + if (resources->brush->brushType() == IMAGE || resources->brush->brushType() == PIPE_IMAGE) { + *dab = resources->brush->paintDevice(cs, di.shape, di.info, + di.subPixel.x(), + di.subPixel.y()); + } else if (di.solidColorFill) { + resources->brush->mask(*dab, + di.paintColor, + di.shape, + di.info, + di.subPixel.x(), di.subPixel.y(), + di.softnessFactor); + } + else { + if (!resources->colorSourceDevice || + *cs != *resources->colorSourceDevice->colorSpace()) { + + resources->colorSourceDevice = new KisPaintDevice(cs); + } + else { + resources->colorSourceDevice->clear(); + } + + QRect maskRect(QPoint(), di.dstDabRect.size()); + resources->colorSource->colorize(resources->colorSourceDevice, maskRect, di.info.pos().toPoint()); + delete resources->colorSourceDevice->convertTo(cs); + + resources->brush->mask(*dab, resources->colorSourceDevice, + di.shape, + di.info, + di.subPixel.x(), di.subPixel.y(), + di.softnessFactor); + } + + if (!di.mirrorProperties.isEmpty()) { + (*dab)->mirror(di.mirrorProperties.horizontalMirror, + di.mirrorProperties.verticalMirror); + } +} + +void postProcessDab(KisFixedPaintDeviceSP dab, + const QPoint &dabTopLeft, + const KisPaintInformation& info, + DabRenderingResources *resources) +{ + if (resources->sharpnessOption) { + resources->sharpnessOption->applyThreshold(dab); + } + + if (resources->textureOption) { + resources->textureOption->apply(dab, dabTopLeft, info); + } +} + +} + diff --git a/plugins/paintops/libpaintop/KisDabCacheUtils.h b/plugins/paintops/libpaintop/KisDabCacheUtils.h new file mode 100644 index 0000000000..6e694a08d3 --- /dev/null +++ b/plugins/paintops/libpaintop/KisDabCacheUtils.h @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KISDABCACHEUTILS_H +#define KISDABCACHEUTILS_H + +#include +#include + +#include "kis_types.h" + +#include +#include "kis_dab_shape.h" + +#include "kritapaintop_export.h" +#include + +class KisBrush; +typedef KisSharedPtr KisBrushSP; + +class KisColorSource; +class KisPressureSharpnessOption; +class KisTextureProperties; + + +namespace KisDabCacheUtils +{ + +struct PAINTOP_EXPORT DabRenderingResources +{ + DabRenderingResources(); + virtual ~DabRenderingResources(); + + virtual void syncResourcesToSeqNo(int seqNo, const KisPaintInformation &info); + + KisBrushSP brush; + QScopedPointer colorSource; + + QScopedPointer sharpnessOption; + QScopedPointer textureOption; + + KisPaintDeviceSP colorSourceDevice; + +private: + DabRenderingResources(const DabRenderingResources &rhs) = delete; +}; + +typedef std::function ResourcesFactory; + +struct PAINTOP_EXPORT DabRequestInfo +{ + DabRequestInfo(const KoColor &_color, + const QPointF &_cursorPoint, + const KisDabShape &_shape, + const KisPaintInformation &_info, + qreal _softnessFactor) + : color(_color), + cursorPoint(_cursorPoint), + shape(_shape), + info(_info), + softnessFactor(_softnessFactor) + { + } + + const KoColor &color; + const QPointF &cursorPoint; + const KisDabShape &shape; + const KisPaintInformation &info; + const qreal softnessFactor; + +private: + DabRequestInfo(const DabRequestInfo &rhs); +}; + +struct PAINTOP_EXPORT DabGenerationInfo +{ + MirrorProperties mirrorProperties; + KisDabShape shape; + QRect dstDabRect; + QPointF subPixel; + bool solidColorFill = true; + KoColor paintColor; + KisPaintInformation info; + qreal softnessFactor = 1.0; + + bool needsPostprocessing = false; +}; + +PAINTOP_EXPORT QRect correctDabRectWhenFetchedFromCache(const QRect &dabRect, + const QSize &realDabSize); + +PAINTOP_EXPORT void generateDab(const DabGenerationInfo &di, + DabRenderingResources *resources, + KisFixedPaintDeviceSP *dab); + +PAINTOP_EXPORT void postProcessDab(KisFixedPaintDeviceSP dab, + const QPoint &dabTopLeft, + const KisPaintInformation& info, + DabRenderingResources *resources); + +} + +template class QSharedPointer; +class KisDabRenderingJob; +typedef QSharedPointer KisDabRenderingJobSP; + +#endif // KISDABCACHEUTILS_H diff --git a/plugins/paintops/libpaintop/forms/wdgcurveoption.ui b/plugins/paintops/libpaintop/forms/wdgcurveoption.ui index 73adac0037..58652eb359 100644 --- a/plugins/paintops/libpaintop/forms/wdgcurveoption.ui +++ b/plugins/paintops/libpaintop/forms/wdgcurveoption.ui @@ -1,443 +1,620 @@ WdgCurveOption 0 0 507 344 15 0 0 Strength: 0 0 20 20 16777215 16777215 190 0 Enable Pen Settings 24 24 16 16 24 24 16 16 24 24 16 16 24 24 16 16 24 24 16 16 24 24 16 16 24 24 16 16 24 24 16 16 Ctrl+S Qt::Horizontal 2 2 0 0 190 0 160 16777215 Qt::Horizontal 2 2 Qt::Vertical 2 2 0 0 TextLabel Qt::Horizontal 40 20 0 0 TextLabel 1 0 220 190 16777215 16777215 0 0 TextLabel Qt::Vertical 20 40 0 0 TextLabel Share curve across all settings true + + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + + Share curve across all settings + + + true + + + + + + + + 1 + 0 + + + + + 0 + 230 + + + + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + TextLabel + + + + + + + + + + + <html><head/><body> +<p>Curve calculation mode changes how 2 or more curve works together<br/></p> +<p> multiply (default): all values from curves multiplies </p><p>(0.8 pressure) * (0.5 speed) = 0.4 <br/></p> +<p> addition: all values from curves adds</p><p>(0.6 pressure) + (0.3 speed) = 0.9<br/></p> +<p> maximum value</p><p>(0.7 pressure), (0.3 speed) = 0.7<br/></p> +<p> minimum value </p><p>(0.7 pressure), (0.3 speed) = 0.3<br/></p> +<p> difference between min and max values</p><p>(0.8 pressure), (0.3 speed), (0.6 fade) = 0.5</p></body></html> + + + Curves calculation mode: + + + + + + + <html><head/><body> +<p>Curve calculation mode changes how 2 or more curve works together<br/></p> +<p> multiply (default): all values from curves multiplies </p><p>(0.8 pressure) * (0.5 speed) = 0.4 <br/></p> +<p> addition: all values from curves adds</p><p>(0.6 pressure) + (0.3 speed) = 0.9<br/></p> +<p> maximum value</p><p>(0.7 pressure), (0.3 speed) = 0.7<br/></p> +<p> minimum value </p><p>(0.7 pressure), (0.3 speed) = 0.3<br/></p> +<p> difference between min and max values</p><p>(0.8 pressure), (0.3 speed), (0.6 fade) = 0.5</p></body></html> + + + false + + + + multiply + + + + + addition + + + + + maximum + + + + + minimum + + + + + difference + + + + + + + + KisDoubleSliderSpinBox QWidget
kis_slider_spin_box.h
1
KisMultiSensorsSelector QWidget
kis_multi_sensors_selector.h
1
KisCurveWidget QWidget
widgets/kis_curve_widget.h
1
diff --git a/plugins/paintops/libpaintop/kis_airbrush_option.cpp b/plugins/paintops/libpaintop/kis_airbrush_option.cpp index 96e527a757..496c5c55ea 100644 --- a/plugins/paintops/libpaintop/kis_airbrush_option.cpp +++ b/plugins/paintops/libpaintop/kis_airbrush_option.cpp @@ -1,139 +1,139 @@ /* * Copyright (c) 2008 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_airbrush_option.h" #include "kis_paintop_settings.h" #include #include #include #include "ui_wdgairbrush.h" const qreal MINIMUM_RATE = 1.0; const qreal MAXIMUM_RATE = 1000.0; const int RATE_NUM_DECIMALS = 2; const qreal RATE_EXPONENT_RATIO = 2.0; const qreal RATE_SINGLE_STEP = 1.0; const qreal DEFAULT_RATE = 20.0; class KisAirbrushWidget: public QWidget, public Ui::WdgAirbrush { public: KisAirbrushWidget(QWidget *parent = 0, bool canIgnoreSpacing = true) : QWidget(parent) { setupUi(this); sliderRate->setRange(MINIMUM_RATE, MAXIMUM_RATE, RATE_NUM_DECIMALS); sliderRate->setExponentRatio(RATE_EXPONENT_RATIO); sliderRate->setSingleStep(RATE_SINGLE_STEP); sliderRate->setValue(DEFAULT_RATE); checkBoxIgnoreSpacing->setVisible(canIgnoreSpacing); checkBoxIgnoreSpacing->setEnabled(canIgnoreSpacing); } }; struct KisAirbrushOption::Private { public: bool ignoreSpacing; // We store the airbrush interval (in milliseconds) instead of the rate because the interval is // likely to be accessed more often. qreal airbrushInterval; - KisAirbrushWidget *configPage; + QScopedPointer configPage; }; KisAirbrushOption::KisAirbrushOption(bool enabled, bool canIgnoreSpacing) : KisPaintOpOption(KisPaintOpOption::COLOR, enabled) , m_d(new Private()) { setObjectName("KisAirbrushOption"); // Initialize GUI. m_checkable = true; - m_d->configPage = new KisAirbrushWidget(nullptr, canIgnoreSpacing); + m_d->configPage.reset(new KisAirbrushWidget(nullptr, canIgnoreSpacing)); connect(m_d->configPage->sliderRate, SIGNAL(valueChanged(qreal)), SLOT(slotIntervalChanged())); connect(m_d->configPage->checkBoxIgnoreSpacing, SIGNAL(toggled(bool)), SLOT(slotIgnoreSpacingChanged())); - setConfigurationPage(m_d->configPage); + setConfigurationPage(m_d->configPage.data()); // Read initial configuration from the GUI. updateIgnoreSpacing(); updateInterval(); } KisAirbrushOption::~KisAirbrushOption() { delete m_d; } void KisAirbrushOption::writeOptionSetting(KisPropertiesConfigurationSP setting) const { KIS_SAFE_ASSERT_RECOVER (m_d->airbrushInterval > 0.0) { m_d->airbrushInterval = 1.0; } setting->setProperty(AIRBRUSH_ENABLED, isChecked()); setting->setProperty(AIRBRUSH_RATE, 1000.0 / m_d->airbrushInterval); setting->setProperty(AIRBRUSH_IGNORE_SPACING, m_d->ignoreSpacing); } void KisAirbrushOption::readOptionSetting(const KisPropertiesConfigurationSP setting) { setChecked(setting->getBool(AIRBRUSH_ENABLED)); // Update settings in the widget. The widget's signals should cause the changes to be propagated // to this->m_d as well. m_d->configPage->sliderRate->setValue(setting->getDouble(AIRBRUSH_RATE, DEFAULT_RATE)); m_d->configPage->checkBoxIgnoreSpacing->setChecked(setting->getBool(AIRBRUSH_IGNORE_SPACING, false)); } qreal KisAirbrushOption::airbrushInterval() const { return m_d->airbrushInterval; } bool KisAirbrushOption::ignoreSpacing() const { return m_d->ignoreSpacing; } void KisAirbrushOption::slotIntervalChanged() { updateInterval(); emitSettingChanged(); } void KisAirbrushOption::slotIgnoreSpacingChanged() { updateIgnoreSpacing(); emitSettingChanged(); } void KisAirbrushOption::updateInterval() { // Get rate in dabs per second, then convert to interval in milliseconds. qreal rate = m_d->configPage->sliderRate->value(); KIS_SAFE_ASSERT_RECOVER(rate > 0.0) { rate = 1.0; } m_d->airbrushInterval = 1000.0 / rate; } void KisAirbrushOption::updateIgnoreSpacing() { m_d->ignoreSpacing = m_d->configPage->checkBoxIgnoreSpacing->isChecked(); } diff --git a/plugins/paintops/libpaintop/kis_bidirectional_mixing_option.cpp b/plugins/paintops/libpaintop/kis_bidirectional_mixing_option.cpp index 80afa13347..10d2b7aef7 100644 --- a/plugins/paintops/libpaintop/kis_bidirectional_mixing_option.cpp +++ b/plugins/paintops/libpaintop/kis_bidirectional_mixing_option.cpp @@ -1,131 +1,131 @@ /* * Copyright (c) 2008 Boudewijn Rempt * Copyright (c) 2008 Emanuele Tamponi * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_bidirectional_mixing_option.h" #include #include #include #include #include #include #include #include #include #include #include #include "kis_bidirectional_mixing_option_widget.h" #include KisBidirectionalMixingOption::KisBidirectionalMixingOption() : m_mixingEnabled(false) { } KisBidirectionalMixingOption::~KisBidirectionalMixingOption() { } void KisBidirectionalMixingOption::apply(KisPaintDeviceSP dab, KisPaintDeviceSP device, KisPainter* painter, qint32 sx, qint32 sy, qint32 sw, qint32 sh, quint8 pressure, const QRect& dstRect) { if (!m_mixingEnabled) return; const KoColorSpace *cs = dab->colorSpace(); KisPaintDeviceSP canvas = new KisPaintDevice(cs); KisPainter p(canvas); p.setCompositeOp(COMPOSITE_COPY); p.bitBlt(sx, sy, device, dstRect.x(), dstRect.y(), sw, sh); int count = cs->channelCount(); QRect srcRect(sx, sy, sw, sh); KisSequentialConstIterator cit(canvas, srcRect); KisSequentialIterator dit(dab, srcRect); QVector cc(count), dc(count); do { if (cs->opacityU8(dit.rawData()) > 10 && cs->opacityU8(cit.rawDataConst()) > 10) { cs->normalisedChannelsValue(cit.rawDataConst(), cc); cs->normalisedChannelsValue(dit.rawData(), dc); for (int i = 0; i < count; i++) { dc[i] = (1.0 - 0.4 * pressure) * cc[i] + 0.4 * pressure * dc[i]; } cs->fromNormalisedChannelsValue(dit.rawData(), dc); if (dit.x() == (int)(sw / 2) && dit.y() == (int)(sh / 2)) { painter->setPaintColor(KoColor(dit.rawData(), cs)); } } dit.nextPixel(); } while(cit.nextPixel()); } void KisBidirectionalMixingOption::applyFixed(KisFixedPaintDeviceSP dab, KisPaintDeviceSP device, KisPainter* painter, qint32 sx, qint32 sy, qint32 sw, qint32 sh, quint8 pressure, const QRect& dstRect) { Q_UNUSED(sx); Q_UNUSED(sy); if (!m_mixingEnabled) return; KisFixedPaintDevice canvas(device->colorSpace()); canvas.setRect(QRect(dstRect.x(), dstRect.y(), sw, sh)); - canvas.initialize(); + canvas.lazyGrowBufferWithoutInitialization(); device->readBytes(canvas.data(), canvas.bounds()); const KoColorSpace* cs = dab->colorSpace(); int channelCount = cs->channelCount(); quint8* dabPointer = dab->data(); quint8* canvasPointer = canvas.data(); QVector cc(channelCount); QVector dc(channelCount); for (int y = 0; y < sh; y++) { for (int x = 0; x < sw; x++) { if (cs->opacityU8(dabPointer) > 10 && cs->opacityU8(canvasPointer) > 10) { cs->normalisedChannelsValue(canvasPointer, cc); cs->normalisedChannelsValue(dabPointer, dc); for (int i = 0; i < channelCount ; i++) { dc[i] = (1.0 - 0.4 * pressure) * cc[i] + 0.4 * pressure * dc[i]; } cs->fromNormalisedChannelsValue(dabPointer, dc); if (x == (int)(sw / 2) && y == (int)(sh / 2)) { painter->setPaintColor(KoColor(dabPointer, cs)); } } } dabPointer += dab->pixelSize(); canvasPointer += canvas.pixelSize(); } } void KisBidirectionalMixingOption::readOptionSetting(const KisPropertiesConfigurationSP setting) { m_mixingEnabled = setting->getBool(BIDIRECTIONAL_MIXING_ENABLED, false); } diff --git a/plugins/paintops/libpaintop/kis_brush_based_paintop.h b/plugins/paintops/libpaintop/kis_brush_based_paintop.h index 305fbf3ecd..3d0ee64c48 100644 --- a/plugins/paintops/libpaintop/kis_brush_based_paintop.h +++ b/plugins/paintops/libpaintop/kis_brush_based_paintop.h @@ -1,101 +1,103 @@ /* * Copyright (c) 2008 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_BRUSH_BASED_PAINTOP_H #define KIS_BRUSH_BASED_PAINTOP_H #include "kritapaintop_export.h" #include #include "kis_dab_cache.h" #include "kis_brush.h" #include "kis_texture_option.h" #include "kis_precision_option.h" #include "kis_airbrush_option.h" #include "kis_pressure_mirror_option.h" #include class KisPropertiesConfiguration; class KisPressureSpacingOption; class KisPressureRateOption; class KisDabCache; /// Internal class TextBrushInitializationWorkaround { public: TextBrushInitializationWorkaround(); ~TextBrushInitializationWorkaround(); static TextBrushInitializationWorkaround* instance(); void preinitialize(KisPropertiesConfigurationSP settings); KisBrushSP tryGetBrush(const KisPropertiesConfigurationSP settings); private: KisBrushSP m_brush; KisPropertiesConfigurationSP m_settings; }; /** * This is a base class for paintops that use a KisBrush or derived * brush to paint with. This is mainly important for the spacing * generation. */ class PAINTOP_EXPORT KisBrushBasedPaintOp : public KisPaintOp { public: KisBrushBasedPaintOp(const KisPropertiesConfigurationSP settings, KisPainter* painter); ~KisBrushBasedPaintOp() override; bool checkSizeTooSmall(qreal scale); KisSpacingInformation effectiveSpacing(qreal scale) const; KisSpacingInformation effectiveSpacing(qreal scale, qreal rotation, const KisPaintInformation &pi) const; KisSpacingInformation effectiveSpacing(qreal scale, qreal rotation, const KisPressureSpacingOption &spacingOption, const KisPaintInformation &pi) const; KisSpacingInformation effectiveSpacing(qreal scale, qreal rotation, const KisAirbrushOption *airbrushOption, const KisPressureSpacingOption *spacingOption, const KisPaintInformation &pi) const; ///Reimplemented, false if brush is 0 bool canPaint() const override; #ifdef HAVE_THREADED_TEXT_RENDERING_WORKAROUND typedef int needs_preinitialization; static void preinitializeOpStatically(KisPaintOpSettingsSP settings); #endif /* HAVE_THREADED_TEXT_RENDERING_WORKAROUND */ private: KisSpacingInformation effectiveSpacing(qreal dabWidth, qreal dabHeight, qreal extraScale, bool isotropicSpacing, qreal rotation, bool axesFlipped) const; protected: // XXX: make private! KisDabCache *m_dabCache; KisBrushSP m_brush; private: KisTextureProperties m_textureProperties; + +protected: KisPressureMirrorOption m_mirrorOption; KisPrecisionOption m_precisionOption; }; #endif diff --git a/plugins/paintops/libpaintop/kis_curve_option.cpp b/plugins/paintops/libpaintop/kis_curve_option.cpp index 97d5e68929..de28f62d07 100644 --- a/plugins/paintops/libpaintop/kis_curve_option.cpp +++ b/plugins/paintops/libpaintop/kis_curve_option.cpp @@ -1,390 +1,434 @@ /* This file is part of the KDE project * Copyright (C) 2008 Boudewijn Rempt * Copyright (C) 2011 Silvio Heinrich * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kis_curve_option.h" #include KisCurveOption::KisCurveOption(const QString& name, KisPaintOpOption::PaintopCategory category, bool checked, qreal value, qreal min, qreal max) : m_name(name) , m_category(category) , m_checkable(true) , m_checked(checked) , m_useCurve(true) , m_useSameCurve(true) , m_separateCurveValue(false) + , m_curveMode(0) { Q_FOREACH (const DynamicSensorType sensorType, KisDynamicSensor::sensorsTypes()) { - KisDynamicSensorSP sensor = KisDynamicSensor::type2Sensor(sensorType); + KisDynamicSensorSP sensor = KisDynamicSensor::type2Sensor(sensorType, m_name); sensor->setActive(false); replaceSensor(sensor); } m_sensorMap[PRESSURE]->setActive(true); setValueRange(min, max); setValue(value); } KisCurveOption::~KisCurveOption() { } const QString& KisCurveOption::name() const { return m_name; } KisPaintOpOption::PaintopCategory KisCurveOption::category() const { return m_category; } qreal KisCurveOption::minValue() const { return m_minValue; } qreal KisCurveOption::maxValue() const { return m_maxValue; } qreal KisCurveOption::value() const { return m_value; } void KisCurveOption::resetAllSensors() { Q_FOREACH (KisDynamicSensorSP sensor, m_sensorMap.values()) { if (sensor->isActive()) { sensor->reset(); } } } void KisCurveOption::writeOptionSetting(KisPropertiesConfigurationSP setting) const { if (m_checkable) { setting->setProperty("Pressure" + m_name, isChecked()); } if (activeSensors().size() == 1) { setting->setProperty(m_name + "Sensor", activeSensors().first()->toXML()); } else { QDomDocument doc = QDomDocument("params"); QDomElement root = doc.createElement("params"); doc.appendChild(root); root.setAttribute("id", "sensorslist"); Q_FOREACH (KisDynamicSensorSP sensor, activeSensors()) { QDomElement childelt = doc.createElement("ChildSensor"); sensor->toXML(doc, childelt); root.appendChild(childelt); } setting->setProperty(m_name + "Sensor", doc.toString()); } setting->setProperty(m_name + "UseCurve", m_useCurve); setting->setProperty(m_name + "UseSameCurve", m_useSameCurve); setting->setProperty(m_name + "Value", m_value); + setting->setProperty(m_name + "curveMode", m_curveMode); } void KisCurveOption::readOptionSetting(KisPropertiesConfigurationSP setting) { m_curveCache.clear(); readNamedOptionSetting(m_name, setting); } void KisCurveOption::lodLimitations(KisPaintopLodLimitations *l) const { Q_UNUSED(l); } void KisCurveOption::readNamedOptionSetting(const QString& prefix, const KisPropertiesConfigurationSP setting) { if (!setting) return; //dbgKrita << "readNamedOptionSetting" << prefix; setting->dump(); if (m_checkable) { setChecked(setting->getBool("Pressure" + prefix, false)); } //dbgKrita << "\tPressure" + prefix << isChecked(); m_sensorMap.clear(); // Replace all sensors with the inactive defaults Q_FOREACH (const DynamicSensorType sensorType, KisDynamicSensor::sensorsTypes()) { - replaceSensor(KisDynamicSensor::type2Sensor(sensorType)); + replaceSensor(KisDynamicSensor::type2Sensor(sensorType, m_name)); } QString sensorDefinition = setting->getString(prefix + "Sensor"); if (!sensorDefinition.contains("sensorslist")) { - KisDynamicSensorSP s = KisDynamicSensor::createFromXML(sensorDefinition); + KisDynamicSensorSP s = KisDynamicSensor::createFromXML(sensorDefinition, m_name); if (s) { replaceSensor(s); s->setActive(true); //dbgKrita << "\tsingle sensor" << s::id(s->sensorType()) << s->isActive() << "added"; } } else { QDomDocument doc; doc.setContent(sensorDefinition); QDomElement elt = doc.documentElement(); QDomNode node = elt.firstChild(); while (!node.isNull()) { if (node.isElement()) { QDomElement childelt = node.toElement(); if (childelt.tagName() == "ChildSensor") { - KisDynamicSensorSP s = KisDynamicSensor::createFromXML(childelt); + KisDynamicSensorSP s = KisDynamicSensor::createFromXML(childelt, m_name); if (s) { replaceSensor(s); s->setActive(true); //dbgKrita << "\tchild sensor" << s::id(s->sensorType()) << s->isActive() << "added"; } } } node = node.nextSibling(); } } // Only load the old curve format if the curve wasn't saved by the sensor // This will give every sensor the same curve. //dbgKrita << ">>>>>>>>>>>" << prefix + "Sensor" << setting->getString(prefix + "Sensor"); if (!setting->getString(prefix + "Sensor").contains("curve")) { //dbgKrita << "\told format"; if (setting->getBool("Custom" + prefix, false)) { Q_FOREACH (KisDynamicSensorSP s, m_sensorMap.values()) { s->setCurve(setting->getCubicCurve("Curve" + prefix)); } } } // At least one sensor needs to be active if (activeSensors().size() == 0) { m_sensorMap[PRESSURE]->setActive(true); } m_value = setting->getDouble(m_name + "Value", m_maxValue); //dbgKrita << "\t" + m_name + "Value" << m_value; m_useCurve = setting->getBool(m_name + "UseCurve", true); //dbgKrita << "\t" + m_name + "UseCurve" << m_useSameCurve; m_useSameCurve = setting->getBool(m_name + "UseSameCurve", true); //dbgKrita << "\t" + m_name + "UseSameCurve" << m_useSameCurve; + m_curveMode = setting->getInt(m_name + "curveMode"); //dbgKrita << "-----------------"; } void KisCurveOption::replaceSensor(KisDynamicSensorSP s) { Q_ASSERT(s); m_sensorMap[s->sensorType()] = s; } KisDynamicSensorSP KisCurveOption::sensor(DynamicSensorType sensorType, bool active) const { if (m_sensorMap.contains(sensorType)) { if (!active) { return m_sensorMap[sensorType]; } else { if (m_sensorMap[sensorType]->isActive()) { return m_sensorMap[sensorType]; } } } return 0; } bool KisCurveOption::isRandom() const { return bool(sensor(FUZZY_PER_DAB, true)) || bool(sensor(FUZZY_PER_STROKE, true)); } bool KisCurveOption::isCurveUsed() const { return m_useCurve; } bool KisCurveOption::isSameCurveUsed() const { return m_useSameCurve; } +int KisCurveOption::getCurveMode() const +{ + return m_curveMode; +} + void KisCurveOption::setSeparateCurveValue(bool separateCurveValue) { m_separateCurveValue = separateCurveValue; } bool KisCurveOption::isCheckable() { return m_checkable; } bool KisCurveOption::isChecked() const { return m_checked; } void KisCurveOption::setChecked(bool checked) { m_checked = checked; } void KisCurveOption::setCurveUsed(bool useCurve) { m_useCurve = useCurve; } +void KisCurveOption::setCurveMode(int mode) +{ + m_curveMode = mode; +} + void KisCurveOption::setCurve(DynamicSensorType sensorType, bool useSameCurve, const KisCubicCurve &curve) { // No switch in state, don't mess with the cache if (useSameCurve == m_useSameCurve) { if (useSameCurve) { Q_FOREACH (KisDynamicSensorSP s, m_sensorMap.values()) { s->setCurve(curve); } } else { KisDynamicSensorSP s = sensor(sensorType, false); if (s) { s->setCurve(curve); } } } else { // moving from not use same curve to use same curve: backup the custom curves if (!m_useSameCurve && useSameCurve) { // Copy the custom curves to the cache and set the new curve on all sensors, active or not m_curveCache.clear(); Q_FOREACH (KisDynamicSensorSP s, m_sensorMap.values()) { m_curveCache[s->sensorType()] = s->curve(); s->setCurve(curve); } } else { //if (m_useSameCurve && !useSameCurve) // Restore the cached curves KisDynamicSensorSP s = 0; Q_FOREACH (DynamicSensorType sensorType, m_curveCache.keys()) { if (m_sensorMap.contains(sensorType)) { s = m_sensorMap[sensorType]; } else { - s = KisDynamicSensor::type2Sensor(sensorType); + s = KisDynamicSensor::type2Sensor(sensorType, m_name); } s->setCurve(m_curveCache[sensorType]); m_sensorMap[sensorType] = s; } s = 0; // And set the current sensor to the current curve if (!m_sensorMap.contains(sensorType)) { - s = KisDynamicSensor::type2Sensor(sensorType); + s = KisDynamicSensor::type2Sensor(sensorType, m_name); } if (s) { s->setCurve(curve); s->setCurve(m_curveCache[sensorType]); } } m_useSameCurve = useSameCurve; } } void KisCurveOption::setValueRange(qreal min, qreal max) { m_minValue = qMin(min, max); m_maxValue = qMax(min, max); } void KisCurveOption::setValue(qreal value) { m_value = qBound(m_minValue, value, m_maxValue); } KisCurveOption::ValueComponents KisCurveOption::computeValueComponents(const KisPaintInformation& info) const { ValueComponents components; if (m_useCurve) { QMap::const_iterator i; + QList sensorValues; for (i = m_sensorMap.constBegin(); i != m_sensorMap.constEnd(); ++i) { KisDynamicSensorSP s(i.value()); if (s->isActive()) { if (s->isAdditive()) { components.additive += s->parameter(info); components.hasAdditive = true; } else if (s->isAbsoluteRotation()) { components.absoluteOffset = s->parameter(info); components.hasAbsoluteOffset =true; } else { - components.scaling *= s->parameter(info); + sensorValues << s->parameter(info); components.hasScaling = true; } } } + + if (sensorValues.count() == 1) { + components.scaling = sensorValues.first(); + } else { + + if (m_curveMode == 1){ // add + components.scaling = 0; + double i; + foreach (i, sensorValues) { + components.scaling += i; + } + } else if (m_curveMode == 2){ //max + components.scaling = *std::max_element(sensorValues.begin(), sensorValues.end()); + + } else if (m_curveMode == 3){ //min + components.scaling = *std::min_element(sensorValues.begin(), sensorValues.end()); + + } else if (m_curveMode == 4){ //difference + double max = *std::max_element(sensorValues.begin(), sensorValues.end()); + double min = *std::min_element(sensorValues.begin(), sensorValues.end()); + components.scaling = max-min; + + } else { //multuply - default + double i; + foreach (i, sensorValues) { + components.scaling *= i; + } + } + } + } if (!m_separateCurveValue) { components.constant = m_value; } components.minSizeLikeValue = m_minValue; components.maxSizeLikeValue = m_maxValue; return components; } qreal KisCurveOption::computeSizeLikeValue(const KisPaintInformation& info) const { const ValueComponents components = computeValueComponents(info); return components.sizeLikeValue(); } qreal KisCurveOption::computeRotationLikeValue(const KisPaintInformation& info, qreal baseValue, bool absoluteAxesFlipped) const { const ValueComponents components = computeValueComponents(info); return components.rotationLikeValue(baseValue, absoluteAxesFlipped); } QList KisCurveOption::sensors() { //dbgKrita << "ID" << name() << "has" << m_sensorMap.count() << "Sensors of which" << sensorList.count() << "are active."; return m_sensorMap.values(); } QList KisCurveOption::activeSensors() const { QList sensorList; Q_FOREACH (KisDynamicSensorSP sensor, m_sensorMap.values()) { if (sensor->isActive()) { sensorList << sensor; } } //dbgKrita << "ID" << name() << "has" << m_sensorMap.count() << "Sensors of which" << sensorList.count() << "are active."; return sensorList; } diff --git a/plugins/paintops/libpaintop/kis_curve_option.h b/plugins/paintops/libpaintop/kis_curve_option.h index d73837d991..d173ba7597 100644 --- a/plugins/paintops/libpaintop/kis_curve_option.h +++ b/plugins/paintops/libpaintop/kis_curve_option.h @@ -1,199 +1,204 @@ /* This file is part of the KDE project * Copyright (C) 2008 Boudewijn Rempt * Copyright (C) 2011 Silvio Heinrich * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KIS_CURVE_OPTION_H #define KIS_CURVE_OPTION_H #include #include #include "kis_paintop_option.h" #include "kis_global.h" #include "kis_paintop_option.h" #include #include "kritapaintop_export.h" #include "kis_dynamic_sensor.h" class KisDynamicSensor; /** * KisCurveOption is the base class for paintop options that are * defined through one or more curves. * * Note: it is NOT a KisPaintOpOption, even though the API is pretty similar! * */ class PAINTOP_EXPORT KisCurveOption { public: KisCurveOption(const QString& name, KisPaintOpOption::PaintopCategory category, bool checked, qreal value = 1.0, qreal min = 0.0, qreal max = 1.0); virtual ~KisCurveOption(); virtual void writeOptionSetting(KisPropertiesConfigurationSP setting) const; virtual void readOptionSetting(KisPropertiesConfigurationSP setting); virtual void lodLimitations(KisPaintopLodLimitations *l) const; const QString& name() const; KisPaintOpOption::PaintopCategory category() const; qreal minValue() const; qreal maxValue() const; qreal value() const; void resetAllSensors(); KisDynamicSensorSP sensor(DynamicSensorType sensorType, bool active) const; void replaceSensor(KisDynamicSensorSP sensor); QList sensors(); QList activeSensors() const; bool isCheckable(); bool isChecked() const; bool isCurveUsed() const; bool isSameCurveUsed() const; bool isRandom() const; + int getCurveMode() const; + void setSeparateCurveValue(bool separateCurveValue); void setChecked(bool checked); void setCurveUsed(bool useCurve); void setCurve(DynamicSensorType sensorType, bool useSameCurve, const KisCubicCurve &curve); void setValue(qreal value); + void setCurveMode(int mode); struct ValueComponents { ValueComponents() : constant(1.0), scaling(1.0), additive(0.0), absoluteOffset(0.0), hasAbsoluteOffset(false), hasScaling(false), hasAdditive(false) { } qreal constant; qreal scaling; qreal additive; qreal absoluteOffset; bool hasAbsoluteOffset; bool hasScaling; bool hasAdditive; qreal minSizeLikeValue; qreal maxSizeLikeValue; /** * @param normalizedBaseAngle canvas rotation alngle normalized to range [0; 1] * @param absoluteAxesFlipped true if underlying image coordinate system is flipped (horiz. mirror != vert. mirror) */ qreal rotationLikeValue(qreal normalizedBaseAngle, bool absoluteAxesFlipped) const { const qreal offset = !hasAbsoluteOffset ? normalizedBaseAngle : absoluteAxesFlipped ? 1.0 - absoluteOffset : absoluteOffset; const qreal realScalingPart = hasScaling ? KisDynamicSensor::scalingToAdditive(scaling) : 0.0; const qreal realAdditivePart = hasAdditive ? additive : 0; qreal value = wrapInRange(2 * offset + constant * realScalingPart + realAdditivePart, -1.0, 1.0); if (qIsNaN(value)) { qWarning() << "rotationLikeValue returns NaN!" << normalizedBaseAngle << absoluteAxesFlipped; value = 0; } return value; } qreal sizeLikeValue() const { const qreal offset = hasAbsoluteOffset ? absoluteOffset : 1.0; const qreal realScalingPart = hasScaling ? scaling : 1.0; const qreal realAdditivePart = hasAdditive ? KisDynamicSensor::additiveToScaling(additive) : 1.0; return qBound(minSizeLikeValue, constant * offset * realScalingPart * realAdditivePart, maxSizeLikeValue); } private: static inline qreal wrapInRange(qreal x, qreal min, qreal max) { const qreal range = max - min; x -= min; if (x < 0.0) { x = range + fmod(x, range); } if (x > range) { x = fmod(x, range); } return x + min; } }; /** * Uses the curves set on the sensors to compute a single * double value that can control the parameters of a brush. * * This value is derives from the falues stored in * ValuesComponents opject. */ ValueComponents computeValueComponents(const KisPaintInformation& info) const; qreal computeSizeLikeValue(const KisPaintInformation &info) const; qreal computeRotationLikeValue(const KisPaintInformation& info, qreal baseValue, bool absoluteAxesFlipped) const; protected: void setValueRange(qreal min, qreal max); /** * Read the option using the prefix in argument */ void readNamedOptionSetting(const QString& prefix, const KisPropertiesConfigurationSP setting); QString m_name; KisPaintOpOption::PaintopCategory m_category; bool m_checkable; bool m_checked; bool m_useCurve; bool m_useSameCurve; bool m_separateCurveValue; + int m_curveMode; + QMap m_sensorMap; QMap m_curveCache; private: qreal m_value; qreal m_minValue; qreal m_maxValue; }; #endif diff --git a/plugins/paintops/libpaintop/kis_curve_option_widget.cpp b/plugins/paintops/libpaintop/kis_curve_option_widget.cpp index 640ca9fada..0b6e2e5824 100644 --- a/plugins/paintops/libpaintop/kis_curve_option_widget.cpp +++ b/plugins/paintops/libpaintop/kis_curve_option_widget.cpp @@ -1,293 +1,301 @@ /* This file is part of the KDE project * Copyright (C) 2008 Boudewijn Rempt * Copyright (C) 2009 Sven Langkamp * Copyright (C) 2011 Silvio Heinrich * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kis_curve_option_widget.h" #include "ui_wdgcurveoption.h" #include "widgets/kis_curve_widget.h" #include "kis_dynamic_sensor.h" #include "kis_global.h" #include "kis_curve_option.h" #include "kis_signals_blocker.h" #include "kis_icon_utils.h" inline void setLabel(QLabel* label, const KisCurveLabel& curve_label) { if (curve_label.icon().isNull()) { label->setText(curve_label.name()); } else { label->setPixmap(QPixmap::fromImage(curve_label.icon())); } } KisCurveOptionWidget::KisCurveOptionWidget(KisCurveOption* curveOption, const QString &minLabel, const QString &maxLabel, bool hideSlider) : KisPaintOpOption(curveOption->category(), curveOption->isChecked()) , m_widget(new QWidget) , m_curveOptionWidget(new Ui_WdgCurveOption()) , m_curveOption(curveOption) { setObjectName("KisCurveOptionWidget"); m_curveOptionWidget->setupUi(m_widget); setConfigurationPage(m_widget); m_curveOptionWidget->sensorSelector->setCurveOption(curveOption); updateSensorCurveLabels(m_curveOptionWidget->sensorSelector->currentHighlighted()); updateCurve(m_curveOptionWidget->sensorSelector->currentHighlighted()); connect(m_curveOptionWidget->curveWidget, SIGNAL(modified()), this, SLOT(transferCurve())); connect(m_curveOptionWidget->sensorSelector, SIGNAL(parametersChanged()), SLOT(emitSettingChanged())); connect(m_curveOptionWidget->sensorSelector, SIGNAL(parametersChanged()), SLOT(updateLabelsOfCurrentSensor())); connect(m_curveOptionWidget->sensorSelector, SIGNAL(highlightedSensorChanged(KisDynamicSensorSP )), SLOT(updateSensorCurveLabels(KisDynamicSensorSP ))); connect(m_curveOptionWidget->sensorSelector, SIGNAL(highlightedSensorChanged(KisDynamicSensorSP )), SLOT(updateCurve(KisDynamicSensorSP ))); connect(m_curveOptionWidget->checkBoxUseSameCurve, SIGNAL(stateChanged(int)), SLOT(transferCurve())); // set all the icons for the curve preset shapes updateThemedIcons(); // various curve preset buttons with predefined curves connect(m_curveOptionWidget->linearCurveButton, SIGNAL(clicked(bool)), this, SLOT(changeCurveLinear())); connect(m_curveOptionWidget->revLinearButton, SIGNAL(clicked(bool)), this, SLOT(changeCurveReverseLinear())); connect(m_curveOptionWidget->jCurveButton, SIGNAL(clicked(bool)), this, SLOT(changeCurveJShape())); connect(m_curveOptionWidget->lCurveButton, SIGNAL(clicked(bool)), this, SLOT(changeCurveLShape())); connect(m_curveOptionWidget->sCurveButton, SIGNAL(clicked(bool)), this, SLOT(changeCurveSShape())); connect(m_curveOptionWidget->reverseSCurveButton, SIGNAL(clicked(bool)), this, SLOT(changeCurveReverseSShape())); connect(m_curveOptionWidget->uCurveButton, SIGNAL(clicked(bool)), this, SLOT(changeCurveUShape())); connect(m_curveOptionWidget->revUCurveButton, SIGNAL(clicked(bool)), this, SLOT(changeCurveArchShape())); m_curveOptionWidget->label_ymin->setText(minLabel); m_curveOptionWidget->label_ymax->setText(maxLabel); m_curveOptionWidget->slider->setRange(curveOption->minValue(), curveOption->maxValue(), 2); m_curveOptionWidget->slider->setValue(curveOption->value()); if (hideSlider) { m_curveOptionWidget->slider->hide(); m_curveOptionWidget->strengthLabel->hide(); } connect(m_curveOptionWidget->checkBoxUseCurve, SIGNAL(stateChanged(int)) , SLOT(updateValues())); + connect(m_curveOptionWidget->curveMode, SIGNAL(currentIndexChanged(int)), SLOT(updateMode())); connect(m_curveOptionWidget->slider, SIGNAL(valueChanged(qreal)), SLOT(updateValues())); } KisCurveOptionWidget::~KisCurveOptionWidget() { delete m_curveOption; delete m_curveOptionWidget; } void KisCurveOptionWidget::writeOptionSetting(KisPropertiesConfigurationSP setting) const { m_curveOption->writeOptionSetting(setting); } void KisCurveOptionWidget::readOptionSetting(const KisPropertiesConfigurationSP setting) { setting->dump(); m_curveOption->readOptionSetting(setting); m_curveOptionWidget->checkBoxUseCurve->setChecked(m_curveOption->isCurveUsed()); m_curveOptionWidget->slider->setValue(m_curveOption->value()); m_curveOptionWidget->checkBoxUseSameCurve->setChecked(m_curveOption->isSameCurveUsed()); + m_curveOptionWidget->curveMode->setCurrentIndex(m_curveOption->getCurveMode()); disableWidgets(!m_curveOption->isCurveUsed()); m_curveOptionWidget->sensorSelector->reload(); m_curveOptionWidget->sensorSelector->setCurrent(m_curveOption->activeSensors().first()); updateSensorCurveLabels(m_curveOptionWidget->sensorSelector->currentHighlighted()); updateCurve(m_curveOptionWidget->sensorSelector->currentHighlighted()); } void KisCurveOptionWidget::lodLimitations(KisPaintopLodLimitations *l) const { m_curveOption->lodLimitations(l); } bool KisCurveOptionWidget::isCheckable() const { return m_curveOption->isCheckable(); } bool KisCurveOptionWidget::isChecked() const { return m_curveOption->isChecked(); } void KisCurveOptionWidget::setChecked(bool checked) { m_curveOption->setChecked(checked); } KisCurveOption* KisCurveOptionWidget::curveOption() { return m_curveOption; } QWidget* KisCurveOptionWidget::curveWidget() { return m_widget; } void KisCurveOptionWidget::transferCurve() { m_curveOptionWidget->sensorSelector->setCurrentCurve(m_curveOptionWidget->curveWidget->curve(), m_curveOptionWidget->checkBoxUseSameCurve->isChecked()); emitSettingChanged(); } void KisCurveOptionWidget::updateSensorCurveLabels(KisDynamicSensorSP sensor) { if (sensor) { m_curveOptionWidget->label_xmin->setText(KisDynamicSensor::minimumLabel(sensor->sensorType())); m_curveOptionWidget->label_xmax->setText(KisDynamicSensor::maximumLabel(sensor->sensorType(), sensor->length())); } } void KisCurveOptionWidget::updateCurve(KisDynamicSensorSP sensor) { if (sensor) { bool blockSignal = m_curveOptionWidget->curveWidget->blockSignals(true); m_curveOptionWidget->curveWidget->setCurve(sensor->curve()); m_curveOptionWidget->curveWidget->blockSignals(blockSignal); } } void KisCurveOptionWidget::updateLabelsOfCurrentSensor() { updateSensorCurveLabels(m_curveOptionWidget->sensorSelector->currentHighlighted()); updateCurve(m_curveOptionWidget->sensorSelector->currentHighlighted()); } void KisCurveOptionWidget::updateValues() { m_curveOption->setValue(m_curveOptionWidget->slider->value()); m_curveOption->setCurveUsed(m_curveOptionWidget->checkBoxUseCurve->isChecked()); disableWidgets(!m_curveOptionWidget->checkBoxUseCurve->isChecked()); emitSettingChanged(); } +void KisCurveOptionWidget::updateMode() +{ + m_curveOption->setCurveMode(m_curveOptionWidget->curveMode->currentIndex()); + emitSettingChanged(); +} + void KisCurveOptionWidget::changeCurveLinear() { QList points; points.push_back(QPointF(0,0)); points.push_back(QPointF(1,1)); m_curveOptionWidget->curveWidget->setCurve(KisCubicCurve(points)); } void KisCurveOptionWidget::changeCurveReverseLinear() { QList points; points.push_back(QPointF(0,1)); points.push_back(QPointF(1,0)); m_curveOptionWidget->curveWidget->setCurve(KisCubicCurve(points)); } void KisCurveOptionWidget::changeCurveSShape() { QList points; points.push_back(QPointF(0,0)); points.push_back(QPointF(0.25,0.1)); points.push_back(QPointF(0.75,0.9)); points.push_back(QPointF(1, 1)); m_curveOptionWidget->curveWidget->setCurve(KisCubicCurve(points)); } void KisCurveOptionWidget::changeCurveReverseSShape() { QList points; points.push_back(QPointF(0,1)); points.push_back(QPointF(0.25,0.9)); points.push_back(QPointF(0.75,0.1)); points.push_back(QPointF(1,0)); m_curveOptionWidget->curveWidget->setCurve(KisCubicCurve(points)); } void KisCurveOptionWidget::changeCurveJShape() { QList points; points.push_back(QPointF(0,0)); points.push_back(QPointF(0.35,0.1)); points.push_back(QPointF(1,1)); m_curveOptionWidget->curveWidget->setCurve(KisCubicCurve(points)); } void KisCurveOptionWidget::changeCurveLShape() { QList points; points.push_back(QPointF(0,1)); points.push_back(QPointF(0.25,0.48)); points.push_back(QPointF(1,0)); m_curveOptionWidget->curveWidget->setCurve(KisCubicCurve(points)); } void KisCurveOptionWidget::changeCurveUShape() { QList points; points.push_back(QPointF(0,1)); points.push_back(QPointF(0.5,0)); points.push_back(QPointF(1,1)); m_curveOptionWidget->curveWidget->setCurve(KisCubicCurve(points)); } void KisCurveOptionWidget::changeCurveArchShape() { QList points; points.push_back(QPointF(0,0)); points.push_back(QPointF(0.5,1)); points.push_back(QPointF(1,0)); m_curveOptionWidget->curveWidget->setCurve(KisCubicCurve(points)); } void KisCurveOptionWidget::disableWidgets(bool disable) { m_curveOptionWidget->checkBoxUseSameCurve->setDisabled(disable); m_curveOptionWidget->curveWidget->setDisabled(disable); m_curveOptionWidget->sensorSelector->setDisabled(disable); m_curveOptionWidget->label_xmax->setDisabled(disable); m_curveOptionWidget->label_xmin->setDisabled(disable); m_curveOptionWidget->label_ymax->setDisabled(disable); m_curveOptionWidget->label_ymin->setDisabled(disable); } void KisCurveOptionWidget::updateThemedIcons() { // set all the icons for the curve preset shapes m_curveOptionWidget->linearCurveButton->setIcon(KisIconUtils::loadIcon("curve-preset-linear")); m_curveOptionWidget->revLinearButton->setIcon(KisIconUtils::loadIcon("curve-preset-linear-reverse")); m_curveOptionWidget->jCurveButton->setIcon(KisIconUtils::loadIcon("curve-preset-j")); m_curveOptionWidget->lCurveButton->setIcon(KisIconUtils::loadIcon("curve-preset-l")); m_curveOptionWidget->sCurveButton->setIcon(KisIconUtils::loadIcon("curve-preset-s")); m_curveOptionWidget->reverseSCurveButton->setIcon(KisIconUtils::loadIcon("curve-preset-s-reverse")); m_curveOptionWidget->uCurveButton->setIcon(KisIconUtils::loadIcon("curve-preset-u")); m_curveOptionWidget->revUCurveButton->setIcon(KisIconUtils::loadIcon("curve-preset-arch")); } diff --git a/plugins/paintops/libpaintop/kis_curve_option_widget.h b/plugins/paintops/libpaintop/kis_curve_option_widget.h index 45bea06a09..eae9d1be19 100644 --- a/plugins/paintops/libpaintop/kis_curve_option_widget.h +++ b/plugins/paintops/libpaintop/kis_curve_option_widget.h @@ -1,81 +1,84 @@ /* This file is part of the KDE project * Copyright (C) 2008 Boudewijn Rempt * Copyright (C) 2009 Sven Langkamp * Copyright (C) 2011 Silvio Heinrich * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KIS_CURVE_OPTION_WIDGET_H #define KIS_CURVE_OPTION_WIDGET_H #include class Ui_WdgCurveOption; class KisCurveOption; +class QComboBox; #include class PAINTOP_EXPORT KisCurveOptionWidget : public KisPaintOpOption { Q_OBJECT public: KisCurveOptionWidget(KisCurveOption* curveOption, const QString &minLabel, const QString &maxLabel, bool hideSlider = false); ~KisCurveOptionWidget() override; void writeOptionSetting(KisPropertiesConfigurationSP setting) const override; void readOptionSetting(const KisPropertiesConfigurationSP setting) override; void lodLimitations(KisPaintopLodLimitations *l) const override; bool isCheckable() const override; bool isChecked() const override; void setChecked(bool checked) override; void show(); protected: KisCurveOption* curveOption(); QWidget* curveWidget(); private Q_SLOTS: void transferCurve(); void updateSensorCurveLabels(KisDynamicSensorSP sensor); void updateCurve(KisDynamicSensorSP sensor); void updateValues(); + void updateMode(); void updateLabelsOfCurrentSensor(); void disableWidgets(bool disable); void updateThemedIcons(); // curve shape preset buttons void changeCurveLinear(); void changeCurveReverseLinear(); void changeCurveSShape(); void changeCurveReverseSShape(); void changeCurveJShape(); void changeCurveLShape(); void changeCurveUShape(); void changeCurveArchShape(); private: QWidget* m_widget; Ui_WdgCurveOption* m_curveOptionWidget; + QComboBox* m_curveMode; KisCurveOption* m_curveOption; }; #endif // KIS_CURVE_OPTION_WIDGET_H diff --git a/plugins/paintops/libpaintop/kis_dab_cache.cpp b/plugins/paintops/libpaintop/kis_dab_cache.cpp index ba03c0388a..7104688eb1 100644 --- a/plugins/paintops/libpaintop/kis_dab_cache.cpp +++ b/plugins/paintops/libpaintop/kis_dab_cache.cpp @@ -1,421 +1,213 @@ /* * Copyright (c) 2012 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_dab_cache.h" #include -#include "kis_color_source.h" #include "kis_paint_device.h" #include "kis_brush.h" -#include -#include -#include -#include #include -#include +#include "kis_color_source.h" +#include "kis_pressure_sharpness_option.h" +#include "kis_texture_option.h" #include -struct PrecisionValues { - qreal angle; - qreal sizeFrac; - qreal subPixel; - qreal softnessFactor; -}; - -const qreal eps = 1e-6; -static const PrecisionValues precisionLevels[] = { - {M_PI / 180, 0.05, 1, 0.01}, - {M_PI / 180, 0.01, 1, 0.01}, - {M_PI / 180, 0, 1, 0.01}, - {M_PI / 180, 0, 0.5, 0.01}, - {eps, 0, eps, eps} -}; - -struct KisDabCache::SavedDabParameters { - KoColor color; - qreal angle; - int width; - int height; - qreal subPixelX; - qreal subPixelY; - qreal softnessFactor; - int index; - MirrorProperties mirrorProperties; - - bool compare(const SavedDabParameters &rhs, int precisionLevel) const { - const PrecisionValues &prec = precisionLevels[precisionLevel]; - - return color == rhs.color && - qAbs(angle - rhs.angle) <= prec.angle && - qAbs(width - rhs.width) <= (int)(prec.sizeFrac * width) && - qAbs(height - rhs.height) <= (int)(prec.sizeFrac * height) && - qAbs(subPixelX - rhs.subPixelX) <= prec.subPixel && - qAbs(subPixelY - rhs.subPixelY) <= prec.subPixel && - qAbs(softnessFactor - rhs.softnessFactor) <= prec.softnessFactor && - index == rhs.index && - mirrorProperties.horizontalMirror == rhs.mirrorProperties.horizontalMirror && - mirrorProperties.verticalMirror == rhs.mirrorProperties.verticalMirror; - } -}; - struct KisDabCache::Private { Private(KisBrushSP brush) - : brush(brush), - mirrorOption(0), - sharpnessOption(0), - textureOption(0), - precisionOption(0), - subPixelPrecisionDisabled(false), - cachedDabParameters(new SavedDabParameters) + : brush(brush) {} + KisFixedPaintDeviceSP dab; KisFixedPaintDeviceSP dabOriginal; KisBrushSP brush; KisPaintDeviceSP colorSourceDevice; - KisPressureMirrorOption *mirrorOption; - KisPressureSharpnessOption *sharpnessOption; - KisTextureProperties *textureOption; - KisPrecisionOption *precisionOption; - bool subPixelPrecisionDisabled; - - SavedDabParameters *cachedDabParameters; + KisPressureSharpnessOption *sharpnessOption = 0; + KisTextureProperties *textureOption = 0; }; KisDabCache::KisDabCache(KisBrushSP brush) : m_d(new Private(brush)) { } KisDabCache::~KisDabCache() { - delete m_d->cachedDabParameters; delete m_d; } -void KisDabCache::setMirrorPostprocessing(KisPressureMirrorOption *option) -{ - m_d->mirrorOption = option; -} - void KisDabCache::setSharpnessPostprocessing(KisPressureSharpnessOption *option) { m_d->sharpnessOption = option; } void KisDabCache::setTexturePostprocessing(KisTextureProperties *option) { m_d->textureOption = option; } -void KisDabCache::setPrecisionOption(KisPrecisionOption *option) +bool KisDabCache::needSeparateOriginal() const { - m_d->precisionOption = option; + return KisDabCacheBase::needSeparateOriginal(m_d->textureOption, m_d->sharpnessOption); } -void KisDabCache::disableSubpixelPrecision() -{ - m_d->subPixelPrecisionDisabled = true; -} - -inline KisDabCache::SavedDabParameters -KisDabCache::getDabParameters(const KoColor& color, - KisDabShape const& shape, - const KisPaintInformation& info, - double subPixelX, double subPixelY, - qreal softnessFactor, - MirrorProperties mirrorProperties) -{ - SavedDabParameters params; - - params.color = color; - params.angle = shape.rotation(); - params.width = m_d->brush->maskWidth(shape, subPixelX, subPixelY, info); - params.height = m_d->brush->maskHeight(shape, subPixelX, subPixelY, info); - params.subPixelX = subPixelX; - params.subPixelY = subPixelY; - params.softnessFactor = softnessFactor; - params.index = m_d->brush->brushIndex(info); - params.mirrorProperties = mirrorProperties; - - return params; -} KisFixedPaintDeviceSP KisDabCache::fetchDab(const KoColorSpace *cs, - const KisColorSource *colorSource, + KisColorSource *colorSource, const QPointF &cursorPoint, KisDabShape const& shape, const KisPaintInformation& info, qreal softnessFactor, QRect *dstDabRect) { return fetchDabCommon(cs, colorSource, KoColor(), cursorPoint, shape, info, softnessFactor, dstDabRect); } KisFixedPaintDeviceSP KisDabCache::fetchDab(const KoColorSpace *cs, const KoColor& color, const QPointF &cursorPoint, KisDabShape const& shape, const KisPaintInformation& info, qreal softnessFactor, QRect *dstDabRect) { return fetchDabCommon(cs, 0, color, cursorPoint, shape, info, softnessFactor, dstDabRect); } -bool KisDabCache::needSeparateOriginal() -{ - return (m_d->textureOption && m_d->textureOption->m_enabled) || - (m_d->sharpnessOption && m_d->sharpnessOption->isChecked()); -} - -struct KisDabCache::DabPosition { - DabPosition(const QRect &_rect, - const QPointF &_subPixel, - qreal _realAngle) - : rect(_rect), - subPixel(_subPixel), - realAngle(_realAngle) { - } - - QRect rect; - QPointF subPixel; - qreal realAngle; -}; - inline -QRect KisDabCache::correctDabRectWhenFetchedFromCache(const QRect &dabRect, - const QSize &realDabSize) -{ - int diffX = (realDabSize.width() - dabRect.width()) / 2; - int diffY = (realDabSize.height() - dabRect.height()) / 2; - - return QRect(dabRect.x() - diffX, dabRect.y() - diffY, - realDabSize.width() , realDabSize.height()); -} - -inline -KisFixedPaintDeviceSP KisDabCache::tryFetchFromCache(const SavedDabParameters ¶ms, - const KisPaintInformation& info, - QRect *dstDabRect) +KisFixedPaintDeviceSP KisDabCache::fetchFromCache(KisDabCacheUtils::DabRenderingResources *resources, + const KisPaintInformation& info, + QRect *dstDabRect) { - int precisionLevel = m_d->precisionOption ? m_d->precisionOption->precisionLevel() - 1 : 3; - - if (!params.compare(*m_d->cachedDabParameters, precisionLevel)) { - return 0; - } - if (needSeparateOriginal()) { *m_d->dab = *m_d->dabOriginal; - *dstDabRect = correctDabRectWhenFetchedFromCache(*dstDabRect, m_d->dab->bounds().size()); - postProcessDab(m_d->dab, dstDabRect->topLeft(), info); + *dstDabRect = KisDabCacheUtils::correctDabRectWhenFetchedFromCache(*dstDabRect, m_d->dab->bounds().size()); + KisDabCacheUtils::postProcessDab(m_d->dab, dstDabRect->topLeft(), info, resources); } else { - *dstDabRect = correctDabRectWhenFetchedFromCache(*dstDabRect, m_d->dab->bounds().size()); + *dstDabRect = KisDabCacheUtils::correctDabRectWhenFetchedFromCache(*dstDabRect, m_d->dab->bounds().size()); } - m_d->brush->notifyCachedDabPainted(info); + resources->brush->notifyCachedDabPainted(info); return m_d->dab; } -qreal positiveFraction(qreal x) { - qint32 unused = 0; - qreal fraction = 0.0; - KisPaintOp::splitCoordinate(x, &unused, &fraction); - - return fraction; -} - -inline -KisDabCache::DabPosition -KisDabCache::calculateDabRect(const QPointF &cursorPoint, - KisDabShape shape, - const KisPaintInformation& info, - const MirrorProperties &mirrorProperties) +/** + * A special hack class that allows creation of temporary object with resources + * without taking ownershop over the option classes + */ +struct TemporaryResourcesWithoutOwning : public KisDabCacheUtils::DabRenderingResources { - qint32 x = 0, y = 0; - qreal subPixelX = 0.0, subPixelY = 0.0; - - if (mirrorProperties.coordinateSystemFlipped) { - shape = KisDabShape(shape.scale(), shape.ratio(), 2 * M_PI - shape.rotation()); + ~TemporaryResourcesWithoutOwning() override { + // we do not own these resources, so just + // release them before destruction + colorSource.take(); + sharpnessOption.take(); + textureOption.take(); } - - QPointF hotSpot = m_d->brush->hotSpot(shape, info); - QPointF pt = cursorPoint - hotSpot; - - if (m_d->sharpnessOption) { - m_d->sharpnessOption->apply(info, pt, x, y, subPixelX, subPixelY); - } - else { - KisPaintOp::splitCoordinate(pt.x(), &x, &subPixelX); - KisPaintOp::splitCoordinate(pt.y(), &y, &subPixelY); - } - - if (m_d->subPixelPrecisionDisabled) { - subPixelX = 0; - subPixelY = 0; - } - - if (qIsNaN(subPixelX)) { - subPixelX = 0; - } - - if (qIsNaN(subPixelY)) { - subPixelY = 0; - } - - int width = m_d->brush->maskWidth(shape, subPixelX, subPixelY, info); - int height = m_d->brush->maskHeight(shape, subPixelX, subPixelY, info); - - if (mirrorProperties.horizontalMirror) { - subPixelX = positiveFraction(-(cursorPoint.x() + hotSpot.x())); - width = m_d->brush->maskWidth(shape, subPixelX, subPixelY, info); - x = qRound(cursorPoint.x() + subPixelX + hotSpot.x()) - width; - } - - if (mirrorProperties.verticalMirror) { - subPixelY = positiveFraction(-(cursorPoint.y() + hotSpot.y())); - height = m_d->brush->maskHeight(shape, subPixelX, subPixelY, info); - y = qRound(cursorPoint.y() + subPixelY + hotSpot.y()) - height; - } - - return DabPosition(QRect(x, y, width, height), - QPointF(subPixelX, subPixelY), - shape.rotation()); -} +}; inline KisFixedPaintDeviceSP KisDabCache::fetchDabCommon(const KoColorSpace *cs, - const KisColorSource *colorSource, + KisColorSource *colorSource, const KoColor& color, const QPointF &cursorPoint, KisDabShape shape, const KisPaintInformation& info, qreal softnessFactor, QRect *dstDabRect) { Q_ASSERT(dstDabRect); - MirrorProperties mirrorProperties; - if (m_d->mirrorOption) { - mirrorProperties = m_d->mirrorOption->apply(info); + bool hasDabInCache = true; + + if (!m_d->dab || *m_d->dab->colorSpace() != *cs) { + m_d->dab = new KisFixedPaintDevice(cs); + hasDabInCache = false; } - DabPosition position = calculateDabRect(cursorPoint, - shape, - info, - mirrorProperties); - shape = KisDabShape(shape.scale(), shape.ratio(), position.realAngle); - *dstDabRect = position.rect; + using namespace KisDabCacheUtils; - bool cachingIsPossible = !colorSource || colorSource->isUniformColor(); - KoColor paintColor = colorSource && colorSource->isUniformColor() ? - colorSource->uniformColor() : color; + // 1. Calculate new dab parameters and whether we can reuse the cache - SavedDabParameters newParams = getDabParameters(paintColor, - shape, info, - position.subPixel.x(), - position.subPixel.y(), - softnessFactor, - mirrorProperties); + TemporaryResourcesWithoutOwning resources; + resources.brush = m_d->brush; + resources.colorSourceDevice = m_d->colorSourceDevice; - if (!m_d->dab || *m_d->dab->colorSpace() != *cs) { - m_d->dab = new KisFixedPaintDevice(cs); - } - else if (cachingIsPossible) { - KisFixedPaintDeviceSP cachedDab = - tryFetchFromCache(newParams, info, dstDabRect); + // NOTE: we use a special subclass of resources that will NOT + // delete options on destruction! + resources.colorSource.reset(colorSource); + resources.sharpnessOption.reset(m_d->sharpnessOption); + resources.textureOption.reset(m_d->textureOption); - if (cachedDab) return cachedDab; - } - if (m_d->brush->brushType() == IMAGE || m_d->brush->brushType() == PIPE_IMAGE) { - m_d->dab = m_d->brush->paintDevice(cs, shape, info, - position.subPixel.x(), - position.subPixel.y()); - } - else if (cachingIsPossible) { - *m_d->cachedDabParameters = newParams; - m_d->brush->mask(m_d->dab, paintColor, shape, - info, - position.subPixel.x(), position.subPixel.y(), - softnessFactor); - } - else { - if (!m_d->colorSourceDevice || *cs != *m_d->colorSourceDevice->colorSpace()) { - m_d->colorSourceDevice = new KisPaintDevice(cs); - } - else { - m_d->colorSourceDevice->clear(); - } + DabGenerationInfo di; + bool shouldUseCache = false; - QRect maskRect(QPoint(), position.rect.size()); - colorSource->colorize(m_d->colorSourceDevice, maskRect, info.pos().toPoint()); - delete m_d->colorSourceDevice->convertTo(cs); + fetchDabGenerationInfo(hasDabInCache, + &resources, + DabRequestInfo( + color, + cursorPoint, + shape, + info, + softnessFactor), + &di, + &shouldUseCache); + + *dstDabRect = di.dstDabRect; - m_d->brush->mask(m_d->dab, m_d->colorSourceDevice, shape, - info, - position.subPixel.x(), position.subPixel.y(), - softnessFactor); - } - if (!mirrorProperties.isEmpty()) { - m_d->dab->mirror(mirrorProperties.horizontalMirror, - mirrorProperties.verticalMirror); + // 2. Try return a saved dab from the cache + + if (shouldUseCache) { + return fetchFromCache(&resources, info, dstDabRect); } - if (needSeparateOriginal()) { + // 3. Generate new dab + + generateDab(di, &resources, &m_d->dab); + + // 4. Do postprocessing + if (di.needsPostprocessing) { if (!m_d->dabOriginal || *cs != *m_d->dabOriginal->colorSpace()) { m_d->dabOriginal = new KisFixedPaintDevice(cs); } *m_d->dabOriginal = *m_d->dab; - } - - postProcessDab(m_d->dab, position.rect.topLeft(), info); - - return m_d->dab; -} -void KisDabCache::postProcessDab(KisFixedPaintDeviceSP dab, - const QPoint &dabTopLeft, - const KisPaintInformation& info) -{ - if (m_d->sharpnessOption) { - m_d->sharpnessOption->applyThreshold(dab); + postProcessDab(m_d->dab, di.dstDabRect.topLeft(), info, &resources); } - if (m_d->textureOption) { - m_d->textureOption->apply(dab, dabTopLeft, info); - } + return m_d->dab; } diff --git a/plugins/paintops/libpaintop/kis_dab_cache.h b/plugins/paintops/libpaintop/kis_dab_cache.h index 442543e475..0a7c610ad6 100644 --- a/plugins/paintops/libpaintop/kis_dab_cache.h +++ b/plugins/paintops/libpaintop/kis_dab_cache.h @@ -1,134 +1,103 @@ /* * Copyright (c) 2012 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_DAB_CACHE_H #define __KIS_DAB_CACHE_H #include "kritapaintop_export.h" + +#include "kis_dab_cache_base.h" + #include "kis_brush.h" class KisColorSource; class KisPressureSharpnessOption; class KisTextureProperties; class KisPressureMirrorOption; class KisPrecisionOption; struct MirrorProperties; /** * @brief The KisDabCache class provides caching for dabs into the brush paintop * * This class adds caching of the dabs to the paintop system of Krita. * Such cache makes the execution of the benchmarks up to 2 times faster. * Subjectively, the real painting becomes much faster, especially with * huge brushes. Artists report up to 20% speed gain while painting. * * Of course, such caching makes the painting a bit less precise: we need * to tolerate subpixel differences to allow the cache to work. Sometimes * small difference in the size of a dab can also be acceptable. That is * why I introduced levels of precision. They are graded from 1 to 5: from * the fastest and less precise to the slowest, but with the best quality. * You can see the slider in the paintop settings dialog. The ToolTip text * explains which features of the brush are sacrificed on each precision * level. * * The texturing and mirroring problems are solved. */ -class PAINTOP_EXPORT KisDabCache +class PAINTOP_EXPORT KisDabCache : public KisDabCacheBase { public: KisDabCache(KisBrushSP brush); ~KisDabCache(); - void setMirrorPostprocessing(KisPressureMirrorOption *option); - void setSharpnessPostprocessing(KisPressureSharpnessOption *option); - void setTexturePostprocessing(KisTextureProperties *option); - void setPrecisionOption(KisPrecisionOption *option); - - /** - * Disables handling of the subPixelX and subPixelY values, this - * is needed at least for the Color Smudge paint op, which reads - * aligned areas from image, so additional offsets generated by - * the subpixel precision should be avoided - */ - void disableSubpixelPrecision(); - - bool needSeparateOriginal(); - KisFixedPaintDeviceSP fetchDab(const KoColorSpace *cs, - const KisColorSource *colorSource, + KisColorSource *colorSource, const QPointF &cursorPoint, KisDabShape const&, const KisPaintInformation& info, qreal softnessFactor, QRect *dstDabRect); KisFixedPaintDeviceSP fetchDab(const KoColorSpace *cs, const KoColor& color, const QPointF &cursorPoint, KisDabShape const&, const KisPaintInformation& info, qreal softnessFactor, QRect *dstDabRect); + void setSharpnessPostprocessing(KisPressureSharpnessOption *option); + void setTexturePostprocessing(KisTextureProperties *option); + + bool needSeparateOriginal() const; private: - struct SavedDabParameters; - struct DabPosition; -private: - inline SavedDabParameters getDabParameters(const KoColor& color, - KisDabShape const&, - const KisPaintInformation& info, - double subPixelX, double subPixelY, - qreal softnessFactor, - MirrorProperties mirrorProperties); - inline KisDabCache::DabPosition - calculateDabRect(const QPointF &cursorPoint, - KisDabShape, - const KisPaintInformation& info, - const MirrorProperties &mirrorProperties); - - inline - QRect correctDabRectWhenFetchedFromCache(const QRect &dabRect, - const QSize &realDabSize); - - inline KisFixedPaintDeviceSP tryFetchFromCache(const SavedDabParameters ¶ms, - const KisPaintInformation& info, - QRect *dstDabRect); + + inline KisFixedPaintDeviceSP fetchFromCache(KisDabCacheUtils::DabRenderingResources *resources, const KisPaintInformation& info, + QRect *dstDabRect); inline KisFixedPaintDeviceSP fetchDabCommon(const KoColorSpace *cs, - const KisColorSource *colorSource, + KisColorSource *colorSource, const KoColor& color, const QPointF &cursorPoint, KisDabShape, const KisPaintInformation& info, qreal softnessFactor, QRect *dstDabRect); - void postProcessDab(KisFixedPaintDeviceSP dab, - const QPoint &dabTopLeft, - const KisPaintInformation& info); - private: struct Private; Private * const m_d; }; #endif /* __KIS_DAB_CACHE_H */ diff --git a/plugins/paintops/libpaintop/kis_dab_cache_base.cpp b/plugins/paintops/libpaintop/kis_dab_cache_base.cpp new file mode 100644 index 0000000000..9fb631abae --- /dev/null +++ b/plugins/paintops/libpaintop/kis_dab_cache_base.cpp @@ -0,0 +1,280 @@ +/* + * Copyright (c) 2012 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "kis_dab_cache_base.h" + +#include +#include "kis_color_source.h" +#include "kis_paint_device.h" +#include "kis_brush.h" +#include +#include +#include +#include +#include +#include + +#include + +struct PrecisionValues { + qreal angle; + qreal sizeFrac; + qreal subPixel; + qreal softnessFactor; +}; + +const qreal eps = 1e-6; +static const PrecisionValues precisionLevels[] = { + {M_PI / 180, 0.05, 1, 0.01}, + {M_PI / 180, 0.01, 1, 0.01}, + {M_PI / 180, 0, 1, 0.01}, + {M_PI / 180, 0, 0.5, 0.01}, + {eps, 0, eps, eps} +}; + +struct KisDabCacheBase::SavedDabParameters { + KoColor color; + qreal angle; + int width; + int height; + qreal subPixelX; + qreal subPixelY; + qreal softnessFactor; + int index; + MirrorProperties mirrorProperties; + + bool compare(const SavedDabParameters &rhs, int precisionLevel) const { + const PrecisionValues &prec = precisionLevels[precisionLevel]; + + return color == rhs.color && + qAbs(angle - rhs.angle) <= prec.angle && + qAbs(width - rhs.width) <= (int)(prec.sizeFrac * width) && + qAbs(height - rhs.height) <= (int)(prec.sizeFrac * height) && + qAbs(subPixelX - rhs.subPixelX) <= prec.subPixel && + qAbs(subPixelY - rhs.subPixelY) <= prec.subPixel && + qAbs(softnessFactor - rhs.softnessFactor) <= prec.softnessFactor && + index == rhs.index && + mirrorProperties.horizontalMirror == rhs.mirrorProperties.horizontalMirror && + mirrorProperties.verticalMirror == rhs.mirrorProperties.verticalMirror; + } +}; + +struct KisDabCacheBase::Private { + + Private() + : mirrorOption(0), + precisionOption(0), + subPixelPrecisionDisabled(false) + {} + + KisPressureMirrorOption *mirrorOption; + KisPrecisionOption *precisionOption; + bool subPixelPrecisionDisabled; + + SavedDabParameters lastSavedDabParameters; + + static qreal positiveFraction(qreal x); +}; + + + +KisDabCacheBase::KisDabCacheBase() + : m_d(new Private()) +{ +} + +KisDabCacheBase::~KisDabCacheBase() +{ + delete m_d; +} + +void KisDabCacheBase::setMirrorPostprocessing(KisPressureMirrorOption *option) +{ + m_d->mirrorOption = option; +} + +void KisDabCacheBase::setPrecisionOption(KisPrecisionOption *option) +{ + m_d->precisionOption = option; +} + +void KisDabCacheBase::disableSubpixelPrecision() +{ + m_d->subPixelPrecisionDisabled = true; +} + +inline KisDabCacheBase::SavedDabParameters +KisDabCacheBase::getDabParameters(KisBrushSP brush, + const KoColor& color, + KisDabShape const& shape, + const KisPaintInformation& info, + double subPixelX, double subPixelY, + qreal softnessFactor, + MirrorProperties mirrorProperties) +{ + SavedDabParameters params; + + params.color = color; + params.angle = shape.rotation(); + params.width = brush->maskWidth(shape, subPixelX, subPixelY, info); + params.height = brush->maskHeight(shape, subPixelX, subPixelY, info); + params.subPixelX = subPixelX; + params.subPixelY = subPixelY; + params.softnessFactor = softnessFactor; + params.index = brush->brushIndex(info); + params.mirrorProperties = mirrorProperties; + + return params; +} + +bool KisDabCacheBase::needSeparateOriginal(KisTextureProperties *textureOption, + KisPressureSharpnessOption *sharpnessOption) const +{ + return (textureOption && textureOption->m_enabled) || + (sharpnessOption && sharpnessOption->isChecked()); +} + +struct KisDabCacheBase::DabPosition { + DabPosition(const QRect &_rect, + const QPointF &_subPixel, + qreal _realAngle) + : rect(_rect), + subPixel(_subPixel), + realAngle(_realAngle) { + } + + QRect rect; + QPointF subPixel; + qreal realAngle; +}; + +qreal KisDabCacheBase::Private::positiveFraction(qreal x) { + qint32 unused = 0; + qreal fraction = 0.0; + KisPaintOp::splitCoordinate(x, &unused, &fraction); + + return fraction; +} + +inline +KisDabCacheBase::DabPosition +KisDabCacheBase::calculateDabRect(KisBrushSP brush, + const QPointF &cursorPoint, + KisDabShape shape, + const KisPaintInformation& info, + const MirrorProperties &mirrorProperties, + KisPressureSharpnessOption *sharpnessOption) +{ + qint32 x = 0, y = 0; + qreal subPixelX = 0.0, subPixelY = 0.0; + + if (mirrorProperties.coordinateSystemFlipped) { + shape = KisDabShape(shape.scale(), shape.ratio(), 2 * M_PI - shape.rotation()); + } + + QPointF hotSpot = brush->hotSpot(shape, info); + QPointF pt = cursorPoint - hotSpot; + + if (sharpnessOption) { + sharpnessOption->apply(info, pt, x, y, subPixelX, subPixelY); + } + else { + KisPaintOp::splitCoordinate(pt.x(), &x, &subPixelX); + KisPaintOp::splitCoordinate(pt.y(), &y, &subPixelY); + } + + if (m_d->subPixelPrecisionDisabled) { + subPixelX = 0; + subPixelY = 0; + } + + if (qIsNaN(subPixelX)) { + subPixelX = 0; + } + + if (qIsNaN(subPixelY)) { + subPixelY = 0; + } + + int width = brush->maskWidth(shape, subPixelX, subPixelY, info); + int height = brush->maskHeight(shape, subPixelX, subPixelY, info); + + if (mirrorProperties.horizontalMirror) { + subPixelX = Private::positiveFraction(-(cursorPoint.x() + hotSpot.x())); + width = brush->maskWidth(shape, subPixelX, subPixelY, info); + x = qRound(cursorPoint.x() + subPixelX + hotSpot.x()) - width; + } + + if (mirrorProperties.verticalMirror) { + subPixelY = Private::positiveFraction(-(cursorPoint.y() + hotSpot.y())); + height = brush->maskHeight(shape, subPixelX, subPixelY, info); + y = qRound(cursorPoint.y() + subPixelY + hotSpot.y()) - height; + } + + return DabPosition(QRect(x, y, width, height), + QPointF(subPixelX, subPixelY), + shape.rotation()); +} + +void KisDabCacheBase::fetchDabGenerationInfo(bool hasDabInCache, + KisDabCacheUtils::DabRenderingResources *resources, + const KisDabCacheUtils::DabRequestInfo &request, + KisDabCacheUtils::DabGenerationInfo *di, + bool *shouldUseCache) +{ + di->info = request.info; + di->softnessFactor = request.softnessFactor; + + if (m_d->mirrorOption) { + di->mirrorProperties = m_d->mirrorOption->apply(request.info); + } + + DabPosition position = calculateDabRect(resources->brush, + request.cursorPoint, + request.shape, + request.info, + di->mirrorProperties, + resources->sharpnessOption.data()); + di->shape = KisDabShape(request.shape.scale(), request.shape.ratio(), position.realAngle); + di->dstDabRect = position.rect; + di->subPixel = position.subPixel; + + di->solidColorFill = !resources->colorSource || resources->colorSource->isUniformColor(); + di->paintColor = resources->colorSource && resources->colorSource->isUniformColor() ? + resources->colorSource->uniformColor() : request.color; + + SavedDabParameters newParams = getDabParameters(resources->brush, + di->paintColor, + di->shape, + di->info, + di->subPixel.x(), + di->subPixel.y(), + di->softnessFactor, + di->mirrorProperties); + + const int precisionLevel = m_d->precisionOption ? m_d->precisionOption->precisionLevel() - 1 : 3; + *shouldUseCache = hasDabInCache && di->solidColorFill && + newParams.compare(m_d->lastSavedDabParameters, precisionLevel); + + if (!*shouldUseCache) { + m_d->lastSavedDabParameters = newParams; + } + + di->needsPostprocessing = needSeparateOriginal(resources->textureOption.data(), resources->sharpnessOption.data()); +} + diff --git a/plugins/paintops/libpaintop/kis_dab_cache_base.h b/plugins/paintops/libpaintop/kis_dab_cache_base.h new file mode 100644 index 0000000000..77b9f7f91c --- /dev/null +++ b/plugins/paintops/libpaintop/kis_dab_cache_base.h @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2012 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef __KIS_DAB_CACHE_BASE_H +#define __KIS_DAB_CACHE_BASE_H + +#include "kritapaintop_export.h" +#include "kis_brush.h" + +#include "KisDabCacheUtils.h" + +class KisColorSource; +class KisPressureSharpnessOption; +class KisTextureProperties; +class KisPressureMirrorOption; +class KisPrecisionOption; +struct MirrorProperties; + + +/** + * @brief The KisDabCacheBase class provides caching for dabs into the brush paintop + * + * This class adds caching of the dabs to the paintop system of Krita. + * Such cache makes the execution of the benchmarks up to 2 times faster. + * Subjectively, the real painting becomes much faster, especially with + * huge brushes. Artists report up to 20% speed gain while painting. + * + * Of course, such caching makes the painting a bit less precise: we need + * to tolerate subpixel differences to allow the cache to work. Sometimes + * small difference in the size of a dab can also be acceptable. That is + * why I introduced levels of precision. They are graded from 1 to 5: from + * the fastest and less precise to the slowest, but with the best quality. + * You can see the slider in the paintop settings dialog. The ToolTip text + * explains which features of the brush are sacrificed on each precision + * level. + * + * The texturing and mirroring problems are solved. + */ +class PAINTOP_EXPORT KisDabCacheBase +{ +public: + KisDabCacheBase(); + ~KisDabCacheBase(); + + void setMirrorPostprocessing(KisPressureMirrorOption *option); + void setPrecisionOption(KisPrecisionOption *option); + + /** + * Disables handling of the subPixelX and subPixelY values, this + * is needed at least for the Color Smudge paint op, which reads + * aligned areas from image, so additional offsets generated by + * the subpixel precision should be avoided + */ + void disableSubpixelPrecision(); + + /** + * Return true if the dab needs postprocesing by special options + * like 'texture' or 'sharpness' + */ + bool needSeparateOriginal(KisTextureProperties *textureOption, + KisPressureSharpnessOption *sharpnessOption) const; + +protected: + /** + * Fetches all the necessary information for dab generation and + * tells if the caller must should reuse the preciously returned dab. * + * Please note that KisDabCacheBase has an internal state, that keeps the + * parameters of the previously generated (on a cache-miss) dab. This function + * automatically updates this state when 'shouldUseCache == false'. Therefore, the + * caller *must* generate the dab if and only if when 'shouldUseCache == false'. + * Othewise the internal state will become inconsistent. + * + * @param hasDabInCache shows if the caller has something in its cache + * @param resources rendering resources available for this dab + * @param color current painting color + * @param cursorPoint cursor point at which the dab should be painted + * @param shape dab shape requested by the caller. It will be modified before + * generation to accomodate the mirroring and rotation options. + * @param info painting info associated with the dab + * @param softnessFactor softness factor + * @param di (OUT) calculated dab generation information + * @param shouldUseCache (OUT) shows whether the caller *must* use cache or not + */ + void fetchDabGenerationInfo(bool hasDabInCache, + KisDabCacheUtils::DabRenderingResources *resources, + const KisDabCacheUtils::DabRequestInfo &request, + /* out */ + KisDabCacheUtils::DabGenerationInfo *di, + bool *shouldUseCache); + +private: + struct SavedDabParameters; + struct DabPosition; +private: + inline SavedDabParameters getDabParameters(KisBrushSP brush, const KoColor& color, + KisDabShape const&, + const KisPaintInformation& info, + double subPixelX, double subPixelY, + qreal softnessFactor, + MirrorProperties mirrorProperties); + + inline KisDabCacheBase::DabPosition + calculateDabRect(KisBrushSP brush, const QPointF &cursorPoint, + KisDabShape, + const KisPaintInformation& info, + const MirrorProperties &mirrorProperties, KisPressureSharpnessOption *sharpnessOption); + +private: + struct Private; + Private * const m_d; +}; + +#endif /* __KIS_DAB_CACHE_BASE_H */ diff --git a/plugins/paintops/libpaintop/kis_dynamic_sensor.cc b/plugins/paintops/libpaintop/kis_dynamic_sensor.cc index 09c18c5215..6e48cc5d4f 100644 --- a/plugins/paintops/libpaintop/kis_dynamic_sensor.cc +++ b/plugins/paintops/libpaintop/kis_dynamic_sensor.cc @@ -1,477 +1,477 @@ /* * Copyright (c) 2007 Cyrille Berger * Copyright (c) 2011 Lukáš Tvrdý * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_dynamic_sensor.h" #include #include "kis_algebra_2d.h" #include "sensors/kis_dynamic_sensors.h" #include "sensors/kis_dynamic_sensor_distance.h" #include "sensors/kis_dynamic_sensor_drawing_angle.h" #include "sensors/kis_dynamic_sensor_time.h" #include "sensors/kis_dynamic_sensor_fade.h" #include "sensors/kis_dynamic_sensor_fuzzy.h" KisDynamicSensor::KisDynamicSensor(DynamicSensorType type) : m_length(-1) , m_type(type) , m_customCurve(false) , m_active(false) { } KisDynamicSensor::~KisDynamicSensor() { } QWidget* KisDynamicSensor::createConfigurationWidget(QWidget* parent, QWidget*) { Q_UNUSED(parent); return 0; } void KisDynamicSensor::reset() { } -KisDynamicSensorSP KisDynamicSensor::id2Sensor(const KoID& id) +KisDynamicSensorSP KisDynamicSensor::id2Sensor(const KoID& id, const QString &parentOptionName) { if (id.id() == PressureId.id()) { return new KisDynamicSensorPressure(); } else if (id.id() == PressureInId.id()) { return new KisDynamicSensorPressureIn(); } else if (id.id() == XTiltId.id()) { return new KisDynamicSensorXTilt(); } else if (id.id() == YTiltId.id()) { return new KisDynamicSensorYTilt(); } else if (id.id() == TiltDirectionId.id()) { return new KisDynamicSensorTiltDirection(); } else if (id.id() == TiltElevationId.id()) { return new KisDynamicSensorTiltElevation(); } else if (id.id() == SpeedId.id()) { return new KisDynamicSensorSpeed(); } else if (id.id() == DrawingAngleId.id()) { return new KisDynamicSensorDrawingAngle(); } else if (id.id() == RotationId.id()) { return new KisDynamicSensorRotation(); } else if (id.id() == DistanceId.id()) { return new KisDynamicSensorDistance(); } else if (id.id() == TimeId.id()) { return new KisDynamicSensorTime(); } else if (id.id() == FuzzyPerDabId.id()) { - return new KisDynamicSensorFuzzy(false); + return new KisDynamicSensorFuzzy(false, parentOptionName); } else if (id.id() == FuzzyPerStrokeId.id()) { - return new KisDynamicSensorFuzzy(true); + return new KisDynamicSensorFuzzy(true, parentOptionName); } else if (id.id() == FadeId.id()) { return new KisDynamicSensorFade(); } else if (id.id() == PerspectiveId.id()) { return new KisDynamicSensorPerspective(); } else if (id.id() == TangentialPressureId.id()) { return new KisDynamicSensorTangentialPressure(); } dbgPlugins << "Unknown transform parameter :" << id.id(); return 0; } DynamicSensorType KisDynamicSensor::id2Type(const KoID &id) { if (id.id() == PressureId.id()) { return PRESSURE; } else if (id.id() == PressureInId.id()) { return PRESSURE_IN; } else if (id.id() == XTiltId.id()) { return XTILT; } else if (id.id() == YTiltId.id()) { return YTILT; } else if (id.id() == TiltDirectionId.id()) { return TILT_DIRECTION; } else if (id.id() == TiltElevationId.id()) { return TILT_ELEVATATION; } else if (id.id() == SpeedId.id()) { return SPEED; } else if (id.id() == DrawingAngleId.id()) { return ANGLE; } else if (id.id() == RotationId.id()) { return ROTATION; } else if (id.id() == DistanceId.id()) { return DISTANCE; } else if (id.id() == TimeId.id()) { return TIME; } else if (id.id() == FuzzyPerDabId.id()) { return FUZZY_PER_DAB; } else if (id.id() == FuzzyPerStrokeId.id()) { return FUZZY_PER_STROKE; } else if (id.id() == FadeId.id()) { return FADE; } else if (id.id() == PerspectiveId.id()) { return PERSPECTIVE; } else if (id.id() == TangentialPressureId.id()) { return TANGENTIAL_PRESSURE; } return UNKNOWN; } -KisDynamicSensorSP KisDynamicSensor::type2Sensor(DynamicSensorType sensorType) +KisDynamicSensorSP KisDynamicSensor::type2Sensor(DynamicSensorType sensorType, const QString &parentOptionName) { switch (sensorType) { case FUZZY_PER_DAB: - return new KisDynamicSensorFuzzy(false); + return new KisDynamicSensorFuzzy(false, parentOptionName); case FUZZY_PER_STROKE: - return new KisDynamicSensorFuzzy(true); + return new KisDynamicSensorFuzzy(true, parentOptionName); case SPEED: return new KisDynamicSensorSpeed(); case FADE: return new KisDynamicSensorFade(); case DISTANCE: return new KisDynamicSensorDistance(); case TIME: return new KisDynamicSensorTime(); case ANGLE: return new KisDynamicSensorDrawingAngle(); case ROTATION: return new KisDynamicSensorRotation(); case PRESSURE: return new KisDynamicSensorPressure(); case XTILT: return new KisDynamicSensorXTilt(); case YTILT: return new KisDynamicSensorYTilt(); case TILT_DIRECTION: return new KisDynamicSensorTiltDirection(); case TILT_ELEVATATION: return new KisDynamicSensorTiltElevation(); case PERSPECTIVE: return new KisDynamicSensorPerspective(); case TANGENTIAL_PRESSURE: return new KisDynamicSensorTangentialPressure(); case PRESSURE_IN: return new KisDynamicSensorPressureIn(); default: return 0; } } QString KisDynamicSensor::minimumLabel(DynamicSensorType sensorType) { switch (sensorType) { case FUZZY_PER_DAB: case FUZZY_PER_STROKE: return QString(); case FADE: return i18n("0"); case DISTANCE: return i18n("0 px"); case TIME: return i18n("0 s"); case ANGLE: return i18n("0°"); case SPEED: return i18n("Slow"); case ROTATION: return i18n("0°"); case PRESSURE: return i18n("Low"); case XTILT: return i18n("-30°"); case YTILT: return i18n("-30°"); case TILT_DIRECTION: return i18n("0°"); case TILT_ELEVATATION: return i18n("90°"); case PERSPECTIVE: return i18n("Far"); case TANGENTIAL_PRESSURE: case PRESSURE_IN: return i18n("Low"); default: return i18n("0.0"); } } QString KisDynamicSensor::maximumLabel(DynamicSensorType sensorType, int max) { switch (sensorType) { case FUZZY_PER_DAB: case FUZZY_PER_STROKE: return QString(); case FADE: if (max < 0) return i18n("1000"); else return i18n("%1", max); case DISTANCE: if (max < 0) return i18n("30 px"); else return i18n("%1 px", max); case TIME: if (max < 0) return i18n("3 s"); else return i18n("%1 s", max / 1000); case ANGLE: return i18n("360°"); case SPEED: return i18n("Fast"); case ROTATION: return i18n("360°"); case PRESSURE: return i18n("High"); case XTILT: return i18n("30°"); case YTILT: return i18n("30°"); case TILT_DIRECTION: return i18n("360°"); case TILT_ELEVATATION: return i18n("0°"); case PERSPECTIVE: return i18n("Near"); case TANGENTIAL_PRESSURE: case PRESSURE_IN: return i18n("High"); default: return i18n("1.0"); }; } -KisDynamicSensorSP KisDynamicSensor::createFromXML(const QString& s) +KisDynamicSensorSP KisDynamicSensor::createFromXML(const QString& s, const QString &parentOptionName) { QDomDocument doc; doc.setContent(s); QDomElement e = doc.documentElement(); - return createFromXML(e); + return createFromXML(e, parentOptionName); } -KisDynamicSensorSP KisDynamicSensor::createFromXML(const QDomElement& e) +KisDynamicSensorSP KisDynamicSensor::createFromXML(const QDomElement& e, const QString &parentOptionName) { QString id = e.attribute("id", ""); - KisDynamicSensorSP sensor = id2Sensor(id); + KisDynamicSensorSP sensor = id2Sensor(id, parentOptionName); if (sensor) { sensor->fromXML(e); } return sensor; } QList KisDynamicSensor::sensorsIds() { QList ids; ids << PressureId << PressureInId << XTiltId << YTiltId << TiltDirectionId << TiltElevationId << SpeedId << DrawingAngleId << RotationId << DistanceId << TimeId << FuzzyPerDabId << FuzzyPerStrokeId << FadeId << PerspectiveId << TangentialPressureId; return ids; } QList KisDynamicSensor::sensorsTypes() { QList sensorTypes; sensorTypes << PRESSURE << PRESSURE_IN << XTILT << YTILT << TILT_DIRECTION << TILT_ELEVATATION << SPEED << ANGLE << ROTATION << DISTANCE << TIME << FUZZY_PER_DAB << FUZZY_PER_STROKE << FADE << PERSPECTIVE << TANGENTIAL_PRESSURE; return sensorTypes; } QString KisDynamicSensor::id(DynamicSensorType sensorType) { switch (sensorType) { case FUZZY_PER_DAB: return "fuzzy"; case FUZZY_PER_STROKE: return "fuzzystroke"; case FADE: return "fade"; case DISTANCE: return "distance"; case TIME: return "time"; case ANGLE: return "drawingangle"; case SPEED: return "speed"; case ROTATION: return "rotation"; case PRESSURE: return "pressure"; case XTILT: return "xtilt"; case YTILT: return "ytilt"; case TILT_DIRECTION: return "ascension"; case TILT_ELEVATATION: return "declination"; case PERSPECTIVE: return "perspective"; case TANGENTIAL_PRESSURE: return "tangentialpressure"; case PRESSURE_IN: return "pressurein"; case SENSORS_LIST: return "sensorslist"; default: return QString(); }; } void KisDynamicSensor::toXML(QDomDocument& doc, QDomElement& elt) const { elt.setAttribute("id", id(sensorType())); if (m_customCurve) { QDomElement curve_elt = doc.createElement("curve"); QDomText text = doc.createTextNode(m_curve.toString()); curve_elt.appendChild(text); elt.appendChild(curve_elt); } } void KisDynamicSensor::fromXML(const QDomElement& e) { Q_ASSERT(e.attribute("id", "") == id(sensorType())); m_customCurve = false; QDomElement curve_elt = e.firstChildElement("curve"); if (!curve_elt.isNull()) { m_customCurve = true; m_curve.fromString(curve_elt.text()); } } qreal KisDynamicSensor::parameter(const KisPaintInformation& info) { const qreal val = value(info); if (m_customCurve) { qreal scaledVal = isAdditive() ? additiveToScaling(val) : val; int offset = qRound(256.0 * qAbs(scaledVal)); qreal newValue = m_curve.floatTransfer(257)[qBound(0, offset, 256)]; scaledVal = KisAlgebra2D::copysign(newValue, scaledVal); return isAdditive() ? scalingToAdditive(scaledVal) : scaledVal; } else { return val; } } void KisDynamicSensor::setCurve(const KisCubicCurve& curve) { m_customCurve = true; m_curve = curve; } const KisCubicCurve& KisDynamicSensor::curve() const { return m_curve; } void KisDynamicSensor::removeCurve() { m_customCurve = false; } bool KisDynamicSensor::hasCustomCurve() const { return m_customCurve; } bool KisDynamicSensor::dependsOnCanvasRotation() const { return true; } bool KisDynamicSensor::isAdditive() const { return false; } bool KisDynamicSensor::isAbsoluteRotation() const { return false; } void KisDynamicSensor::setActive(bool active) { m_active = active; } bool KisDynamicSensor::isActive() const { return m_active; } diff --git a/plugins/paintops/libpaintop/kis_dynamic_sensor.h b/plugins/paintops/libpaintop/kis_dynamic_sensor.h index b852f0a44e..ae26dbb1e2 100644 --- a/plugins/paintops/libpaintop/kis_dynamic_sensor.h +++ b/plugins/paintops/libpaintop/kis_dynamic_sensor.h @@ -1,217 +1,217 @@ /* * Copyright (c) 2006 Cyrille Berger * Copyright (c) 2011 Lukáš Tvrdý * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_DYNAMIC_SENSOR_H_ #define _KIS_DYNAMIC_SENSOR_H_ #include #include #include #include #include "kis_serializable_configuration.h" #include "kis_curve_label.h" #include #include #include class QWidget; class KisPaintInformation; const KoID FuzzyPerDabId("fuzzy", ki18n("Fuzzy Dab")); ///< generate a random number const KoID FuzzyPerStrokeId("fuzzystroke", ki18n("Fuzzy Stroke")); ///< generate a random number const KoID SpeedId("speed", ki18n("Speed")); ///< generate a number depending on the speed of the cursor const KoID FadeId("fade", ki18n("Fade")); ///< generate a number that increase every time you call it (e.g. per dab) const KoID DistanceId("distance", ki18n("Distance")); ///< generate a number that increase with distance const KoID TimeId("time", ki18n("Time")); ///< generate a number that increase with time const KoID DrawingAngleId("drawingangle", ki18n("Drawing angle")); ///< number depending on the angle const KoID RotationId("rotation", ki18n("Rotation")); ///< rotation coming from the device const KoID PressureId("pressure", ki18n("Pressure")); ///< number depending on the pressure const KoID PressureInId("pressurein", ki18n("PressureIn")); ///< number depending on the pressure const KoID XTiltId("xtilt", ki18n("X-Tilt")); ///< number depending on X-tilt const KoID YTiltId("ytilt", ki18n("Y-Tilt")); ///< number depending on Y-tilt /** * "TiltDirection" and "TiltElevation" parameters are written to * preset files as "ascension" and "declination" to keep backward * compatibility with older presets from the days when they were called * differently. */ const KoID TiltDirectionId("ascension", ki18n("Tilt direction")); /// < number depending on the X and Y tilt, tilt direction is 0 when stylus nib points to you and changes clockwise from -180 to +180. const KoID TiltElevationId("declination", ki18n("Tilt elevation")); /// < tilt elevation is 90 when stylus is perpendicular to tablet and 0 when it's parallel to tablet const KoID PerspectiveId("perspective", ki18n("Perspective")); ///< number depending on the distance on the perspective grid const KoID TangentialPressureId("tangentialpressure", ki18n("Tangential pressure")); ///< the wheel on an airbrush device const KoID SensorsListId("sensorslist", "SHOULD NOT APPEAR IN THE UI !"); ///< this a non user-visible sensor that can store a list of other sensors, and multiply their output class KisDynamicSensor; typedef KisSharedPtr KisDynamicSensorSP; enum DynamicSensorType { FUZZY_PER_DAB, FUZZY_PER_STROKE, SPEED, FADE, DISTANCE, TIME, ANGLE, ROTATION, PRESSURE, XTILT, YTILT, TILT_DIRECTION, TILT_ELEVATATION, PERSPECTIVE, TANGENTIAL_PRESSURE, SENSORS_LIST, PRESSURE_IN, UNKNOWN = 255 }; /** * Sensors are used to extract from KisPaintInformation a single * double value which can be used to control the parameters of * a brush. */ class PAINTOP_EXPORT KisDynamicSensor : public KisSerializableConfiguration { public: enum ParameterSign { NegativeParameter = -1, UnSignedParameter = 0, PositiveParameter = 1 }; protected: KisDynamicSensor(DynamicSensorType type); public: ~KisDynamicSensor() override; /** * @return the value of this sensor for the given KisPaintInformation */ qreal parameter(const KisPaintInformation& info); /** * This function is call before beginning a stroke to reset the sensor. * Default implementation does nothing. */ virtual void reset(); /** * @param selector is a \ref QWidget that countains a signal called "parametersChanged()" */ virtual QWidget* createConfigurationWidget(QWidget* parent, QWidget* selector); /** * Creates a sensor from its identifiant. */ - static KisDynamicSensorSP id2Sensor(const KoID& id); - static KisDynamicSensorSP id2Sensor(const QString& s) { - return id2Sensor(KoID(s)); + static KisDynamicSensorSP id2Sensor(const KoID& id, const QString &parentOptionName); + static KisDynamicSensorSP id2Sensor(const QString& s, const QString &parentOptionName) { + return id2Sensor(KoID(s), parentOptionName); } static DynamicSensorType id2Type(const KoID& id); static DynamicSensorType id2Type(const QString& s) { return id2Type(KoID(s)); } /** * type2Sensor creates a new sensor for the give type */ - static KisDynamicSensorSP type2Sensor(DynamicSensorType sensorType); + static KisDynamicSensorSP type2Sensor(DynamicSensorType sensorType, const QString &parentOptionName); static QString minimumLabel(DynamicSensorType sensorType); static QString maximumLabel(DynamicSensorType sensorType, int max = -1); - static KisDynamicSensorSP createFromXML(const QString&); - static KisDynamicSensorSP createFromXML(const QDomElement&); + static KisDynamicSensorSP createFromXML(const QString&, const QString &parentOptionName); + static KisDynamicSensorSP createFromXML(const QDomElement&, const QString &parentOptionName); /** * @return the list of sensors */ static QList sensorsIds(); static QList sensorsTypes(); /** * @return the identifiant of this sensor */ static QString id(DynamicSensorType sensorType); using KisSerializableConfiguration::fromXML; using KisSerializableConfiguration::toXML; void toXML(QDomDocument&, QDomElement&) const override; void fromXML(const QDomElement&) override; void setCurve(const KisCubicCurve& curve); const KisCubicCurve& curve() const; void removeCurve(); bool hasCustomCurve() const; void setActive(bool active); bool isActive() const; virtual bool dependsOnCanvasRotation() const; virtual bool isAdditive() const; virtual bool isAbsoluteRotation() const; inline DynamicSensorType sensorType() const { return m_type; } /** * @return the currently set length or -1 if not relevant */ int length() { return m_length; } public: static inline qreal scalingToAdditive(qreal x) { return -1.0 + 2.0 * x; } static inline qreal additiveToScaling(qreal x) { return 0.5 * (1.0 + x); } protected: virtual qreal value(const KisPaintInformation& info) = 0; int m_length; private: Q_DISABLE_COPY(KisDynamicSensor) DynamicSensorType m_type; bool m_customCurve; KisCubicCurve m_curve; bool m_active; }; #endif diff --git a/plugins/paintops/libpaintop/kis_multi_sensors_model_p.cpp b/plugins/paintops/libpaintop/kis_multi_sensors_model_p.cpp index 8616b67f88..7defecc556 100644 --- a/plugins/paintops/libpaintop/kis_multi_sensors_model_p.cpp +++ b/plugins/paintops/libpaintop/kis_multi_sensors_model_p.cpp @@ -1,124 +1,124 @@ /* * Copyright (c) 2011 Cyrille Berger * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_multi_sensors_model_p.h" #include "kis_dynamic_sensor.h" #include "kis_curve_option.h" KisMultiSensorsModel::KisMultiSensorsModel(QObject* parent) : QAbstractListModel(parent) , m_curveOption(0) { } KisMultiSensorsModel::~KisMultiSensorsModel() { } void KisMultiSensorsModel::setCurveOption(KisCurveOption *curveOption) { beginResetModel(); m_curveOption = curveOption; endResetModel(); } int KisMultiSensorsModel::rowCount(const QModelIndex &/*parent*/) const { if (m_curveOption) { return m_curveOption->sensors().size(); } else { return 0; } } QVariant KisMultiSensorsModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); if (role == Qt::DisplayRole) { return KisDynamicSensor::sensorsIds()[index.row()].name(); } else if (role == Qt::CheckStateRole) { QString selectedSensorId = KisDynamicSensor::sensorsIds()[index.row()].id(); KisDynamicSensorSP sensor = m_curveOption->sensor(KisDynamicSensor::id2Type(selectedSensorId), false); if (sensor) { //dbgKrita << sensor->id() << sensor->isActive(); return QVariant(sensor->isActive() ? Qt::Checked : Qt::Unchecked); } else { return QVariant(Qt::Unchecked); } } return QVariant(); } bool KisMultiSensorsModel::setData(const QModelIndex &index, const QVariant &value, int role) { bool result = false; if (role == Qt::CheckStateRole) { bool checked = (value.toInt() == Qt::Checked); if (checked || m_curveOption->activeSensors().size() != 1) { // Don't uncheck the last sensor (but why not?) KisDynamicSensorSP sensor = m_curveOption->sensor(KisDynamicSensor::id2Type(KisDynamicSensor::sensorsIds()[index.row()].id()), false); if (!sensor) { - sensor = KisDynamicSensor::id2Sensor(KisDynamicSensor::sensorsIds()[index.row()].id()); + sensor = KisDynamicSensor::id2Sensor(KisDynamicSensor::sensorsIds()[index.row()].id(), "NOT_VALID_NAME"); m_curveOption->replaceSensor(sensor); } sensor->setActive(checked); emit(parametersChanged()); result = true; } } return result; } Qt::ItemFlags KisMultiSensorsModel::flags(const QModelIndex & /*index */) const { return Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled; } KisDynamicSensorSP KisMultiSensorsModel::getSensor(const QModelIndex& index) { if (!index.isValid()) return 0; QString id = KisDynamicSensor::sensorsIds()[index.row()].id(); return m_curveOption->sensor(KisDynamicSensor::id2Type(id), false); } void KisMultiSensorsModel::setCurrentCurve(const QModelIndex& currentIndex, const KisCubicCurve& curve, bool useSameCurve) { if (!currentIndex.isValid()) return; QString selectedSensorId = KisDynamicSensor::sensorsIds()[currentIndex.row()].id(); m_curveOption->setCurve(KisDynamicSensor::id2Type(selectedSensorId), useSameCurve, curve); } QModelIndex KisMultiSensorsModel::sensorIndex(KisDynamicSensorSP arg1) { return index(KisDynamicSensor::sensorsIds().indexOf(KoID(KisDynamicSensor::id(arg1->sensorType())))); } void KisMultiSensorsModel::resetCurveOption() { beginResetModel(); endResetModel(); } diff --git a/plugins/paintops/libpaintop/kis_pressure_flow_opacity_option.cpp b/plugins/paintops/libpaintop/kis_pressure_flow_opacity_option.cpp index c05e72927e..007f585323 100644 --- a/plugins/paintops/libpaintop/kis_pressure_flow_opacity_option.cpp +++ b/plugins/paintops/libpaintop/kis_pressure_flow_opacity_option.cpp @@ -1,91 +1,103 @@ /* * Copyright (c) 2011 Silvio Heinrich * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "kis_pressure_flow_opacity_option.h" #include "kis_paint_action_type_option.h" #include #include #include #include #include #include KisFlowOpacityOption::KisFlowOpacityOption(KisNodeSP currentNode) : KisCurveOption("Opacity", KisPaintOpOption::GENERAL, true, 1.0, 0.0, 1.0) , m_flow(1.0) { setCurveUsed(true); setSeparateCurveValue(true); m_checkable = false; m_nodeHasIndirectPaintingSupport = currentNode && dynamic_cast(currentNode.data()); } void KisFlowOpacityOption::writeOptionSetting(KisPropertiesConfigurationSP setting) const { KisCurveOption::writeOptionSetting(setting); setting->setProperty("FlowValue", m_flow); } void KisFlowOpacityOption::readOptionSetting(const KisPropertiesConfigurationSP setting) { KisCurveOption::readOptionSetting(setting); setFlow(setting->getDouble("FlowValue", 1.0)); m_paintActionType = setting->getInt("PaintOpAction", BUILDUP); } qreal KisFlowOpacityOption::getFlow() const { return m_flow; } qreal KisFlowOpacityOption::getStaticOpacity() const { return value(); } qreal KisFlowOpacityOption::getDynamicOpacity(const KisPaintInformation& info) const { return computeSizeLikeValue(info); } void KisFlowOpacityOption::setFlow(qreal flow) { m_flow = qBound(qreal(0), flow, qreal(1)); } void KisFlowOpacityOption::setOpacity(qreal opacity) { setValue(opacity); } void KisFlowOpacityOption::apply(KisPainter* painter, const KisPaintInformation& info) { - if (m_paintActionType == WASH && m_nodeHasIndirectPaintingSupport) - painter->setOpacityUpdateAverage(quint8(getDynamicOpacity(info) * 255.0)); - else - painter->setOpacityUpdateAverage(quint8(getStaticOpacity() * getDynamicOpacity(info) * 255.0)); + quint8 opacity = OPACITY_OPAQUE_U8; + quint8 flow = OPACITY_OPAQUE_U8; - painter->setFlow(quint8(getFlow() * 255.0)); + apply(info, &opacity, &flow); + + painter->setOpacityUpdateAverage(opacity); + painter->setFlow(flow); +} + +void KisFlowOpacityOption::apply(const KisPaintInformation &info, quint8 *opacity, quint8 *flow) +{ + if (m_paintActionType == WASH && m_nodeHasIndirectPaintingSupport) { + *opacity = quint8(getDynamicOpacity(info) * 255.0); + } else { + *opacity = quint8(getStaticOpacity() * getDynamicOpacity(info) * 255.0); + } + + *flow = quint8(getFlow() * 255.0); } diff --git a/plugins/paintops/libpaintop/kis_pressure_flow_opacity_option.h b/plugins/paintops/libpaintop/kis_pressure_flow_opacity_option.h index f822372fe6..bb52cf6ec9 100644 --- a/plugins/paintops/libpaintop/kis_pressure_flow_opacity_option.h +++ b/plugins/paintops/libpaintop/kis_pressure_flow_opacity_option.h @@ -1,54 +1,55 @@ /* * Copyright (c) 2011 Silvio Heinrich * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KIS_PRESSURE_FLOW_OPACITY_OPTION_H #define KIS_PRESSURE_FLOW_OPACITY_OPTION_H #include "kis_curve_option.h" #include #include #include class KisPaintInformation; class KisPainter; class PAINTOP_EXPORT KisFlowOpacityOption: public KisCurveOption { public: KisFlowOpacityOption(KisNodeSP currentNode); ~KisFlowOpacityOption() override { } void writeOptionSetting(KisPropertiesConfigurationSP setting) const override; void readOptionSetting(const KisPropertiesConfigurationSP setting) override; void setFlow(qreal flow); void setOpacity(qreal opacity); void apply(KisPainter* painter, const KisPaintInformation& info); + void apply(const KisPaintInformation& info, quint8 *opacity, quint8 *flow); qreal getFlow() const; qreal getStaticOpacity() const; qreal getDynamicOpacity(const KisPaintInformation& info) const; protected: qreal m_flow; int m_paintActionType; bool m_nodeHasIndirectPaintingSupport; }; #endif //KIS_PRESSURE_FLOW_OPACITY_OPTION_H diff --git a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_distance.cc b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_distance.cc index 193bbbc474..02347e68dc 100644 --- a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_distance.cc +++ b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_distance.cc @@ -1,90 +1,84 @@ /* * Copyright (c) 2007 Cyrille Berger * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_dynamic_sensor_distance.h" #include #include #include "ui_SensorDistanceConfiguration.h" #include KisDynamicSensorDistance::KisDynamicSensorDistance() : KisDynamicSensor(DISTANCE) - , m_measuredDistance(0.0) , m_periodic(true) { setLength(30); } qreal KisDynamicSensorDistance::value(const KisPaintInformation& pi) { if (pi.isHoveringMode()) return 1.0; - m_measuredDistance += pi.drawingDistance(); - m_measuredDistance = m_periodic ? - fmod(m_measuredDistance, m_length) : - qMin(m_measuredDistance, (qreal)m_length); + const qreal distance = + m_periodic ? + fmod(pi.totalStrokeLength(), m_length) : + qMin(pi.totalStrokeLength(), (qreal)m_length); - return m_measuredDistance / m_length; -} - -void KisDynamicSensorDistance::reset() -{ - m_measuredDistance = 0; + return distance / m_length; } void KisDynamicSensorDistance::setPeriodic(bool periodic) { m_periodic = periodic; } void KisDynamicSensorDistance::setLength(int length) { m_length = length; } QWidget* KisDynamicSensorDistance::createConfigurationWidget(QWidget* parent, QWidget* ss) { QWidget* wdg = new QWidget(parent); Ui_SensorDistanceConfiguration stc; stc.setupUi(wdg); stc.checkBoxRepeat->setChecked(m_periodic); connect(stc.checkBoxRepeat, SIGNAL(toggled(bool)), SLOT(setPeriodic(bool))); connect(stc.checkBoxRepeat, SIGNAL(toggled(bool)), ss, SIGNAL(parametersChanged())); stc.spinBoxLength->setValue(m_length); connect(stc.spinBoxLength, SIGNAL(valueChanged(int)), SLOT(setLength(int))); connect(stc.spinBoxLength, SIGNAL(valueChanged(int)), ss, SIGNAL(parametersChanged())); return wdg; } void KisDynamicSensorDistance::toXML(QDomDocument& doc, QDomElement& e) const { KisDynamicSensor::toXML(doc, e); e.setAttribute("periodic", m_periodic); e.setAttribute("length", m_length); } void KisDynamicSensorDistance::fromXML(const QDomElement& e) { KisDynamicSensor::fromXML(e); m_periodic = e.attribute("periodic", "0").toInt(); m_length = e.attribute("length", "30").toInt(); } diff --git a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_distance.h b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_distance.h index 89ba405370..7cc1b7093a 100644 --- a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_distance.h +++ b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_distance.h @@ -1,47 +1,45 @@ /* * Copyright (c) 2006 Cyrille Berger * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_DYNAMIC_SENSOR_DISTANCE_H_ #define _KIS_DYNAMIC_SENSOR_DISTANCE_H_ #include "kis_dynamic_sensor.h" // class KisDynamicSensorDistance : public QObject, public KisDynamicSensor { Q_OBJECT public: using KisSerializableConfiguration::fromXML; using KisSerializableConfiguration::toXML; KisDynamicSensorDistance(); ~KisDynamicSensorDistance() override { } qreal value(const KisPaintInformation&) override; - void reset() override; QWidget* createConfigurationWidget(QWidget* parent, QWidget*) override; public Q_SLOTS: virtual void setPeriodic(bool periodic); virtual void setLength(int length); void toXML(QDomDocument&, QDomElement&) const override; void fromXML(const QDomElement&) override; private: - qreal m_measuredDistance; bool m_periodic; }; #endif diff --git a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_drawing_angle.cpp b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_drawing_angle.cpp index f8f2e00bcc..b276a91ad2 100644 --- a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_drawing_angle.cpp +++ b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_drawing_angle.cpp @@ -1,189 +1,178 @@ /* * Copyright (c) 2013 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_dynamic_sensor_drawing_angle.h" #include #include #include #include #include #include KisDynamicSensorDrawingAngle::KisDynamicSensorDrawingAngle() : KisDynamicSensor(ANGLE), m_fanCornersEnabled(false), m_fanCornersStep(30), m_angleOffset(0), - m_dabIndex(0), m_lockedAngle(0), m_lockedAngleMode(false) { } void KisDynamicSensorDrawingAngle::reset() { - m_dabIndex = 0; } qreal KisDynamicSensorDrawingAngle::value(const KisPaintInformation& info) { /* so that we are in 0.0..1.0 */ - qreal ret = 0.5 + info.drawingAngle() / (2.0 * M_PI) + m_angleOffset/360.0; + qreal ret = 0.5 + info.drawingAngle(m_lockedAngleMode) / (2.0 * M_PI) + m_angleOffset/360.0; // check if m_angleOffset pushed us out of bounds if (ret > 1.0) ret -= 1.0; - if (!info.isHoveringMode() && m_lockedAngleMode) { - if (!m_dabIndex) { - info.lockCurrentDrawingAngle(1.0); - } else { - info.lockCurrentDrawingAngle(0.5); - } - m_dabIndex++; - } - return ret; } bool KisDynamicSensorDrawingAngle::dependsOnCanvasRotation() const { return false; } bool KisDynamicSensorDrawingAngle::isAbsoluteRotation() const { return true; } void KisDynamicSensorDrawingAngle::updateGUI() { const bool fanEnabled = !m_chkLockedMode->isChecked(); m_chkFanCorners->setEnabled(fanEnabled); m_intFanCornersStep->setEnabled(fanEnabled); } QWidget* KisDynamicSensorDrawingAngle::createConfigurationWidget(QWidget* parent, QWidget *ss) { QWidget *w = new QWidget(parent); m_chkLockedMode = new QCheckBox(i18n("Lock"), w); m_chkLockedMode->setChecked(m_lockedAngleMode); connect(m_chkLockedMode, SIGNAL(stateChanged(int)), SLOT(setLockedAngleMode(int))); connect(m_chkLockedMode, SIGNAL(stateChanged(int)), SLOT(updateGUI())); m_chkFanCorners = new QCheckBox(i18n("Fan Corners"), w); connect(m_chkFanCorners, SIGNAL(stateChanged(int)), SLOT(setFanCornersEnabled(int))); m_chkFanCorners->setChecked(m_fanCornersEnabled); m_intFanCornersStep = new KisSliderSpinBox(w); m_intFanCornersStep->setRange(5, 90); m_intFanCornersStep->setSingleStep(1); m_intFanCornersStep->setSuffix(i18n("°")); connect(m_intFanCornersStep, SIGNAL(valueChanged(int)), SLOT(setFanCornersStep(int))); m_intFanCornersStep->setValue(m_fanCornersStep); KisSliderSpinBox *angleOffset = new KisSliderSpinBox(w); angleOffset->setRange(0, 359); angleOffset->setSingleStep(1); angleOffset->setSuffix(i18n("°")); connect(angleOffset, SIGNAL(valueChanged(int)), SLOT(setAngleOffset(int))); angleOffset->setValue(m_angleOffset); QVBoxLayout* l = new QVBoxLayout(w); l->addWidget(m_chkLockedMode); l->addWidget(m_chkFanCorners); l->addWidget(m_intFanCornersStep); l->addWidget(new QLabel(i18n("Angle Offset"))); l->addWidget(angleOffset); updateGUI(); connect(angleOffset, SIGNAL(valueChanged(int)), ss, SIGNAL(parametersChanged())); connect(m_chkLockedMode, SIGNAL(stateChanged(int)), ss, SIGNAL(parametersChanged())); connect(m_chkFanCorners, SIGNAL(stateChanged(int)), ss, SIGNAL(parametersChanged())); connect(m_intFanCornersStep, SIGNAL(valueChanged(int)), ss, SIGNAL(parametersChanged())); w->setLayout(l); return w; } bool KisDynamicSensorDrawingAngle::fanCornersEnabled() const { return m_fanCornersEnabled && !m_lockedAngleMode; } int KisDynamicSensorDrawingAngle::fanCornersStep() const { return m_fanCornersStep; } int KisDynamicSensorDrawingAngle::angleOffset() const { return m_angleOffset; } void KisDynamicSensorDrawingAngle::setFanCornersEnabled(int state) { m_fanCornersEnabled = state; } void KisDynamicSensorDrawingAngle::setFanCornersStep(int angle) { m_fanCornersStep = angle; } void KisDynamicSensorDrawingAngle::setLockedAngleMode(int value) { m_lockedAngleMode = value; } void KisDynamicSensorDrawingAngle::setAngleOffset(int angle) { Q_ASSERT(angle >= 0 && angle < 360);//dont include 360 m_angleOffset = angle; } void KisDynamicSensorDrawingAngle::toXML(QDomDocument &doc, QDomElement &e) const { KisDynamicSensor::toXML(doc, e); e.setAttribute("fanCornersEnabled", m_fanCornersEnabled); e.setAttribute("fanCornersStep", m_fanCornersStep); e.setAttribute("angleOffset", m_angleOffset); e.setAttribute("lockedAngleMode", m_lockedAngleMode); } void KisDynamicSensorDrawingAngle::fromXML(const QDomElement &e) { KisDynamicSensor::fromXML(e); m_fanCornersEnabled = e.attribute("fanCornersEnabled", "0").toInt(); m_fanCornersStep = e.attribute("fanCornersStep", "30").toInt(); m_angleOffset = e.attribute("angleOffset", "0").toInt(); m_lockedAngleMode = e.attribute("lockedAngleMode", "0").toInt(); } diff --git a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_drawing_angle.h b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_drawing_angle.h index a21b71ace2..8c6fcb7970 100644 --- a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_drawing_angle.h +++ b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_drawing_angle.h @@ -1,71 +1,70 @@ /* * Copyright (c) 2013 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_DYNAMIC_SENSOR_DRAWING_ANGLE_H #define __KIS_DYNAMIC_SENSOR_DRAWING_ANGLE_H #include "kis_dynamic_sensor.h" class QCheckBox; class KisSliderSpinBox; class KisDynamicSensorDrawingAngle : public QObject, public KisDynamicSensor { Q_OBJECT public: KisDynamicSensorDrawingAngle(); qreal value(const KisPaintInformation& info) override; bool dependsOnCanvasRotation() const override; bool isAbsoluteRotation() const override; QWidget* createConfigurationWidget(QWidget* parent, QWidget*) override; using KisSerializableConfiguration::fromXML; using KisSerializableConfiguration::toXML; void toXML(QDomDocument&, QDomElement&) const override; void fromXML(const QDomElement&) override; bool fanCornersEnabled() const; int fanCornersStep() const; int angleOffset() const; void reset() override; public Q_SLOTS: void setFanCornersEnabled(int state); void setFanCornersStep(int angle); void setAngleOffset(int angle); void setLockedAngleMode(int value); void updateGUI(); private: bool m_fanCornersEnabled; int m_fanCornersStep; int m_angleOffset; // in degrees - int m_dabIndex; qreal m_lockedAngle; bool m_lockedAngleMode; QCheckBox *m_chkLockedMode; QCheckBox *m_chkFanCorners; KisSliderSpinBox *m_intFanCornersStep; }; #endif /* __KIS_DYNAMIC_SENSOR_DRAWING_ANGLE_H */ diff --git a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_fade.cpp b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_fade.cpp index d8dd1dd7a9..8eeff9ae21 100644 --- a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_fade.cpp +++ b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_fade.cpp @@ -1,102 +1,89 @@ /* * Copyright (c) 2010 Lukáš Tvrdý * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_dynamic_sensor_fade.h" #include #include #include "ui_SensorFadeConfiguration.h" #include static const int DEFAULT_LENGTH = 1000; KisDynamicSensorFade::KisDynamicSensorFade() : KisDynamicSensor(FADE) - , m_counter(0) , m_periodic(false) { setLength(DEFAULT_LENGTH); } qreal KisDynamicSensorFade::value(const KisPaintInformation& pi) { if (pi.isHoveringMode()) return 1.0; - if (m_counter > m_length) { - if (m_periodic) { - reset(); - } - else { - m_counter = m_length; - } - } + const int currentValue = + m_periodic ? + pi.currentDabSeqNo() % m_length : + qMin(pi.currentDabSeqNo(), m_length); - qreal result = m_counter / qreal(m_length); - m_counter++; - - return result; -} - -void KisDynamicSensorFade::reset() -{ - m_counter = 0; + return qreal(currentValue) / m_length; } void KisDynamicSensorFade::setPeriodic(bool periodic) { m_periodic = periodic; } void KisDynamicSensorFade::setLength(int length) { m_length = length; } QWidget* KisDynamicSensorFade::createConfigurationWidget(QWidget* parent, QWidget* ss) { QWidget* wdg = new QWidget(parent); Ui_SensorFadeConfiguration stc; stc.setupUi(wdg); stc.checkBoxRepeat->setChecked(m_periodic); stc.spinBoxLength->setSuffix(i18n(" px")); stc.spinBoxLength->setExponentRatio(3.0); connect(stc.checkBoxRepeat, SIGNAL(toggled(bool)), SLOT(setPeriodic(bool))); connect(stc.checkBoxRepeat, SIGNAL(toggled(bool)), ss, SIGNAL(parametersChanged())); stc.spinBoxLength->setValue(m_length); connect(stc.spinBoxLength, SIGNAL(valueChanged(int)), SLOT(setLength(int))); connect(stc.spinBoxLength, SIGNAL(valueChanged(int)), ss, SIGNAL(parametersChanged())); return wdg; } void KisDynamicSensorFade::toXML(QDomDocument& doc, QDomElement& e) const { KisDynamicSensor::toXML(doc, e); e.setAttribute("periodic", m_periodic); e.setAttribute("length", m_length); } void KisDynamicSensorFade::fromXML(const QDomElement& e) { KisDynamicSensor::fromXML(e); m_periodic = e.attribute("periodic", "0").toInt(); m_length = e.attribute("length", QString::number(DEFAULT_LENGTH)).toInt(); } diff --git a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_fade.h b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_fade.h index abfaf1b844..4384b93d89 100644 --- a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_fade.h +++ b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_fade.h @@ -1,48 +1,46 @@ /* * Copyright (c) 2010 Lukáš Tvrdý * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_DYNAMIC_SENSOR_FADE_H_ #define _KIS_DYNAMIC_SENSOR_FADE_H_ #include "kis_dynamic_sensor.h" class KisDynamicSensorFade : public QObject, public KisDynamicSensor { Q_OBJECT public: using KisSerializableConfiguration::fromXML; using KisSerializableConfiguration::toXML; KisDynamicSensorFade(); ~KisDynamicSensorFade() override { } qreal value(const KisPaintInformation&) override; - void reset() override; QWidget* createConfigurationWidget(QWidget* parent, QWidget*) override; public Q_SLOTS: virtual void setPeriodic(bool periodic); virtual void setLength(int length); void toXML(QDomDocument&, QDomElement&) const override; void fromXML(const QDomElement&) override; private: - int m_counter; bool m_periodic; }; #endif diff --git a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_fuzzy.cpp b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_fuzzy.cpp index 44b087aacb..7d3344bc2d 100644 --- a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_fuzzy.cpp +++ b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_fuzzy.cpp @@ -1,71 +1,64 @@ /* * Copyright (c) 2013 Dmitry Kazakov * Copyright (c) 2014 Mohit Goyal * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_dynamic_sensor_fuzzy.h" #include #include #include #include -KisDynamicSensorFuzzy::KisDynamicSensorFuzzy(bool fuzzyPerStroke) +KisDynamicSensorFuzzy::KisDynamicSensorFuzzy(bool fuzzyPerStroke, const QString &parentOptionName) : KisDynamicSensor(fuzzyPerStroke ? FUZZY_PER_STROKE : FUZZY_PER_DAB), m_fuzzyPerStroke(fuzzyPerStroke), - m_isInitialized(false), - m_savedValue(0.0) + m_perStrokeRandomSourceKey(parentOptionName + "FuzzyStroke") { } void KisDynamicSensorFuzzy::reset() { - m_isInitialized = false; } bool KisDynamicSensorFuzzy::isAdditive() const { return true; } qreal KisDynamicSensorFuzzy::value(const KisPaintInformation &info) { - if (m_fuzzyPerStroke && m_isInitialized) { - return m_savedValue; - } - qreal result = 0.0; if (!info.isHoveringMode()) { - result = info.randomSource()->generateNormalized(); + result = m_fuzzyPerStroke ? + info.perStrokeRandomSource()->generateNormalized(m_perStrokeRandomSourceKey) : + info.randomSource()->generateNormalized(); result = 2.0 * result - 1.0; - - m_isInitialized = true; - m_savedValue = result; } return result; } bool KisDynamicSensorFuzzy::dependsOnCanvasRotation() const { return false; } diff --git a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_fuzzy.h b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_fuzzy.h index 90222639f2..efd37753c4 100644 --- a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_fuzzy.h +++ b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_fuzzy.h @@ -1,53 +1,52 @@ /* * Copyright (c) 2013 Dmitry Kazakov * Copyright (c) 2014 Mohit Goyal * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_DYNAMIC_SENSOR_FUZZY_H #define KIS_DYNAMIC_SENSOR_FUZZY_H #include "kis_dynamic_sensor.h" #include #include #include #include #include #include class KisDynamicSensorFuzzy : public QObject, public KisDynamicSensor { Q_OBJECT public: bool dependsOnCanvasRotation() const override; bool isAdditive() const override; - KisDynamicSensorFuzzy(bool fuzzyPerStroke = false); + KisDynamicSensorFuzzy(bool fuzzyPerStroke, const QString &parentOptionName); ~KisDynamicSensorFuzzy() override {} qreal value(const KisPaintInformation &info) override; void reset() override; private: const bool m_fuzzyPerStroke; - bool m_isInitialized; - qreal m_savedValue; + QString m_perStrokeRandomSourceKey; }; #endif // KIS_DYNAMIC_SENSOR_FUZZY_H diff --git a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_time.cc b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_time.cc index afba5a47c1..9c9a4d9fad 100644 --- a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_time.cc +++ b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_time.cc @@ -1,112 +1,94 @@ /* * Copyright (c) 2007,2010 Cyrille Berger * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_dynamic_sensor_time.h" #include #include #include "ui_SensorTimeConfiguration.h" #include KisDynamicSensorTime::KisDynamicSensorTime() : KisDynamicSensor(TIME) - , m_time(0) , m_periodic(true) - , m_lastTime(0) { setLength(3); } qreal KisDynamicSensorTime::value(const KisPaintInformation& pi) { if (pi.isHoveringMode()) return 1.0; - qreal curtime = pi.currentTime(); - - if (curtime >= m_lastTime) { - m_time += curtime - m_lastTime; - } else { - // safely handle the situation when currentTime() < m_lastTime - m_time = 0; - } - - m_lastTime = curtime; - - if (m_time > m_length) { - if (m_periodic) { - m_time = m_time % m_length; - } - else { - m_time = m_length; - } - } - return m_time / qreal(m_length); + const qreal currentTime = + m_periodic ? + std::fmod(pi.currentTime(), m_length) : + qMin(pi.currentTime(), qreal(m_length)); + + return currentTime / qreal(m_length); } void KisDynamicSensorTime::reset() { - m_lastTime = 0; - m_time = 0; } void KisDynamicSensorTime::setPeriodic(bool periodic) { m_periodic = periodic; } void KisDynamicSensorTime::setLength(qreal length) { m_length = (int)(length * 1000); // convert to milliseconds } QWidget* KisDynamicSensorTime::createConfigurationWidget(QWidget* parent, QWidget* ss) { QWidget* wdg = new QWidget(parent); Ui_SensorTimeConfiguration stc; stc.setupUi(wdg); stc.checkBoxRepeat->setChecked(m_periodic); connect(stc.checkBoxRepeat, SIGNAL(toggled(bool)), SLOT(setPeriodic(bool))); connect(stc.checkBoxRepeat, SIGNAL(toggled(bool)), ss, SIGNAL(parametersChanged())); stc.spinBoxDuration->setRange(0.02, 10.0, 2); stc.spinBoxDuration->setSuffix(i18n(" s")); stc.spinBoxDuration->setValue(m_length / 1000); connect(stc.spinBoxDuration, SIGNAL(valueChanged(qreal)), SLOT(setLength(qreal))); connect(stc.spinBoxDuration, SIGNAL(valueChanged(qreal)), ss, SIGNAL(parametersChanged())); return wdg; } void KisDynamicSensorTime::toXML(QDomDocument& doc, QDomElement& e) const { KisDynamicSensor::toXML(doc, e); e.setAttribute("periodic", m_periodic); e.setAttribute("duration", m_length); } void KisDynamicSensorTime::fromXML(const QDomElement& e) { KisDynamicSensor::fromXML(e); m_periodic = e.attribute("periodic", "0").toInt(); m_length = e.attribute("duration", "30").toInt(); } diff --git a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_time.h b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_time.h index acfcd3e1d4..82e458cdc0 100644 --- a/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_time.h +++ b/plugins/paintops/libpaintop/sensors/kis_dynamic_sensor_time.h @@ -1,49 +1,47 @@ /* * Copyright (c) 2006 Cyrille Berger * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; version 2.1 of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef _KIS_DYNAMIC_SENSOR_TIME_H_ #define _KIS_DYNAMIC_SENSOR_TIME_H_ #include "kis_dynamic_sensor.h" // class KisDynamicSensorTime : public QObject, public KisDynamicSensor { Q_OBJECT public: using KisSerializableConfiguration::fromXML; using KisSerializableConfiguration::toXML; KisDynamicSensorTime(); ~KisDynamicSensorTime() override { } qreal value(const KisPaintInformation&) override; void reset() override; QWidget* createConfigurationWidget(QWidget* parent, QWidget*) override; public Q_SLOTS: virtual void setPeriodic(bool periodic); virtual void setLength(qreal length); void toXML(QDomDocument&, QDomElement&) const override; void fromXML(const QDomElement&) override; private: - int m_time; bool m_periodic; - int m_lastTime; }; #endif diff --git a/plugins/paintops/libpaintop/tests/kis_sensors_test.cpp b/plugins/paintops/libpaintop/tests/kis_sensors_test.cpp index b02b26d70e..94c7bc55b8 100644 --- a/plugins/paintops/libpaintop/tests/kis_sensors_test.cpp +++ b/plugins/paintops/libpaintop/tests/kis_sensors_test.cpp @@ -1,51 +1,51 @@ /* * Copyright (c) 2010 Cyrille Berger * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_sensors_test.h" #include #include KisSensorsTest::KisSensorsTest() { paintInformations.append(KisPaintInformation(QPointF(0, 0))); paintInformations.append(KisPaintInformation(QPointF(0, 1))); paintInformations.append(KisPaintInformation(QPointF(1, 2))); paintInformations.append(KisPaintInformation(QPointF(2, 2))); paintInformations.append(KisPaintInformation(QPointF(3, 1))); paintInformations.append(KisPaintInformation(QPointF(3, 0))); paintInformations.append(KisPaintInformation(QPointF(2, -1))); paintInformations.append(KisPaintInformation(QPointF(1, -1))); } void KisSensorsTest::testDrawingAngle() { - KisDynamicSensorSP sensor = KisDynamicSensor::id2Sensor(DrawingAngleId); + KisDynamicSensorSP sensor = KisDynamicSensor::id2Sensor(DrawingAngleId, "testname"); testBound(sensor); } void KisSensorsTest::testBound(KisDynamicSensorSP sensor) { Q_FOREACH (const KisPaintInformation & pi, paintInformations) { double v = sensor->parameter(pi); QVERIFY(v >= 0.0); QVERIFY(v <= 1.0); } } QTEST_MAIN(KisSensorsTest) diff --git a/plugins/tools/basictools/strokes/move_stroke_strategy.cpp b/plugins/tools/basictools/strokes/move_stroke_strategy.cpp index 33841c61de..8cb4958ed4 100644 --- a/plugins/tools/basictools/strokes/move_stroke_strategy.cpp +++ b/plugins/tools/basictools/strokes/move_stroke_strategy.cpp @@ -1,228 +1,230 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "move_stroke_strategy.h" #include #include "kis_image_interfaces.h" #include "kis_node.h" #include "commands_new/kis_update_command.h" #include "commands_new/kis_node_move_command2.h" #include "kis_layer_utils.h" #include "krita_utils.h" MoveStrokeStrategy::MoveStrokeStrategy(KisNodeList nodes, KisUpdatesFacade *updatesFacade, KisStrokeUndoFacade *undoFacade) : KisStrokeStrategyUndoCommandBased(kundo2_i18n("Move"), false, undoFacade), m_nodes(), m_updatesFacade(updatesFacade), m_updatesEnabled(true) { m_nodes = KisLayerUtils::sortAndFilterMergableInternalNodes(nodes, true); KritaUtils::filterContainer(m_nodes, [this](KisNodeSP node) { return !KisLayerUtils::checkIsCloneOf(node, m_nodes) && node->isEditable(); }); Q_FOREACH(KisNodeSP subtree, m_nodes) { KisLayerUtils::recursiveApplyNodes( subtree, [this](KisNodeSP node) { if (KisLayerUtils::checkIsCloneOf(node, m_nodes) || !node->isEditable()) { m_blacklistedNodes.insert(node); } }); } setSupportsWrapAroundMode(true); } MoveStrokeStrategy::MoveStrokeStrategy(const MoveStrokeStrategy &rhs) : KisStrokeStrategyUndoCommandBased(rhs), m_nodes(rhs.m_nodes), m_blacklistedNodes(rhs.m_blacklistedNodes), m_updatesFacade(rhs.m_updatesFacade), m_finalOffset(rhs.m_finalOffset), m_dirtyRect(rhs.m_dirtyRect), m_dirtyRects(rhs.m_dirtyRects), m_updatesEnabled(rhs.m_updatesEnabled) { } void MoveStrokeStrategy::saveInitialNodeOffsets(KisNodeSP node) { if (!m_blacklistedNodes.contains(node)) { m_initialNodeOffsets.insert(node, QPoint(node->x(), node->y())); } KisNodeSP child = node->firstChild(); while(child) { saveInitialNodeOffsets(child); child = child->nextSibling(); } } void MoveStrokeStrategy::initStrokeCallback() { Q_FOREACH(KisNodeSP node, m_nodes) { saveInitialNodeOffsets(node); } KisStrokeStrategyUndoCommandBased::initStrokeCallback(); } void MoveStrokeStrategy::finishStrokeCallback() { Q_FOREACH (KisNodeSP node, m_nodes) { KUndo2Command *updateCommand = new KisUpdateCommand(node, m_dirtyRects[node], m_updatesFacade, true); addMoveCommands(node, updateCommand); notifyCommandDone(KUndo2CommandSP(updateCommand), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } if (!m_updatesEnabled) { Q_FOREACH (KisNodeSP node, m_nodes) { m_updatesFacade->refreshGraphAsync(node, m_dirtyRects[node]); } } KisStrokeStrategyUndoCommandBased::finishStrokeCallback(); } void MoveStrokeStrategy::cancelStrokeCallback() { if (!m_nodes.isEmpty()) { // FIXME: make cancel job exclusive instead m_updatesFacade->blockUpdates(); moveAndUpdate(QPoint()); m_updatesFacade->unblockUpdates(); } KisStrokeStrategyUndoCommandBased::cancelStrokeCallback(); } void MoveStrokeStrategy::doStrokeCallback(KisStrokeJobData *data) { Data *d = dynamic_cast(data); if(!m_nodes.isEmpty() && d) { moveAndUpdate(d->offset); /** * NOTE: we do not care about threading here, because * all our jobs are declared sequential */ m_finalOffset = d->offset; } else { KisStrokeStrategyUndoCommandBased::doStrokeCallback(data); } } void MoveStrokeStrategy::moveAndUpdate(QPoint offset) { Q_FOREACH (KisNodeSP node, m_nodes) { QRect dirtyRect = moveNode(node, offset); m_dirtyRects[node] |= dirtyRect; if (m_updatesEnabled) { m_updatesFacade->refreshGraphAsync(node, dirtyRect); } } } QRect MoveStrokeStrategy::moveNode(KisNodeSP node, QPoint offset) { QRect dirtyRect; if (!m_blacklistedNodes.contains(node)) { dirtyRect = node->extent(); QPoint newOffset = m_initialNodeOffsets[node] + offset; /** * Some layers, e.g. clones need an update to change extent(), so * calculate the dirty rect manually */ QPoint currentOffset(node->x(), node->y()); dirtyRect |= dirtyRect.translated(newOffset - currentOffset); node->setX(newOffset.x()); node->setY(newOffset.y()); KisNodeMoveCommand2::tryNotifySelection(node); } KisNodeSP child = node->firstChild(); while(child) { dirtyRect |= moveNode(child, offset); child = child->nextSibling(); } return dirtyRect; } void MoveStrokeStrategy::addMoveCommands(KisNodeSP node, KUndo2Command *parent) { if (!m_blacklistedNodes.contains(node)) { QPoint nodeOffset(node->x(), node->y()); new KisNodeMoveCommand2(node, nodeOffset - m_finalOffset, nodeOffset, parent); } KisNodeSP child = node->firstChild(); while(child) { addMoveCommands(child, parent); child = child->nextSibling(); } } void MoveStrokeStrategy::setUpdatesEnabled(bool value) { m_updatesEnabled = value; } bool checkSupportsLodMoves(KisNodeSP subtree) { return !KisLayerUtils::recursiveFindNode( subtree, [](KisNodeSP node) -> bool { return !node->supportsLodMoves(); }); } KisStrokeStrategy* MoveStrokeStrategy::createLodClone(int levelOfDetail) { + Q_UNUSED(levelOfDetail); + Q_FOREACH (KisNodeSP node, m_nodes) { if (!checkSupportsLodMoves(node)) return 0; } MoveStrokeStrategy *clone = new MoveStrokeStrategy(*this); this->setUpdatesEnabled(false); return clone; } diff --git a/plugins/tools/selectiontools/kis_tool_select_elliptical.cc b/plugins/tools/selectiontools/kis_tool_select_elliptical.cc index fd00f98fe9..df26677de9 100644 --- a/plugins/tools/selectiontools/kis_tool_select_elliptical.cc +++ b/plugins/tools/selectiontools/kis_tool_select_elliptical.cc @@ -1,101 +1,100 @@ /* * kis_tool_select_elliptical.cc -- part of Krita * * Copyright (c) 2004 Boudewijn Rempt (boud@valdyas.org) * Copyright (c) 2007 Sven Langkamp * Copyright (c) 2015 Michael Abrahams * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_tool_select_elliptical.h" #include #include "kis_painter.h" #include #include "kis_selection_options.h" #include "kis_canvas2.h" #include "kis_pixel_selection.h" #include "kis_selection_tool_helper.h" #include "kis_shape_tool_helper.h" #include "KisViewManager.h" #include "kis_selection_manager.h" __KisToolSelectEllipticalLocal::__KisToolSelectEllipticalLocal(KoCanvasBase *canvas) : KisToolEllipseBase(canvas, KisToolEllipseBase::SELECT, KisCursor::load("tool_elliptical_selection_cursor.png", 6, 6)) { setObjectName("tool_select_elliptical"); } void __KisToolSelectEllipticalLocal::finishRect(const QRectF &rect) { KisCanvas2 * kisCanvas = dynamic_cast(canvas()); Q_ASSERT(kisCanvas); KisSelectionToolHelper helper(kisCanvas, kundo2_i18n("Select Ellipse")); if (helper.tryDeselectCurrentSelection(pixelToView(rect), selectionAction())) { return; } if (selectionMode() == PIXEL_SELECTION) { KisPixelSelectionSP tmpSel = new KisPixelSelection(); KisPainter painter(tmpSel); painter.setPaintColor(KoColor(Qt::black, tmpSel->colorSpace())); - painter.setPaintOpPreset(currentPaintOpPreset(), currentNode(), currentImage()); painter.setAntiAliasPolygonFill(antiAliasSelection()); painter.setFillStyle(KisPainter::FillStyleForegroundColor); painter.setStrokeStyle(KisPainter::StrokeStyleNone); painter.paintEllipse(rect); QPainterPath cache; cache.addEllipse(rect); tmpSel->setOutlineCache(cache); helper.selectPixelSelection(tmpSel, selectionAction()); } else { QRectF ptRect = convertToPt(rect); KoShape* shape = KisShapeToolHelper::createEllipseShape(ptRect); helper.addSelectionShape(shape); } } KisToolSelectElliptical::KisToolSelectElliptical(KoCanvasBase *canvas): KisToolSelectEllipticalTemplate(canvas, i18n("Elliptical Selection")) { connect(&m_widgetHelper, &KisSelectionToolConfigWidgetHelper::selectionActionChanged, this, &KisToolSelectElliptical::setSelectionAction); } void KisToolSelectElliptical::setSelectionAction(int action) { changeSelectionAction(action); } QMenu* KisToolSelectElliptical::popupActionsMenu() { KisCanvas2 * kisCanvas = dynamic_cast(canvas()); Q_ASSERT(kisCanvas); return KisSelectionToolHelper::getSelectionContextMenu(kisCanvas); } diff --git a/plugins/tools/selectiontools/kis_tool_select_outline.cc b/plugins/tools/selectiontools/kis_tool_select_outline.cc index e33fa4f738..9bd049a44e 100644 --- a/plugins/tools/selectiontools/kis_tool_select_outline.cc +++ b/plugins/tools/selectiontools/kis_tool_select_outline.cc @@ -1,273 +1,272 @@ /* * kis_tool_select_freehand.h - part of Krayon^WKrita * * Copyright (c) 2000 John Califf * Copyright (c) 2002 Patrick Julien * Copyright (c) 2004 Boudewijn Rempt * Copyright (c) 2007 Sven Langkamp * Copyright (c) 2015 Michael Abrahams * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_tool_select_outline.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_painter.h" #include #include "canvas/kis_canvas2.h" #include "kis_pixel_selection.h" #include "kis_selection_tool_helper.h" #include "kis_algebra_2d.h" #define FEEDBACK_LINE_WIDTH 2 KisToolSelectOutline::KisToolSelectOutline(KoCanvasBase * canvas) : KisToolSelect(canvas, KisCursor::load("tool_outline_selection_cursor.png", 5, 5), i18n("Outline Selection")), m_continuedMode(false) { connect(&m_widgetHelper, &KisSelectionToolConfigWidgetHelper::selectionActionChanged, this, &KisToolSelectOutline::setSelectionAction); } KisToolSelectOutline::~KisToolSelectOutline() { } void KisToolSelectOutline::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Control) { m_continuedMode = true; } KisToolSelect::keyPressEvent(event); } void KisToolSelectOutline::keyReleaseEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Control || !(event->modifiers() & Qt::ControlModifier)) { m_continuedMode = false; if (mode() != PAINT_MODE && !m_points.isEmpty()) { finishSelectionAction(); } } KisToolSelect::keyReleaseEvent(event); } void KisToolSelectOutline::mouseMoveEvent(KoPointerEvent *event) { m_lastCursorPos = convertToPixelCoord(event); if (m_continuedMode && mode() != PAINT_MODE) { updateContinuedMode(); } } void KisToolSelectOutline::beginPrimaryAction(KoPointerEvent *event) { KisToolSelectBase::beginPrimaryAction(event); if (!selectionEditable()) { event->ignore(); return; } setMode(KisTool::PAINT_MODE); if (m_continuedMode && !m_points.isEmpty()) { m_paintPath.lineTo(pixelToView(convertToPixelCoord(event))); } else { m_paintPath.moveTo(pixelToView(convertToPixelCoord(event))); } m_points.append(convertToPixelCoord(event)); } void KisToolSelectOutline::continuePrimaryAction(KoPointerEvent *event) { CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE); KisToolSelectBase::continuePrimaryAction(event); QPointF point = convertToPixelCoord(event); m_paintPath.lineTo(pixelToView(point)); m_points.append(point); updateFeedback(); } void KisToolSelectOutline::endPrimaryAction(KoPointerEvent *event) { Q_UNUSED(event); CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE); KisToolSelectBase::endPrimaryAction(event); setMode(KisTool::HOVER_MODE); if (!m_continuedMode) { finishSelectionAction(); } } void KisToolSelectOutline::finishSelectionAction() { KisCanvas2 * kisCanvas = dynamic_cast(canvas()); KIS_ASSERT_RECOVER_RETURN(kisCanvas); kisCanvas->updateCanvas(); QRectF boundingViewRect = pixelToView(KisAlgebra2D::accumulateBounds(m_points)); KisSelectionToolHelper helper(kisCanvas, kundo2_i18n("Select by Outline")); if (m_points.count() > 2 && !helper.tryDeselectCurrentSelection(boundingViewRect, selectionAction())) { QApplication::setOverrideCursor(KisCursor::waitCursor()); if (selectionMode() == PIXEL_SELECTION) { KisPixelSelectionSP tmpSel = KisPixelSelectionSP(new KisPixelSelection()); KisPainter painter(tmpSel); painter.setPaintColor(KoColor(Qt::black, tmpSel->colorSpace())); - painter.setPaintOpPreset(currentPaintOpPreset(), currentNode(), currentImage()); painter.setAntiAliasPolygonFill(antiAliasSelection()); painter.setFillStyle(KisPainter::FillStyleForegroundColor); painter.setStrokeStyle(KisPainter::StrokeStyleNone); painter.paintPolygon(m_points); QPainterPath cache; cache.addPolygon(m_points); cache.closeSubpath(); tmpSel->setOutlineCache(cache); helper.selectPixelSelection(tmpSel, selectionAction()); } else { KoPathShape* path = new KoPathShape(); path->setShapeId(KoPathShapeId); QTransform resolutionMatrix; resolutionMatrix.scale(1 / currentImage()->xRes(), 1 / currentImage()->yRes()); path->moveTo(resolutionMatrix.map(m_points[0])); for (int i = 1; i < m_points.count(); i++) path->lineTo(resolutionMatrix.map(m_points[i])); path->close(); path->normalize(); helper.addSelectionShape(path); } QApplication::restoreOverrideCursor(); } m_points.clear(); m_paintPath = QPainterPath(); } void KisToolSelectOutline::paint(QPainter& gc, const KoViewConverter &converter) { Q_UNUSED(converter); if ((mode() == KisTool::PAINT_MODE || m_continuedMode) && !m_points.isEmpty()) { QPainterPath outline = m_paintPath; if (m_continuedMode && mode() != KisTool::PAINT_MODE) { outline.lineTo(pixelToView(m_lastCursorPos)); } paintToolOutline(&gc, outline); } } void KisToolSelectOutline::updateFeedback() { if (m_points.count() > 1) { qint32 lastPointIndex = m_points.count() - 1; QRectF updateRect = QRectF(m_points[lastPointIndex - 1], m_points[lastPointIndex]).normalized(); updateRect = kisGrowRect(updateRect, FEEDBACK_LINE_WIDTH); updateCanvasPixelRect(updateRect); } } void KisToolSelectOutline::updateContinuedMode() { if (!m_points.isEmpty()) { qint32 lastPointIndex = m_points.count() - 1; QRectF updateRect = QRectF(m_points[lastPointIndex - 1], m_lastCursorPos).normalized(); updateRect = kisGrowRect(updateRect, FEEDBACK_LINE_WIDTH); updateCanvasPixelRect(updateRect); } } void KisToolSelectOutline::deactivate() { KisCanvas2 * kisCanvas = dynamic_cast(canvas()); KIS_ASSERT_RECOVER_RETURN(kisCanvas); kisCanvas->updateCanvas(); m_continuedMode = false; KisTool::deactivate(); } void KisToolSelectOutline::setSelectionAction(int action) { changeSelectionAction(action); } QMenu* KisToolSelectOutline::popupActionsMenu() { KisCanvas2 * kisCanvas = dynamic_cast(canvas()); Q_ASSERT(kisCanvas); return KisSelectionToolHelper::getSelectionContextMenu(kisCanvas); } diff --git a/plugins/tools/selectiontools/kis_tool_select_polygonal.cc b/plugins/tools/selectiontools/kis_tool_select_polygonal.cc index 46363277b1..5ceb25760c 100644 --- a/plugins/tools/selectiontools/kis_tool_select_polygonal.cc +++ b/plugins/tools/selectiontools/kis_tool_select_polygonal.cc @@ -1,113 +1,112 @@ /* * kis_tool_select_polygonal.h - part of Krayon^WKrita * * Copyright (c) 2000 John Califf * Copyright (c) 2002 Patrick Julien * Copyright (c) 2004 Boudewijn Rempt * Copyright (c) 2007 Sven Langkamp * Copyright (c) 2015 Michael Abrahams * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_tool_select_polygonal.h" #include #include "kis_painter.h" #include #include "kis_selection_options.h" #include "kis_canvas2.h" #include "kis_pixel_selection.h" #include "kis_selection_tool_helper.h" #include "kis_shape_tool_helper.h" #include "KisViewManager.h" #include "kis_selection_manager.h" __KisToolSelectPolygonalLocal::__KisToolSelectPolygonalLocal(KoCanvasBase *canvas) : KisToolPolylineBase(canvas, KisToolPolylineBase::SELECT, KisCursor::load("tool_polygonal_selection_cursor.png", 6, 6)) { setObjectName("tool_select_polygonal"); } void __KisToolSelectPolygonalLocal::finishPolyline(const QVector &points) { KisCanvas2 * kisCanvas = dynamic_cast(canvas()); Q_ASSERT(kisCanvas); if (!kisCanvas) return; KisSelectionToolHelper helper(kisCanvas, kundo2_i18n("Select Polygon")); if (selectionMode() == PIXEL_SELECTION) { KisPixelSelectionSP tmpSel = new KisPixelSelection(); KisPainter painter(tmpSel); painter.setPaintColor(KoColor(Qt::black, tmpSel->colorSpace())); - painter.setPaintOpPreset(currentPaintOpPreset(), currentNode(), currentImage()); painter.setAntiAliasPolygonFill(antiAliasSelection()); painter.setFillStyle(KisPainter::FillStyleForegroundColor); painter.setStrokeStyle(KisPainter::StrokeStyleNone); painter.paintPolygon(points); QPainterPath cache; cache.addPolygon(points); cache.closeSubpath(); tmpSel->setOutlineCache(cache); helper.selectPixelSelection(tmpSel, selectionAction()); } else { KoPathShape* path = new KoPathShape(); path->setShapeId(KoPathShapeId); QTransform resolutionMatrix; resolutionMatrix.scale(1 / currentImage()->xRes(), 1 / currentImage()->yRes()); path->moveTo(resolutionMatrix.map(points[0])); for (int i = 1; i < points.count(); i++) path->lineTo(resolutionMatrix.map(points[i])); path->close(); path->normalize(); helper.addSelectionShape(path); } } KisToolSelectPolygonal::KisToolSelectPolygonal(KoCanvasBase *canvas): KisToolSelectBase<__KisToolSelectPolygonalLocal>(canvas, i18n("Polygonal Selection")) { connect(&m_widgetHelper, &KisSelectionToolConfigWidgetHelper::selectionActionChanged, this, &KisToolSelectPolygonal::setSelectionAction); } void KisToolSelectPolygonal::setSelectionAction(int action) { changeSelectionAction(action); } QMenu* KisToolSelectPolygonal::popupActionsMenu() { KisCanvas2 * kisCanvas = dynamic_cast(canvas()); Q_ASSERT(kisCanvas); return KisSelectionToolHelper::getSelectionContextMenu(kisCanvas); } diff --git a/plugins/tools/tool_transform2/kis_liquify_paint_helper.cpp b/plugins/tools/tool_transform2/kis_liquify_paint_helper.cpp index 3f6a8a9009..82bcf34f7b 100644 --- a/plugins/tools/tool_transform2/kis_liquify_paint_helper.cpp +++ b/plugins/tools/tool_transform2/kis_liquify_paint_helper.cpp @@ -1,142 +1,142 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_liquify_paint_helper.h" #include "kis_algebra_2d.h" #include "KoPointerEvent.h" #include #include "kis_painting_information_builder.h" #include "kis_liquify_transform_worker.h" #include #include "kis_coordinates_converter.h" #include "kis_liquify_paintop.h" #include "kis_liquify_properties.h" struct KisLiquifyPaintHelper::Private { Private(const KisCoordinatesConverter *_converter) : converter(_converter), infoBuilder(new KisConverterPaintingInformationBuilder(converter)), hasPaintedAtLeastOnce(false) { } KisPaintInformation previousPaintInfo; QScopedPointer paintOp; KisDistanceInformation currentDistance; const KisCoordinatesConverter *converter; QScopedPointer infoBuilder; QTime strokeTime; bool hasPaintedAtLeastOnce; KisDistanceInformation previousDistanceInfo; KisPaintOpUtils::PositionHistory lastOutlinePos; void updatePreviousPaintInfo(const KisPaintInformation &info); }; KisLiquifyPaintHelper::KisLiquifyPaintHelper(const KisCoordinatesConverter *converter) : m_d(new Private(converter)) { } KisLiquifyPaintHelper::~KisLiquifyPaintHelper() { } void KisLiquifyPaintHelper::Private::updatePreviousPaintInfo(const KisPaintInformation &info) { QPointF prevPos = lastOutlinePos.pushThroughHistory(info.pos()); qreal angle = KisAlgebra2D::directionBetweenPoints(prevPos, info.pos(), 0); previousDistanceInfo = - KisDistanceInformation(prevPos, 0, angle); + KisDistanceInformation(prevPos, angle); previousPaintInfo = info; } QPainterPath KisLiquifyPaintHelper::brushOutline(const KisLiquifyProperties &props) { KisPaintInformation::DistanceInformationRegistrar registrar = m_d->previousPaintInfo.registerDistanceInformation(&m_d->previousDistanceInfo); return KisLiquifyPaintop::brushOutline(props, m_d->previousPaintInfo); } void KisLiquifyPaintHelper::configurePaintOp(const KisLiquifyProperties &props, KisLiquifyTransformWorker *worker) { m_d->paintOp.reset(new KisLiquifyPaintop(props, worker)); } void KisLiquifyPaintHelper::startPaint(KoPointerEvent *event, const KoCanvasResourceManager *manager) { KIS_ASSERT_RECOVER_RETURN(m_d->paintOp); m_d->strokeTime.start(); KisPaintInformation pi = m_d->infoBuilder->startStroke(event, m_d->strokeTime.elapsed(), manager); m_d->updatePreviousPaintInfo(pi); m_d->hasPaintedAtLeastOnce = false; } void KisLiquifyPaintHelper::continuePaint(KoPointerEvent *event) { KIS_ASSERT_RECOVER_RETURN(m_d->paintOp); KisPaintInformation pi = m_d->infoBuilder->continueStroke(event, m_d->strokeTime.elapsed()); KisPaintOpUtils::paintLine(*m_d->paintOp.data(), m_d->previousPaintInfo, pi, &m_d->currentDistance, false, false); m_d->updatePreviousPaintInfo(pi); m_d->hasPaintedAtLeastOnce = true; } bool KisLiquifyPaintHelper::endPaint(KoPointerEvent *event) { KIS_ASSERT_RECOVER(m_d->paintOp) { return false; } if (!m_d->hasPaintedAtLeastOnce) { KisPaintInformation pi = m_d->infoBuilder->continueStroke(event, m_d->strokeTime.elapsed()); pi.paintAt(*m_d->paintOp.data(), &m_d->previousDistanceInfo); } m_d->paintOp.reset(); return !m_d->hasPaintedAtLeastOnce; } void KisLiquifyPaintHelper::hoverPaint(KoPointerEvent *event) { QPointF imagePoint = m_d->converter->documentToImage(event->pos()); KisPaintInformation pi = m_d->infoBuilder->hover(imagePoint, event); m_d->updatePreviousPaintInfo(pi); } diff --git a/sdk/tests/KisRectsCollisionsTracker.h b/sdk/tests/KisRectsCollisionsTracker.h new file mode 100644 index 0000000000..b2af80fde3 --- /dev/null +++ b/sdk/tests/KisRectsCollisionsTracker.h @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef KISRECTSCOLLISIONSTRACKER_H +#define KISRECTSCOLLISIONSTRACKER_H + +#include +#include +#include +#include + +#include "kis_assert.h" + + +class KisRectsCollisionsTracker +{ +public: + + void startAccessingRect(const QRect &rc) { + QMutexLocker l(&m_mutex); + + checkUniqueAccessImpl(rc, "start"); + m_rectsInProgress.append(rc); + } + + void endAccessingRect(const QRect &rc) { + QMutexLocker l(&m_mutex); + const bool result = m_rectsInProgress.removeOne(rc); + KIS_SAFE_ASSERT_RECOVER_NOOP(result); + checkUniqueAccessImpl(rc, "end"); + } + +private: + + bool checkUniqueAccessImpl(const QRect &rect, const QString &tag) { + + Q_FOREACH (const QRect &rc, m_rectsInProgress) { + if (rc != rect && rect.intersects(rc)) { + ENTER_FUNCTION() << "FAIL: concurrect access from" << rect << "to" << rc << tag; + return false; + } + } + + return true; + } + +private: + QList m_rectsInProgress; + QMutex m_mutex; +}; + +#endif // KISRECTSCOLLISIONSTRACKER_H diff --git a/sdk/tests/stroke_testing_utils.cpp b/sdk/tests/stroke_testing_utils.cpp index bc47272732..19eb4e475e 100644 --- a/sdk/tests/stroke_testing_utils.cpp +++ b/sdk/tests/stroke_testing_utils.cpp @@ -1,350 +1,362 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "stroke_testing_utils.h" #include #include #include #include #include #include #include #include #include "kis_canvas_resource_provider.h" #include "kis_image.h" #include "kis_paint_device.h" #include "kis_paint_layer.h" #include "kis_group_layer.h" +#include #include "testutil.h" KisImageSP utils::createImage(KisUndoStore *undoStore, const QSize &imageSize) { QRect imageRect(0,0,imageSize.width(),imageSize.height()); const KoColorSpace * cs = KoColorSpaceRegistry::instance()->rgb8(); KisImageSP image = new KisImage(undoStore, imageRect.width(), imageRect.height(), cs, "stroke test"); KisPaintLayerSP paintLayer1 = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8); KisPaintLayerSP paintLayer2 = new KisPaintLayer(image, "paint2", OPACITY_OPAQUE_U8); KisPaintLayerSP paintLayer3 = new KisPaintLayer(image, "paint3", OPACITY_OPAQUE_U8); KisPaintLayerSP paintLayer4 = new KisPaintLayer(image, "paint4", OPACITY_OPAQUE_U8); KisPaintLayerSP paintLayer5 = new KisPaintLayer(image, "paint5", OPACITY_OPAQUE_U8); image->lock(); image->addNode(paintLayer1); image->addNode(paintLayer2); image->addNode(paintLayer3); image->addNode(paintLayer4); image->addNode(paintLayer5); image->unlock(); return image; } KoCanvasResourceManager* utils::createResourceManager(KisImageWSP image, KisNodeSP node, const QString &presetFileName) { KoCanvasResourceManager *manager = new KoCanvasResourceManager(); + KisViewManager::initializeResourceManager(manager); QVariant i; i.setValue(KoColor(Qt::black, image->colorSpace())); manager->setResource(KoCanvasResourceManager::ForegroundColor, i); i.setValue(KoColor(Qt::white, image->colorSpace())); manager->setResource(KoCanvasResourceManager::BackgroundColor, i); i.setValue(static_cast(0)); manager->setResource(KisCanvasResourceProvider::CurrentPattern, i); manager->setResource(KisCanvasResourceProvider::CurrentGradient, i); manager->setResource(KisCanvasResourceProvider::CurrentGeneratorConfiguration, i); if(!node) { node = image->root(); while(node && !dynamic_cast(node.data())) { node = node->firstChild(); } Q_ASSERT(node && dynamic_cast(node.data())); } i.setValue(node); manager->setResource(KisCanvasResourceProvider::CurrentKritaNode, i); KisPaintOpPresetSP preset; if (!presetFileName.isEmpty()) { QString fullFileName = TestUtil::fetchDataFileLazy(presetFileName); preset = new KisPaintOpPreset(fullFileName); bool presetValid = preset->load(); Q_ASSERT(presetValid); Q_UNUSED(presetValid); i.setValue(preset); manager->setResource(KisCanvasResourceProvider::CurrentPaintOpPreset, i); } i.setValue(COMPOSITE_OVER); manager->setResource(KisCanvasResourceProvider::CurrentCompositeOp, i); i.setValue(false); manager->setResource(KisCanvasResourceProvider::MirrorHorizontal, i); i.setValue(false); manager->setResource(KisCanvasResourceProvider::MirrorVertical, i); i.setValue(1.0); manager->setResource(KisCanvasResourceProvider::Opacity, i); i.setValue(1.0); manager->setResource(KisCanvasResourceProvider::HdrExposure, i); return manager; } utils::StrokeTester::StrokeTester(const QString &name, const QSize &imageSize, const QString &presetFilename) : m_name(name), m_imageSize(imageSize), m_presetFilename(presetFilename), m_numIterations(1), m_baseFuzziness(1) { } utils::StrokeTester::~StrokeTester() { } void utils::StrokeTester::setNumIterations(int value) { m_numIterations = value; } void utils::StrokeTester::setBaseFuzziness(int value) { m_baseFuzziness = value; } void utils::StrokeTester::testSimpleStroke() { testOneStroke(false, true, false, true); } +int utils::StrokeTester::lastStrokeTime() const +{ + return m_strokeTime; +} + void utils::StrokeTester::test() { testOneStroke(false, false, false); testOneStroke(false, true, false); testOneStroke(true, false, false); testOneStroke(true, true, false); // The same but with updates (compare against projection) testOneStroke(false, false, false, true); testOneStroke(false, true, false, true); testOneStroke(true, false, false, true); testOneStroke(true, true, false, true); // The same, but with an external layer testOneStroke(false, false, true); testOneStroke(false, true, true); testOneStroke(true, false, true); testOneStroke(true, true, true); } void utils::StrokeTester::benchmark() { // not cancelled, indirect painting, internal, no updates, no qimage doStroke(false, true, false, false, false); } void utils::StrokeTester::testOneStroke(bool cancelled, bool indirectPainting, bool externalLayer, bool testUpdates) { QString testName = formatTestName(m_name, cancelled, indirectPainting, externalLayer); dbgKrita << "Testcase:" << testName << "(comare against " << (testUpdates ? "projection" : "layer") << ")"; QImage resultImage; resultImage = doStroke(cancelled, indirectPainting, externalLayer, testUpdates); QImage refImage; refImage.load(referenceFile(testName)); QPoint temp; if(!TestUtil::compareQImages(temp, refImage, resultImage, m_baseFuzziness, m_baseFuzziness)) { refImage.save(dumpReferenceFile(testName)); resultImage.save(resultFile(testName)); QFAIL("Images do not coincide"); } } QString utils::StrokeTester::formatTestName(const QString &baseName, bool cancelled, bool indirectPainting, bool externalLayer) { QString result = baseName; result += "_" + m_presetFilename; result += indirectPainting ? "_indirect" : "_incremental"; result += cancelled ? "_cancelled" : "_finished"; result += externalLayer ? "_external" : "_internal"; return result; } QString utils::StrokeTester::referenceFile(const QString &testName) { QString path = QString(FILES_DATA_DIR) + QDir::separator() + m_name + QDir::separator(); path += testName; path += ".png"; return path; } QString utils::StrokeTester::dumpReferenceFile(const QString &testName) { QString path = QString(FILES_OUTPUT_DIR) + QDir::separator(); path += testName; path += "_expected"; path += ".png"; return path; } QString utils::StrokeTester::resultFile(const QString &testName) { QString path = QString(FILES_OUTPUT_DIR) + QDir::separator(); path += testName; path += ".png"; return path; } QImage utils::StrokeTester::doStroke(bool cancelled, bool indirectPainting, bool externalLayer, bool testUpdates, bool needQImage) { KisImageSP image = utils::createImage(0, m_imageSize); KoCanvasResourceManager *manager = utils::createResourceManager(image, 0, m_presetFilename); KisNodeSP currentNode; for (int i = 0; i < m_numIterations; i++) { modifyResourceManager(manager, image, i); KisResourcesSnapshotSP resources = new KisResourcesSnapshot(image, image->rootLayer()->firstChild(), manager); if(externalLayer) { KisNodeSP externalNode = new KisPaintLayer(0, "extlyr", OPACITY_OPAQUE_U8, image->colorSpace()); resources->setCurrentNode(externalNode); Q_ASSERT(resources->currentNode() == externalNode); } initImage(image, resources->currentNode(), i); + QElapsedTimer strokeTime; + strokeTime.start(); + KisStrokeStrategy *stroke = createStroke(indirectPainting, resources, image); m_strokeId = image->startStroke(stroke); addPaintingJobs(image, resources, i); if(!cancelled) { image->endStroke(m_strokeId); } else { image->cancelStroke(m_strokeId); } image->waitForDone(); + + m_strokeTime = strokeTime.elapsed(); currentNode = resources->currentNode(); } beforeCheckingResult(image, currentNode); QImage resultImage; if(needQImage) { KisPaintDeviceSP device = testUpdates ? image->projection() : currentNode->paintDevice(); resultImage = device->convertToQImage(0, 0, 0, image->width(), image->height()); } image = 0; delete manager; return resultImage; } void utils::StrokeTester::modifyResourceManager(KoCanvasResourceManager *manager, KisImageWSP image, int iteration) { Q_UNUSED(iteration); modifyResourceManager(manager, image); } void utils::StrokeTester::modifyResourceManager(KoCanvasResourceManager *manager, KisImageWSP image) { Q_UNUSED(manager); Q_UNUSED(image); } void utils::StrokeTester::initImage(KisImageWSP image, KisNodeSP activeNode, int iteration) { Q_UNUSED(iteration); initImage(image, activeNode); } void utils::StrokeTester::initImage(KisImageWSP image, KisNodeSP activeNode) { Q_UNUSED(image); Q_UNUSED(activeNode); } void utils::StrokeTester::addPaintingJobs(KisImageWSP image, KisResourcesSnapshotSP resources, int iteration) { Q_UNUSED(iteration); addPaintingJobs(image, resources); } void utils::StrokeTester::addPaintingJobs(KisImageWSP image, KisResourcesSnapshotSP resources) { Q_UNUSED(image); Q_UNUSED(resources); } void utils::StrokeTester::beforeCheckingResult(KisImageWSP image, KisNodeSP activeNode) { Q_UNUSED(image); Q_UNUSED(activeNode); } diff --git a/sdk/tests/stroke_testing_utils.h b/sdk/tests/stroke_testing_utils.h index 27fba8f358..de702524b0 100644 --- a/sdk/tests/stroke_testing_utils.h +++ b/sdk/tests/stroke_testing_utils.h @@ -1,107 +1,110 @@ /* * Copyright (c) 2011 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __STROKE_TESTING_UTILS_H #define __STROKE_TESTING_UTILS_H #include #include #include "kis_node.h" #include "kis_types.h" #include "kis_stroke_strategy.h" #include "kis_resources_snapshot.h" class KisUndoStore; namespace utils { KisImageSP createImage(KisUndoStore *undoStore, const QSize &imageSize); KoCanvasResourceManager* createResourceManager(KisImageWSP image, KisNodeSP node = 0, const QString &presetFileName = "autobrush_300px.kpp"); class StrokeTester { public: StrokeTester(const QString &name, const QSize &imageSize, const QString &presetFileName = "autobrush_300px.kpp"); virtual ~StrokeTester(); void testSimpleStroke(); void test(); void benchmark(); void setNumIterations(int value); void setBaseFuzziness(int value); + int lastStrokeTime() const; + protected: KisStrokeId strokeId() { return m_strokeId; } virtual void modifyResourceManager(KoCanvasResourceManager *manager, KisImageWSP image, int iteration); virtual void initImage(KisImageWSP image, KisNodeSP activeNode, int iteration); // overload virtual void modifyResourceManager(KoCanvasResourceManager *manager, KisImageWSP image); // overload virtual void initImage(KisImageWSP image, KisNodeSP activeNode); virtual void beforeCheckingResult(KisImageWSP image, KisNodeSP activeNode); virtual KisStrokeStrategy* createStroke(bool indirectPainting, KisResourcesSnapshotSP resources, KisImageWSP image) = 0; virtual void addPaintingJobs(KisImageWSP image, KisResourcesSnapshotSP resources, int iteration); // overload virtual void addPaintingJobs(KisImageWSP image, KisResourcesSnapshotSP resources); private: void testOneStroke(bool cancelled, bool indirectPainting, bool externalLayer, bool testUpdates = false); QImage doStroke(bool cancelled, bool indirectPainting, bool externalLayer, bool testUpdates = false, bool needQImage = true); QString formatTestName(const QString &baseName, bool cancelled, bool indirectPainting, bool externalLayer); QString referenceFile(const QString &testName); QString dumpReferenceFile(const QString &testName); QString resultFile(const QString &testName); private: KisStrokeId m_strokeId; QString m_name; QSize m_imageSize; QString m_presetFilename; int m_numIterations; int m_baseFuzziness; + int m_strokeTime = 0; }; } #endif /* __STROKE_TESTING_UTILS_H */ diff --git a/sdk/tests/testutil.h b/sdk/tests/testutil.h index fc19b75cf3..3970c73949 100644 --- a/sdk/tests/testutil.h +++ b/sdk/tests/testutil.h @@ -1,427 +1,492 @@ /* * Copyright (c) 2007 Boudewijn Rempt * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef TEST_UTIL #define TEST_UTIL #include #include #include #include #include #include #include #include #include #include #include #include "kis_node_graph_listener.h" #include "kis_iterator_ng.h" #include "kis_image.h" #include "testing_nodes.h" #ifndef FILES_DATA_DIR #define FILES_DATA_DIR "." #endif #ifndef FILES_DEFAULT_DATA_DIR #define FILES_DEFAULT_DATA_DIR "." #endif #include "qimage_test_util.h" /** * Routines that are useful for writing efficient tests */ namespace TestUtil { inline KisNodeSP findNode(KisNodeSP root, const QString &name) { if(root->name() == name) return root; KisNodeSP child = root->firstChild(); while (child) { if((root = findNode(child, name))) return root; child = child->nextSibling(); } return KisNodeSP(); } inline void dumpNodeStack(KisNodeSP node, QString prefix = QString("\t")) { qDebug() << node->name(); KisNodeSP child = node->firstChild(); while (child) { if (child->childCount() > 0) { dumpNodeStack(child, prefix + "\t"); } else { qDebug() << prefix << child->name(); } child = child->nextSibling(); } } class TestProgressBar : public KoProgressProxy { public: TestProgressBar() : m_min(0), m_max(0), m_value(0) {} int maximum() const override { return m_max; } void setValue(int value) override { m_value = value; } void setRange(int min, int max) override { m_min = min; m_max = max; } void setFormat(const QString &format) override { m_format = format; } void setAutoNestedName(const QString &name) { m_autoNestedName = name; KoProgressProxy::setAutoNestedName(name); } int min() { return m_min; } int max() { return m_max; } int value() { return m_value; } QString format() { return m_format; } QString autoNestedName() { return m_autoNestedName; } private: int m_min; int m_max; int m_value; QString m_format; QString m_autoNestedName; }; inline bool comparePaintDevices(QPoint & pt, const KisPaintDeviceSP dev1, const KisPaintDeviceSP dev2) { // QTime t; // t.start(); QRect rc1 = dev1->exactBounds(); QRect rc2 = dev2->exactBounds(); if (rc1 != rc2) { pt.setX(-1); pt.setY(-1); } KisHLineConstIteratorSP iter1 = dev1->createHLineConstIteratorNG(0, 0, rc1.width()); KisHLineConstIteratorSP iter2 = dev2->createHLineConstIteratorNG(0, 0, rc1.width()); int pixelSize = dev1->pixelSize(); for (int y = 0; y < rc1.height(); ++y) { do { if (memcmp(iter1->oldRawData(), iter2->oldRawData(), pixelSize) != 0) return false; } while (iter1->nextPixel() && iter2->nextPixel()); iter1->nextRow(); iter2->nextRow(); } // qDebug() << "comparePaintDevices time elapsed:" << t.elapsed(); return true; } template inline bool comparePaintDevicesClever(const KisPaintDeviceSP dev1, const KisPaintDeviceSP dev2, channel_type alphaThreshold = 0) { QRect rc1 = dev1->exactBounds(); QRect rc2 = dev2->exactBounds(); if (rc1 != rc2) { qDebug() << "Devices have different size" << ppVar(rc1) << ppVar(rc2); return false; } KisHLineConstIteratorSP iter1 = dev1->createHLineConstIteratorNG(0, 0, rc1.width()); KisHLineConstIteratorSP iter2 = dev2->createHLineConstIteratorNG(0, 0, rc1.width()); int pixelSize = dev1->pixelSize(); for (int y = 0; y < rc1.height(); ++y) { do { if (memcmp(iter1->oldRawData(), iter2->oldRawData(), pixelSize) != 0) { const channel_type* p1 = reinterpret_cast(iter1->oldRawData()); const channel_type* p2 = reinterpret_cast(iter2->oldRawData()); if (p1[3] < alphaThreshold && p2[3] < alphaThreshold) continue; qDebug() << "Failed compare paint devices:" << iter1->x() << iter1->y(); qDebug() << "src:" << p1[0] << p1[1] << p1[2] << p1[3]; qDebug() << "dst:" << p2[0] << p2[1] << p2[2] << p2[3]; return false; } } while (iter1->nextPixel() && iter2->nextPixel()); iter1->nextRow(); iter2->nextRow(); } return true; } #ifdef FILES_OUTPUT_DIR struct ExternalImageChecker { ExternalImageChecker(const QString &prefix, const QString &testName) : m_prefix(prefix), m_testName(testName), m_success(true), m_maxFailingPixels(100), m_fuzzy(1) { } void setMaxFailingPixels(int value) { m_maxFailingPixels = value; } void setFuzzy(int fuzzy){ m_fuzzy = fuzzy; } bool testPassed() const { return m_success; } inline bool checkDevice(KisPaintDeviceSP device, KisImageSP image, const QString &caseName) { bool result = checkQImageExternal(device->convertToQImage(0, image->bounds()), m_testName, m_prefix, caseName, m_fuzzy, m_fuzzy, m_maxFailingPixels); m_success &= result; return result; } inline bool checkImage(KisImageSP image, const QString &testName) { bool result = checkDevice(image->projection(), image, testName); m_success &= result; return result; } private: QString m_prefix; QString m_testName; bool m_success; int m_maxFailingPixels; int m_fuzzy; }; #endif inline quint8 alphaDevicePixel(KisPaintDeviceSP dev, qint32 x, qint32 y) { KisHLineConstIteratorSP iter = dev->createHLineConstIteratorNG(x, y, 1); const quint8 *pix = iter->oldRawData(); return *pix; } inline void alphaDeviceSetPixel(KisPaintDeviceSP dev, qint32 x, qint32 y, quint8 s) { KisHLineIteratorSP iter = dev->createHLineIteratorNG(x, y, 1); quint8 *pix = iter->rawData(); *pix = s; } inline bool checkAlphaDeviceFilledWithPixel(KisPaintDeviceSP dev, const QRect &rc, quint8 expected) { KisHLineIteratorSP it = dev->createHLineIteratorNG(rc.x(), rc.y(), rc.width()); for (int y = rc.y(); y < rc.y() + rc.height(); y++) { for (int x = rc.x(); x < rc.x() + rc.width(); x++) { if(*((quint8*)it->rawData()) != expected) { errKrita << "At point:" << x << y; errKrita << "Expected pixel:" << expected; errKrita << "Actual pixel: " << *((quint8*)it->rawData()); return false; } it->nextPixel(); } it->nextRow(); } return true; } class TestNode : public DefaultNode { Q_OBJECT public: KisNodeSP clone() const override { return KisNodeSP(new TestNode(*this)); } }; class TestGraphListener : public KisNodeGraphListener { public: void aboutToAddANode(KisNode *parent, int index) override { KisNodeGraphListener::aboutToAddANode(parent, index); beforeInsertRow = true; } void nodeHasBeenAdded(KisNode *parent, int index) override { KisNodeGraphListener::nodeHasBeenAdded(parent, index); afterInsertRow = true; } void aboutToRemoveANode(KisNode *parent, int index) override { KisNodeGraphListener::aboutToRemoveANode(parent, index); beforeRemoveRow = true; } void nodeHasBeenRemoved(KisNode *parent, int index) override { KisNodeGraphListener::nodeHasBeenRemoved(parent, index); afterRemoveRow = true; } void aboutToMoveNode(KisNode *parent, int oldIndex, int newIndex) override { KisNodeGraphListener::aboutToMoveNode(parent, oldIndex, newIndex); beforeMove = true; } void nodeHasBeenMoved(KisNode *parent, int oldIndex, int newIndex) override { KisNodeGraphListener::nodeHasBeenMoved(parent, oldIndex, newIndex); afterMove = true; } bool beforeInsertRow; bool afterInsertRow; bool beforeRemoveRow; bool afterRemoveRow; bool beforeMove; bool afterMove; void resetBools() { beforeRemoveRow = false; afterRemoveRow = false; beforeInsertRow = false; afterInsertRow = false; beforeMove = false; afterMove = false; } }; } #include #include #include "kis_undo_stores.h" namespace TestUtil { struct MaskParent { MaskParent(const QRect &_imageRect = QRect(0,0,512,512)) : imageRect(_imageRect) { const KoColorSpace * cs = KoColorSpaceRegistry::instance()->rgb8(); undoStore = new KisSurrogateUndoStore(); image = new KisImage(undoStore, imageRect.width(), imageRect.height(), cs, "test image"); layer = KisPaintLayerSP(new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8)); image->addNode(KisNodeSP(layer.data())); } KisSurrogateUndoStore *undoStore; const QRect imageRect; KisImageSP image; KisPaintLayerSP layer; }; } namespace TestUtil { class MeasureAvgPortion { public: MeasureAvgPortion(int period) : m_period(period), m_val(0), m_total(0), m_cycles(0) { } ~MeasureAvgPortion() { printValues(true); } void addVal(int x) { m_val += x; } void addTotal(int x) { m_total += x; m_cycles++; printValues(); } private: void printValues(bool force = false) { if (m_cycles > m_period || force) { qDebug() << "Val / Total:" << qreal(m_val) / qreal(m_total); qDebug() << "Avg. Val: " << qreal(m_val) / m_cycles; qDebug() << "Avg. Total: " << qreal(m_total) / m_cycles; qDebug() << ppVar(m_val) << ppVar(m_total) << ppVar(m_cycles); m_val = 0; m_total = 0; m_cycles = 0; } } private: int m_period; qint64 m_val; qint64 m_total; qint64 m_cycles; }; +struct MeasureDistributionStats { + MeasureDistributionStats(int numBins, const QString &name = QString()) + : m_numBins(numBins), + m_name(name) + { + reset(); + } + + void reset() { + m_values.clear(); + m_values.resize(m_numBins); + } + + void addValue(int value) { + addValue(value, 1); + } + + void addValue(int value, int increment) { + KIS_SAFE_ASSERT_RECOVER_RETURN(value >= 0); + + if (value >= m_numBins) { + m_values[m_numBins - 1] += increment; + } else { + m_values[value] += increment; + } + } + + void print() { + qCritical() << "============= Stats =============="; + + if (!m_name.isEmpty()) { + qCritical() << "Name:" << m_name; + } + + int total = 0; + + for (int i = 0; i < m_numBins; i++) { + total += m_values[i]; + } + + for (int i = 0; i < m_numBins; i++) { + if (!m_values[i]) continue; + + const QString lastMarker = i == m_numBins - 1 ? "> " : " "; + + const QString line = + QString(" %1%2: %3 (%4%)") + .arg(lastMarker) + .arg(i, 3) + .arg(m_values[i], 5) + .arg(qreal(m_values[i]) / total * 100.0, 7, 'g', 2); + + qCritical() << qPrintable(line); + } + qCritical() << "---- ----"; + qCritical() << qPrintable(QString("Total: %1").arg(total)); + qCritical() << "=================================="; + } + +private: + QVector m_values; + int m_numBins = 0; + QString m_name; +}; + QStringList getHierarchy(KisNodeSP root, const QString &prefix = ""); bool checkHierarchy(KisNodeSP root, const QStringList &expected); } #endif