diff --git a/libs/flake/KoPathShape.cpp b/libs/flake/KoPathShape.cpp index 0bb0fda87c..812005e6c5 100644 --- a/libs/flake/KoPathShape.cpp +++ b/libs/flake/KoPathShape.cpp @@ -1,1692 +1,1695 @@ /* This file is part of the KDE project Copyright (C) 2006-2008, 2010-2011 Thorsten Zachmann Copyright (C) 2006-2011 Jan Hambrecht Copyright (C) 2007-2009 Thomas Zander Copyright (C) 2011 Jean-Nicolas Artaud 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 "KoPathShape.h" #include "KoPathShape_p.h" #include "KoPathSegment.h" #include "KoOdfWorkaround.h" #include "KoPathPoint.h" #include "KoShapeStrokeModel.h" #include "KoViewConverter.h" #include "KoPathShapeLoader.h" #include "KoShapeSavingContext.h" #include "KoShapeLoadingContext.h" #include "KoShapeShadow.h" #include "KoShapeBackground.h" #include "KoShapeContainer.h" #include "KoFilterEffectStack.h" #include "KoMarker.h" #include "KoShapeStroke.h" #include "KoInsets.h" #include #include #include #include #include #include #include #include "KisQPainterStateSaver.h" #include #include #include "kis_global.h" #include // for qIsNaN static bool qIsNaNPoint(const QPointF &p) { return qIsNaN(p.x()) || qIsNaN(p.y()); } KoPathShape::Private::Private() : QSharedData() , fillRule(Qt::OddEvenFill) , autoFillMarkers(false) { } KoPathShape::Private::Private(const Private &rhs) : QSharedData() , fillRule(rhs.fillRule) , markersNew(rhs.markersNew) , autoFillMarkers(rhs.autoFillMarkers) { } QRectF KoPathShape::Private::handleRect(const QPointF &p, qreal radius) const { return QRectF(p.x() - radius, p.y() - radius, 2*radius, 2*radius); } void KoPathShape::Private::applyViewboxTransformation(const KoXmlElement &element) { // apply viewbox transformation const QRect viewBox = KoPathShape::loadOdfViewbox(element); if (! viewBox.isEmpty()) { // load the desired size QSizeF size; size.setWidth(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "width", QString()))); size.setHeight(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "height", QString()))); // load the desired position QPointF pos; pos.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x", QString()))); pos.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y", QString()))); // create matrix to transform original path data into desired size and position QTransform viewMatrix; viewMatrix.translate(-viewBox.left(), -viewBox.top()); viewMatrix.scale(size.width() / viewBox.width(), size.height() / viewBox.height()); viewMatrix.translate(pos.x(), pos.y()); // transform the path data map(viewMatrix); } } KoPathShape::KoPathShape() : KoTosContainer() , d(new Private) { } KoPathShape::KoPathShape(const KoPathShape &rhs) : KoTosContainer(rhs) , d(rhs.d) { KoSubpathList subpaths; Q_FOREACH (KoSubpath *subPath, rhs.d->subpaths) { KoSubpath *clonedSubPath = new KoSubpath(); Q_FOREACH (KoPathPoint *point, *subPath) { *clonedSubPath << new KoPathPoint(*point, this); } subpaths << clonedSubPath; } d->subpaths = subpaths; } KoPathShape::~KoPathShape() { clear(); } KoShape *KoPathShape::cloneShape() const { return new KoPathShape(*this); } void KoPathShape::saveContourOdf(KoShapeSavingContext &context, const QSizeF &scaleFactor) const { if (d->subpaths.length() <= 1) { QTransform matrix; matrix.scale(scaleFactor.width(), scaleFactor.height()); QString points; KoSubpath *subPath = d->subpaths.first(); KoSubpath::const_iterator pointIt(subPath->constBegin()); KoPathPoint *currPoint= 0; // iterate over all points for (; pointIt != subPath->constEnd(); ++pointIt) { currPoint = *pointIt; if (currPoint->activeControlPoint1() || currPoint->activeControlPoint2()) { break; } const QPointF p = matrix.map(currPoint->point()); points += QString("%1,%2 ").arg(qRound(1000*p.x())).arg(qRound(1000*p.y())); } if (currPoint && !(currPoint->activeControlPoint1() || currPoint->activeControlPoint2())) { context.xmlWriter().startElement("draw:contour-polygon"); context.xmlWriter().addAttribute("svg:width", size().width()); context.xmlWriter().addAttribute("svg:height", size().height()); const QSizeF s(size()); QString viewBox = QString("0 0 %1 %2").arg(qRound(1000*s.width())).arg(qRound(1000*s.height())); context.xmlWriter().addAttribute("svg:viewBox", viewBox); context.xmlWriter().addAttribute("draw:points", points); context.xmlWriter().addAttribute("draw:recreate-on-edit", "true"); context.xmlWriter().endElement(); return; } } // if we get here we couldn't save as polygon - let-s try contour-path context.xmlWriter().startElement("draw:contour-path"); saveOdfAttributes(context, OdfViewbox); context.xmlWriter().addAttribute("svg:d", toString()); context.xmlWriter().addAttribute("calligra:nodeTypes", d->nodeTypes()); context.xmlWriter().addAttribute("draw:recreate-on-edit", "true"); context.xmlWriter().endElement(); } void KoPathShape::saveOdf(KoShapeSavingContext & context) const { context.xmlWriter().startElement("draw:path"); saveOdfAttributes(context, OdfAllAttributes | OdfViewbox); context.xmlWriter().addAttribute("svg:d", toString()); context.xmlWriter().addAttribute("calligra:nodeTypes", d->nodeTypes()); saveOdfCommonChildElements(context); saveText(context); context.xmlWriter().endElement(); } bool KoPathShape::loadContourOdf(const KoXmlElement &element, KoShapeLoadingContext &, const QSizeF &scaleFactor) { // first clear the path data from the default path clear(); if (element.localName() == "contour-polygon") { QString points = element.attributeNS(KoXmlNS::draw, "points").simplified(); points.replace(',', ' '); points.remove('\r'); points.remove('\n'); bool firstPoint = true; const QStringList coordinateList = points.split(' '); for (QStringList::ConstIterator it = coordinateList.constBegin(); it != coordinateList.constEnd(); ++it) { QPointF point; point.setX((*it).toDouble()); ++it; point.setY((*it).toDouble()); if (firstPoint) { moveTo(point); firstPoint = false; } else lineTo(point); } close(); } else if (element.localName() == "contour-path") { KoPathShapeLoader loader(this); loader.parseSvg(element.attributeNS(KoXmlNS::svg, "d"), true); d->loadNodeTypes(element); } // apply viewbox transformation const QRect viewBox = KoPathShape::loadOdfViewbox(element); if (! viewBox.isEmpty()) { QSizeF size; size.setWidth(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "width", QString()))); size.setHeight(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "height", QString()))); // create matrix to transform original path data into desired size and position QTransform viewMatrix; viewMatrix.translate(-viewBox.left(), -viewBox.top()); viewMatrix.scale(scaleFactor.width(), scaleFactor.height()); viewMatrix.scale(size.width() / viewBox.width(), size.height() / viewBox.height()); // transform the path data d->map(viewMatrix); } setTransformation(QTransform()); return true; } bool KoPathShape::loadOdf(const KoXmlElement & element, KoShapeLoadingContext &context) { loadOdfAttributes(element, context, OdfMandatories | OdfAdditionalAttributes | OdfCommonChildElements); // first clear the path data from the default path clear(); if (element.localName() == "line") { QPointF start; start.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x1", ""))); start.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y1", ""))); QPointF end; end.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x2", ""))); end.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y2", ""))); moveTo(start); lineTo(end); } else if (element.localName() == "polyline" || element.localName() == "polygon") { QString points = element.attributeNS(KoXmlNS::draw, "points").simplified(); points.replace(',', ' '); points.remove('\r'); points.remove('\n'); bool firstPoint = true; const QStringList coordinateList = points.split(' '); for (QStringList::ConstIterator it = coordinateList.constBegin(); it != coordinateList.constEnd(); ++it) { QPointF point; point.setX((*it).toDouble()); ++it; point.setY((*it).toDouble()); if (firstPoint) { moveTo(point); firstPoint = false; } else lineTo(point); } if (element.localName() == "polygon") close(); } else { // path loading KoPathShapeLoader loader(this); loader.parseSvg(element.attributeNS(KoXmlNS::svg, "d"), true); d->loadNodeTypes(element); } d->applyViewboxTransformation(element); QPointF pos = normalize(); setTransformation(QTransform()); if (element.hasAttributeNS(KoXmlNS::svg, "x") || element.hasAttributeNS(KoXmlNS::svg, "y")) { pos.setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x", QString()))); pos.setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y", QString()))); } setPosition(pos); loadOdfAttributes(element, context, OdfTransformation); // now that the correct transformation is set up // apply that matrix to the path geometry so that // we don't transform the stroke d->map(transformation()); setTransformation(QTransform()); normalize(); loadText(element, context); return true; } QString KoPathShape::saveStyle(KoGenStyle &style, KoShapeSavingContext &context) const { style.addProperty("svg:fill-rule", d->fillRule == Qt::OddEvenFill ? "evenodd" : "nonzero"); QSharedPointer lineBorder = qSharedPointerDynamicCast(stroke()); qreal lineWidth = 0; if (lineBorder) { lineWidth = lineBorder->lineWidth(); } Q_UNUSED(lineWidth) return KoTosContainer::saveStyle(style, context); } void KoPathShape::loadStyle(const KoXmlElement & element, KoShapeLoadingContext &context) { KoTosContainer::loadStyle(element, context); KoStyleStack &styleStack = context.odfLoadingContext().styleStack(); styleStack.setTypeProperties("graphic"); if (styleStack.hasProperty(KoXmlNS::svg, "fill-rule")) { QString rule = styleStack.property(KoXmlNS::svg, "fill-rule"); d->fillRule = (rule == "nonzero") ? Qt::WindingFill : Qt::OddEvenFill; } else { d->fillRule = Qt::WindingFill; #ifndef NWORKAROUND_ODF_BUGS KoOdfWorkaround::fixMissingFillRule(d->fillRule, context); #endif } QSharedPointer lineBorder = qSharedPointerDynamicCast(stroke()); qreal lineWidth = 0; if (lineBorder) { lineWidth = lineBorder->lineWidth(); } Q_UNUSED(lineWidth); } QRect KoPathShape::loadOdfViewbox(const KoXmlElement & element) { QRect viewbox; QString data = element.attributeNS(KoXmlNS::svg, QLatin1String("viewBox")); if (! data.isEmpty()) { data.replace(QLatin1Char(','), QLatin1Char(' ')); const QStringList coordinates = data.simplified().split(QLatin1Char(' '), QString::SkipEmptyParts); if (coordinates.count() == 4) { viewbox.setRect(coordinates.at(0).toInt(), coordinates.at(1).toInt(), coordinates.at(2).toInt(), coordinates.at(3).toInt()); } } return viewbox; } void KoPathShape::clear() { Q_FOREACH (KoSubpath *subpath, d->subpaths) { Q_FOREACH (KoPathPoint *point, *subpath) delete point; delete subpath; } d->subpaths.clear(); notifyPointsChanged(); } void KoPathShape::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { KisQPainterStateSaver saver(&painter); applyConversion(painter, converter); QPainterPath path(outline()); path.setFillRule(d->fillRule); if (background()) { background()->paint(painter, converter, paintContext, path); } //d->paintDebug(painter); } #ifndef NDEBUG void KoPathShape::Private::paintDebug(QPainter &painter) { KoSubpathList::const_iterator pathIt(subpaths.constBegin()); int i = 0; QPen pen(Qt::black, 0); painter.save(); painter.setPen(pen); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { ++i; KoPathPoint *point = (*it); QRectF r(point->point(), QSizeF(5, 5)); r.translate(-2.5, -2.5); QPen pen(Qt::black, 0); painter.setPen(pen); if (point->activeControlPoint1() && point->activeControlPoint2()) { QBrush b(Qt::red); painter.setBrush(b); } else if (point->activeControlPoint1()) { QBrush b(Qt::yellow); painter.setBrush(b); } else if (point->activeControlPoint2()) { QBrush b(Qt::darkYellow); painter.setBrush(b); } painter.drawEllipse(r); } } painter.restore(); debugFlake << "nop =" << i; } void KoPathShape::Private::debugPath() const { KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { debugFlake << "p:" << (*pathIt) << "," << *it << "," << (*it)->point() << "," << (*it)->properties(); } } } #endif void KoPathShape::paintPoints(KisHandlePainterHelper &handlesHelper) { KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); for (; pathIt != d->subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) (*it)->paint(handlesHelper, KoPathPoint::Node); } } QRectF KoPathShape::outlineRect() const { return outline().boundingRect(); } QPainterPath KoPathShape::outline() const { QPainterPath path; Q_FOREACH (KoSubpath * subpath, d->subpaths) { KoPathPoint * lastPoint = subpath->first(); bool activeCP = false; Q_FOREACH (KoPathPoint * currPoint, *subpath) { KoPathPoint::PointProperties currProperties = currPoint->properties(); if (currPoint == subpath->first()) { if (currProperties & KoPathPoint::StartSubpath) { Q_ASSERT(!qIsNaNPoint(currPoint->point())); path.moveTo(currPoint->point()); } } else if (activeCP && currPoint->activeControlPoint1()) { Q_ASSERT(!qIsNaNPoint(lastPoint->controlPoint2())); Q_ASSERT(!qIsNaNPoint(currPoint->controlPoint1())); Q_ASSERT(!qIsNaNPoint(currPoint->point())); path.cubicTo( lastPoint->controlPoint2(), currPoint->controlPoint1(), currPoint->point()); } else if (activeCP || currPoint->activeControlPoint1()) { Q_ASSERT(!qIsNaNPoint(lastPoint->controlPoint2())); Q_ASSERT(!qIsNaNPoint(currPoint->controlPoint1())); path.quadTo( activeCP ? lastPoint->controlPoint2() : currPoint->controlPoint1(), currPoint->point()); } else { Q_ASSERT(!qIsNaNPoint(currPoint->point())); path.lineTo(currPoint->point()); } if (currProperties & KoPathPoint::CloseSubpath && currProperties & KoPathPoint::StopSubpath) { // add curve when there is a curve on the way to the first point KoPathPoint * firstPoint = subpath->first(); Q_ASSERT(!qIsNaNPoint(firstPoint->point())); if (currPoint->activeControlPoint2() && firstPoint->activeControlPoint1()) { path.cubicTo( currPoint->controlPoint2(), firstPoint->controlPoint1(), firstPoint->point()); } else if (currPoint->activeControlPoint2() || firstPoint->activeControlPoint1()) { Q_ASSERT(!qIsNaNPoint(currPoint->point())); Q_ASSERT(!qIsNaNPoint(currPoint->controlPoint1())); path.quadTo( currPoint->activeControlPoint2() ? currPoint->controlPoint2() : firstPoint->controlPoint1(), firstPoint->point()); } path.closeSubpath(); } if (currPoint->activeControlPoint2()) { activeCP = true; } else { activeCP = false; } lastPoint = currPoint; } } return path; } QRectF KoPathShape::boundingRect() const { const QTransform transform = absoluteTransformation(0); /** * First we approximate the insets of the stroke by rendering a fat bezier curve * with width set to the maximum inset of miters and markers. The are swept by this * curve will be a good approximation of the real curve bounding rect. */ qreal outlineSweepWidth = 0; const QSharedPointer lineBorder = qSharedPointerDynamicCast(stroke()); if (lineBorder) { outlineSweepWidth = lineBorder->lineWidth(); } if (stroke()) { KoInsets inset; stroke()->strokeInsets(this, inset); const qreal maxInset = std::max({inset.left, inset.top, inset.right, inset.bottom}); // insets extend outside the shape, but width extends both inside and outside, // so we should multiply insets by 2.0 outlineSweepWidth = std::max({outlineSweepWidth, 2.0 * maxInset, 2.0 * stroke()->strokeMaxMarkersInset(this)}); } QPen pen(Qt::black, outlineSweepWidth); // select round joins and caps to ensure it sweeps exactly // 'outlineSweepWidth' pixels in every possible pen.setJoinStyle(Qt::RoundJoin); pen.setCapStyle(Qt::RoundCap); QRectF bb = transform.map(pathStroke(pen)).boundingRect(); if (shadow()) { KoInsets insets; shadow()->insets(insets); bb.adjust(-insets.left, -insets.top, insets.right, insets.bottom); } if (filterEffectStack()) { QRectF clipRect = filterEffectStack()->clipRectForBoundingRect(QRectF(QPointF(), size())); bb |= transform.mapRect(clipRect); } return bb; } QSizeF KoPathShape::size() const { // don't call boundingRect here as it uses absoluteTransformation // which itself uses size() -> leads to infinite recursion return outlineRect().size(); } void KoPathShape::setSize(const QSizeF &newSize) { QTransform matrix(resizeMatrix(newSize)); KoShape::setSize(newSize); d->map(matrix); } QTransform KoPathShape::resizeMatrix(const QSizeF & newSize) const { QSizeF oldSize = size(); if (oldSize.width() == 0.0) { oldSize.setWidth(0.000001); } if (oldSize.height() == 0.0) { oldSize.setHeight(0.000001); } QSizeF sizeNew(newSize); if (sizeNew.width() == 0.0) { sizeNew.setWidth(0.000001); } if (sizeNew.height() == 0.0) { sizeNew.setHeight(0.000001); } return QTransform(sizeNew.width() / oldSize.width(), 0, 0, sizeNew.height() / oldSize.height(), 0, 0); } KoPathPoint * KoPathShape::moveTo(const QPointF &p) { KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StartSubpath | KoPathPoint::StopSubpath); KoSubpath * path = new KoSubpath; path->push_back(point); d->subpaths.push_back(path); notifyPointsChanged(); return point; } KoPathPoint * KoPathShape::lineTo(const QPointF &p) { if (d->subpaths.empty()) { moveTo(QPointF(0, 0)); } KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); KoPathPoint * lastPoint = d->subpaths.last()->last(); updateLastPriv(&lastPoint); d->subpaths.last()->push_back(point); notifyPointsChanged(); return point; } KoPathPoint * KoPathShape::curveTo(const QPointF &c1, const QPointF &c2, const QPointF &p) { if (d->subpaths.empty()) { moveTo(QPointF(0, 0)); } KoPathPoint * lastPoint = d->subpaths.last()->last(); updateLastPriv(&lastPoint); lastPoint->setControlPoint2(c1); KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); point->setControlPoint1(c2); d->subpaths.last()->push_back(point); notifyPointsChanged(); return point; } KoPathPoint * KoPathShape::curveTo(const QPointF &c, const QPointF &p) { if (d->subpaths.empty()) moveTo(QPointF(0, 0)); KoPathPoint * lastPoint = d->subpaths.last()->last(); updateLastPriv(&lastPoint); lastPoint->setControlPoint2(c); KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); d->subpaths.last()->push_back(point); notifyPointsChanged(); return point; } KoPathPoint * KoPathShape::arcTo(qreal rx, qreal ry, qreal startAngle, qreal sweepAngle) { if (d->subpaths.empty()) { moveTo(QPointF(0, 0)); } KoPathPoint * lastPoint = d->subpaths.last()->last(); if (lastPoint->properties() & KoPathPoint::CloseSubpath) { lastPoint = d->subpaths.last()->first(); } QPointF startpoint(lastPoint->point()); KoPathPoint * newEndPoint = lastPoint; QPointF curvePoints[12]; int pointCnt = arcToCurve(rx, ry, startAngle, sweepAngle, startpoint, curvePoints); for (int i = 0; i < pointCnt; i += 3) { newEndPoint = curveTo(curvePoints[i], curvePoints[i+1], curvePoints[i+2]); } return newEndPoint; } int KoPathShape::arcToCurve(qreal rx, qreal ry, qreal startAngle, qreal sweepAngle, const QPointF & offset, QPointF * curvePoints) const { int pointCnt = 0; // check Parameters if (sweepAngle == 0.0) return pointCnt; sweepAngle = qBound(-360.0, sweepAngle, 360.0); if (rx == 0 || ry == 0) { //TODO } // split angles bigger than 90° so that it gives a good approximation to the circle qreal parts = ceil(qAbs(sweepAngle / 90.0)); qreal sa_rad = startAngle * M_PI / 180.0; qreal partangle = sweepAngle / parts; qreal endangle = startAngle + partangle; qreal se_rad = endangle * M_PI / 180.0; qreal sinsa = sin(sa_rad); qreal cossa = cos(sa_rad); qreal kappa = 4.0 / 3.0 * tan((se_rad - sa_rad) / 4); // startpoint is at the last point is the path but when it is closed // it is at the first point QPointF startpoint(offset); //center berechnen QPointF center(startpoint - QPointF(cossa * rx, -sinsa * ry)); //debugFlake <<"kappa" << kappa <<"parts" << parts; for (int part = 0; part < parts; ++part) { // start tangent curvePoints[pointCnt++] = QPointF(startpoint - QPointF(sinsa * rx * kappa, cossa * ry * kappa)); qreal sinse = sin(se_rad); qreal cosse = cos(se_rad); // end point QPointF endpoint(center + QPointF(cosse * rx, -sinse * ry)); // end tangent curvePoints[pointCnt++] = QPointF(endpoint - QPointF(-sinse * rx * kappa, -cosse * ry * kappa)); curvePoints[pointCnt++] = endpoint; // set the endpoint as next start point startpoint = endpoint; sinsa = sinse; cossa = cosse; endangle += partangle; se_rad = endangle * M_PI / 180.0; } return pointCnt; } void KoPathShape::close() { if (d->subpaths.empty()) { return; } closeSubpathPriv(d->subpaths.last()); } void KoPathShape::closeMerge() { if (d->subpaths.empty()) { return; } closeMergeSubpathPriv(d->subpaths.last()); } QPointF KoPathShape::normalize() { QPointF tl(outline().boundingRect().topLeft()); QTransform matrix; matrix.translate(-tl.x(), -tl.y()); d->map(matrix); // keep the top left point of the object applyTransformation(matrix.inverted()); shapeChangedPriv(ContentChanged); return tl; } void KoPathShape::Private::map(const QTransform &matrix) { KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { - (*it)->map(matrix); + // It's possible there are null points in the map... + if (*it) { + (*it)->map(matrix); + } } } } void KoPathShape::updateLastPriv(KoPathPoint **lastPoint) { // check if we are about to add a new point to a closed subpath if ((*lastPoint)->properties() & KoPathPoint::StopSubpath && (*lastPoint)->properties() & KoPathPoint::CloseSubpath) { // get the first point of the subpath KoPathPoint *subpathStart = d->subpaths.last()->first(); // clone the first point of the subpath... KoPathPoint * newLastPoint = new KoPathPoint(*subpathStart, this); // ... and make it a normal point newLastPoint->setProperties(KoPathPoint::Normal); // now start a new subpath with the cloned start point KoSubpath *path = new KoSubpath; path->push_back(newLastPoint); d->subpaths.push_back(path); *lastPoint = newLastPoint; } else { // the subpath was not closed so the formerly last point // of the subpath is no end point anymore (*lastPoint)->unsetProperty(KoPathPoint::StopSubpath); } (*lastPoint)->unsetProperty(KoPathPoint::CloseSubpath); } QList KoPathShape::pointsAt(const QRectF &r) const { QList result; KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); for (; pathIt != d->subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { if (r.contains((*it)->point())) result.append(*it); else if ((*it)->activeControlPoint1() && r.contains((*it)->controlPoint1())) result.append(*it); else if ((*it)->activeControlPoint2() && r.contains((*it)->controlPoint2())) result.append(*it); } } return result; } QList KoPathShape::segmentsAt(const QRectF &r) const { QList segments; int subpathCount = d->subpaths.count(); for (int subpathIndex = 0; subpathIndex < subpathCount; ++subpathIndex) { KoSubpath * subpath = d->subpaths[subpathIndex]; int pointCount = subpath->count(); bool subpathClosed = isClosedSubpath(subpathIndex); for (int pointIndex = 0; pointIndex < pointCount; ++pointIndex) { if (pointIndex == (pointCount - 1) && ! subpathClosed) break; KoPathSegment s(subpath->at(pointIndex), subpath->at((pointIndex + 1) % pointCount)); QRectF controlRect = s.controlPointRect(); if (! r.intersects(controlRect) && ! controlRect.contains(r)) continue; QRectF bound = s.boundingRect(); if (! r.intersects(bound) && ! bound.contains(r)) continue; segments.append(s); } } return segments; } KoPathPointIndex KoPathShape::pathPointIndex(const KoPathPoint *point) const { for (int subpathIndex = 0; subpathIndex < d->subpaths.size(); ++subpathIndex) { KoSubpath * subpath = d->subpaths.at(subpathIndex); for (int pointPos = 0; pointPos < subpath->size(); ++pointPos) { if (subpath->at(pointPos) == point) { return KoPathPointIndex(subpathIndex, pointPos); } } } return KoPathPointIndex(-1, -1); } KoPathPoint * KoPathShape::pointByIndex(const KoPathPointIndex &pointIndex) const { KoSubpath *subpath = d->subPath(pointIndex.first); if (subpath == 0 || pointIndex.second < 0 || pointIndex.second >= subpath->size()) return 0; return subpath->at(pointIndex.second); } KoPathSegment KoPathShape::segmentByIndex(const KoPathPointIndex &pointIndex) const { KoPathSegment segment(0, 0); KoSubpath *subpath = d->subPath(pointIndex.first); if (subpath != 0 && pointIndex.second >= 0 && pointIndex.second < subpath->size()) { KoPathPoint * point = subpath->at(pointIndex.second); int index = pointIndex.second; // check if we have a (closing) segment starting from the last point if ((index == subpath->size() - 1) && point->properties() & KoPathPoint::CloseSubpath) index = 0; else ++index; if (index < subpath->size()) { segment = KoPathSegment(point, subpath->at(index)); } } return segment; } int KoPathShape::pointCount() const { int i = 0; KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); for (; pathIt != d->subpaths.constEnd(); ++pathIt) { i += (*pathIt)->size(); } return i; } int KoPathShape::subpathCount() const { return d->subpaths.count(); } int KoPathShape::subpathPointCount(int subpathIndex) const { KoSubpath *subpath = d->subPath(subpathIndex); if (subpath == 0) return -1; return subpath->size(); } bool KoPathShape::isClosedSubpath(int subpathIndex) const { KoSubpath *subpath = d->subPath(subpathIndex); if (subpath == 0) return false; const bool firstClosed = subpath->first()->properties() & KoPathPoint::CloseSubpath; const bool lastClosed = subpath->last()->properties() & KoPathPoint::CloseSubpath; return firstClosed && lastClosed; } bool KoPathShape::insertPoint(KoPathPoint* point, const KoPathPointIndex &pointIndex) { KoSubpath *subpath = d->subPath(pointIndex.first); if (subpath == 0 || pointIndex.second < 0 || pointIndex.second > subpath->size()) return false; KoPathPoint::PointProperties properties = point->properties(); properties &= ~KoPathPoint::StartSubpath; properties &= ~KoPathPoint::StopSubpath; properties &= ~KoPathPoint::CloseSubpath; // check if new point starts subpath if (pointIndex.second == 0) { properties |= KoPathPoint::StartSubpath; // subpath was closed if (subpath->last()->properties() & KoPathPoint::CloseSubpath) { // keep the path closed properties |= KoPathPoint::CloseSubpath; } // old first point does not start the subpath anymore subpath->first()->unsetProperty(KoPathPoint::StartSubpath); } // check if new point stops subpath else if (pointIndex.second == subpath->size()) { properties |= KoPathPoint::StopSubpath; // subpath was closed if (subpath->last()->properties() & KoPathPoint::CloseSubpath) { // keep the path closed properties = properties | KoPathPoint::CloseSubpath; } // old last point does not end subpath anymore subpath->last()->unsetProperty(KoPathPoint::StopSubpath); } point->setProperties(properties); point->setParent(this); subpath->insert(pointIndex.second , point); notifyPointsChanged(); return true; } KoPathPoint * KoPathShape::removePoint(const KoPathPointIndex &pointIndex) { KoSubpath *subpath = d->subPath(pointIndex.first); if (subpath == 0 || pointIndex.second < 0 || pointIndex.second >= subpath->size()) return 0; KoPathPoint * point = subpath->takeAt(pointIndex.second); point->setParent(0); //don't do anything (not even crash), if there was only one point if (pointCount()==0) { return point; } // check if we removed the first point else if (pointIndex.second == 0) { // first point removed, set new StartSubpath subpath->first()->setProperty(KoPathPoint::StartSubpath); // check if path was closed if (subpath->last()->properties() & KoPathPoint::CloseSubpath) { // keep path closed subpath->first()->setProperty(KoPathPoint::CloseSubpath); } } // check if we removed the last point else if (pointIndex.second == subpath->size()) { // use size as point is already removed // last point removed, set new StopSubpath subpath->last()->setProperty(KoPathPoint::StopSubpath); // check if path was closed if (point->properties() & KoPathPoint::CloseSubpath) { // keep path closed subpath->last()->setProperty(KoPathPoint::CloseSubpath); } } notifyPointsChanged(); return point; } bool KoPathShape::breakAfter(const KoPathPointIndex &pointIndex) { KoSubpath *subpath = d->subPath(pointIndex.first); if (!subpath || pointIndex.second < 0 || pointIndex.second > subpath->size() - 2 || isClosedSubpath(pointIndex.first)) return false; KoSubpath * newSubpath = new KoSubpath; int size = subpath->size(); for (int i = pointIndex.second + 1; i < size; ++i) { newSubpath->append(subpath->takeAt(pointIndex.second + 1)); } // now make the first point of the new subpath a starting node newSubpath->first()->setProperty(KoPathPoint::StartSubpath); // the last point of the old subpath is now an ending node subpath->last()->setProperty(KoPathPoint::StopSubpath); // insert the new subpath after the broken one d->subpaths.insert(pointIndex.first + 1, newSubpath); notifyPointsChanged(); return true; } bool KoPathShape::join(int subpathIndex) { KoSubpath *subpath = d->subPath(subpathIndex); KoSubpath *nextSubpath = d->subPath(subpathIndex + 1); if (!subpath || !nextSubpath || isClosedSubpath(subpathIndex) || isClosedSubpath(subpathIndex+1)) return false; // the last point of the subpath does not end the subpath anymore subpath->last()->unsetProperty(KoPathPoint::StopSubpath); // the first point of the next subpath does not start a subpath anymore nextSubpath->first()->unsetProperty(KoPathPoint::StartSubpath); // append the second subpath to the first Q_FOREACH (KoPathPoint * p, *nextSubpath) subpath->append(p); // remove the nextSubpath from path d->subpaths.removeAt(subpathIndex + 1); // delete it as it is no longer possible to use it delete nextSubpath; notifyPointsChanged(); return true; } bool KoPathShape::moveSubpath(int oldSubpathIndex, int newSubpathIndex) { KoSubpath *subpath = d->subPath(oldSubpathIndex); if (subpath == 0 || newSubpathIndex >= d->subpaths.size()) return false; if (oldSubpathIndex == newSubpathIndex) return true; d->subpaths.removeAt(oldSubpathIndex); d->subpaths.insert(newSubpathIndex, subpath); notifyPointsChanged(); return true; } KoPathPointIndex KoPathShape::openSubpath(const KoPathPointIndex &pointIndex) { KoSubpath *subpath = d->subPath(pointIndex.first); if (!subpath || pointIndex.second < 0 || pointIndex.second >= subpath->size() || !isClosedSubpath(pointIndex.first)) return KoPathPointIndex(-1, -1); KoPathPoint * oldStartPoint = subpath->first(); // the old starting node no longer starts the subpath oldStartPoint->unsetProperty(KoPathPoint::StartSubpath); // the old end node no longer closes the subpath subpath->last()->unsetProperty(KoPathPoint::StopSubpath); // reorder the subpath for (int i = 0; i < pointIndex.second; ++i) { subpath->append(subpath->takeFirst()); } // make the first point a start node subpath->first()->setProperty(KoPathPoint::StartSubpath); // make the last point an end node subpath->last()->setProperty(KoPathPoint::StopSubpath); notifyPointsChanged(); return pathPointIndex(oldStartPoint); } KoPathPointIndex KoPathShape::closeSubpath(const KoPathPointIndex &pointIndex) { KoSubpath *subpath = d->subPath(pointIndex.first); if (!subpath || pointIndex.second < 0 || pointIndex.second >= subpath->size() || isClosedSubpath(pointIndex.first)) return KoPathPointIndex(-1, -1); KoPathPoint * oldStartPoint = subpath->first(); // the old starting node no longer starts the subpath oldStartPoint->unsetProperty(KoPathPoint::StartSubpath); // the old end node no longer ends the subpath subpath->last()->unsetProperty(KoPathPoint::StopSubpath); // reorder the subpath for (int i = 0; i < pointIndex.second; ++i) { subpath->append(subpath->takeFirst()); } subpath->first()->setProperty(KoPathPoint::StartSubpath); subpath->last()->setProperty(KoPathPoint::StopSubpath); closeSubpathPriv(subpath); notifyPointsChanged(); return pathPointIndex(oldStartPoint); } bool KoPathShape::reverseSubpath(int subpathIndex) { KoSubpath *subpath = d->subPath(subpathIndex); if (subpath == 0) return false; int size = subpath->size(); for (int i = 0; i < size; ++i) { KoPathPoint *p = subpath->takeAt(i); p->reverse(); subpath->prepend(p); } // adjust the position dependent properties KoPathPoint *first = subpath->first(); KoPathPoint *last = subpath->last(); KoPathPoint::PointProperties firstProps = first->properties(); KoPathPoint::PointProperties lastProps = last->properties(); firstProps |= KoPathPoint::StartSubpath; firstProps &= ~KoPathPoint::StopSubpath; lastProps |= KoPathPoint::StopSubpath; lastProps &= ~KoPathPoint::StartSubpath; if (firstProps & KoPathPoint::CloseSubpath) { firstProps |= KoPathPoint::CloseSubpath; lastProps |= KoPathPoint::CloseSubpath; } first->setProperties(firstProps); last->setProperties(lastProps); notifyPointsChanged(); return true; } KoSubpath * KoPathShape::removeSubpath(int subpathIndex) { KoSubpath *subpath = d->subPath(subpathIndex); if (subpath != 0) { Q_FOREACH (KoPathPoint* point, *subpath) { point->setParent(this); } d->subpaths.removeAt(subpathIndex); } notifyPointsChanged(); return subpath; } bool KoPathShape::addSubpath(KoSubpath * subpath, int subpathIndex) { if (subpathIndex < 0 || subpathIndex > d->subpaths.size()) return false; Q_FOREACH (KoPathPoint* point, *subpath) { point->setParent(this); } d->subpaths.insert(subpathIndex, subpath); notifyPointsChanged(); return true; } int KoPathShape::combine(KoPathShape *path) { int insertSegmentPosition = -1; if (!path) return insertSegmentPosition; QTransform pathMatrix = path->absoluteTransformation(0); QTransform myMatrix = absoluteTransformation(0).inverted(); Q_FOREACH (KoSubpath* subpath, path->d->subpaths) { KoSubpath *newSubpath = new KoSubpath(); Q_FOREACH (KoPathPoint* point, *subpath) { KoPathPoint *newPoint = new KoPathPoint(*point, this); newPoint->map(pathMatrix); newPoint->map(myMatrix); newSubpath->append(newPoint); } d->subpaths.append(newSubpath); if (insertSegmentPosition < 0) { insertSegmentPosition = d->subpaths.size() - 1; } } normalize(); notifyPointsChanged(); return insertSegmentPosition; } bool KoPathShape::separate(QList & separatedPaths) { if (! d->subpaths.size()) return false; QTransform myMatrix = absoluteTransformation(0); Q_FOREACH (KoSubpath* subpath, d->subpaths) { KoPathShape *shape = new KoPathShape(); shape->setStroke(stroke()); shape->setBackground(background()); shape->setShapeId(shapeId()); shape->setZIndex(zIndex()); KoSubpath *newSubpath = new KoSubpath(); Q_FOREACH (KoPathPoint* point, *subpath) { KoPathPoint *newPoint = new KoPathPoint(*point, shape); newPoint->map(myMatrix); newSubpath->append(newPoint); } shape->d->subpaths.append(newSubpath); shape->normalize(); // NOTE: shape cannot have any listeners yet, so no notification about // points modification is needed separatedPaths.append(shape); } return true; } void KoPathShape::closeSubpathPriv(KoSubpath *subpath) { if (! subpath) return; subpath->last()->setProperty(KoPathPoint::CloseSubpath); subpath->first()->setProperty(KoPathPoint::CloseSubpath); notifyPointsChanged(); } void KoPathShape::closeMergeSubpathPriv(KoSubpath *subpath) { if (! subpath || subpath->size() < 2) return; KoPathPoint * lastPoint = subpath->last(); KoPathPoint * firstPoint = subpath->first(); // check if first and last points are coincident if (lastPoint->point() == firstPoint->point()) { // we are removing the current last point and // reuse its first control point if active firstPoint->setProperty(KoPathPoint::StartSubpath); firstPoint->setProperty(KoPathPoint::CloseSubpath); if (lastPoint->activeControlPoint1()) firstPoint->setControlPoint1(lastPoint->controlPoint1()); // remove last point delete subpath->takeLast(); // the new last point closes the subpath now lastPoint = subpath->last(); lastPoint->setProperty(KoPathPoint::StopSubpath); lastPoint->setProperty(KoPathPoint::CloseSubpath); notifyPointsChanged(); } else { closeSubpathPriv(subpath); } } const KoSubpathList &KoPathShape::subpaths() const { return d->subpaths; } KoSubpathList &KoPathShape::subpaths() { return d->subpaths; } void KoPathShape::map(const QTransform &matrix) { return d->map(matrix); } KoSubpath *KoPathShape::Private::subPath(int subpathIndex) const { if (subpathIndex < 0 || subpathIndex >= subpaths.size()) return 0; return subpaths.at(subpathIndex); } QString KoPathShape::pathShapeId() const { return KoPathShapeId; } QString KoPathShape::toString(const QTransform &matrix) const { QString pathString; // iterate over all subpaths KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); for (; pathIt != d->subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator pointIt((*pathIt)->constBegin()); // keep a pointer to the first point of the subpath KoPathPoint *firstPoint(*pointIt); // keep a pointer to the previous point of the subpath KoPathPoint *lastPoint = firstPoint; // keep track if the previous point has an active control point 2 bool activeControlPoint2 = false; // iterate over all points of the current subpath for (; pointIt != (*pathIt)->constEnd(); ++pointIt) { KoPathPoint *currPoint(*pointIt); if (!currPoint) { qWarning() << "Found a zero point in the shape's path!"; continue; } // first point of subpath ? if (currPoint == firstPoint) { // are we starting a subpath ? if (currPoint->properties() & KoPathPoint::StartSubpath) { const QPointF p = matrix.map(currPoint->point()); pathString += QString("M%1 %2").arg(p.x()).arg(p.y()); } } // end point of curve segment ? else if (activeControlPoint2 || currPoint->activeControlPoint1()) { // check if we have a cubic or quadratic curve const bool isCubic = activeControlPoint2 && currPoint->activeControlPoint1(); KoPathSegment cubicSeg = isCubic ? KoPathSegment(lastPoint, currPoint) : KoPathSegment(lastPoint, currPoint).toCubic(); const QPointF cp1 = matrix.map(cubicSeg.first()->controlPoint2()); const QPointF cp2 = matrix.map(cubicSeg.second()->controlPoint1()); const QPointF p = matrix.map(cubicSeg.second()->point()); pathString += QString("C%1 %2 %3 %4 %5 %6") .arg(cp1.x()).arg(cp1.y()) .arg(cp2.x()).arg(cp2.y()) .arg(p.x()).arg(p.y()); } // end point of line segment! else { const QPointF p = matrix.map(currPoint->point()); pathString += QString("L%1 %2").arg(p.x()).arg(p.y()); } // last point closes subpath ? if (currPoint->properties() & KoPathPoint::StopSubpath && currPoint->properties() & KoPathPoint::CloseSubpath) { // add curve when there is a curve on the way to the first point if (currPoint->activeControlPoint2() || firstPoint->activeControlPoint1()) { // check if we have a cubic or quadratic curve const bool isCubic = currPoint->activeControlPoint2() && firstPoint->activeControlPoint1(); KoPathSegment cubicSeg = isCubic ? KoPathSegment(currPoint, firstPoint) : KoPathSegment(currPoint, firstPoint).toCubic(); const QPointF cp1 = matrix.map(cubicSeg.first()->controlPoint2()); const QPointF cp2 = matrix.map(cubicSeg.second()->controlPoint1()); const QPointF p = matrix.map(cubicSeg.second()->point()); pathString += QString("C%1 %2 %3 %4 %5 %6") .arg(cp1.x()).arg(cp1.y()) .arg(cp2.x()).arg(cp2.y()) .arg(p.x()).arg(p.y()); } pathString += QString("Z"); } activeControlPoint2 = currPoint->activeControlPoint2(); lastPoint = currPoint; } } return pathString; } char nodeType(const KoPathPoint * point) { if (point->properties() & KoPathPoint::IsSmooth) { return 's'; } else if (point->properties() & KoPathPoint::IsSymmetric) { return 'z'; } else { return 'c'; } } QString KoPathShape::Private::nodeTypes() const { QString types; KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { if (it == (*pathIt)->constBegin()) { types.append('c'); } else { types.append(nodeType(*it)); } if ((*it)->properties() & KoPathPoint::StopSubpath && (*it)->properties() & KoPathPoint::CloseSubpath) { KoPathPoint * firstPoint = (*pathIt)->first(); types.append(nodeType(firstPoint)); } } } return types; } void updateNodeType(KoPathPoint * point, const QChar & nodeType) { if (nodeType == 's') { point->setProperty(KoPathPoint::IsSmooth); } else if (nodeType == 'z') { point->setProperty(KoPathPoint::IsSymmetric); } } void KoPathShape::Private::loadNodeTypes(const KoXmlElement &element) { if (element.hasAttributeNS(KoXmlNS::calligra, "nodeTypes")) { QString nodeTypes = element.attributeNS(KoXmlNS::calligra, "nodeTypes"); QString::const_iterator nIt(nodeTypes.constBegin()); KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it, nIt++) { // be sure not to crash if there are not enough nodes in nodeTypes if (nIt == nodeTypes.constEnd()) { warnFlake << "not enough nodes in calligra:nodeTypes"; return; } // the first node is always of type 'c' if (it != (*pathIt)->constBegin()) { updateNodeType(*it, *nIt); } if ((*it)->properties() & KoPathPoint::StopSubpath && (*it)->properties() & KoPathPoint::CloseSubpath) { ++nIt; updateNodeType((*pathIt)->first(), *nIt); } } } } } Qt::FillRule KoPathShape::fillRule() const { return d->fillRule; } void KoPathShape::setFillRule(Qt::FillRule fillRule) { d->fillRule = fillRule; } KoPathShape * KoPathShape::createShapeFromPainterPath(const QPainterPath &path) { KoPathShape * shape = new KoPathShape(); int elementCount = path.elementCount(); for (int i = 0; i < elementCount; i++) { QPainterPath::Element element = path.elementAt(i); switch (element.type) { case QPainterPath::MoveToElement: shape->moveTo(QPointF(element.x, element.y)); break; case QPainterPath::LineToElement: shape->lineTo(QPointF(element.x, element.y)); break; case QPainterPath::CurveToElement: shape->curveTo(QPointF(element.x, element.y), QPointF(path.elementAt(i + 1).x, path.elementAt(i + 1).y), QPointF(path.elementAt(i + 2).x, path.elementAt(i + 2).y)); break; default: continue; } } shape->setShapeId(KoPathShapeId); //shape->normalize(); return shape; } bool KoPathShape::hitTest(const QPointF &position) const { if (parent() && parent()->isClipped(this) && ! parent()->hitTest(position)) return false; QPointF point = absoluteTransformation(0).inverted().map(position); const QPainterPath outlinePath = outline(); if (stroke()) { KoInsets insets; stroke()->strokeInsets(this, insets); QRectF roi(QPointF(-insets.left, -insets.top), QPointF(insets.right, insets.bottom)); roi.moveCenter(point); if (outlinePath.intersects(roi) || outlinePath.contains(roi)) return true; } else { if (outlinePath.contains(point)) return true; } // if there is no shadow we can as well just leave if (! shadow()) return false; // the shadow has an offset to the shape, so we simply // check if the position minus the shadow offset hits the shape point = absoluteTransformation(0).inverted().map(position - shadow()->offset()); return outlinePath.contains(point); } void KoPathShape::setMarker(KoMarker *marker, KoFlake::MarkerPosition pos) { if (!marker && d->markersNew.contains(pos)) { d->markersNew.remove(pos); } else { d->markersNew[pos] = marker; } } KoMarker *KoPathShape::marker(KoFlake::MarkerPosition pos) const { return d->markersNew[pos].data(); } bool KoPathShape::hasMarkers() const { return !d->markersNew.isEmpty(); } bool KoPathShape::autoFillMarkers() const { return d->autoFillMarkers; } void KoPathShape::setAutoFillMarkers(bool value) { d->autoFillMarkers = value; } void KoPathShape::recommendPointSelectionChange(const QList &newSelection) { Q_FOREACH (KoShape::ShapeChangeListener *listener, listeners()) { PointSelectionChangeListener *pointListener = dynamic_cast(listener); if (pointListener) { pointListener->recommendPointSelectionChange(this, newSelection); } } } void KoPathShape::notifyPointsChanged() { Q_FOREACH (KoShape::ShapeChangeListener *listener, listeners()) { PointSelectionChangeListener *pointListener = dynamic_cast(listener); if (pointListener) { pointListener->notifyPathPointsChanged(this); } } } QPainterPath KoPathShape::pathStroke(const QPen &pen) const { if (d->subpaths.isEmpty()) { return QPainterPath(); } QPainterPath pathOutline; QPainterPathStroker stroker; stroker.setWidth(0); stroker.setJoinStyle(Qt::MiterJoin); QPair firstSegments; QPair lastSegments; // TODO: these variables are never(!) initialized! KoPathPoint *firstPoint = 0; KoPathPoint *lastPoint = 0; KoPathPoint *secondPoint = 0; KoPathPoint *preLastPoint = 0; KoSubpath *firstSubpath = d->subpaths.first(); stroker.setWidth(pen.widthF()); stroker.setJoinStyle(pen.joinStyle()); stroker.setMiterLimit(pen.miterLimit()); stroker.setCapStyle(pen.capStyle()); stroker.setDashOffset(pen.dashOffset()); stroker.setDashPattern(pen.dashPattern()); // shortent the path to make it look nice // replace the point temporarily in case there is an arrow // BE AWARE: this changes the content of the path so that outline give the correct values. if (firstPoint) { firstSubpath->first() = firstSegments.second.first(); if (secondPoint) { (*firstSubpath)[1] = firstSegments.second.second(); } } if (lastPoint) { if (preLastPoint) { (*firstSubpath)[firstSubpath->count() - 2] = lastSegments.first.first(); } firstSubpath->last() = lastSegments.first.second(); } QPainterPath path = stroker.createStroke(outline()); if (firstPoint) { firstSubpath->first() = firstPoint; if (secondPoint) { (*firstSubpath)[1] = secondPoint; } } if (lastPoint) { if (preLastPoint) { (*firstSubpath)[firstSubpath->count() - 2] = preLastPoint; } firstSubpath->last() = lastPoint; } pathOutline.addPath(path); pathOutline.setFillRule(Qt::WindingFill); return pathOutline; } void KoPathShape::PointSelectionChangeListener::notifyShapeChanged(KoShape::ChangeType type, KoShape *shape) { Q_UNUSED(type); Q_UNUSED(shape); } diff --git a/libs/flake/resources/KoGamutMask.cpp b/libs/flake/resources/KoGamutMask.cpp index 10ce479e85..40f4a4e175 100644 --- a/libs/flake/resources/KoGamutMask.cpp +++ b/libs/flake/resources/KoGamutMask.cpp @@ -1,427 +1,432 @@ /* * Copyright (c) 2018 Anna Medonosova * * 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 "KoGamutMask.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include KoGamutMaskShape::KoGamutMaskShape(KoShape* shape) : m_maskShape(shape) , m_shapePaintingContext(KoShapePaintingContext()) { } KoGamutMaskShape::KoGamutMaskShape() {}; KoGamutMaskShape::~KoGamutMaskShape() {}; KoShape* KoGamutMaskShape::koShape() { return m_maskShape; } bool KoGamutMaskShape::coordIsClear(const QPointF& coord, const KoViewConverter& viewConverter, int maskRotation) const { // apply mask rotation to coord const KisGamutMaskViewConverter& converter = dynamic_cast(viewConverter); QPointF centerPoint(converter.viewSize().width()*0.5, converter.viewSize().height()*0.5); QTransform rotationTransform; rotationTransform.translate(centerPoint.x(), centerPoint.y()); rotationTransform.rotate(-maskRotation); rotationTransform.translate(-centerPoint.x(), -centerPoint.y()); QPointF rotatedCoord = rotationTransform.map(coord); QPointF translatedPoint = viewConverter.viewToDocument(rotatedCoord); bool isClear = m_maskShape->hitTest(translatedPoint); return isClear; } void KoGamutMaskShape::paint(QPainter &painter, const KoViewConverter& viewConverter, int maskRotation) { painter.save(); // apply mask rotation before drawing QPointF centerPoint(painter.viewport().width()*0.5, painter.viewport().height()*0.5); painter.translate(centerPoint); painter.rotate(maskRotation); painter.translate(-centerPoint); painter.setTransform(m_maskShape->absoluteTransformation(&viewConverter) * painter.transform()); m_maskShape->paint(painter, viewConverter, m_shapePaintingContext); painter.restore(); } void KoGamutMaskShape::paintStroke(QPainter &painter, const KoViewConverter &viewConverter, int maskRotation) { painter.save(); // apply mask rotation before drawing QPointF centerPoint(painter.viewport().width()*0.5, painter.viewport().height()*0.5); painter.translate(centerPoint); painter.rotate(maskRotation); painter.translate(-centerPoint); painter.setTransform(m_maskShape->absoluteTransformation(&viewConverter) * painter.transform()); m_maskShape->paintStroke(painter, viewConverter, m_shapePaintingContext); painter.restore(); } -struct Q_DECL_HIDDEN KoGamutMask::Private { +struct KoGamutMask::Private { QString name; QString title; QString description; QByteArray data; QVector maskShapes; QVector previewShapes; QSizeF maskSize; - int rotation; + int rotation {0}; }; KoGamutMask::KoGamutMask(const QString& filename) : KoResource(filename) - , d(new Private()) + , d(new Private) { d->maskSize = QSizeF(144.0,144.0); setRotation(0); } KoGamutMask::KoGamutMask() : KoResource(QString()) - , d(new Private()) + , d(new Private) { d->maskSize = QSizeF(144.0,144.0); setRotation(0); } KoGamutMask::KoGamutMask(KoGamutMask* rhs) : QObject(0) , KoResource(QString()) - , d(new Private()) + , d(new Private) { setFilename(rhs->filename()); setTitle(rhs->title()); setDescription(rhs->description()); d->maskSize = rhs->d->maskSize; QList newShapes; for(KoShape* sh: rhs->koShapes()) { newShapes.append(sh->cloneShape()); } setMaskShapes(newShapes); setValid(true); } +KoGamutMask::~KoGamutMask() +{ + delete d; +} + bool KoGamutMask::coordIsClear(const QPointF& coord, KoViewConverter &viewConverter, bool preview) { QVector* shapeVector; if (preview && !d->previewShapes.isEmpty()) { shapeVector = &d->previewShapes; } else { shapeVector = &d->maskShapes; } for(KoGamutMaskShape* shape: *shapeVector) { if (shape->coordIsClear(coord, viewConverter, rotation()) == true) { return true; } } return false; } void KoGamutMask::paint(QPainter &painter, KoViewConverter& viewConverter, bool preview) { QVector* shapeVector; if (preview && !d->previewShapes.isEmpty()) { shapeVector = &d->previewShapes; } else { shapeVector = &d->maskShapes; } for(KoGamutMaskShape* shape: *shapeVector) { shape->paint(painter, viewConverter, rotation()); } } void KoGamutMask::paintStroke(QPainter &painter, KoViewConverter &viewConverter, bool preview) { QVector* shapeVector; if (preview && !d->previewShapes.isEmpty()) { shapeVector = &d->previewShapes; } else { shapeVector = &d->maskShapes; } for(KoGamutMaskShape* shape: *shapeVector) { shape->paintStroke(painter, viewConverter, rotation()); } } bool KoGamutMask::load() { QFile file(filename()); if (file.size() == 0) return false; if (!file.open(QIODevice::ReadOnly)) { warnFlake << "Can't open file " << filename(); return false; } bool res = loadFromDevice(&file); file.close(); return res; } bool KoGamutMask::loadFromDevice(QIODevice *dev) { if (!dev->isOpen()) dev->open(QIODevice::ReadOnly); d->data = dev->readAll(); // TODO: test KIS_ASSERT_RECOVER_RETURN_VALUE(d->data.size() != 0, false); if (filename().isNull()) { warnFlake << "Cannot load gamut mask" << name() << "there is no filename set"; return false; } if (d->data.isNull()) { QFile file(filename()); if (file.size() == 0) { warnFlake << "Cannot load gamut mask" << name() << "there is no data available"; return false; } file.open(QIODevice::ReadOnly); d->data = file.readAll(); file.close(); } QBuffer buf(&d->data); buf.open(QBuffer::ReadOnly); QScopedPointer store(KoStore::createStore(&buf, KoStore::Read, "application/x-krita-gamutmask", KoStore::Zip)); if (!store || store->bad()) return false; bool storeOpened = store->open("gamutmask.svg"); if (!storeOpened) { return false; } QByteArray data; data.resize(store->size()); QByteArray ba = store->read(store->size()); store->close(); QString errorMsg; int errorLine = 0; int errorColumn = 0; KoXmlDocument xmlDocument = SvgParser::createDocumentFromSvg(ba, &errorMsg, &errorLine, &errorColumn); if (xmlDocument.isNull()) { errorFlake << "Parsing error in " << filename() << "! Aborting!" << endl << " In line: " << errorLine << ", column: " << errorColumn << endl << " Error message: " << errorMsg << endl; errorFlake << "Parsing error in the main document at line" << errorLine << ", column" << errorColumn << endl << "Error message: " << errorMsg; return false; } KoDocumentResourceManager manager; SvgParser parser(&manager); parser.setResolution(QRectF(0,0,100,100), 72); // initialize with default values QSizeF fragmentSize; QList shapes = parser.parseSvg(xmlDocument.documentElement(), &fragmentSize); d->maskSize = fragmentSize; d->title = parser.documentTitle(); setName(d->title); d->description = parser.documentDescription(); setMaskShapes(shapes); if (store->open("preview.png")) { KoStoreDevice previewDev(store.data()); previewDev.open(QIODevice::ReadOnly); QImage preview = QImage(); preview.load(&previewDev, "PNG"); setImage(preview); (void)store->close(); } buf.close(); setValid(true); return true; } void KoGamutMask::setMaskShapes(QList shapes) { setMaskShapesToVector(shapes, d->maskShapes); } bool KoGamutMask::save() { QFile file(filename()); if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { return false; } saveToDevice(&file); file.close(); return true; } QList KoGamutMask::koShapes() const { QList shapes; for(KoGamutMaskShape* maskShape: d->maskShapes) { shapes.append(maskShape->koShape()); } return shapes; } bool KoGamutMask::saveToDevice(QIODevice *dev) const { KoStore* store(KoStore::createStore(dev, KoStore::Write, "application/x-krita-gamutmask", KoStore::Zip)); if (!store || store->bad()) return false; QList shapes = koShapes(); std::sort(shapes.begin(), shapes.end(), KoShape::compareShapeZIndex); if (!store->open("gamutmask.svg")) { return false; } KoStoreDevice storeDev(store); storeDev.open(QIODevice::WriteOnly); SvgWriter writer(shapes); writer.setDocumentTitle(d->title); writer.setDocumentDescription(d->description); writer.save(storeDev, d->maskSize); if (!store->close()) { return false; } if (!store->open("preview.png")) { return false; } KoStoreDevice previewDev(store); previewDev.open(QIODevice::WriteOnly); image().save(&previewDev, "PNG"); if (!store->close()) { return false; } return store->finalize(); } QString KoGamutMask::title() { return d->title; } void KoGamutMask::setTitle(QString title) { d->title = title; setName(title); } QString KoGamutMask::description() { return d->description; } void KoGamutMask::setDescription(QString description) { d->description = description; } int KoGamutMask::rotation() { return d->rotation; } void KoGamutMask::setRotation(int rotation) { d->rotation = rotation; } QSizeF KoGamutMask::maskSize() { return d->maskSize; } void KoGamutMask::setPreviewMaskShapes(QList shapes) { setMaskShapesToVector(shapes, d->previewShapes); } void KoGamutMask::setMaskShapesToVector(QList shapes, QVector &targetVector) { targetVector.clear(); for(KoShape* sh: shapes) { KoGamutMaskShape* maskShape = new KoGamutMaskShape(sh); targetVector.append(maskShape); } } // clean up when ending mask preview void KoGamutMask::clearPreview() { d->previewShapes.clear(); } diff --git a/libs/flake/resources/KoGamutMask.h b/libs/flake/resources/KoGamutMask.h index bcaa2edb3a..b359066ecc 100644 --- a/libs/flake/resources/KoGamutMask.h +++ b/libs/flake/resources/KoGamutMask.h @@ -1,100 +1,101 @@ /* * Copyright (c) 2018 Anna Medonosova * * 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 KOGAMUTMASK_H #define KOGAMUTMASK_H #include #include #include #include #include #include #include #include #include class KoViewConverter; class KoGamutMaskShape { public: KoGamutMaskShape(KoShape* shape); KoGamutMaskShape(); ~KoGamutMaskShape(); bool coordIsClear(const QPointF& coord, const KoViewConverter& viewConverter, int maskRotation) const; QPainterPath outline(); void paint(QPainter &painter, const KoViewConverter& viewConverter, int maskRotation); void paintStroke(QPainter &painter, const KoViewConverter& viewConverter, int maskRotation); KoShape* koShape(); private: KoShape* m_maskShape; KoShapePaintingContext m_shapePaintingContext; }; /** * @brief The resource type for gamut masks used by the artistic color selector */ class KRITAFLAKE_EXPORT KoGamutMask : public QObject, public KoResource { Q_OBJECT public: KoGamutMask(const QString &filename); KoGamutMask(); KoGamutMask(KoGamutMask *rhs); + ~KoGamutMask() override; bool coordIsClear(const QPointF& coord, KoViewConverter& viewConverter, bool preview); bool load() override; bool loadFromDevice(QIODevice *dev) override; bool save() override; bool saveToDevice(QIODevice* dev) const override; void paint(QPainter &painter, KoViewConverter& viewConverter, bool preview); void paintStroke(QPainter &painter, KoViewConverter& viewConverter, bool preview); QString title(); void setTitle(QString title); QString description(); void setDescription(QString description); int rotation(); void setRotation(int rotation); QSizeF maskSize(); void setMaskShapes(QList shapes); void setPreviewMaskShapes(QList shapes); QList koShapes() const; void clearPreview(); private: void setMaskShapesToVector(QList shapes, QVector& targetVector); struct Private; Private* const d; }; #endif // KOGAMUTMASK_H diff --git a/libs/flake/text/KoSvgTextShape.cpp b/libs/flake/text/KoSvgTextShape.cpp index 69bb642ba7..5f52abd6e2 100644 --- a/libs/flake/text/KoSvgTextShape.cpp +++ b/libs/flake/text/KoSvgTextShape.cpp @@ -1,634 +1,638 @@ /* * 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 "KoSvgTextShape.h" #include #include #include "KoSvgText.h" #include "KoSvgTextProperties.h" #include #include #include #include #include #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include class KoSvgTextShape::Private : public QSharedData { public: Private() : QSharedData() { } Private(const Private &) : QSharedData() { } std::vector> cachedLayouts; std::vector cachedLayoutsOffsets; QThread *cachedLayoutsWorkingThread = 0; void clearAssociatedOutlines(KoShape *rootShape); }; KoSvgTextShape::KoSvgTextShape() : KoSvgTextChunkShape() , d(new Private) { setShapeId(KoSvgTextShape_SHAPEID); } KoSvgTextShape::KoSvgTextShape(const KoSvgTextShape &rhs) : KoSvgTextChunkShape(rhs) , d(rhs.d) { setShapeId(KoSvgTextShape_SHAPEID); // QTextLayout has no copy-ctor, so just relayout everything! relayout(); } KoSvgTextShape::~KoSvgTextShape() { } KoShape *KoSvgTextShape::cloneShape() const { return new KoSvgTextShape(*this); } void KoSvgTextShape::shapeChanged(ChangeType type, KoShape *shape) { KoSvgTextChunkShape::shapeChanged(type, shape); if (type == StrokeChanged || type == BackgroundChanged || type == ContentChanged) { relayout(); } } void KoSvgTextShape::paintComponent(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_UNUSED(paintContext); /** * HACK ALERT: * QTextLayout should only be accessed from the thread it has been created in. * If the cached layout has been created in a different thread, we should just * recreate the layouts in the current thread to be able to render them. */ if (QThread::currentThread() != d->cachedLayoutsWorkingThread) { relayout(); } applyConversion(painter, converter); for (int i = 0; i < (int)d->cachedLayouts.size(); i++) { d->cachedLayouts[i]->draw(&painter, d->cachedLayoutsOffsets[i]); } /** * HACK ALERT: * The layouts of non-gui threads must be destroyed in the same thread * they have been created. Because the thread might be restarted in the * meantime or just destroyed, meaning that the per-thread freetype data * will not be available. */ if (QThread::currentThread() != qApp->thread()) { d->cachedLayouts.clear(); d->cachedLayoutsOffsets.clear(); d->cachedLayoutsWorkingThread = 0; } } void KoSvgTextShape::paintStroke(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_UNUSED(painter); Q_UNUSED(converter); Q_UNUSED(paintContext); // do nothing! everything is painted in paintComponent() } QPainterPath KoSvgTextShape::textOutline() { QPainterPath result; result.setFillRule(Qt::WindingFill); for (int i = 0; i < (int)d->cachedLayouts.size(); i++) { const QPointF layoutOffset = d->cachedLayoutsOffsets[i]; const QTextLayout *layout = d->cachedLayouts[i].get(); for (int j = 0; j < layout->lineCount(); j++) { QTextLine line = layout->lineAt(j); Q_FOREACH (const QGlyphRun &run, line.glyphRuns()) { const QVector indexes = run.glyphIndexes(); const QVector positions = run.positions(); const QRawFont font = run.rawFont(); KIS_SAFE_ASSERT_RECOVER(indexes.size() == positions.size()) { continue; } for (int k = 0; k < indexes.size(); k++) { QPainterPath glyph = font.pathForGlyph(indexes[k]); glyph.translate(positions[k] + layoutOffset); result += glyph; } const qreal thickness = font.lineThickness(); const QRectF runBounds = run.boundingRect(); if (run.overline()) { // the offset is calculated to be consistent with the way how Qt renders the text const qreal y = line.y(); QRectF overlineBlob(runBounds.x(), y, runBounds.width(), thickness); overlineBlob.translate(layoutOffset); QPainterPath path; path.addRect(overlineBlob); // don't use direct addRect, because it doesn't care about Qt::WindingFill result += path; } if (run.strikeOut()) { // the offset is calculated to be consistent with the way how Qt renders the text const qreal y = line.y() + 0.5 * line.height(); QRectF strikeThroughBlob(runBounds.x(), y, runBounds.width(), thickness); strikeThroughBlob.translate(layoutOffset); QPainterPath path; path.addRect(strikeThroughBlob); // don't use direct addRect, because it doesn't care about Qt::WindingFill result += path; } if (run.underline()) { const qreal y = line.y() + line.ascent() + font.underlinePosition(); QRectF underlineBlob(runBounds.x(), y, runBounds.width(), thickness); underlineBlob.translate(layoutOffset); QPainterPath path; path.addRect(underlineBlob); // don't use direct addRect, because it doesn't care about Qt::WindingFill result += path; } } } } return result; } void KoSvgTextShape::resetTextShape() { KoSvgTextChunkShape::resetTextShape(); relayout(); } struct TextChunk { QString text; QVector formats; Qt::LayoutDirection direction = Qt::LeftToRight; Qt::Alignment alignment = Qt::AlignLeading; struct SubChunkOffset { QPointF offset; int start = 0; }; QVector offsets; boost::optional xStartPos; boost::optional yStartPos; QPointF applyStartPosOverride(const QPointF &pos) const { QPointF result = pos; if (xStartPos) { result.rx() = *xStartPos; } if (yStartPos) { result.ry() = *yStartPos; } return result; } }; QVector mergeIntoChunks(const QVector &subChunks) { QVector chunks; for (auto it = subChunks.begin(); it != subChunks.end(); ++it) { if (it->transformation.startsNewChunk() || it == subChunks.begin()) { TextChunk newChunk = TextChunk(); newChunk.direction = it->format.layoutDirection(); newChunk.alignment = it->format.calculateAlignment(); newChunk.xStartPos = it->transformation.xPos; newChunk.yStartPos = it->transformation.yPos; chunks.append(newChunk); } TextChunk ¤tChunk = chunks.last(); if (it->transformation.hasRelativeOffset()) { TextChunk::SubChunkOffset o; o.start = currentChunk.text.size(); o.offset = it->transformation.relativeOffset(); KIS_SAFE_ASSERT_RECOVER_NOOP(!o.offset.isNull()); currentChunk.offsets.append(o); } QTextLayout::FormatRange formatRange; formatRange.start = currentChunk.text.size(); formatRange.length = it->text.size(); formatRange.format = it->format; currentChunk.formats.append(formatRange); currentChunk.text += it->text; } return chunks; } /** * Qt's QTextLayout has a weird trait, it doesn't count space characters as * distinct characters in QTextLayout::setNumColumns(), that is, if we want to * position a block of text that starts with a space character in a specific * position, QTextLayout will drop this space and will move the text to the left. * * That is why we have a special wrapper object that ensures that no spaces are * dropped and their horizontal advance parameter is taken into account. */ struct LayoutChunkWrapper { LayoutChunkWrapper(QTextLayout *layout) : m_layout(layout) { } QPointF addTextChunk(int startPos, int length, const QPointF &textChunkStartPos) { QPointF currentTextPos = textChunkStartPos; const int lastPos = startPos + length - 1; KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(startPos == m_addedChars, currentTextPos); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(lastPos < m_layout->text().size(), currentTextPos); QTextLine line; std::swap(line, m_danglingLine); if (!line.isValid()) { line = m_layout->createLine(); } // skip all the space characters that were not included into the Qt's text line const int currentLineStart = line.isValid() ? line.textStart() : startPos + length; while (startPos < currentLineStart && startPos <= lastPos) { currentTextPos.rx() += skipSpaceCharacter(startPos); startPos++; } if (startPos <= lastPos) { // defines the number of columns to look for glyphs const int numChars = lastPos - startPos + 1; // Tabs break the normal column flow // grow to avoid missing glyphs int charOffset = 0; while (line.textLength() < numChars) { line.setNumColumns(numChars + charOffset); charOffset++; } line.setPosition(currentTextPos - QPointF(0, line.ascent())); currentTextPos.rx() += line.horizontalAdvance(); // skip all the space characters that were not included into the Qt's text line for (int i = line.textStart() + line.textLength(); i < lastPos; i++) { currentTextPos.rx() += skipSpaceCharacter(i); } } else { // keep the created but unused line for future use std::swap(line, m_danglingLine); } m_addedChars += length; return currentTextPos; } private: qreal skipSpaceCharacter(int pos) { const QTextCharFormat format = formatForPos(pos, m_layout->formats()); const QChar skippedChar = m_layout->text()[pos]; KIS_SAFE_ASSERT_RECOVER_NOOP(skippedChar.isSpace() || !skippedChar.isPrint()); QFontMetrics metrics(format.font()); - return metrics.horizontalAdvance(skippedChar); + #if QT_VERSION >= 0x051100 + return metrics.horizontalAdvance(skippedChar); + #else + return metrics.width(skippedChar); + #endif } static QTextCharFormat formatForPos(int pos, const QVector &formats) { Q_FOREACH (const QTextLayout::FormatRange &range, formats) { if (pos >= range.start && pos < range.start + range.length) { return range.format; } } KIS_SAFE_ASSERT_RECOVER_NOOP(0 && "pos should be within the bounds of the layouted text"); return QTextCharFormat(); } private: int m_addedChars = 0; QTextLayout *m_layout; QTextLine m_danglingLine; }; void KoSvgTextShape::relayout() { d->cachedLayouts.clear(); d->cachedLayoutsOffsets.clear(); d->cachedLayoutsWorkingThread = QThread::currentThread(); QPointF currentTextPos; QVector textChunks = mergeIntoChunks(layoutInterface()->collectSubChunks()); Q_FOREACH (const TextChunk &chunk, textChunks) { std::unique_ptr layout(new QTextLayout()); QTextOption option; // WARNING: never activate this option! It breaks the RTL text layout! //option.setFlags(QTextOption::ShowTabsAndSpaces); option.setWrapMode(QTextOption::WrapAnywhere); option.setUseDesignMetrics(true); // TODO: investigate if it is needed? option.setTextDirection(chunk.direction); layout->setText(chunk.text); layout->setTextOption(option); layout->setFormats(chunk.formats); layout->setCacheEnabled(true); layout->beginLayout(); currentTextPos = chunk.applyStartPosOverride(currentTextPos); const QPointF anchorPointPos = currentTextPos; int lastSubChunkStart = 0; QPointF lastSubChunkOffset; LayoutChunkWrapper wrapper(layout.get()); for (int i = 0; i <= chunk.offsets.size(); i++) { const bool isFinalPass = i == chunk.offsets.size(); const int length = !isFinalPass ? chunk.offsets[i].start - lastSubChunkStart : chunk.text.size() - lastSubChunkStart; if (length > 0) { currentTextPos += lastSubChunkOffset; currentTextPos = wrapper.addTextChunk(lastSubChunkStart, length, currentTextPos); } if (!isFinalPass) { lastSubChunkOffset = chunk.offsets[i].offset; lastSubChunkStart = chunk.offsets[i].start; } } layout->endLayout(); QPointF diff; if (chunk.alignment & Qt::AlignTrailing || chunk.alignment & Qt::AlignHCenter) { if (chunk.alignment & Qt::AlignTrailing) { diff = currentTextPos - anchorPointPos; } else if (chunk.alignment & Qt::AlignHCenter) { diff = 0.5 * (currentTextPos - anchorPointPos); } // TODO: fix after t2b text implemented diff.ry() = 0; } d->cachedLayouts.push_back(std::move(layout)); d->cachedLayoutsOffsets.push_back(-diff); } d->clearAssociatedOutlines(this); for (int i = 0; i < int(d->cachedLayouts.size()); i++) { const QTextLayout &layout = *d->cachedLayouts[i]; const QPointF layoutOffset = d->cachedLayoutsOffsets[i]; using namespace KoSvgText; Q_FOREACH (const QTextLayout::FormatRange &range, layout.formats()) { const KoSvgCharChunkFormat &format = static_cast(range.format); AssociatedShapeWrapper wrapper = format.associatedShapeWrapper(); const int rangeStart = range.start; const int safeRangeLength = range.length > 0 ? range.length : layout.text().size() - rangeStart; if (safeRangeLength <= 0) continue; const int rangeEnd = range.start + safeRangeLength - 1; const int firstLineIndex = layout.lineForTextPosition(rangeStart).lineNumber(); const int lastLineIndex = layout.lineForTextPosition(rangeEnd).lineNumber(); for (int i = firstLineIndex; i <= lastLineIndex; i++) { const QTextLine line = layout.lineAt(i); // It might happen that the range contains only one (or two) // symbol that is a whitespace symbol. In such a case we should // just skip this (invalid) line. if (!line.isValid()) continue; const int posStart = qMax(line.textStart(), rangeStart); const int posEnd = qMin(line.textStart() + line.textLength() - 1, rangeEnd); const QList glyphRuns = line.glyphRuns(posStart, posEnd - posStart + 1); Q_FOREACH (const QGlyphRun &run, glyphRuns) { const QPointF firstPosition = run.positions().first(); const quint32 firstGlyphIndex = run.glyphIndexes().first(); const QPointF lastPosition = run.positions().last(); const quint32 lastGlyphIndex = run.glyphIndexes().last(); const QRawFont rawFont = run.rawFont(); const QRectF firstGlyphRect = rawFont.boundingRect(firstGlyphIndex).translated(firstPosition); const QRectF lastGlyphRect = rawFont.boundingRect(lastGlyphIndex).translated(lastPosition); QRectF rect = run.boundingRect(); /** * HACK ALERT: there is a bug in a way how Qt calculates boundingRect() * of the glyph run. It doesn't care about left and right bearings * of the border chars in the run, therefore it becomes cropped. * * Here we just add a half-char width margin to both sides * of the glyph run to make sure the glyphs are fully painted. * * BUG: 389528 * BUG: 392068 */ rect.setLeft(qMin(rect.left(), lastGlyphRect.left()) - 0.5 * firstGlyphRect.width()); rect.setRight(qMax(rect.right(), lastGlyphRect.right()) + 0.5 * lastGlyphRect.width()); wrapper.addCharacterRect(rect.translated(layoutOffset)); } } } } } void KoSvgTextShape::Private::clearAssociatedOutlines(KoShape *rootShape) { KoSvgTextChunkShape *chunkShape = dynamic_cast(rootShape); KIS_SAFE_ASSERT_RECOVER_RETURN(chunkShape); chunkShape->layoutInterface()->clearAssociatedOutline(); Q_FOREACH (KoShape *child, chunkShape->shapes()) { clearAssociatedOutlines(child); } } bool KoSvgTextShape::isRootTextNode() const { return true; } KoSvgTextShapeFactory::KoSvgTextShapeFactory() : KoShapeFactoryBase(KoSvgTextShape_SHAPEID, i18n("Text")) { setToolTip(i18n("SVG Text Shape")); setIconName(koIconNameCStr("x-shape-text")); setLoadingPriority(5); setXmlElementNames(KoXmlNS::svg, QStringList("text")); KoShapeTemplate t; t.name = i18n("SVG Text"); t.iconName = koIconName("x-shape-text"); t.toolTip = i18n("SVG Text Shape"); addTemplate(t); } KoShape *KoSvgTextShapeFactory::createDefaultShape(KoDocumentResourceManager *documentResources) const { debugFlake << "Create default svg text shape"; KoSvgTextShape *shape = new KoSvgTextShape(); shape->setShapeId(KoSvgTextShape_SHAPEID); KoSvgTextShapeMarkupConverter converter(shape); converter.convertFromSvg("Lorem ipsum dolor sit amet, consectetur adipiscing elit.", "", QRectF(0, 0, 200, 60), documentResources->documentResolution()); debugFlake << converter.errors() << converter.warnings(); return shape; } KoShape *KoSvgTextShapeFactory::createShape(const KoProperties *params, KoDocumentResourceManager *documentResources) const { KoSvgTextShape *shape = new KoSvgTextShape(); shape->setShapeId(KoSvgTextShape_SHAPEID); QString svgText = params->stringProperty("svgText", "Lorem ipsum dolor sit amet, consectetur adipiscing elit."); QString defs = params->stringProperty("defs" , ""); QRectF shapeRect = QRectF(0, 0, 200, 60); QVariant rect = params->property("shapeRect"); if (rect.type()==QVariant::RectF) { shapeRect = rect.toRectF(); } KoSvgTextShapeMarkupConverter converter(shape); converter.convertFromSvg(svgText, defs, shapeRect, documentResources->documentResolution()); shape->setPosition(shapeRect.topLeft()); return shape; } bool KoSvgTextShapeFactory::supports(const KoXmlElement &/*e*/, KoShapeLoadingContext &/*context*/) const { return false; } diff --git a/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp b/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp index 45c1d087b1..446112092d 100644 --- a/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp +++ b/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp @@ -1,1255 +1,1255 @@ /* * 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 "KoSvgTextShapeMarkupConverter.h" #include "klocalizedstring.h" #include "kis_assert.h" #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_dom_utils.h" #include #include struct KoSvgTextShapeMarkupConverter::Private { Private(KoSvgTextShape *_shape) : shape(_shape) {} KoSvgTextShape *shape; QStringList errors; QStringList warnings; void clearErrors() { errors.clear(); warnings.clear(); } }; KoSvgTextShapeMarkupConverter::KoSvgTextShapeMarkupConverter(KoSvgTextShape *shape) : d(new Private(shape)) { } KoSvgTextShapeMarkupConverter::~KoSvgTextShapeMarkupConverter() { } bool KoSvgTextShapeMarkupConverter::convertToSvg(QString *svgText, QString *stylesText) { d->clearErrors(); QBuffer shapesBuffer; QBuffer stylesBuffer; shapesBuffer.open(QIODevice::WriteOnly); stylesBuffer.open(QIODevice::WriteOnly); { SvgSavingContext savingContext(shapesBuffer, stylesBuffer); savingContext.setStrippedTextMode(true); SvgWriter writer({d->shape}); writer.saveDetached(savingContext); } shapesBuffer.close(); stylesBuffer.close(); *svgText = QString::fromUtf8(shapesBuffer.data()); *stylesText = QString::fromUtf8(stylesBuffer.data()); return true; } bool KoSvgTextShapeMarkupConverter::convertFromSvg(const QString &svgText, const QString &stylesText, const QRectF &boundsInPixels, qreal pixelsPerInch) { debugFlake << "convertFromSvg. text:" << svgText << "styles:" << stylesText << "bounds:" << boundsInPixels << "ppi:" << pixelsPerInch; d->clearErrors(); QString errorMessage; int errorLine = 0; int errorColumn = 0; const QString fullText = QString("\n%1\n%2\n\n").arg(stylesText).arg(svgText); KoXmlDocument doc = SvgParser::createDocumentFromSvg(fullText, &errorMessage, &errorLine, &errorColumn); if (doc.isNull()) { d->errors << QString("line %1, col %2: %3").arg(errorLine).arg(errorColumn).arg(errorMessage); return false; } d->shape->resetTextShape(); KoDocumentResourceManager resourceManager; SvgParser parser(&resourceManager); parser.setResolution(boundsInPixels, pixelsPerInch); KoXmlElement root = doc.documentElement(); KoXmlNode node = root.firstChild(); bool textNodeFound = false; for (; !node.isNull(); node = node.nextSibling()) { KoXmlElement el = node.toElement(); if (el.isNull()) continue; if (el.tagName() == "defs") { parser.parseDefsElement(el); } else if (el.tagName() == "text") { if (textNodeFound) { d->errors << i18n("More than one 'text' node found!"); return false; } KoShape *shape = parser.parseTextElement(el, d->shape); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(shape == d->shape, false); textNodeFound = true; break; } else { d->errors << i18n("Unknown node of type \'%1\' found!", el.tagName()); return false; } } if (!textNodeFound) { d->errors << i18n("No \'text\' node found!"); return false; } return true; } bool KoSvgTextShapeMarkupConverter::convertToHtml(QString *htmlText) { d->clearErrors(); QBuffer shapesBuffer; shapesBuffer.open(QIODevice::WriteOnly); { HtmlWriter writer({d->shape}); if (!writer.save(shapesBuffer)) { d->errors = writer.errors(); d->warnings = writer.warnings(); return false; } } shapesBuffer.close(); *htmlText = QString(shapesBuffer.data()); debugFlake << "\t\t" << *htmlText; return true; } bool KoSvgTextShapeMarkupConverter::convertFromHtml(const QString &htmlText, QString *svgText, QString *styles) { debugFlake << ">>>>>>>>>>>" << htmlText; QBuffer svgBuffer; svgBuffer.open(QIODevice::WriteOnly); QXmlStreamReader htmlReader(htmlText); QXmlStreamWriter svgWriter(&svgBuffer); svgWriter.setAutoFormatting(true); QStringRef elementName; bool newLine = false; int lineCount = 0; QString bodyEm = "1em"; QString em; QString p("p"); //previous style string is for keeping formatting proper on linebreaks and appendstyle is for specific tags QString previousStyleString; QString appendStyle; while (!htmlReader.atEnd()) { QXmlStreamReader::TokenType token = htmlReader.readNext(); switch (token) { case QXmlStreamReader::StartElement: { newLine = false; if (htmlReader.name() == "br") { debugFlake << "\tdoing br"; svgWriter.writeEndElement(); elementName = QStringRef(&p); em = bodyEm; appendStyle = previousStyleString; } else { elementName = htmlReader.name(); em = ""; } if (elementName == "body") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("text"); appendStyle = QString(); } else if (elementName == "p") { // new line debugFlake << "\t\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); newLine = true; if (em.isEmpty()) { em = bodyEm; appendStyle = QString(); } lineCount++; } else if (elementName == "span") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = QString(); } else if (elementName == "b" || elementName == "strong") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "font-weight:700;"; } else if (elementName == "i" || elementName == "em") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "font-style:italic;"; } else if (elementName == "u") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "text-decoration:underline"; } QXmlStreamAttributes attributes = htmlReader.attributes(); QString textAlign; if (attributes.hasAttribute("align")) { textAlign = attributes.value("align").toString(); } if (attributes.hasAttribute("style")) { QString filteredStyles; QStringList svgStyles = QString("font-family font-size font-weight font-variant word-spacing text-decoration font-style font-size-adjust font-stretch direction letter-spacing").split(" "); QStringList styles = attributes.value("style").toString().split(";"); for(int i=0; i 1) { debugFlake << "\t\tAdvancing to the next line"; svgWriter.writeAttribute("x", "0"); svgWriter.writeAttribute("dy", em); } break; } case QXmlStreamReader::EndElement: { if (htmlReader.name() == "br") break; if (elementName == "p" || elementName == "span" || elementName == "body") { debugFlake << "\tEndElement" << htmlReader.name() << "(" << elementName << ")"; svgWriter.writeEndElement(); } break; } case QXmlStreamReader::Characters: { if (elementName == "style") { *styles = htmlReader.text().toString(); } else { if (!htmlReader.isWhitespace()) { debugFlake << "\tCharacters:" << htmlReader.text(); svgWriter.writeCharacters(htmlReader.text().toString()); } } break; } default: ; } } if (htmlReader.hasError()) { d->errors << htmlReader.errorString(); return false; } if (svgWriter.hasError()) { d->errors << i18n("Unknown error writing SVG text element"); return false; } *svgText = QString::fromUtf8(svgBuffer.data()); return true; } void postCorrectBlockHeight(QTextDocument *doc, qreal currLineAscent, qreal prevLineAscent, qreal prevLineDescent, int prevBlockCursorPosition, qreal currentBlockAbsoluteLineOffset) { KIS_SAFE_ASSERT_RECOVER_RETURN(prevBlockCursorPosition >= 0); QTextCursor postCorrectionCursor(doc); postCorrectionCursor.setPosition(prevBlockCursorPosition); if (!postCorrectionCursor.isNull()) { const qreal relativeLineHeight = ((currentBlockAbsoluteLineOffset - currLineAscent + prevLineAscent) / (prevLineAscent + prevLineDescent)) * 100.0; QTextBlockFormat format = postCorrectionCursor.blockFormat(); format.setLineHeight(relativeLineHeight, QTextBlockFormat::ProportionalHeight); postCorrectionCursor.setBlockFormat(format); postCorrectionCursor = QTextCursor(); } } QTextFormat findMostCommonFormat(const QList &allFormats) { QTextCharFormat mostCommonFormat; QSet propertyIds; /** * Get all existing property ids */ Q_FOREACH (const QTextFormat &format, allFormats) { const QMap formatProperties = format.properties(); Q_FOREACH (int id, formatProperties.keys()) { propertyIds.insert(id); } } /** * Filter out properties that do not exist in some formats. Otherwise, the * global format may override the default value used in these formats * (and yes, we do not have access to the default values to use them * in difference calculation algorithm */ Q_FOREACH (const QTextFormat &format, allFormats) { for (auto it = propertyIds.begin(); it != propertyIds.end();) { if (!format.hasProperty(*it)) { it = propertyIds.erase(it); } else { ++it; } } if (propertyIds.isEmpty()) break; } if (!propertyIds.isEmpty()) { QMap> propertyFrequency; /** * Calculate the frequency of values used in *all* the formats */ Q_FOREACH (const QTextFormat &format, allFormats) { const QMap formatProperties = format.properties(); Q_FOREACH (int id, propertyIds) { KIS_SAFE_ASSERT_RECOVER_BREAK(formatProperties.contains(id)); propertyFrequency[id][formatProperties.value(id)]++; } } /** * Add the most popular property value to the set of most common properties */ for (auto it = propertyFrequency.constBegin(); it != propertyFrequency.constEnd(); ++it) { const int id = it.key(); const QMap allValues = it.value(); int maxCount = 0; QVariant maxValue; for (auto valIt = allValues.constBegin(); valIt != allValues.constEnd(); ++valIt) { if (valIt.value() > maxCount) { maxCount = valIt.value(); maxValue = valIt.key(); } } KIS_SAFE_ASSERT_RECOVER_BREAK(maxCount > 0); mostCommonFormat.setProperty(id, maxValue); } } return mostCommonFormat; } qreal calcLineWidth(const QTextBlock &block) { const QString blockText = block.text(); QTextLayout lineLayout; lineLayout.setText(blockText); lineLayout.setFont(block.charFormat().font()); lineLayout.setFormats(block.textFormats()); lineLayout.setTextOption(block.layout()->textOption()); lineLayout.beginLayout(); QTextLine fullLine = lineLayout.createLine(); if (!fullLine.isValid()) { fullLine.setNumColumns(blockText.size()); } lineLayout.endLayout(); return lineLayout.boundingRect().width(); } bool KoSvgTextShapeMarkupConverter::convertDocumentToSvg(const QTextDocument *doc, QString *svgText) { QBuffer svgBuffer; svgBuffer.open(QIODevice::WriteOnly); QXmlStreamWriter svgWriter(&svgBuffer); // disable auto-formatting to avoid axtra spaces appearing here and there svgWriter.setAutoFormatting(false); qreal maxParagraphWidth = 0.0; QTextCharFormat mostCommonCharFormat; QTextBlockFormat mostCommonBlockFormat; struct LineInfo { LineInfo() {} LineInfo(QTextBlock _block, int _numSkippedLines) : block(_block), numSkippedLines(_numSkippedLines) {} QTextBlock block; int numSkippedLines = 0; }; QVector lineInfoList; { QTextBlock block = doc->begin(); QList allCharFormats; QList allBlockFormats; int numSequentialEmptyLines = 0; while (block.isValid()) { if (!block.text().trimmed().isEmpty()) { lineInfoList.append(LineInfo(block, numSequentialEmptyLines)); numSequentialEmptyLines = 0; maxParagraphWidth = qMax(maxParagraphWidth, calcLineWidth(block)); allBlockFormats.append(block.blockFormat()); Q_FOREACH (const QTextLayout::FormatRange &range, block.textFormats()) { QTextFormat format = range.format; allCharFormats.append(format); } } else { numSequentialEmptyLines++; } block = block.next(); } mostCommonCharFormat = findMostCommonFormat(allCharFormats).toCharFormat(); mostCommonBlockFormat = findMostCommonFormat(allBlockFormats).toBlockFormat(); } //Okay, now the actual writing. QTextBlock block = doc->begin(); Q_UNUSED(block); svgWriter.writeStartElement("text"); { const QString commonTextStyle = style(mostCommonCharFormat, mostCommonBlockFormat); if (!commonTextStyle.isEmpty()) { svgWriter.writeAttribute("style", commonTextStyle); } } int prevBlockRelativeLineSpacing = mostCommonBlockFormat.lineHeight(); int prevBlockLineType = mostCommonBlockFormat.lineHeightType(); qreal prevBlockAscent = 0.0; qreal prevBlockDescent= 0.0; Q_FOREACH (const LineInfo &info, lineInfoList) { QTextBlock block = info.block; const QTextBlockFormat blockFormatDiff = formatDifference(block.blockFormat(), mostCommonBlockFormat).toBlockFormat(); QTextCharFormat blockCharFormatDiff = QTextCharFormat(); const QVector formats = block.textFormats(); if (formats.size()==1) { blockCharFormatDiff = formatDifference(formats.at(0).format, mostCommonCharFormat).toCharFormat(); } const QTextLayout *layout = block.layout(); const QTextLine line = layout->lineAt(0); svgWriter.writeStartElement("tspan"); const QString text = block.text(); /** * Mindblowing part: QTextEdit uses a hi-end algorithm for auto-estimation for the text * directionality, so the user expects his text being saved to SVG with the same * directionality. Just emulate behavior of direction="auto", which is not supported by * SVG 1.1 * * BUG: 392064 */ bool isRightToLeft = false; for (int i = 0; i < text.size(); i++) { const QChar ch = text[i]; if (ch.direction() == QChar::DirR || ch.direction() == QChar::DirAL) { isRightToLeft = true; break; } else if (ch.direction() == QChar::DirL) { break; } } if (isRightToLeft) { svgWriter.writeAttribute("direction", "rtl"); svgWriter.writeAttribute("unicode-bidi", "embed"); } { const QString blockStyleString = style(blockCharFormatDiff, blockFormatDiff); if (!blockStyleString.isEmpty()) { svgWriter.writeAttribute("style", blockStyleString); } } /** * The alignment rule will be inverted while rendering the text in the text shape * (according to the standard the alignment is defined not by "left" or "right", * but by "start" and "end", which inverts for rtl text) */ Qt::Alignment blockAlignment = block.blockFormat().alignment(); if (isRightToLeft) { if (blockAlignment & Qt::AlignLeft) { blockAlignment &= ~Qt::AlignLeft; blockAlignment |= Qt::AlignRight; } else if (blockAlignment & Qt::AlignRight) { blockAlignment &= ~Qt::AlignRight; blockAlignment |= Qt::AlignLeft; } } if (blockAlignment & Qt::AlignHCenter) { svgWriter.writeAttribute("x", KisDomUtils::toString(0.5 * maxParagraphWidth) + "pt"); } else if (blockAlignment & Qt::AlignRight) { svgWriter.writeAttribute("x", KisDomUtils::toString(maxParagraphWidth) + "pt"); } else { svgWriter.writeAttribute("x", "0"); } if (block.blockNumber() > 0) { const qreal lineHeightPt = line.ascent() - prevBlockAscent + (prevBlockAscent + prevBlockDescent) * qreal(prevBlockRelativeLineSpacing) / 100.0; const qreal currentLineSpacing = (info.numSkippedLines + 1) * lineHeightPt; svgWriter.writeAttribute("dy", KisDomUtils::toString(currentLineSpacing) + "pt"); } prevBlockRelativeLineSpacing = blockFormatDiff.hasProperty(QTextFormat::LineHeight) ? blockFormatDiff.lineHeight() : mostCommonBlockFormat.lineHeight(); prevBlockLineType = blockFormatDiff.hasProperty(QTextFormat::LineHeightType) ? blockFormatDiff.lineHeightType() : mostCommonBlockFormat.lineHeightType(); if (prevBlockLineType == QTextBlockFormat::SingleHeight) { //single line will set lineHeight to 100% prevBlockRelativeLineSpacing = 100; } prevBlockAscent = line.ascent(); prevBlockDescent = line.descent(); if (formats.size()>1) { QStringList texts; QVector charFormats; for (int f=0; ferrors << i18n("Unknown error writing SVG text element"); return false; } *svgText = QString::fromUtf8(svgBuffer.data()).trimmed(); return true; } void parseTextAttributes(const QXmlStreamAttributes &elementAttributes, QTextCharFormat &charFormat, QTextBlockFormat &blockFormat) { QString styleString; // we convert all the presentation attributes into styles QString presentationAttributes; for (int a = 0; a < elementAttributes.size(); a++) { if (elementAttributes.at(a).name() != "style") { presentationAttributes .append(elementAttributes.at(a).name().toString()) .append(":") .append(elementAttributes.at(a).value().toString()) .append(";"); } } if (presentationAttributes.endsWith(";")) { presentationAttributes.chop(1); } if (elementAttributes.hasAttribute("style")) { styleString = elementAttributes.value("style").toString(); if (styleString.endsWith(";")) { styleString.chop(1); } } if (!styleString.isEmpty() || !presentationAttributes.isEmpty()) { //add attributes to parse them as part of the style. styleString.append(";") .append(presentationAttributes); QStringList styles = styleString.split(";"); QVector formats = KoSvgTextShapeMarkupConverter::stylesFromString(styles, charFormat, blockFormat); charFormat = formats.at(0).toCharFormat(); blockFormat = formats.at(1).toBlockFormat(); } } bool KoSvgTextShapeMarkupConverter::convertSvgToDocument(const QString &svgText, QTextDocument *doc) { QXmlStreamReader svgReader(svgText.trimmed()); doc->clear(); QTextCursor cursor(doc); struct BlockFormatRecord { BlockFormatRecord() {} BlockFormatRecord(QTextBlockFormat _blockFormat, QTextCharFormat _charFormat) : blockFormat(_blockFormat), charFormat(_charFormat) {} QTextBlockFormat blockFormat; QTextCharFormat charFormat; }; QStack formatStack; formatStack.push(BlockFormatRecord(QTextBlockFormat(), QTextCharFormat())); qreal currBlockAbsoluteLineOffset = 0.0; int prevBlockCursorPosition = -1; qreal prevLineDescent = 0.0; qreal prevLineAscent = 0.0; boost::optional previousBlockAbsoluteXOffset = boost::none; while (!svgReader.atEnd()) { QXmlStreamReader::TokenType token = svgReader.readNext(); switch (token) { case QXmlStreamReader::StartElement: { bool newBlock = false; QTextBlockFormat newBlockFormat; QTextCharFormat newCharFormat; qreal absoluteLineOffset = 1.0; // fetch format of the parent block and make it default if (formatStack.size() >= 2) { newBlockFormat = formatStack[formatStack.size() - 2].blockFormat; newCharFormat = formatStack[formatStack.size() - 2].charFormat; } { const QXmlStreamAttributes elementAttributes = svgReader.attributes(); parseTextAttributes(elementAttributes, newCharFormat, newBlockFormat); // mnemonic for a newline is (dy != 0 && x == 0) boost::optional blockAbsoluteXOffset = boost::none; if (elementAttributes.hasAttribute("x")) { QString xString = elementAttributes.value("x").toString(); if (xString.contains("pt")) { xString = xString.remove("pt").trimmed(); } blockAbsoluteXOffset = KisDomUtils::toDouble(xString); } if (previousBlockAbsoluteXOffset && blockAbsoluteXOffset && qFuzzyCompare(*previousBlockAbsoluteXOffset, *blockAbsoluteXOffset) && svgReader.name() != "text" && elementAttributes.hasAttribute("dy")) { QString dyString = elementAttributes.value("dy").toString(); if (dyString.contains("pt")) { dyString = dyString.remove("pt").trimmed(); } KIS_SAFE_ASSERT_RECOVER_NOOP(formatStack.isEmpty() == (svgReader.name() == "text")); absoluteLineOffset = KisDomUtils::toDouble(dyString); newBlock = absoluteLineOffset > 0; } if (elementAttributes.hasAttribute("x")) { previousBlockAbsoluteXOffset = blockAbsoluteXOffset; } } //hack doc->setTextWidth(100); doc->setTextWidth(-1); if (newBlock && absoluteLineOffset > 0) { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!formatStack.isEmpty(), false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cursor.block().layout()->lineCount() > 0, false); QTextLine line = cursor.block().layout()->lineAt(0); if (prevBlockCursorPosition >= 0) { postCorrectBlockHeight(doc, line.ascent(), prevLineAscent, prevLineDescent, prevBlockCursorPosition, currBlockAbsoluteLineOffset); } prevBlockCursorPosition = cursor.position(); prevLineAscent = line.ascent(); prevLineDescent = line.descent(); currBlockAbsoluteLineOffset = absoluteLineOffset; cursor.insertBlock(); cursor.setCharFormat(formatStack.top().charFormat); cursor.setBlockFormat(formatStack.top().blockFormat); } cursor.mergeCharFormat(newCharFormat); cursor.mergeBlockFormat(newBlockFormat); formatStack.push(BlockFormatRecord(cursor.blockFormat(), cursor.charFormat())); break; } case QXmlStreamReader::EndElement: { if (svgReader.name() != "text") { formatStack.pop(); KIS_SAFE_ASSERT_RECOVER(!formatStack.isEmpty()) { break; } cursor.setCharFormat(formatStack.top().charFormat); cursor.setBlockFormat(formatStack.top().blockFormat); } break; } case QXmlStreamReader::Characters: { if (!svgReader.isWhitespace()) { cursor.insertText(svgReader.text().toString()); } break; } default: break; } } if (prevBlockCursorPosition >= 0) { QTextLine line = cursor.block().layout()->lineAt(0); postCorrectBlockHeight(doc, line.ascent(), prevLineAscent, prevLineDescent, prevBlockCursorPosition, currBlockAbsoluteLineOffset); } if (svgReader.hasError()) { d->errors << svgReader.errorString(); return false; } doc->setModified(false); return true; } QStringList KoSvgTextShapeMarkupConverter::errors() const { return d->errors; } QStringList KoSvgTextShapeMarkupConverter::warnings() const { return d->warnings; } QString KoSvgTextShapeMarkupConverter::style(QTextCharFormat format, QTextBlockFormat blockFormat, QTextCharFormat mostCommon) { QStringList style; for(int i=0; i KoSvgTextShapeMarkupConverter::stylesFromString(QStringList styles, QTextCharFormat currentCharFormat, QTextBlockFormat currentBlockFormat) { Q_UNUSED(currentBlockFormat); QVector formats; QTextCharFormat charFormat; charFormat.setTextOutline(currentCharFormat.textOutline()); QTextBlockFormat blockFormat; - SvgGraphicsContext *context = new SvgGraphicsContext(); + QScopedPointer context(new SvgGraphicsContext()); for (int i=0; i props = reference.properties(); for (QMap::ConstIterator it = props.begin(), end = props.end(); it != end; ++it) if (it.value() == test.property(it.key())) { // Some props must not be removed as default state gets in the way. if (it.key() == 0x2023) { // TextUnderlineStyle continue; } else if (it.key() == 0x2033) { // FontLetterSpacingType continue; } diff.clearProperty(it.key()); } return diff; } diff --git a/libs/image/kis_image_config.cpp b/libs/image/kis_image_config.cpp index 6530b6955b..ab4550d59e 100644 --- a/libs/image/kis_image_config.cpp +++ b/libs/image/kis_image_config.cpp @@ -1,634 +1,635 @@ /* * 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_image_config.h" #include #include #include #include #include #include "kis_debug.h" #include #include #include #include #include "kis_global.h" #include #include #ifdef Q_OS_MACOS #include #endif KisImageConfig::KisImageConfig(bool readOnly) : m_config(KSharedConfig::openConfig()->group(QString())) , m_readOnly(readOnly) { if (!readOnly) { KIS_SAFE_ASSERT_RECOVER_RETURN(qApp->thread() == QThread::currentThread()); } #ifdef Q_OS_MACOS // clear /var/folders/ swap path set by old broken Krita swap implementation in order to use new default swap dir. QString swap = m_config.readEntry("swaplocation", ""); if (swap.startsWith("/var/folders/")) { m_config.deleteEntry("swaplocation"); } #endif } KisImageConfig::~KisImageConfig() { if (m_readOnly) return; if (qApp->thread() != QThread::currentThread()) { dbgKrita << "KisImageConfig: requested config synchronization from nonGUI thread! Called from" << kisBacktrace(); return; } m_config.sync(); } bool KisImageConfig::enableProgressReporting(bool requestDefault) const { return !requestDefault ? m_config.readEntry("enableProgressReporting", true) : true; } void KisImageConfig::setEnableProgressReporting(bool value) { m_config.writeEntry("enableProgressReporting", value); } bool KisImageConfig::enablePerfLog(bool requestDefault) const { return !requestDefault ? m_config.readEntry("enablePerfLog", false) :false; } void KisImageConfig::setEnablePerfLog(bool value) { m_config.writeEntry("enablePerfLog", value); } qreal KisImageConfig::transformMaskOffBoundsReadArea() const { return m_config.readEntry("transformMaskOffBoundsReadArea", 0.5); } int KisImageConfig::updatePatchHeight() const { return m_config.readEntry("updatePatchHeight", 512); } void KisImageConfig::setUpdatePatchHeight(int value) { m_config.writeEntry("updatePatchHeight", value); } int KisImageConfig::updatePatchWidth() const { return m_config.readEntry("updatePatchWidth", 512); } void KisImageConfig::setUpdatePatchWidth(int value) { m_config.writeEntry("updatePatchWidth", value); } qreal KisImageConfig::maxCollectAlpha() const { return m_config.readEntry("maxCollectAlpha", 2.5); } qreal KisImageConfig::maxMergeAlpha() const { return m_config.readEntry("maxMergeAlpha", 1.); } qreal KisImageConfig::maxMergeCollectAlpha() const { return m_config.readEntry("maxMergeCollectAlpha", 1.5); } qreal KisImageConfig::schedulerBalancingRatio() const { /** * updates-queue-size / strokes-queue-size */ return m_config.readEntry("schedulerBalancingRatio", 100.); } void KisImageConfig::setSchedulerBalancingRatio(qreal value) { m_config.writeEntry("schedulerBalancingRatio", value); } int KisImageConfig::maxSwapSize(bool requestDefault) const { return !requestDefault ? m_config.readEntry("maxSwapSize", 4096) : 4096; // in MiB } void KisImageConfig::setMaxSwapSize(int value) { m_config.writeEntry("maxSwapSize", value); } int KisImageConfig::swapSlabSize() const { return m_config.readEntry("swapSlabSize", 64); // in MiB } void KisImageConfig::setSwapSlabSize(int value) { m_config.writeEntry("swapSlabSize", value); } int KisImageConfig::swapWindowSize() const { return m_config.readEntry("swapWindowSize", 16); // in MiB } void KisImageConfig::setSwapWindowSize(int value) { m_config.writeEntry("swapWindowSize", value); } int KisImageConfig::tilesHardLimit() const { qreal hp = qreal(memoryHardLimitPercent()) / 100.0; qreal pp = qreal(memoryPoolLimitPercent()) / 100.0; return totalRAM() * hp * (1 - pp); } int KisImageConfig::tilesSoftLimit() const { qreal sp = qreal(memorySoftLimitPercent()) / 100.0; return tilesHardLimit() * sp; } int KisImageConfig::poolLimit() const { qreal hp = qreal(memoryHardLimitPercent()) / 100.0; qreal pp = qreal(memoryPoolLimitPercent()) / 100.0; return totalRAM() * hp * pp; } qreal KisImageConfig::memoryHardLimitPercent(bool requestDefault) const { return !requestDefault ? m_config.readEntry("memoryHardLimitPercent", 50.) : 50.; } void KisImageConfig::setMemoryHardLimitPercent(qreal value) { m_config.writeEntry("memoryHardLimitPercent", value); } qreal KisImageConfig::memorySoftLimitPercent(bool requestDefault) const { return !requestDefault ? m_config.readEntry("memorySoftLimitPercent", 2.) : 2.; } void KisImageConfig::setMemorySoftLimitPercent(qreal value) { m_config.writeEntry("memorySoftLimitPercent", value); } qreal KisImageConfig::memoryPoolLimitPercent(bool requestDefault) const { return !requestDefault ? m_config.readEntry("memoryPoolLimitPercent", 0.0) : 0.0; } void KisImageConfig::setMemoryPoolLimitPercent(qreal value) { m_config.writeEntry("memoryPoolLimitPercent", value); } QString KisImageConfig::safelyGetWritableTempLocation(const QString &suffix, const QString &configKey, bool requestDefault) const { #ifdef Q_OS_MACOS // On OSX, QDir::tempPath() gives us a folder we cannot reply upon (usually // something like /var/folders/.../...) and that will have vanished when we // try to create the tmp file in KisMemoryWindow::KisMemoryWindow using // swapFileTemplate. thus, we just pick the home folder if swapDir does not // tell us otherwise. // the other option here would be to use a "garbled name" temp file (i.e. no name // KRITA_SWAP_FILE_XXXXXX) in an obscure /var/folders place, which is not // nice to the user. having a clearly named swap file in the home folder is // much nicer to Krita's users. // furthermore, this is just a default and swapDir can always be configured // to another location. QString swap = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + QDir::separator() + suffix; #else Q_UNUSED(suffix); QString swap = QDir::tempPath(); #endif if (requestDefault) { return swap; } const QString configuredSwap = m_config.readEntry(configKey, swap); if (!configuredSwap.isEmpty()) { swap = configuredSwap; } QString chosenLocation; QStringList proposedSwapLocations; proposedSwapLocations << swap; proposedSwapLocations << QDir::tempPath(); proposedSwapLocations << QDir::homePath(); Q_FOREACH (const QString location, proposedSwapLocations) { if (!QFileInfo(location).isWritable()) continue; /** * On NTFS, isWritable() doesn't check for attributes due to performance * reasons, so we should try it in a brute-force way... * (yes, there is a hacky-global-variable workaround, but let's be safe) */ QTemporaryFile tempFile; tempFile.setFileTemplate(location + QDir::separator() + "krita_test_swap_location"); if (tempFile.open() && !tempFile.fileName().isEmpty()) { chosenLocation = location; break; } } if (chosenLocation.isEmpty()) { qCritical() << "CRITICAL: no writable location for a swap file found! Tried the following paths:" << proposedSwapLocations; qCritical() << "CRITICAL: hope I don't crash..."; chosenLocation = swap; } if (chosenLocation != swap) { qWarning() << "WARNING: configured swap location is not writable, using a fall-back location" << swap << "->" << chosenLocation; } return chosenLocation; } QString KisImageConfig::swapDir(bool requestDefault) { return safelyGetWritableTempLocation("swap", "swaplocation", requestDefault); } void KisImageConfig::setSwapDir(const QString &swapDir) { m_config.writeEntry("swaplocation", swapDir); } int KisImageConfig::numberOfOnionSkins() const { return m_config.readEntry("numberOfOnionSkins", 10); } void KisImageConfig::setNumberOfOnionSkins(int value) { m_config.writeEntry("numberOfOnionSkins", value); } int KisImageConfig::onionSkinTintFactor() const { return m_config.readEntry("onionSkinTintFactor", 192); } void KisImageConfig::setOnionSkinTintFactor(int value) { m_config.writeEntry("onionSkinTintFactor", value); } int KisImageConfig::onionSkinOpacity(int offset) const { int value = m_config.readEntry("onionSkinOpacity_" + QString::number(offset), -1); if (value < 0) { const int num = numberOfOnionSkins(); const qreal dx = qreal(qAbs(offset)) / num; value = 0.7 * exp(-pow2(dx) / 0.5) * 255; } return value; } void KisImageConfig::setOnionSkinOpacity(int offset, int value) { m_config.writeEntry("onionSkinOpacity_" + QString::number(offset), value); } bool KisImageConfig::onionSkinState(int offset) const { bool enableByDefault = (qAbs(offset) <= 2); return m_config.readEntry("onionSkinState_" + QString::number(offset), enableByDefault); } void KisImageConfig::setOnionSkinState(int offset, bool value) { m_config.writeEntry("onionSkinState_" + QString::number(offset), value); } QColor KisImageConfig::onionSkinTintColorBackward() const { return m_config.readEntry("onionSkinTintColorBackward", QColor(Qt::red)); } void KisImageConfig::setOnionSkinTintColorBackward(const QColor &value) { m_config.writeEntry("onionSkinTintColorBackward", value); } QColor KisImageConfig::onionSkinTintColorForward() const { return m_config.readEntry("oninSkinTintColorForward", QColor(Qt::green)); } void KisImageConfig::setOnionSkinTintColorForward(const QColor &value) { m_config.writeEntry("oninSkinTintColorForward", value); } bool KisImageConfig::lazyFrameCreationEnabled(bool requestDefault) const { return !requestDefault ? m_config.readEntry("lazyFrameCreationEnabled", true) : true; } void KisImageConfig::setLazyFrameCreationEnabled(bool value) { m_config.writeEntry("lazyFrameCreationEnabled", value); } #if defined Q_OS_LINUX #include #elif defined Q_OS_FREEBSD || defined Q_OS_NETBSD || defined Q_OS_OPENBSD #include #elif defined Q_OS_WIN #include #elif defined Q_OS_MACOS #include #include #endif int KisImageConfig::totalRAM() { // let's think that default memory size is 1000MiB int totalMemory = 1000; // MiB int error = 1; #if defined Q_OS_LINUX struct sysinfo info; error = sysinfo(&info); if(!error) { totalMemory = info.totalram * info.mem_unit / (1UL << 20); } #elif defined Q_OS_FREEBSD || defined Q_OS_NETBSD || defined Q_OS_OPENBSD u_long physmem; # if defined HW_PHYSMEM64 // NetBSD only int mib[] = {CTL_HW, HW_PHYSMEM64}; # else int mib[] = {CTL_HW, HW_PHYSMEM}; # endif size_t len = sizeof(physmem); error = sysctl(mib, 2, &physmem, &len, 0, 0); if(!error) { totalMemory = physmem >> 20; } #elif defined Q_OS_WIN MEMORYSTATUSEX status; status.dwLength = sizeof(status); error = !GlobalMemoryStatusEx(&status); if (!error) { totalMemory = status.ullTotalPhys >> 20; } // For 32 bit windows, the total memory available is at max the 2GB per process memory limit. # if defined ENV32BIT totalMemory = qMin(totalMemory, 2000); # endif #elif defined Q_OS_MACOS int mib[2] = { CTL_HW, HW_MEMSIZE }; u_int namelen = sizeof(mib) / sizeof(mib[0]); uint64_t size; size_t len = sizeof(size); errno = 0; if (sysctl(mib, namelen, &size, &len, 0, 0) >= 0) { totalMemory = size >> 20; error = 0; } else { dbgKrita << "sysctl(\"hw.memsize\") raised error" << strerror(errno); } #endif if (error) { warnKrita << "Cannot get the size of your RAM. Using 1 GiB by default."; } return totalMemory; } bool KisImageConfig::showAdditionalOnionSkinsSettings(bool requestDefault) const { return !requestDefault ? m_config.readEntry("showAdditionalOnionSkinsSettings", true) : true; } void KisImageConfig::setShowAdditionalOnionSkinsSettings(bool value) { m_config.writeEntry("showAdditionalOnionSkinsSettings", value); } int KisImageConfig::defaultFrameColorLabel() const { return m_config.readEntry("defaultFrameColorLabel", 0); } void KisImageConfig::setDefaultFrameColorLabel(int label) { m_config.writeEntry("defaultFrameColorLabel", label); } KisProofingConfigurationSP KisImageConfig::defaultProofingconfiguration() { KisProofingConfiguration *proofingConfig= new KisProofingConfiguration(); proofingConfig->proofingProfile = m_config.readEntry("defaultProofingProfileName", "Chemical proof"); proofingConfig->proofingModel = m_config.readEntry("defaultProofingProfileModel", "CMYKA"); proofingConfig->proofingDepth = m_config.readEntry("defaultProofingProfileDepth", "U8"); proofingConfig->intent = (KoColorConversionTransformation::Intent)m_config.readEntry("defaultProofingProfileIntent", 3); if (m_config.readEntry("defaultProofingBlackpointCompensation", true)) { proofingConfig->conversionFlags |= KoColorConversionTransformation::ConversionFlag::BlackpointCompensation; } else { proofingConfig->conversionFlags = proofingConfig->conversionFlags & ~KoColorConversionTransformation::ConversionFlag::BlackpointCompensation; } QColor def(Qt::green); m_config.readEntry("defaultProofingGamutwarning", def); KoColor col(KoColorSpaceRegistry::instance()->rgb8()); col.fromQColor(def); col.setOpacity(1.0); proofingConfig->warningColor = col; proofingConfig->adaptationState = (double)m_config.readEntry("defaultProofingAdaptationState", 1.0); return toQShared(proofingConfig); } void KisImageConfig::setDefaultProofingConfig(const KoColorSpace *proofingSpace, int proofingIntent, bool blackPointCompensation, KoColor warningColor, double adaptationState) { m_config.writeEntry("defaultProofingProfileName", proofingSpace->profile()->name()); m_config.writeEntry("defaultProofingProfileModel", proofingSpace->colorModelId().id()); m_config.writeEntry("defaultProofingProfileDepth", proofingSpace->colorDepthId().id()); m_config.writeEntry("defaultProofingProfileIntent", proofingIntent); m_config.writeEntry("defaultProofingBlackpointCompensation", blackPointCompensation); QColor c; c = warningColor.toQColor(); m_config.writeEntry("defaultProofingGamutwarning", c); m_config.writeEntry("defaultProofingAdaptationState",adaptationState); } bool KisImageConfig::useLodForColorizeMask(bool requestDefault) const { return !requestDefault ? m_config.readEntry("useLodForColorizeMask", false) : false; } void KisImageConfig::setUseLodForColorizeMask(bool value) { m_config.writeEntry("useLodForColorizeMask", value); } int KisImageConfig::maxNumberOfThreads(bool defaultValue) const { return (defaultValue ? QThread::idealThreadCount() : m_config.readEntry("maxNumberOfThreads", QThread::idealThreadCount())); } void KisImageConfig::setMaxNumberOfThreads(int value) { if (value == QThread::idealThreadCount()) { m_config.deleteEntry("maxNumberOfThreads"); } else { m_config.writeEntry("maxNumberOfThreads", value); } } int KisImageConfig::frameRenderingClones(bool defaultValue) const { const int defaultClonesCount = qMax(1, maxNumberOfThreads(defaultValue) / 2); return defaultValue ? defaultClonesCount : m_config.readEntry("frameRenderingClones", defaultClonesCount); } void KisImageConfig::setFrameRenderingClones(int value) { m_config.writeEntry("frameRenderingClones", value); } int KisImageConfig::fpsLimit(bool defaultValue) const { - return defaultValue ? 100 : m_config.readEntry("fpsLimit", 100); + int limit = defaultValue ? 100 : m_config.readEntry("fpsLimit", 100); + return limit > 0 ? limit : 1; } void KisImageConfig::setFpsLimit(int value) { m_config.writeEntry("fpsLimit", value); } bool KisImageConfig::useOnDiskAnimationCacheSwapping(bool defaultValue) const { return defaultValue ? true : m_config.readEntry("useOnDiskAnimationCacheSwapping", true); } void KisImageConfig::setUseOnDiskAnimationCacheSwapping(bool value) { m_config.writeEntry("useOnDiskAnimationCacheSwapping", value); } QString KisImageConfig::animationCacheDir(bool defaultValue) const { return safelyGetWritableTempLocation("animation_cache", "animationCacheDir", defaultValue); } void KisImageConfig::setAnimationCacheDir(const QString &value) { m_config.writeEntry("animationCacheDir", value); } bool KisImageConfig::useAnimationCacheFrameSizeLimit(bool defaultValue) const { return defaultValue ? true : m_config.readEntry("useAnimationCacheFrameSizeLimit", true); } void KisImageConfig::setUseAnimationCacheFrameSizeLimit(bool value) { m_config.writeEntry("useAnimationCacheFrameSizeLimit", value); } int KisImageConfig::animationCacheFrameSizeLimit(bool defaultValue) const { return defaultValue ? 2500 : m_config.readEntry("animationCacheFrameSizeLimit", 2500); } void KisImageConfig::setAnimationCacheFrameSizeLimit(int value) { m_config.writeEntry("animationCacheFrameSizeLimit", value); } bool KisImageConfig::useAnimationCacheRegionOfInterest(bool defaultValue) const { return defaultValue ? true : m_config.readEntry("useAnimationCacheRegionOfInterest", true); } void KisImageConfig::setUseAnimationCacheRegionOfInterest(bool value) { m_config.writeEntry("useAnimationCacheRegionOfInterest", value); } qreal KisImageConfig::animationCacheRegionOfInterestMargin(bool defaultValue) const { return defaultValue ? 0.25 : m_config.readEntry("animationCacheRegionOfInterestMargin", 0.25); } void KisImageConfig::setAnimationCacheRegionOfInterestMargin(qreal value) { m_config.writeEntry("animationCacheRegionOfInterestMargin", value); } QColor KisImageConfig::selectionOverlayMaskColor(bool defaultValue) const { QColor def(255, 0, 0, 128); return (defaultValue ? def : m_config.readEntry("selectionOverlayMaskColor", def)); } void KisImageConfig::setSelectionOverlayMaskColor(const QColor &color) { m_config.writeEntry("selectionOverlayMaskColor", color); } diff --git a/libs/image/kis_layer_utils.cpp b/libs/image/kis_layer_utils.cpp index 5ffd9b7446..ec1d49d91c 100644 --- a/libs/image/kis_layer_utils.cpp +++ b/libs/image/kis_layer_utils.cpp @@ -1,1559 +1,1571 @@ /* * 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_layer_utils.h" #include #include #include #include #include "kis_painter.h" #include "kis_image.h" #include "kis_node.h" #include "kis_layer.h" #include "kis_paint_layer.h" #include "kis_clone_layer.h" #include "kis_group_layer.h" #include "kis_selection.h" #include "kis_selection_mask.h" #include "kis_meta_data_merge_strategy.h" #include #include "commands/kis_image_layer_add_command.h" #include "commands/kis_image_layer_remove_command.h" #include "commands/kis_image_layer_move_command.h" #include "commands/kis_image_change_layers_command.h" #include "commands_new/kis_activate_selection_mask_command.h" #include "commands/kis_image_change_visibility_command.h" #include "kis_abstract_projection_plane.h" #include "kis_processing_applicator.h" #include "kis_image_animation_interface.h" #include "kis_keyframe_channel.h" #include "kis_command_utils.h" #include "commands_new/kis_change_projection_color_command.h" #include "kis_layer_properties_icons.h" #include "lazybrush/kis_colorize_mask.h" #include "commands/kis_node_property_list_command.h" #include "commands/kis_node_compositeop_command.h" #include #include "krita_utils.h" #include "kis_image_signal_router.h" namespace KisLayerUtils { void fetchSelectionMasks(KisNodeList mergedNodes, QVector &selectionMasks) { foreach (KisNodeSP node, mergedNodes) { Q_FOREACH(KisNodeSP child, node->childNodes(QStringList("KisSelectionMask"), KoProperties())) { KisSelectionMaskSP mask = qobject_cast(child.data()); if (mask) { selectionMasks.append(mask); } } } } struct MergeDownInfoBase { MergeDownInfoBase(KisImageSP _image) : image(_image), storage(new SwitchFrameCommand::SharedStorage()) { } virtual ~MergeDownInfoBase() {} KisImageWSP image; QVector selectionMasks; KisNodeSP dstNode; SwitchFrameCommand::SharedStorageSP storage; QSet frames; bool useInTimeline = false; bool enableOnionSkins = false; virtual KisNodeList allSrcNodes() = 0; KisLayerSP dstLayer() { return qobject_cast(dstNode.data()); } }; struct MergeDownInfo : public MergeDownInfoBase { MergeDownInfo(KisImageSP _image, KisLayerSP _prevLayer, KisLayerSP _currLayer) : MergeDownInfoBase(_image), prevLayer(_prevLayer), currLayer(_currLayer) { frames = fetchLayerFramesRecursive(prevLayer) | fetchLayerFramesRecursive(currLayer); useInTimeline = prevLayer->useInTimeline() || currLayer->useInTimeline(); const KisPaintLayer *paintLayer = qobject_cast(currLayer.data()); if (paintLayer) enableOnionSkins |= paintLayer->onionSkinEnabled(); paintLayer = qobject_cast(prevLayer.data()); if (paintLayer) enableOnionSkins |= paintLayer->onionSkinEnabled(); } KisLayerSP prevLayer; KisLayerSP currLayer; KisNodeList allSrcNodes() override { KisNodeList mergedNodes; mergedNodes << currLayer; mergedNodes << prevLayer; return mergedNodes; } }; struct MergeMultipleInfo : public MergeDownInfoBase { MergeMultipleInfo(KisImageSP _image, KisNodeList _mergedNodes) : MergeDownInfoBase(_image), mergedNodes(_mergedNodes) { foreach (KisNodeSP node, mergedNodes) { frames |= fetchLayerFramesRecursive(node); useInTimeline |= node->useInTimeline(); const KisPaintLayer *paintLayer = qobject_cast(node.data()); if (paintLayer) { enableOnionSkins |= paintLayer->onionSkinEnabled(); } } } KisNodeList mergedNodes; bool nodesCompositingVaries = false; KisNodeList allSrcNodes() override { return mergedNodes; } }; typedef QSharedPointer MergeDownInfoBaseSP; typedef QSharedPointer MergeDownInfoSP; typedef QSharedPointer MergeMultipleInfoSP; struct FillSelectionMasks : public KUndo2Command { FillSelectionMasks(MergeDownInfoBaseSP info) : m_info(info) {} void redo() override { fetchSelectionMasks(m_info->allSrcNodes(), m_info->selectionMasks); } private: MergeDownInfoBaseSP m_info; }; struct DisableColorizeKeyStrokes : public KisCommandUtils::AggregateCommand { DisableColorizeKeyStrokes(MergeDownInfoBaseSP info) : m_info(info) {} void populateChildCommands() override { Q_FOREACH (KisNodeSP node, m_info->allSrcNodes()) { recursiveApplyNodes(node, [this] (KisNodeSP node) { if (dynamic_cast(node.data()) && KisLayerPropertiesIcons::nodeProperty(node, KisLayerPropertiesIcons::colorizeEditKeyStrokes, true).toBool()) { KisBaseNode::PropertyList props = node->sectionModelProperties(); KisLayerPropertiesIcons::setNodeProperty(&props, KisLayerPropertiesIcons::colorizeEditKeyStrokes, false); addCommand(new KisNodePropertyListCommand(node, props)); } }); } } private: MergeDownInfoBaseSP m_info; }; struct DisableOnionSkins : public KisCommandUtils::AggregateCommand { DisableOnionSkins(MergeDownInfoBaseSP info) : m_info(info) {} void populateChildCommands() override { Q_FOREACH (KisNodeSP node, m_info->allSrcNodes()) { recursiveApplyNodes(node, [this] (KisNodeSP node) { if (KisLayerPropertiesIcons::nodeProperty(node, KisLayerPropertiesIcons::onionSkins, false).toBool()) { KisBaseNode::PropertyList props = node->sectionModelProperties(); KisLayerPropertiesIcons::setNodeProperty(&props, KisLayerPropertiesIcons::onionSkins, false); addCommand(new KisNodePropertyListCommand(node, props)); } }); } } private: MergeDownInfoBaseSP m_info; }; struct DisableExtraCompositing : public KisCommandUtils::AggregateCommand { DisableExtraCompositing(MergeMultipleInfoSP info) : m_info(info) {} void populateChildCommands() override { /** * We disable extra compositing only in case all the layers have * the same compositing properties, therefore, we can just sum them using * Normal blend mode */ if (m_info->nodesCompositingVaries) return; // we should disable dirty requests on **redo only**, otherwise // the state of the layers will not be recovered on undo m_info->image->disableDirtyRequests(); Q_FOREACH (KisNodeSP node, m_info->allSrcNodes()) { if (node->compositeOpId() != COMPOSITE_OVER) { addCommand(new KisNodeCompositeOpCommand(node, node->compositeOpId(), COMPOSITE_OVER)); } if (KisLayerPropertiesIcons::nodeProperty(node, KisLayerPropertiesIcons::inheritAlpha, false).toBool()) { KisBaseNode::PropertyList props = node->sectionModelProperties(); KisLayerPropertiesIcons::setNodeProperty(&props, KisLayerPropertiesIcons::inheritAlpha, false); addCommand(new KisNodePropertyListCommand(node, props)); } } m_info->image->enableDirtyRequests(); } private: MergeMultipleInfoSP m_info; }; struct DisablePassThroughForHeadsOnly : public KisCommandUtils::AggregateCommand { DisablePassThroughForHeadsOnly(MergeDownInfoBaseSP info, bool skipIfDstIsGroup = false) : m_info(info), m_skipIfDstIsGroup(skipIfDstIsGroup) { } void populateChildCommands() override { if (m_skipIfDstIsGroup && m_info->dstLayer() && m_info->dstLayer()->inherits("KisGroupLayer")) { return; } Q_FOREACH (KisNodeSP node, m_info->allSrcNodes()) { if (KisLayerPropertiesIcons::nodeProperty(node, KisLayerPropertiesIcons::passThrough, false).toBool()) { KisBaseNode::PropertyList props = node->sectionModelProperties(); KisLayerPropertiesIcons::setNodeProperty(&props, KisLayerPropertiesIcons::passThrough, false); addCommand(new KisNodePropertyListCommand(node, props)); } } } private: MergeDownInfoBaseSP m_info; bool m_skipIfDstIsGroup; }; struct RefreshHiddenAreas : public KUndo2Command { RefreshHiddenAreas(MergeDownInfoBaseSP info) : m_info(info) {} void redo() override { KisImageAnimationInterface *interface = m_info->image->animationInterface(); const QRect preparedRect = !interface->externalFrameActive() ? m_info->image->bounds() : QRect(); foreach (KisNodeSP node, m_info->allSrcNodes()) { refreshHiddenAreaAsync(node, preparedRect); } } private: QRect realNodeExactBounds(KisNodeSP rootNode, QRect currentRect = QRect()) { KisNodeSP node = rootNode->firstChild(); while(node) { currentRect |= realNodeExactBounds(node, currentRect); node = node->nextSibling(); } if (!rootNode->isFakeNode()) { // TODO: it would be better to count up changeRect inside // node's extent() method currentRect |= rootNode->projectionPlane()->changeRect(rootNode->exactBounds()); } return currentRect; } void refreshHiddenAreaAsync(KisNodeSP rootNode, const QRect &preparedArea) { QRect realNodeRect = realNodeExactBounds(rootNode); if (!preparedArea.contains(realNodeRect)) { QRegion dirtyRegion = realNodeRect; dirtyRegion -= preparedArea; foreach(const QRect &rc, dirtyRegion.rects()) { m_info->image->refreshGraphAsync(rootNode, rc, realNodeRect); } } } private: MergeDownInfoBaseSP m_info; }; struct RefreshDelayedUpdateLayers : public KUndo2Command { RefreshDelayedUpdateLayers(MergeDownInfoBaseSP info) : m_info(info) {} void redo() override { foreach (KisNodeSP node, m_info->allSrcNodes()) { forceAllDelayedNodesUpdate(node); } } private: MergeDownInfoBaseSP m_info; }; struct KeepMergedNodesSelected : public KisCommandUtils::AggregateCommand { KeepMergedNodesSelected(MergeDownInfoSP info, bool finalizing) : m_singleInfo(info), m_finalizing(finalizing) {} KeepMergedNodesSelected(MergeMultipleInfoSP info, KisNodeSP putAfter, bool finalizing) : m_multipleInfo(info), m_finalizing(finalizing), m_putAfter(putAfter) {} void populateChildCommands() override { KisNodeSP prevNode; KisNodeSP nextNode; KisNodeList prevSelection; KisNodeList nextSelection; KisImageSP image; if (m_singleInfo) { prevNode = m_singleInfo->currLayer; nextNode = m_singleInfo->dstNode; image = m_singleInfo->image; } else if (m_multipleInfo) { prevNode = m_putAfter; nextNode = m_multipleInfo->dstNode; prevSelection = m_multipleInfo->allSrcNodes(); image = m_multipleInfo->image; } if (!m_finalizing) { addCommand(new KeepNodesSelectedCommand(prevSelection, KisNodeList(), prevNode, KisNodeSP(), image, false)); } else { addCommand(new KeepNodesSelectedCommand(KisNodeList(), nextSelection, KisNodeSP(), nextNode, image, true)); } } private: MergeDownInfoSP m_singleInfo; MergeMultipleInfoSP m_multipleInfo; bool m_finalizing; KisNodeSP m_putAfter; }; struct CreateMergedLayer : public KisCommandUtils::AggregateCommand { CreateMergedLayer(MergeDownInfoSP info) : m_info(info) {} void populateChildCommands() override { // actual merging done by KisLayer::createMergedLayer (or specialized descendant) m_info->dstNode = m_info->currLayer->createMergedLayerTemplate(m_info->prevLayer); if (m_info->frames.size() > 0) { m_info->dstNode->enableAnimation(); m_info->dstNode->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); } m_info->dstNode->setUseInTimeline(m_info->useInTimeline); KisPaintLayer *dstPaintLayer = qobject_cast(m_info->dstNode.data()); if (dstPaintLayer) { dstPaintLayer->setOnionSkinEnabled(m_info->enableOnionSkins); } } private: MergeDownInfoSP m_info; }; struct CreateMergedLayerMultiple : public KisCommandUtils::AggregateCommand { CreateMergedLayerMultiple(MergeMultipleInfoSP info, const QString name = QString() ) : m_info(info), m_name(name) {} void populateChildCommands() override { QString mergedLayerName; if (m_name.isEmpty()){ const QString mergedLayerSuffix = i18n("Merged"); mergedLayerName = m_info->mergedNodes.first()->name(); if (!mergedLayerName.endsWith(mergedLayerSuffix)) { mergedLayerName = QString("%1 %2") .arg(mergedLayerName).arg(mergedLayerSuffix); } } else { mergedLayerName = m_name; } KisPaintLayer *dstPaintLayer = new KisPaintLayer(m_info->image, mergedLayerName, OPACITY_OPAQUE_U8); m_info->dstNode = dstPaintLayer; if (m_info->frames.size() > 0) { m_info->dstNode->enableAnimation(); m_info->dstNode->getKeyframeChannel(KisKeyframeChannel::Content.id(), true); } auto channelFlagsLazy = [](KisNodeSP node) { KisLayer *layer = dynamic_cast(node.data()); return layer ? layer->channelFlags() : QBitArray(); }; QString compositeOpId; QBitArray channelFlags; bool compositionVaries = false; bool isFirstCycle = true; foreach (KisNodeSP node, m_info->allSrcNodes()) { if (isFirstCycle) { compositeOpId = node->compositeOpId(); channelFlags = channelFlagsLazy(node); isFirstCycle = false; } else if (compositeOpId != node->compositeOpId() || channelFlags != channelFlagsLazy(node)) { compositionVaries = true; break; } KisLayerSP layer = qobject_cast(node.data()); if (layer && layer->layerStyle()) { compositionVaries = true; break; } } if (!compositionVaries) { if (!compositeOpId.isEmpty()) { m_info->dstNode->setCompositeOpId(compositeOpId); } if (m_info->dstLayer() && !channelFlags.isEmpty()) { m_info->dstLayer()->setChannelFlags(channelFlags); } } m_info->nodesCompositingVaries = compositionVaries; m_info->dstNode->setUseInTimeline(m_info->useInTimeline); dstPaintLayer->setOnionSkinEnabled(m_info->enableOnionSkins); } private: MergeMultipleInfoSP m_info; QString m_name; }; struct MergeLayers : public KisCommandUtils::AggregateCommand { MergeLayers(MergeDownInfoSP info) : m_info(info) {} void populateChildCommands() override { // actual merging done by KisLayer::createMergedLayer (or specialized descendant) m_info->currLayer->fillMergedLayerTemplate(m_info->dstLayer(), m_info->prevLayer); } private: MergeDownInfoSP m_info; }; struct MergeLayersMultiple : public KisCommandUtils::AggregateCommand { MergeLayersMultiple(MergeMultipleInfoSP info) : m_info(info) {} void populateChildCommands() override { KisPainter gc(m_info->dstNode->paintDevice()); foreach (KisNodeSP node, m_info->allSrcNodes()) { QRect rc = node->exactBounds() | m_info->image->bounds(); node->projectionPlane()->apply(&gc, rc); } } private: MergeMultipleInfoSP m_info; }; struct MergeMetaData : public KUndo2Command { MergeMetaData(MergeDownInfoSP info, const KisMetaData::MergeStrategy* strategy) : m_info(info), m_strategy(strategy) {} void redo() override { QRect layerProjectionExtent = m_info->currLayer->projection()->extent(); QRect prevLayerProjectionExtent = m_info->prevLayer->projection()->extent(); int prevLayerArea = prevLayerProjectionExtent.width() * prevLayerProjectionExtent.height(); int layerArea = layerProjectionExtent.width() * layerProjectionExtent.height(); QList scores; double norm = qMax(prevLayerArea, layerArea); scores.append(prevLayerArea / norm); scores.append(layerArea / norm); QList srcs; srcs.append(m_info->prevLayer->metaData()); srcs.append(m_info->currLayer->metaData()); m_strategy->merge(m_info->dstLayer()->metaData(), srcs, scores); } private: MergeDownInfoSP m_info; const KisMetaData::MergeStrategy *m_strategy; }; KeepNodesSelectedCommand::KeepNodesSelectedCommand(const KisNodeList &selectedBefore, const KisNodeList &selectedAfter, KisNodeSP activeBefore, KisNodeSP activeAfter, KisImageSP image, bool finalize, KUndo2Command *parent) : FlipFlopCommand(finalize, parent), m_selectedBefore(selectedBefore), m_selectedAfter(selectedAfter), m_activeBefore(activeBefore), m_activeAfter(activeAfter), m_image(image) { } void KeepNodesSelectedCommand::partB() { KisImageSignalType type; if (getState() == State::FINALIZING) { type = ComplexNodeReselectionSignal(m_activeAfter, m_selectedAfter); } else { type = ComplexNodeReselectionSignal(m_activeBefore, m_selectedBefore); } m_image->signalRouter()->emitNotification(type); } SelectGlobalSelectionMask::SelectGlobalSelectionMask(KisImageSP image) : m_image(image) { } void SelectGlobalSelectionMask::redo() { KisImageSignalType type = ComplexNodeReselectionSignal(m_image->rootLayer()->selectionMask(), KisNodeList()); m_image->signalRouter()->emitNotification(type); } RemoveNodeHelper::~RemoveNodeHelper() { } /** * The removal of two nodes in one go may be a bit tricky, because one * of them may be the clone of another. If we remove the source of a * clone layer, it will reincarnate into a paint layer. In this case * the pointer to the second layer will be lost. * * That's why we need to care about the order of the nodes removal: * the clone --- first, the source --- last. */ void RemoveNodeHelper::safeRemoveMultipleNodes(KisNodeList nodes, KisImageSP image) { const bool lastLayer = scanForLastLayer(image, nodes); auto isNodeWeird = [] (KisNodeSP node) { const bool normalCompositeMode = node->compositeOpId() == COMPOSITE_OVER; KisLayer *layer = dynamic_cast(node.data()); const bool hasInheritAlpha = layer && layer->alphaChannelDisabled(); return !normalCompositeMode && !hasInheritAlpha; }; while (!nodes.isEmpty()) { KisNodeList::iterator it = nodes.begin(); while (it != nodes.end()) { if (!checkIsSourceForClone(*it, nodes)) { KisNodeSP node = *it; addCommandImpl(new KisImageLayerRemoveCommand(image, node, !isNodeWeird(node), true)); it = nodes.erase(it); } else { ++it; } } } if (lastLayer) { KisLayerSP newLayer = new KisPaintLayer(image.data(), image->nextLayerName(), OPACITY_OPAQUE_U8, image->colorSpace()); addCommandImpl(new KisImageLayerAddCommand(image, newLayer, image->root(), KisNodeSP(), false, false)); } } bool RemoveNodeHelper::checkIsSourceForClone(KisNodeSP src, const KisNodeList &nodes) { foreach (KisNodeSP node, nodes) { if (node == src) continue; KisCloneLayer *clone = dynamic_cast(node.data()); if (clone && KisNodeSP(clone->copyFrom()) == src) { return true; } } return false; } bool RemoveNodeHelper::scanForLastLayer(KisImageWSP image, KisNodeList nodesToRemove) { bool removeLayers = false; Q_FOREACH(KisNodeSP nodeToRemove, nodesToRemove) { if (qobject_cast(nodeToRemove.data())) { removeLayers = true; break; } } if (!removeLayers) return false; bool lastLayer = true; KisNodeSP node = image->root()->firstChild(); while (node) { if (!nodesToRemove.contains(node) && qobject_cast(node.data()) && !node->isFakeNode()) { lastLayer = false; break; } node = node->nextSibling(); } return lastLayer; } SimpleRemoveLayers::SimpleRemoveLayers(const KisNodeList &nodes, KisImageSP image) : m_nodes(nodes), m_image(image) { } void SimpleRemoveLayers::populateChildCommands() { if (m_nodes.isEmpty()) return; safeRemoveMultipleNodes(m_nodes, m_image); } void SimpleRemoveLayers::addCommandImpl(KUndo2Command *cmd) { addCommand(cmd); } struct InsertNode : public KisCommandUtils::AggregateCommand { InsertNode(MergeDownInfoBaseSP info, KisNodeSP putAfter) : m_info(info), m_putAfter(putAfter) {} void populateChildCommands() override { addCommand(new KisImageLayerAddCommand(m_info->image, m_info->dstNode, m_putAfter->parent(), m_putAfter, true, false)); } private: virtual void addCommandImpl(KUndo2Command *cmd) { addCommand(cmd); } private: MergeDownInfoBaseSP m_info; KisNodeSP m_putAfter; }; struct CleanUpNodes : private RemoveNodeHelper, public KisCommandUtils::AggregateCommand { CleanUpNodes(MergeDownInfoBaseSP info, KisNodeSP putAfter) : m_info(info), m_putAfter(putAfter) {} static void findPerfectParent(KisNodeList nodesToDelete, KisNodeSP &putAfter, KisNodeSP &parent) { if (!putAfter) { putAfter = nodesToDelete.last(); } // Add the new merged node on top of the active node // -- checking all parents if they are included in nodesToDelete // Not every descendant is included in nodesToDelete even if in fact // they are going to be deleted, so we need to check it. // If we consider the path from root to the putAfter node, // if there are any nodes marked for deletion, any node afterwards // is going to be deleted, too. // example: root . . . . . ! ! . . ! ! ! ! . . . . putAfter // it should be: root . . . . . ! ! ! ! ! ! ! ! ! ! ! ! !putAfter // and here: root . . . . X ! ! . . ! ! ! ! . . . . putAfter // you can see which node is "the perfect ancestor" // (marked X; called "parent" in the function arguments). // and here: root . . . . . O ! . . ! ! ! ! . . . . putAfter // you can see which node is "the topmost deleted ancestor" (marked 'O') KisNodeSP node = putAfter->parent(); bool foundDeletedAncestor = false; KisNodeSP topmostAncestorToDelete = nullptr; while (node) { if (nodesToDelete.contains(node) && !nodesToDelete.contains(node->parent())) { foundDeletedAncestor = true; topmostAncestorToDelete = node; // Here node is to be deleted and its parent is not, // so its parent is the one of the first not deleted (="perfect") ancestors. // We need the one that is closest to the top (root) } node = node->parent(); } if (foundDeletedAncestor) { parent = topmostAncestorToDelete->parent(); putAfter = topmostAncestorToDelete; } else { parent = putAfter->parent(); // putAfter (and none of its ancestors) is to be deleted, so its parent is the first not deleted ancestor } } void populateChildCommands() override { KisNodeList nodesToDelete = m_info->allSrcNodes(); KisNodeSP parent; findPerfectParent(nodesToDelete, m_putAfter, parent); if (!parent) { KisNodeSP oldRoot = m_info->image->root(); KisNodeSP newRoot(new KisGroupLayer(m_info->image, "root", OPACITY_OPAQUE_U8)); + // copy all fake nodes into the new image + KisLayerUtils::recursiveApplyNodes(oldRoot, [this, oldRoot, newRoot] (KisNodeSP node) { + if (node->isFakeNode() && node->parent() == oldRoot) { + addCommand(new KisImageLayerAddCommand(m_info->image, + node->clone(), + newRoot, + KisNodeSP(), + false, false)); + + } + }); + addCommand(new KisImageLayerAddCommand(m_info->image, m_info->dstNode, newRoot, KisNodeSP(), true, false)); addCommand(new KisImageChangeLayersCommand(m_info->image, oldRoot, newRoot)); } else { addCommand(new KisImageLayerAddCommand(m_info->image, m_info->dstNode, parent, m_putAfter, true, false)); /** * We can merge selection masks, in this case dstLayer is not defined! */ if (m_info->dstLayer()) { reparentSelectionMasks(m_info->image, m_info->dstLayer(), m_info->selectionMasks); } KisNodeList safeNodesToDelete = m_info->allSrcNodes(); for (KisNodeList::iterator it = safeNodesToDelete.begin(); it != safeNodesToDelete.end(); ++it) { KisNodeSP node = *it; if (node->userLocked() && node->visible()) { addCommand(new KisImageChangeVisibilityCommand(false, node)); } } KritaUtils::filterContainer(safeNodesToDelete, [](KisNodeSP node) { return !node->userLocked(); }); safeRemoveMultipleNodes(safeNodesToDelete, m_info->image); } } private: void addCommandImpl(KUndo2Command *cmd) override { addCommand(cmd); } void reparentSelectionMasks(KisImageSP image, KisLayerSP newLayer, const QVector &selectionMasks) { KIS_SAFE_ASSERT_RECOVER_RETURN(newLayer); foreach (KisSelectionMaskSP mask, selectionMasks) { addCommand(new KisImageLayerMoveCommand(image, mask, newLayer, newLayer->lastChild())); addCommand(new KisActivateSelectionMaskCommand(mask, false)); } } private: MergeDownInfoBaseSP m_info; KisNodeSP m_putAfter; }; SwitchFrameCommand::SharedStorage::~SharedStorage() { } SwitchFrameCommand::SwitchFrameCommand(KisImageSP image, int time, bool finalize, SharedStorageSP storage) : FlipFlopCommand(finalize), m_image(image), m_newTime(time), m_storage(storage) {} SwitchFrameCommand::~SwitchFrameCommand() {} void SwitchFrameCommand::partA() { KisImageAnimationInterface *interface = m_image->animationInterface(); const int currentTime = interface->currentTime(); if (currentTime == m_newTime) { m_storage->value = m_newTime; return; } interface->image()->disableUIUpdates(); interface->saveAndResetCurrentTime(m_newTime, &m_storage->value); } void SwitchFrameCommand::partB() { KisImageAnimationInterface *interface = m_image->animationInterface(); const int currentTime = interface->currentTime(); if (currentTime == m_storage->value) { return; } interface->restoreCurrentTime(&m_storage->value); interface->image()->enableUIUpdates(); } struct AddNewFrame : public KisCommandUtils::AggregateCommand { AddNewFrame(MergeDownInfoBaseSP info, int frame) : m_info(info), m_frame(frame) {} void populateChildCommands() override { KUndo2Command *cmd = new KisCommandUtils::SkipFirstRedoWrapper(); KisKeyframeChannel *channel = m_info->dstNode->getKeyframeChannel(KisKeyframeChannel::Content.id()); KisKeyframeSP keyframe = channel->addKeyframe(m_frame, cmd); applyKeyframeColorLabel(keyframe); addCommand(cmd); } void applyKeyframeColorLabel(KisKeyframeSP dstKeyframe) { Q_FOREACH(KisNodeSP srcNode, m_info->allSrcNodes()) { Q_FOREACH(KisKeyframeChannel *channel, srcNode->keyframeChannels().values()) { KisKeyframeSP keyframe = channel->keyframeAt(m_frame); if (!keyframe.isNull() && keyframe->colorLabel() != 0) { dstKeyframe->setColorLabel(keyframe->colorLabel()); return; } } } dstKeyframe->setColorLabel(0); } private: MergeDownInfoBaseSP m_info; int m_frame; }; QSet fetchLayerFrames(KisNodeSP node) { KisKeyframeChannel *channel = node->getKeyframeChannel(KisKeyframeChannel::Content.id()); if (!channel) return QSet(); return channel->allKeyframeIds(); } QSet fetchLayerFramesRecursive(KisNodeSP rootNode) { QSet frames = fetchLayerFrames(rootNode); KisNodeSP node = rootNode->firstChild(); while(node) { frames |= fetchLayerFramesRecursive(node); node = node->nextSibling(); } return frames; } void updateFrameJobs(FrameJobs *jobs, KisNodeSP node) { QSet frames = fetchLayerFrames(node); if (frames.isEmpty()) { (*jobs)[0].insert(node); } else { foreach (int frame, frames) { (*jobs)[frame].insert(node); } } } void updateFrameJobsRecursive(FrameJobs *jobs, KisNodeSP rootNode) { updateFrameJobs(jobs, rootNode); KisNodeSP node = rootNode->firstChild(); while(node) { updateFrameJobsRecursive(jobs, node); node = node->nextSibling(); } } void mergeDown(KisImageSP image, KisLayerSP layer, const KisMetaData::MergeStrategy* strategy) { if (!layer->prevSibling()) return; // XXX: this breaks if we allow free mixing of masks and layers KisLayerSP prevLayer = qobject_cast(layer->prevSibling().data()); if (!prevLayer) return; if (!layer->visible() && !prevLayer->visible()) { return; } KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisProcessingApplicator applicator(image, 0, KisProcessingApplicator::NONE, emitSignals, kundo2_i18n("Merge Down")); if (layer->visible() && prevLayer->visible()) { MergeDownInfoSP info(new MergeDownInfo(image, prevLayer, layer)); // disable key strokes on all colorize masks, all onion skins on // paint layers and wait until update is finished with a barrier applicator.applyCommand(new DisableColorizeKeyStrokes(info)); applicator.applyCommand(new DisableOnionSkins(info)); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); applicator.applyCommand(new KeepMergedNodesSelected(info, false)); applicator.applyCommand(new FillSelectionMasks(info)); applicator.applyCommand(new CreateMergedLayer(info), KisStrokeJobData::BARRIER); // NOTE: shape layer may have emitted spontaneous jobs during layer creation, // wait for them to complete! applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); // in two-layer mode we disable pass through only when the destination layer // is not a group layer applicator.applyCommand(new DisablePassThroughForHeadsOnly(info, true)); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); if (info->frames.size() > 0) { foreach (int frame, info->frames) { applicator.applyCommand(new SwitchFrameCommand(info->image, frame, false, info->storage)); applicator.applyCommand(new AddNewFrame(info, frame)); applicator.applyCommand(new RefreshHiddenAreas(info)); applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new MergeLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new SwitchFrameCommand(info->image, frame, true, info->storage)); } } else { applicator.applyCommand(new RefreshHiddenAreas(info)); applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new MergeLayers(info), KisStrokeJobData::BARRIER); } applicator.applyCommand(new MergeMetaData(info, strategy), KisStrokeJobData::BARRIER); applicator.applyCommand(new CleanUpNodes(info, layer), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new KeepMergedNodesSelected(info, true)); } else if (layer->visible()) { applicator.applyCommand(new KeepNodesSelectedCommand(KisNodeList(), KisNodeList(), layer, KisNodeSP(), image, false)); applicator.applyCommand( new SimpleRemoveLayers(KisNodeList() << prevLayer, image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new KeepNodesSelectedCommand(KisNodeList(), KisNodeList(), KisNodeSP(), layer, image, true)); } else if (prevLayer->visible()) { applicator.applyCommand(new KeepNodesSelectedCommand(KisNodeList(), KisNodeList(), layer, KisNodeSP(), image, false)); applicator.applyCommand( new SimpleRemoveLayers(KisNodeList() << layer, image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new KeepNodesSelectedCommand(KisNodeList(), KisNodeList(), KisNodeSP(), prevLayer, image, true)); } applicator.end(); } bool checkIsChildOf(KisNodeSP node, const KisNodeList &parents) { KisNodeList nodeParents; KisNodeSP parent = node->parent(); while (parent) { nodeParents << parent; parent = parent->parent(); } foreach(KisNodeSP perspectiveParent, parents) { if (nodeParents.contains(perspectiveParent)) { return true; } } return false; } bool checkIsCloneOf(KisNodeSP node, const KisNodeList &nodes) { bool result = false; KisCloneLayer *clone = dynamic_cast(node.data()); if (clone) { KisNodeSP cloneSource = KisNodeSP(clone->copyFrom()); Q_FOREACH(KisNodeSP subtree, nodes) { result = recursiveFindNode(subtree, [cloneSource](KisNodeSP node) -> bool { return node == cloneSource; }); if (!result) { result = checkIsCloneOf(cloneSource, nodes); } if (result) { break; } } } return result; } void filterMergableNodes(KisNodeList &nodes, bool allowMasks) { KisNodeList::iterator it = nodes.begin(); while (it != nodes.end()) { if ((!allowMasks && !qobject_cast(it->data())) || checkIsChildOf(*it, nodes)) { //qDebug() << "Skipping node" << ppVar((*it)->name()); it = nodes.erase(it); } else { ++it; } } } void sortMergableNodes(KisNodeSP root, KisNodeList &inputNodes, KisNodeList &outputNodes) { KisNodeList::iterator it = std::find(inputNodes.begin(), inputNodes.end(), root); if (it != inputNodes.end()) { outputNodes << *it; inputNodes.erase(it); } if (inputNodes.isEmpty()) { return; } KisNodeSP child = root->firstChild(); while (child) { sortMergableNodes(child, inputNodes, outputNodes); child = child->nextSibling(); } /** * By the end of recursion \p inputNodes must be empty */ KIS_ASSERT_RECOVER_NOOP(root->parent() || inputNodes.isEmpty()); } KisNodeList sortMergableNodes(KisNodeSP root, KisNodeList nodes) { KisNodeList result; sortMergableNodes(root, nodes, result); return result; } KisNodeList sortAndFilterMergableInternalNodes(KisNodeList nodes, bool allowMasks) { KIS_ASSERT_RECOVER(!nodes.isEmpty()) { return nodes; } KisNodeSP root; Q_FOREACH(KisNodeSP node, nodes) { KisNodeSP localRoot = node; while (localRoot->parent()) { localRoot = localRoot->parent(); } if (!root) { root = localRoot; } KIS_ASSERT_RECOVER(root == localRoot) { return nodes; } } KisNodeList result; sortMergableNodes(root, nodes, result); filterMergableNodes(result, allowMasks); return result; } void addCopyOfNameTag(KisNodeSP node) { const QString prefix = i18n("Copy of"); QString newName = node->name(); if (!newName.startsWith(prefix)) { newName = QString("%1 %2").arg(prefix).arg(newName); node->setName(newName); } } KisNodeList findNodesWithProps(KisNodeSP root, const KoProperties &props, bool excludeRoot) { KisNodeList nodes; if ((!excludeRoot || root->parent()) && root->check(props)) { nodes << root; } KisNodeSP node = root->firstChild(); while (node) { nodes += findNodesWithProps(node, props, excludeRoot); node = node->nextSibling(); } return nodes; } KisNodeList filterInvisibleNodes(const KisNodeList &nodes, KisNodeList *invisibleNodes, KisNodeSP *putAfter) { KIS_ASSERT_RECOVER(invisibleNodes) { return nodes; } KIS_ASSERT_RECOVER(putAfter) { return nodes; } KisNodeList visibleNodes; int putAfterIndex = -1; Q_FOREACH(KisNodeSP node, nodes) { if (node->visible() || node->userLocked()) { visibleNodes << node; } else { *invisibleNodes << node; if (node == *putAfter) { putAfterIndex = visibleNodes.size() - 1; } } } if (!visibleNodes.isEmpty() && putAfterIndex >= 0) { putAfterIndex = qBound(0, putAfterIndex, visibleNodes.size() - 1); *putAfter = visibleNodes[putAfterIndex]; } return visibleNodes; } void filterUnlockedNodes(KisNodeList &nodes) { KisNodeList::iterator it = nodes.begin(); while (it != nodes.end()) { if ((*it)->userLocked()) { it = nodes.erase(it); } else { ++it; } } } void changeImageDefaultProjectionColor(KisImageSP image, const KoColor &color) { KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisProcessingApplicator applicator(image, image->root(), KisProcessingApplicator::RECURSIVE, emitSignals, kundo2_i18n("Change projection color"), 0, 142857 + 1); applicator.applyCommand(new KisChangeProjectionColorCommand(image, color), KisStrokeJobData::BARRIER, KisStrokeJobData::EXCLUSIVE); applicator.end(); } void mergeMultipleLayersImpl(KisImageSP image, KisNodeList mergedNodes, KisNodeSP putAfter, bool flattenSingleLayer, const KUndo2MagicString &actionName, bool cleanupNodes = true, const QString layerName = QString()) { if (!putAfter) { putAfter = mergedNodes.first(); } filterMergableNodes(mergedNodes); { KisNodeList tempNodes; std::swap(mergedNodes, tempNodes); sortMergableNodes(image->root(), tempNodes, mergedNodes); } if (mergedNodes.size() <= 1 && (!flattenSingleLayer && mergedNodes.size() == 1)) return; KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; emitSignals << ComplexNodeReselectionSignal(KisNodeSP(), KisNodeList(), KisNodeSP(), mergedNodes); KisProcessingApplicator applicator(image, 0, KisProcessingApplicator::NONE, emitSignals, actionName); KisNodeList originalNodes = mergedNodes; KisNodeList invisibleNodes; mergedNodes = filterInvisibleNodes(originalNodes, &invisibleNodes, &putAfter); if (!invisibleNodes.isEmpty() && !mergedNodes.isEmpty()) { /* If the putAfter node is invisible, * we should instead pick one of the nodes * to be merged to avoid a null putAfter. */ if (!putAfter->visible()){ putAfter = mergedNodes.first(); } applicator.applyCommand( new SimpleRemoveLayers(invisibleNodes, image), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } if (mergedNodes.size() > 1 || invisibleNodes.isEmpty()) { MergeMultipleInfoSP info(new MergeMultipleInfo(image, mergedNodes)); // disable key strokes on all colorize masks, all onion skins on // paint layers and wait until update is finished with a barrier applicator.applyCommand(new DisableColorizeKeyStrokes(info)); applicator.applyCommand(new DisableOnionSkins(info)); applicator.applyCommand(new DisablePassThroughForHeadsOnly(info)); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); applicator.applyCommand(new KeepMergedNodesSelected(info, putAfter, false)); applicator.applyCommand(new FillSelectionMasks(info)); applicator.applyCommand(new CreateMergedLayerMultiple(info, layerName), KisStrokeJobData::BARRIER); applicator.applyCommand(new DisableExtraCompositing(info)); applicator.applyCommand(new KUndo2Command(), KisStrokeJobData::BARRIER); if (!info->frames.isEmpty()) { foreach (int frame, info->frames) { applicator.applyCommand(new SwitchFrameCommand(info->image, frame, false, info->storage)); applicator.applyCommand(new AddNewFrame(info, frame)); applicator.applyCommand(new RefreshHiddenAreas(info)); applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new MergeLayersMultiple(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new SwitchFrameCommand(info->image, frame, true, info->storage)); } } else { applicator.applyCommand(new RefreshHiddenAreas(info)); applicator.applyCommand(new RefreshDelayedUpdateLayers(info), KisStrokeJobData::BARRIER); applicator.applyCommand(new MergeLayersMultiple(info), KisStrokeJobData::BARRIER); } //applicator.applyCommand(new MergeMetaData(info, strategy), KisStrokeJobData::BARRIER); if (cleanupNodes){ applicator.applyCommand(new CleanUpNodes(info, putAfter), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } else { applicator.applyCommand(new InsertNode(info, putAfter), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); } applicator.applyCommand(new KeepMergedNodesSelected(info, putAfter, true)); } applicator.end(); } void mergeMultipleLayers(KisImageSP image, KisNodeList mergedNodes, KisNodeSP putAfter) { mergeMultipleLayersImpl(image, mergedNodes, putAfter, false, kundo2_i18n("Merge Selected Nodes")); } void newLayerFromVisible(KisImageSP image, KisNodeSP putAfter) { KisNodeList mergedNodes; mergedNodes << image->root(); mergeMultipleLayersImpl(image, mergedNodes, putAfter, true, kundo2_i18n("New From Visible"), false, i18nc("New layer created from all the visible layers", "Visible")); } struct MergeSelectionMasks : public KisCommandUtils::AggregateCommand { MergeSelectionMasks(MergeDownInfoBaseSP info, KisNodeSP putAfter) : m_info(info), m_putAfter(putAfter){} void populateChildCommands() override { KisNodeSP parent; CleanUpNodes::findPerfectParent(m_info->allSrcNodes(), m_putAfter, parent); KisLayerSP parentLayer; do { parentLayer = qobject_cast(parent.data()); parent = parent->parent(); } while(!parentLayer && parent); KisSelectionSP selection = new KisSelection(); foreach (KisNodeSP node, m_info->allSrcNodes()) { KisMaskSP mask = dynamic_cast(node.data()); if (!mask) continue; selection->pixelSelection()->applySelection( mask->selection()->pixelSelection(), SELECTION_ADD); } KisSelectionMaskSP mergedMask = new KisSelectionMask(m_info->image); mergedMask->initSelection(parentLayer); mergedMask->setSelection(selection); m_info->dstNode = mergedMask; } private: MergeDownInfoBaseSP m_info; KisNodeSP m_putAfter; }; struct ActivateSelectionMask : public KisCommandUtils::AggregateCommand { ActivateSelectionMask(MergeDownInfoBaseSP info) : m_info(info) {} void populateChildCommands() override { KisSelectionMaskSP mergedMask = dynamic_cast(m_info->dstNode.data()); addCommand(new KisActivateSelectionMaskCommand(mergedMask, true)); } private: MergeDownInfoBaseSP m_info; }; bool tryMergeSelectionMasks(KisImageSP image, KisNodeList mergedNodes, KisNodeSP putAfter) { QList selectionMasks; for (auto it = mergedNodes.begin(); it != mergedNodes.end(); /*noop*/) { KisSelectionMaskSP mask = dynamic_cast(it->data()); if (!mask) { it = mergedNodes.erase(it); } else { selectionMasks.append(mask); ++it; } } if (mergedNodes.isEmpty()) return false; KisLayerSP parentLayer = qobject_cast(selectionMasks.first()->parent().data()); KIS_ASSERT_RECOVER(parentLayer) { return 0; } KisImageSignalVector emitSignals; emitSignals << ModifiedSignal; KisProcessingApplicator applicator(image, 0, KisProcessingApplicator::NONE, emitSignals, kundo2_i18n("Merge Selection Masks")); MergeMultipleInfoSP info(new MergeMultipleInfo(image, mergedNodes)); applicator.applyCommand(new MergeSelectionMasks(info, putAfter)); applicator.applyCommand(new CleanUpNodes(info, putAfter), KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); applicator.applyCommand(new ActivateSelectionMask(info)); applicator.end(); return true; } void flattenLayer(KisImageSP image, KisLayerSP layer) { if (!layer->childCount() && !layer->layerStyle()) return; KisNodeList mergedNodes; mergedNodes << layer; mergeMultipleLayersImpl(image, mergedNodes, layer, true, kundo2_i18n("Flatten Layer")); } void flattenImage(KisImageSP image, KisNodeSP activeNode) { if (!activeNode) { activeNode = image->root()->lastChild(); } KisNodeList mergedNodes; mergedNodes << image->root(); mergeMultipleLayersImpl(image, mergedNodes, activeNode, true, kundo2_i18n("Flatten Image")); } KisSimpleUpdateCommand::KisSimpleUpdateCommand(KisNodeList nodes, bool finalize, KUndo2Command *parent) : FlipFlopCommand(finalize, parent), m_nodes(nodes) { } void KisSimpleUpdateCommand::partB() { updateNodes(m_nodes); } void KisSimpleUpdateCommand::updateNodes(const KisNodeList &nodes) { Q_FOREACH(KisNodeSP node, nodes) { node->setDirty(node->extent()); } } KisNodeSP recursiveFindNode(KisNodeSP node, std::function func) { if (func(node)) { return node; } node = node->firstChild(); while (node) { KisNodeSP resultNode = recursiveFindNode(node, func); if (resultNode) { return resultNode; } node = node->nextSibling(); } return 0; } KisNodeSP findNodeByUuid(KisNodeSP root, const QUuid &uuid) { return recursiveFindNode(root, [uuid] (KisNodeSP node) { return node->uuid() == uuid; }); } void forceAllDelayedNodesUpdate(KisNodeSP root) { KisLayerUtils::recursiveApplyNodes(root, [] (KisNodeSP node) { KisDelayedUpdateNodeInterface *delayedUpdate = dynamic_cast(node.data()); if (delayedUpdate) { delayedUpdate->forceUpdateTimedNode(); } }); } bool hasDelayedNodeWithUpdates(KisNodeSP root) { return recursiveFindNode(root, [] (KisNodeSP node) { KisDelayedUpdateNodeInterface *delayedUpdate = dynamic_cast(node.data()); return delayedUpdate ? delayedUpdate->hasPendingTimedUpdates() : false; }); } KisImageSP findImageByHierarchy(KisNodeSP node) { while (node) { const KisLayer *layer = dynamic_cast(node.data()); if (layer) { return layer->image(); } node = node->parent(); } return 0; } } diff --git a/libs/image/kis_onion_skin_cache.cpp b/libs/image/kis_onion_skin_cache.cpp index 5f760d2031..03904ad563 100644 --- a/libs/image/kis_onion_skin_cache.cpp +++ b/libs/image/kis_onion_skin_cache.cpp @@ -1,132 +1,132 @@ /* * Copyright (c) 2016 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_onion_skin_cache.h" #include #include #include #include "kis_paint_device.h" #include "kis_onion_skin_compositor.h" #include "kis_default_bounds.h" #include "kis_image.h" #include "kis_raster_keyframe_channel.h" struct KisOnionSkinCache::Private { KisPaintDeviceSP cachedProjection; int cacheTime = 0; int cacheConfigSeqNo = 0; int framesHash = 0; QReadWriteLock lock; bool checkCacheValid(KisPaintDeviceSP source, KisOnionSkinCompositor *compositor) { const KisRasterKeyframeChannel *keyframes = source->keyframeChannel(); const int time = source->defaultBounds()->currentTime(); const int seqNo = compositor->configSeqNo(); const int hash = keyframes->framesHash(); return time == cacheTime && cacheConfigSeqNo == seqNo && framesHash == hash; } void updateCacheMetrics(KisPaintDeviceSP source, KisOnionSkinCompositor *compositor) { const KisRasterKeyframeChannel *keyframes = source->keyframeChannel(); const int time = source->defaultBounds()->currentTime(); const int seqNo = compositor->configSeqNo(); const int hash = keyframes->framesHash(); cacheTime = time; cacheConfigSeqNo = seqNo; framesHash = hash; } }; KisOnionSkinCache::KisOnionSkinCache() : m_d(new Private) { } KisOnionSkinCache::~KisOnionSkinCache() { } KisPaintDeviceSP KisOnionSkinCache::projection(KisPaintDeviceSP source) { KisOnionSkinCompositor *compositor = KisOnionSkinCompositor::instance(); KisPaintDeviceSP cachedProjection; QReadLocker readLocker(&m_d->lock); cachedProjection = m_d->cachedProjection; if (!cachedProjection || !m_d->checkCacheValid(source, compositor)) { readLocker.unlock(); QWriteLocker writeLocker(&m_d->lock); cachedProjection = m_d->cachedProjection; if (!cachedProjection || !m_d->checkCacheValid(source, compositor)) { if (!cachedProjection) { cachedProjection = new KisPaintDevice(source->colorSpace()); } else { cachedProjection->setDefaultBounds(new KisDefaultBounds()); cachedProjection->clear(); } const QRect extent = compositor->calculateExtent(source); compositor->composite(source, cachedProjection, extent); cachedProjection->setDefaultBounds(source->defaultBounds()); /** * It might happen that the lod planes has already been * generated for all the devices, so we should cold-init them * for the onion skins. */ const int lod = source->defaultBounds()->currentLevelOfDetail(); if (lod > 0) { - KisPaintDevice::LodDataStruct *data = cachedProjection->createLodDataStruct(lod); - cachedProjection->updateLodDataStruct(data, extent); - cachedProjection->uploadLodDataStruct(data); + QScopedPointer data(cachedProjection->createLodDataStruct(lod)); + cachedProjection->updateLodDataStruct(data.data(), extent); + cachedProjection->uploadLodDataStruct(data.data()); } m_d->updateCacheMetrics(source, compositor); m_d->cachedProjection = cachedProjection; } } return cachedProjection; } void KisOnionSkinCache::reset() { QWriteLocker writeLocker(&m_d->lock); m_d->cachedProjection = 0; } KisPaintDeviceSP KisOnionSkinCache::lodCapableDevice() const { return m_d->cachedProjection; } diff --git a/libs/image/kis_time_range.cpp b/libs/image/kis_time_range.cpp index c166e4d48e..6d801505a5 100644 --- a/libs/image/kis_time_range.cpp +++ b/libs/image/kis_time_range.cpp @@ -1,151 +1,151 @@ /* * 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_time_range.h" #include #include "kis_keyframe_channel.h" #include "kis_node.h" #include "kis_layer_utils.h" struct KisTimeRangeStaticRegistrar { KisTimeRangeStaticRegistrar() { qRegisterMetaType("KisTimeRange"); } }; static KisTimeRangeStaticRegistrar __registrar; QDebug operator<<(QDebug dbg, const KisTimeRange &r) { dbg.nospace() << "KisTimeRange(" << r.start() << ", " << r.end() << ")"; return dbg.space(); } KisTimeRange KisTimeRange::calculateIdenticalFramesRecursive(const KisNode *node, int time) { KisTimeRange range = KisTimeRange::infinite(0); KisLayerUtils::recursiveApplyNodes(node, [&range, time] (const KisNode *node) { if (node->visible()) { range &= calculateNodeIdenticalFrames(node, time); } }); return range; } KisTimeRange KisTimeRange::calculateAffectedFramesRecursive(const KisNode *node, int time) { KisTimeRange range; KisLayerUtils::recursiveApplyNodes(node, [&range, time] (const KisNode *node) { if (node->visible()) { range |= calculateNodeIdenticalFrames(node, time); } }); return range; } KisTimeRange KisTimeRange::calculateNodeIdenticalFrames(const KisNode *node, int time) { KisTimeRange range = KisTimeRange::infinite(0); const QMap channels = node->keyframeChannels(); Q_FOREACH (const KisKeyframeChannel *channel, channels) { // Intersection range &= channel->identicalFrames(time); } return range; } KisTimeRange KisTimeRange::calculateNodeAffectedFrames(const KisNode *node, int time) { KisTimeRange range; if (!node->visible()) return range; const QMap channels = node->keyframeChannels(); // TODO: channels should report to the image which channel exactly has changed // to avoid the dirty range to be stretched into infinity! if (channels.isEmpty() || !channels.contains(KisKeyframeChannel::Content.id())) { range = KisTimeRange::infinite(0); return range; } Q_FOREACH (const KisKeyframeChannel *channel, channels) { // Union range |= channel->affectedFrames(time); } return range; } namespace KisDomUtils { void saveValue(QDomElement *parent, const QString &tag, const KisTimeRange &range) { QDomDocument doc = parent->ownerDocument(); QDomElement e = doc.createElement(tag); parent->appendChild(e); e.setAttribute("type", "timerange"); if (range.isValid()) { e.setAttribute("from", toString(range.start())); if (!range.isInfinite()) { e.setAttribute("to", toString(range.end())); } } } bool loadValue(const QDomElement &parent, const QString &tag, KisTimeRange *range) { QDomElement e; if (!findOnlyElement(parent, tag, &e)) return false; if (!Private::checkType(e, "timerange")) return false; int start = toInt(e.attribute("from", "-1")); int end = toInt(e.attribute("to", "-1")); if (start == -1) { - range = new KisTimeRange(); + *range = KisTimeRange(); } else if (end == -1) { *range = KisTimeRange::infinite(start); } else { *range = KisTimeRange::fromTime(start, end); } return true; } } diff --git a/libs/image/krita_utils.cpp b/libs/image/krita_utils.cpp index 09b70edcac..98219152c3 100644 --- a/libs/image/krita_utils.cpp +++ b/libs/image/krita_utils.cpp @@ -1,518 +1,518 @@ /* * 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(true); return QSize(cfg.updatePatchWidth(), cfg.updatePatchHeight()); } QVector splitRectIntoPatches(const QRect &rc, const QSize &patchSize) { using namespace KisAlgebra2D; QVector patches; const qint32 firstCol = divideFloor(rc.x(), patchSize.width()); const qint32 firstRow = divideFloor(rc.y(), patchSize.height()); // TODO: check if -1 is needed here const qint32 lastCol = divideFloor(rc.x() + rc.width(), patchSize.width()); const qint32 lastRow = divideFloor(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); + return QLocale().toString(value, '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 y = 0; y < size.height(); ++y) { for(int x = 0; x < size.width(); ++x) { const QRgb pixel = image.pixel(x,y); const int gray = qGray(pixel); dstImage.setPixel(x, y, 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); while (dstIt.nextPixel()) { const quint8 *dstPtr = dstIt.rawDataConst(); func(*dstPtr); } } void filterAlpha8Device(KisPaintDeviceSP dev, const QRect &rc, std::function func) { KisSequentialIterator dstIt(dev, rc); while (dstIt.nextPixel()) { quint8 *dstPtr = dstIt.rawData(); *dstPtr = func(*dstPtr); } } 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); } } void mirrorPoint(Qt::Orientation dir, const QPoint ¢er, QPointF *pt) { if (dir == Qt::Horizontal) { pt->rx() = -(pt->x() - qreal(center.x())) + center.x(); } else /* if (dir == Qt::Vertical) */ { pt->ry() = -(pt->y() - qreal(center.y())) + center.y(); } } qreal colorDifference(const QColor &c1, const QColor &c2) { const qreal dr = c1.redF() - c2.redF(); const qreal dg = c1.greenF() - c2.greenF(); const qreal db = c1.blueF() - c2.blueF(); return std::sqrt(2 * pow2(dr) + 4 * pow2(dg) + 3 * pow2(db)); } void dragColor(QColor *color, const QColor &baseColor, qreal threshold) { while (colorDifference(*color, baseColor) < threshold) { QColor newColor = *color; if (newColor.lightnessF() > baseColor.lightnessF()) { newColor = newColor.lighter(120); } else { newColor = newColor.darker(120); } if (newColor == *color) { break; } *color = newColor; } } } diff --git a/libs/image/tiles3/kis_hline_iterator.cpp b/libs/image/tiles3/kis_hline_iterator.cpp index 47c6de8d32..fe5558845e 100644 --- a/libs/image/tiles3/kis_hline_iterator.cpp +++ b/libs/image/tiles3/kis_hline_iterator.cpp @@ -1,232 +1,231 @@ /* * 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_hline_iterator.h" KisHLineIterator2::KisHLineIterator2(KisDataManager *dataManager, qint32 x, qint32 y, qint32 w, qint32 offsetX, qint32 offsetY, bool writable, KisIteratorCompleteListener *competionListener) : KisBaseIterator(dataManager, writable, competionListener), m_offsetX(offsetX), m_offsetY(offsetY) { x -= m_offsetX; y -= m_offsetY; - Q_ASSERT(dataManager != 0); + Q_ASSERT(dataManager); - Q_ASSERT(w > 0); // for us, to warn us when abusing the iterators - if (w < 1) w = 1; // for release mode, to make sure there's always at least one pixel read. + if (w < 1) w = 1; // To make sure there's always at least one pixel read. m_x = x; m_y = y; m_left = x; m_right = x + w - 1; m_top = y; m_havePixels = (w == 0) ? false : true; if (m_left > m_right) { m_havePixels = false; return; } m_leftCol = xToCol(m_left); m_rightCol = xToCol(m_right); m_row = yToRow(m_y); m_yInTile = calcYInTile(m_y, m_row); m_leftInLeftmostTile = m_left - m_leftCol * KisTileData::WIDTH; m_tilesCacheSize = m_rightCol - m_leftCol + 1; m_tilesCache.resize(m_tilesCacheSize); m_tileWidth = m_pixelSize * KisTileData::HEIGHT; // let's prealocate first row for (quint32 i = 0; i < m_tilesCacheSize; i++){ fetchTileDataForCache(m_tilesCache[i], m_leftCol + i, m_row); } m_index = 0; switchToTile(m_leftInLeftmostTile); } void KisHLineIterator2::resetPixelPos() { m_x = m_left; m_index = 0; switchToTile(m_leftInLeftmostTile); m_havePixels = true; } void KisHLineIterator2::resetRowPos() { m_y = m_top; m_row = yToRow(m_y); m_yInTile = calcYInTile(m_y, m_row); preallocateTiles(); resetPixelPos(); } bool KisHLineIterator2::nextPixel() { // We won't increment m_x here as integer can overflow here if (m_x >= m_right) { //return !m_isDoneFlag; return m_havePixels = false; } else { ++m_x; m_data += m_pixelSize; if (m_x <= m_rightmostInTile) m_oldData += m_pixelSize; else { // Switching to the beginning of the next tile ++m_index; switchToTile(0); } } return m_havePixels; } void KisHLineIterator2::nextRow() { m_x = m_left; ++m_y; if (++m_yInTile < KisTileData::HEIGHT) { /* do nothing, usual case */ } else { ++m_row; m_yInTile = 0; preallocateTiles(); } m_index = 0; switchToTile(m_leftInLeftmostTile); m_havePixels = true; } qint32 KisHLineIterator2::nConseqPixels() const { return qMin(m_rightmostInTile, m_right) - m_x + 1; } bool KisHLineIterator2::nextPixels(qint32 n) { Q_ASSERT_X(!(m_x > 0 && (m_x + n) < 0), "hlineIt+=", "Integer overflow"); qint32 previousCol = xToCol(m_x); // We won't increment m_x here first as integer can overflow if (m_x >= m_right || (m_x += n) > m_right) { m_havePixels = false; } else { qint32 col = xToCol(m_x); // if we are in the same column in tiles if (col == previousCol) { m_data += n * m_pixelSize; } else { qint32 xInTile = calcXInTile(m_x, col); m_index += col - previousCol; switchToTile(xInTile); } } return m_havePixels; } KisHLineIterator2::~KisHLineIterator2() { for (uint i = 0; i < m_tilesCacheSize; i++) { unlockTile(m_tilesCache[i].tile); unlockOldTile(m_tilesCache[i].oldtile); } } quint8* KisHLineIterator2::rawData() { return m_data; } const quint8* KisHLineIterator2::oldRawData() const { return m_oldData; } const quint8* KisHLineIterator2::rawDataConst() const { return m_data; } void KisHLineIterator2::switchToTile(qint32 xInTile) { // The caller must ensure that we are not out of bounds Q_ASSERT(m_index < m_tilesCacheSize); m_data = m_tilesCache[m_index].data; m_oldData = m_tilesCache[m_index].oldData; int offset_row = m_pixelSize * (m_yInTile * KisTileData::WIDTH); m_data += offset_row; m_rightmostInTile = (m_leftCol + m_index + 1) * KisTileData::WIDTH - 1; int offset_col = m_pixelSize * xInTile; m_data += offset_col; m_oldData += offset_row + offset_col; } void KisHLineIterator2::fetchTileDataForCache(KisTileInfo& kti, qint32 col, qint32 row) { m_dataManager->getTilesPair(col, row, m_writable, &kti.tile, &kti.oldtile); lockTile(kti.tile); kti.data = kti.tile->data(); lockOldTile(kti.oldtile); kti.oldData = kti.oldtile->data(); } void KisHLineIterator2::preallocateTiles() { for (quint32 i = 0; i < m_tilesCacheSize; ++i){ unlockTile(m_tilesCache[i].tile); unlockOldTile(m_tilesCache[i].oldtile); fetchTileDataForCache(m_tilesCache[i], m_leftCol + i, m_row); } } qint32 KisHLineIterator2::x() const { return m_x + m_offsetX; } qint32 KisHLineIterator2::y() const { return m_y + m_offsetY; } diff --git a/libs/pigment/KoColor.cpp b/libs/pigment/KoColor.cpp index e922dfe3ad..0266f27661 100644 --- a/libs/pigment/KoColor.cpp +++ b/libs/pigment/KoColor.cpp @@ -1,450 +1,454 @@ /* * 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" #include "kis_assert.h" #include #include #ifdef HAVE_OPENEXR #include #endif namespace { struct DefaultKoColorInitializer { 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."; #endif #endif } + ~DefaultKoColorInitializer() { + delete value; + } + 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()); } KoColor KoColor::convertedTo(const KoColorSpace *cs, KoColorConversionTransformation::Intent renderingIntent, KoColorConversionTransformation::ConversionFlags conversionFlags) const { KoColor result(*this); result.convertTo(cs, renderingIntent, conversionFlags); return result; } KoColor KoColor::convertedTo(const KoColorSpace *cs) const { return convertedTo(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); } } void KoColor::subtract(const KoColor &value) { KIS_SAFE_ASSERT_RECOVER_RETURN(*m_colorSpace == *value.colorSpace()); QVector channels1(m_colorSpace->channelCount()); QVector channels2(m_colorSpace->channelCount()); m_colorSpace->normalisedChannelsValue(m_data, channels1); m_colorSpace->normalisedChannelsValue(value.data(), channels2); for (int i = 0; i < channels1.size(); i++) { channels1[i] -= channels2[i]; } m_colorSpace->fromNormalisedChannelsValue(m_data, channels1); } KoColor KoColor::subtracted(const KoColor &value) const { KoColor result(*this); result.subtract(value); return result; } void KoColor::add(const KoColor &value) { KIS_SAFE_ASSERT_RECOVER_RETURN(*m_colorSpace == *value.colorSpace()); QVector channels1(m_colorSpace->channelCount()); QVector channels2(m_colorSpace->channelCount()); m_colorSpace->normalisedChannelsValue(m_data, channels1); m_colorSpace->normalisedChannelsValue(value.data(), channels2); for (int i = 0; i < channels1.size(); i++) { channels1[i] += channels2[i]; } m_colorSpace->fromNormalisedChannelsValue(m_data, channels1); } KoColor KoColor::added(const KoColor &value) const { KoColor result(*this); result.add(value); return result; } #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& channelDepthId) { bool ok; return fromXML(elt, channelDepthId, &ok); } KoColor KoColor::fromXML(const QDomElement& elt, const QString& channelDepthId, 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, channelDepthId, 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::toXML() const { QDomDocument cdataDoc = QDomDocument("color"); QDomElement cdataRoot = cdataDoc.createElement("color"); cdataDoc.appendChild(cdataRoot); cdataRoot.setAttribute("channeldepth", colorSpace()->colorDepthId().id()); toXML(cdataDoc, cdataRoot); return cdataDoc.toString(); } KoColor KoColor::fromXML(const QString &xml) { KoColor c; QDomDocument doc; if (doc.setContent(xml)) { QDomElement e = doc.documentElement().firstChild().toElement(); QString channelDepthID = e.attribute("channeldepth", Integer16BitsColorDepthID.id()); bool ok; c = KoColor::fromXML(e, channelDepthID, &ok); } return c; } 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(" "); } QDebug operator<<(QDebug dbg, const KoColor &color) { dbg.nospace() << "KoColor (" << color.colorSpace()->id(); QList channels = color.colorSpace()->channels(); for (auto it = channels.constBegin(); it != channels.constEnd(); ++it) { KoChannelInfo *ch = (*it); dbg.nospace() << ", " << ch->name() << ":"; switch (ch->channelValueType()) { case KoChannelInfo::UINT8: { const quint8 *ptr = reinterpret_cast(color.data() + ch->pos()); dbg.nospace() << *ptr; break; } case KoChannelInfo::UINT16: { const quint16 *ptr = reinterpret_cast(color.data() + ch->pos()); dbg.nospace() << *ptr; break; } case KoChannelInfo::UINT32: { const quint32 *ptr = reinterpret_cast(color.data() + ch->pos()); dbg.nospace() << *ptr; break; } case KoChannelInfo::FLOAT16: { #ifdef HAVE_OPENEXR const half *ptr = reinterpret_cast(color.data() + ch->pos()); dbg.nospace() << *ptr; #else const quint16 *ptr = reinterpret_cast(color.data() + ch->pos()); dbg.nospace() << "UNSUPPORTED_F16(" << *ptr << ")"; #endif break; } case KoChannelInfo::FLOAT32: { const float *ptr = reinterpret_cast(color.data() + ch->pos()); dbg.nospace() << *ptr; break; } case KoChannelInfo::FLOAT64: { const double *ptr = reinterpret_cast(color.data() + ch->pos()); dbg.nospace() << *ptr; break; } case KoChannelInfo::INT8: { const qint8 *ptr = reinterpret_cast(color.data() + ch->pos()); dbg.nospace() << *ptr; break; } case KoChannelInfo::INT16: { const qint16 *ptr = reinterpret_cast(color.data() + ch->pos()); dbg.nospace() << *ptr; break; } case KoChannelInfo::OTHER: { const quint8 *ptr = reinterpret_cast(color.data() + ch->pos()); dbg.nospace() << "undef(" << *ptr << ")"; break; } } } dbg.nospace() << ")"; return dbg.space(); } diff --git a/libs/pigment/resources/KisSwatchGroup.cpp b/libs/pigment/resources/KisSwatchGroup.cpp index d2780171d9..d2840d06af 100644 --- a/libs/pigment/resources/KisSwatchGroup.cpp +++ b/libs/pigment/resources/KisSwatchGroup.cpp @@ -1,217 +1,231 @@ /* This file is part of the KDE project Copyright (c) 2005 Boudewijn Rempt Copyright (c) 2016 L. E. Segovia Copyright (c) 2018 Michael Zhou 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 "KisSwatchGroup.h" struct KisSwatchGroup::Private { typedef QMap Column; Private() : name(QString()) , colorMatrix(DEFAULT_COLUMN_COUNT) , colorCount(0) , rowCount(DEFAULT_ROW_COUNT) { } static int DEFAULT_COLUMN_COUNT; static int DEFAULT_ROW_COUNT; QString name; QVector colorMatrix; int colorCount; int rowCount; }; int KisSwatchGroup::Private::DEFAULT_COLUMN_COUNT = 16; int KisSwatchGroup::Private::DEFAULT_ROW_COUNT = 20; KisSwatchGroup::KisSwatchGroup() : d(new Private) { } KisSwatchGroup::~KisSwatchGroup() { } KisSwatchGroup::KisSwatchGroup(const KisSwatchGroup &rhs) : d(new Private(*rhs.d)) { } KisSwatchGroup &KisSwatchGroup::operator =(const KisSwatchGroup &rhs) { if (&rhs == this) { return *this; } d.reset(new Private(*rhs.d)); return *this; } void KisSwatchGroup::setEntry(const KisSwatch &e, int column, int row) { Q_ASSERT(column < d->colorMatrix.size() && column >= 0 && row >= 0); if (row >= d->rowCount) { setRowCount(row + 1); } if (!checkEntry(column, row)) { d->colorCount++; } d->colorMatrix[column][row] = e; } bool KisSwatchGroup::checkEntry(int column, int row) const { - if (row >= d->rowCount || column >= d->colorMatrix.size() || column < 0) { + if (row >= d->rowCount) { return false; } - if (!d->colorMatrix[column].contains(row)) { + + if (column >= d->colorMatrix.size()){ return false; } + + if (column < 0) { + return false; + } + + if (row >= d->colorMatrix[column].size()) { + return false; + } + + if (!d->colorMatrix[column][row].isValid()) { + return false; + } + return true; } bool KisSwatchGroup::removeEntry(int column, int row) { if (d->colorCount == 0) { return false; } if (row >= d->rowCount || column >= d->colorMatrix.size() || column < 0) { return false; } // QMap::remove returns 1 if key found else 0 if (d->colorMatrix[column].remove(row)) { d->colorCount -= 1; return true; } else { return false; } } void KisSwatchGroup::setColumnCount(int columnCount) { Q_ASSERT(columnCount >= 0); if (columnCount < d->colorMatrix.size()) { int newColorCount = 0; for (int i = 0; i < columnCount; i++ ) { newColorCount += d->colorMatrix[i].size(); } d->colorCount = newColorCount; } d->colorMatrix.resize(columnCount); } int KisSwatchGroup::columnCount() const { return d->colorMatrix.size(); } KisSwatch KisSwatchGroup::getEntry(int column, int row) const { Q_ASSERT(checkEntry(column, row)); return d->colorMatrix[column][row]; } void KisSwatchGroup::addEntry(const KisSwatch &e) { if (columnCount() == 0) { setColumnCount(Private::DEFAULT_COLUMN_COUNT); } if (d->colorCount == 0) { setEntry(e, 0, 0); return; } int y = 0; for (const Private::Column &c : d->colorMatrix) { if (c.isEmpty()) { continue; } if (y < c.lastKey()) { y = c.lastKey(); } } for (int x = d->colorMatrix.size() - 1; x >= 0; x--) { if (checkEntry(x, y)) { // if the last entry's at the rightmost column, // add e to the leftmost column of the next row // and increase row count if (++x == d->colorMatrix.size()) { x = 0; y++; } // else just add it to the right setEntry(e, x, y); break; } } } void KisSwatchGroup::clear() { d->colorMatrix.clear(); } void KisSwatchGroup::setRowCount(int newRowCount) { d->rowCount = newRowCount; for (Private::Column &c : d->colorMatrix) { for (int k : c.keys()) { if (k >= newRowCount) { c.remove(k); d->colorCount--; } } } } int KisSwatchGroup::rowCount() const { return d->rowCount; } int KisSwatchGroup::colorCount() const { return d->colorCount; } QList KisSwatchGroup::infoList() const { QList res; int column = 0; for (const Private::Column &c : d->colorMatrix) { int i = 0; for (const KisSwatch &s : c.values()) { SwatchInfo info = {d->name, s, c.keys()[i++], column}; res.append(info); } column++; } return res; } void KisSwatchGroup::setName(const QString &name) { d->name = name; } QString KisSwatchGroup::name() const { return d->name; } diff --git a/libs/ui/KisDocument.cpp b/libs/ui/KisDocument.cpp index 698853856e..85e7be645e 100644 --- a/libs/ui/KisDocument.cpp +++ b/libs/ui/KisDocument.cpp @@ -1,2207 +1,2217 @@ /* This file is part of the Krita project * * Copyright (C) 2014 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 "KisMainWindow.h" // XXX: remove #include // XXX: remove #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 #include #include #include #include #include #include #include #include #include #include #include // Krita Image #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_layer_utils.h" // Local #include "KisViewManager.h" #include "kis_clipboard.h" #include "widgets/kis_custom_image_widget.h" #include "canvas/kis_canvas2.h" #include "flake/kis_shape_controller.h" #include "kis_statusbar.h" #include "widgets/kis_progress_widget.h" #include "kis_canvas_resource_provider.h" #include "KisResourceServerProvider.h" #include "kis_node_manager.h" #include "KisPart.h" #include "KisApplication.h" #include "KisDocument.h" #include "KisImportExportManager.h" #include "KisView.h" #include "kis_grid_config.h" #include "kis_guides_config.h" #include "kis_image_barrier_lock_adapter.h" #include "KisReferenceImagesLayer.h" #include #include "kis_config_notifier.h" #include "kis_async_action_feedback.h" #include "KisCloneDocumentStroke.h" #include #include #include "kis_simple_stroke_strategy.h" // Define the protocol used here for embedded documents' URL // This used to "store" but QUrl didn't like it, // so let's simply make it "tar" ! #define STORE_PROTOCOL "tar" // The internal path is a hack to make QUrl happy and for document children #define INTERNAL_PROTOCOL "intern" #define INTERNAL_PREFIX "intern:/" // Warning, keep it sync in koStore.cc #include using namespace std; namespace { constexpr int errorMessageTimeout = 5000; constexpr int successMessageTimeout = 1000; } /********************************************************** * * KisDocument * **********************************************************/ //static QString KisDocument::newObjectName() { static int s_docIFNumber = 0; QString name; name.setNum(s_docIFNumber++); name.prepend("document_"); return name; } class UndoStack : public KUndo2Stack { public: UndoStack(KisDocument *doc) : KUndo2Stack(doc), m_doc(doc) { } void setIndex(int idx) override { KisImageWSP image = this->image(); image->requestStrokeCancellation(); if(image->tryBarrierLock()) { KUndo2Stack::setIndex(idx); image->unlock(); } } void notifySetIndexChangedOneCommand() override { KisImageWSP image = this->image(); image->unlock(); /** * Some very weird commands may emit blocking signals to * the GUI (e.g. KisGuiContextCommand). Here is the best thing * we can do to avoid the deadlock */ while(!image->tryBarrierLock()) { QApplication::processEvents(); } } void undo() override { KisImageWSP image = this->image(); image->requestUndoDuringStroke(); if (image->tryUndoUnfinishedLod0Stroke() == UNDO_OK) { return; } if(image->tryBarrierLock()) { KUndo2Stack::undo(); image->unlock(); } } void redo() override { KisImageWSP image = this->image(); if(image->tryBarrierLock()) { KUndo2Stack::redo(); image->unlock(); } } private: KisImageWSP image() { KisImageWSP currentImage = m_doc->image(); Q_ASSERT(currentImage); return currentImage; } private: KisDocument *m_doc; }; class Q_DECL_HIDDEN KisDocument::Private { public: Private(KisDocument *_q) : q(_q) , docInfo(new KoDocumentInfo(_q)) // deleted by QObject , importExportManager(new KisImportExportManager(_q)) // deleted manually , autoSaveTimer(new QTimer(_q)) , undoStack(new UndoStack(_q)) // deleted by QObject , m_bAutoDetectedMime(false) , modified(false) , readwrite(true) , firstMod(QDateTime::currentDateTime()) , lastMod(firstMod) , nserver(new KisNameServer(1)) , imageIdleWatcher(2000 /*ms*/) , globalAssistantsColor(KisConfig(true).defaultAssistantsColor()) , savingLock(&savingMutex) , batchMode(false) { if (QLocale().measurementSystem() == QLocale::ImperialSystem) { unit = KoUnit::Inch; } else { unit = KoUnit::Centimeter; } } Private(const Private &rhs, KisDocument *_q) : q(_q) , docInfo(new KoDocumentInfo(*rhs.docInfo, _q)) , importExportManager(new KisImportExportManager(_q)) , autoSaveTimer(new QTimer(_q)) , undoStack(new UndoStack(_q)) , nserver(new KisNameServer(*rhs.nserver)) , preActivatedNode(0) // the node is from another hierarchy! , imageIdleWatcher(2000 /*ms*/) , savingLock(&savingMutex) { copyFromImpl(rhs, _q, CONSTRUCT); } ~Private() { // Don't delete m_d->shapeController because it's in a QObject hierarchy. delete nserver; } KisDocument *q = 0; KoDocumentInfo *docInfo = 0; KoUnit unit; KisImportExportManager *importExportManager = 0; // The filter-manager to use when loading/saving [for the options] QByteArray mimeType; // The actual mimetype of the document QByteArray outputMimeType; // The mimetype to use when saving QTimer *autoSaveTimer; QString lastErrorMessage; // see openFile() QString lastWarningMessage; int autoSaveDelay = 300; // in seconds, 0 to disable. bool modifiedAfterAutosave = false; bool isAutosaving = false; bool disregardAutosaveFailure = false; int autoSaveFailureCount = 0; KUndo2Stack *undoStack = 0; KisGuidesConfig guidesConfig; KisMirrorAxisConfig mirrorAxisConfig; bool m_bAutoDetectedMime = false; // whether the mimetype in the arguments was detected by the part itself QUrl m_url; // local url - the one displayed to the user. QString m_file; // Local file - the only one the part implementation should deal with. QMutex savingMutex; bool modified = false; bool readwrite = false; QDateTime firstMod; QDateTime lastMod; KisNameServer *nserver; KisImageSP image; KisImageSP savingImage; KisNodeWSP preActivatedNode; KisShapeController* shapeController = 0; KoShapeController* koShapeController = 0; KisIdleWatcher imageIdleWatcher; QScopedPointer imageIdleConnection; QList assistants; QColor globalAssistantsColor; - KisSharedPtr referenceImagesLayer; - QList paletteList; bool ownsPaletteList = false; KisGridConfig gridConfig; StdLockableWrapper savingLock; bool modifiedWhileSaving = false; QScopedPointer backgroundSaveDocument; QPointer savingUpdater; QFuture childSavingFuture; KritaUtils::ExportFileJob backgroundSaveJob; bool isRecovered = false; bool batchMode { false }; void syncDecorationsWrapperLayerState(); void setImageAndInitIdleWatcher(KisImageSP _image) { image = _image; imageIdleWatcher.setTrackedImage(image); if (image) { imageIdleConnection.reset( new KisSignalAutoConnection( &imageIdleWatcher, SIGNAL(startedIdleMode()), image.data(), SLOT(explicitRegenerateLevelOfDetail()))); } } void copyFrom(const Private &rhs, KisDocument *q); void copyFromImpl(const Private &rhs, KisDocument *q, KisDocument::CopyPolicy policy); /// clones the palette list oldList /// the ownership of the returned KoColorSet * belongs to the caller QList clonePaletteList(const QList &oldList); class StrippedSafeSavingLocker; }; void KisDocument::Private::syncDecorationsWrapperLayerState() { if (!this->image) return; KisImageSP image = this->image; KisDecorationsWrapperLayerSP decorationsLayer = KisLayerUtils::findNodeByType(image->root()); const bool needsDecorationsWrapper = gridConfig.showGrid() || (guidesConfig.showGuides() && guidesConfig.hasGuides()) || !assistants.isEmpty(); struct SyncDecorationsWrapperStroke : public KisSimpleStrokeStrategy { SyncDecorationsWrapperStroke(KisDocument *document, bool needsDecorationsWrapper) : KisSimpleStrokeStrategy("sync-decorations-wrapper", kundo2_noi18n("start-isolated-mode")), m_document(document), m_needsDecorationsWrapper(needsDecorationsWrapper) { this->enableJob(JOB_INIT, true, KisStrokeJobData::SEQUENTIAL, KisStrokeJobData::EXCLUSIVE); setClearsRedoOnStart(false); } void initStrokeCallback() { KisDecorationsWrapperLayerSP decorationsLayer = KisLayerUtils::findNodeByType(m_document->image()->root()); if (m_needsDecorationsWrapper && !decorationsLayer) { m_document->image()->addNode(new KisDecorationsWrapperLayer(m_document)); } else if (!m_needsDecorationsWrapper && decorationsLayer) { m_document->image()->removeNode(decorationsLayer); } } private: KisDocument *m_document = 0; bool m_needsDecorationsWrapper = false; }; KisStrokeId id = image->startStroke(new SyncDecorationsWrapperStroke(q, needsDecorationsWrapper)); image->endStroke(id); } void KisDocument::Private::copyFrom(const Private &rhs, KisDocument *q) { copyFromImpl(rhs, q, KisDocument::REPLACE); } void KisDocument::Private::copyFromImpl(const Private &rhs, KisDocument *q, KisDocument::CopyPolicy policy) { if (policy == REPLACE) { delete docInfo; } docInfo = (new KoDocumentInfo(*rhs.docInfo, q)); unit = rhs.unit; mimeType = rhs.mimeType; outputMimeType = rhs.outputMimeType; if (policy == REPLACE) { q->setGuidesConfig(rhs.guidesConfig); q->setMirrorAxisConfig(rhs.mirrorAxisConfig); q->setModified(rhs.modified); q->setAssistants(KisPaintingAssistant::cloneAssistantList(rhs.assistants)); q->setGridConfig(rhs.gridConfig); } else { // in CONSTRUCT mode, we cannot use the functions of KisDocument // because KisDocument does not yet have a pointer to us. guidesConfig = rhs.guidesConfig; mirrorAxisConfig = rhs.mirrorAxisConfig; modified = rhs.modified; assistants = KisPaintingAssistant::cloneAssistantList(rhs.assistants); gridConfig = rhs.gridConfig; } m_bAutoDetectedMime = rhs.m_bAutoDetectedMime; m_url = rhs.m_url; m_file = rhs.m_file; readwrite = rhs.readwrite; firstMod = rhs.firstMod; lastMod = rhs.lastMod; // XXX: the display properties will be shared between different snapshots globalAssistantsColor = rhs.globalAssistantsColor; if (policy == REPLACE) { QList newPaletteList = clonePaletteList(rhs.paletteList); q->setPaletteList(newPaletteList, /* emitSignal = */ true); // we still do not own palettes if we did not } else { paletteList = rhs.paletteList; } batchMode = rhs.batchMode; } QList KisDocument::Private::clonePaletteList(const QList &oldList) { QList newList; Q_FOREACH (KoColorSet *palette, oldList) { newList << new KoColorSet(*palette); } return newList; } class KisDocument::Private::StrippedSafeSavingLocker { public: StrippedSafeSavingLocker(QMutex *savingMutex, KisImageSP image) : m_locked(false) , m_image(image) , m_savingLock(savingMutex) , m_imageLock(image, true) { /** * Initial try to lock both objects. Locking the image guards * us from any image composition threads running in the * background, while savingMutex guards us from entering the * saving code twice by autosave and main threads. * * Since we are trying to lock multiple objects, so we should * do it in a safe manner. */ m_locked = std::try_lock(m_imageLock, m_savingLock) < 0; if (!m_locked) { m_image->requestStrokeEnd(); QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); // one more try... m_locked = std::try_lock(m_imageLock, m_savingLock) < 0; } } ~StrippedSafeSavingLocker() { if (m_locked) { m_imageLock.unlock(); m_savingLock.unlock(); } } bool successfullyLocked() const { return m_locked; } private: bool m_locked; KisImageSP m_image; StdLockableWrapper m_savingLock; KisImageBarrierLockAdapter m_imageLock; }; KisDocument::KisDocument() : d(new Private(this)) { connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); connect(d->undoStack, SIGNAL(cleanChanged(bool)), this, SLOT(slotUndoStackCleanChanged(bool))); connect(d->autoSaveTimer, SIGNAL(timeout()), this, SLOT(slotAutoSave())); setObjectName(newObjectName()); // preload the krita resources KisResourceServerProvider::instance(); d->shapeController = new KisShapeController(this, d->nserver); d->koShapeController = new KoShapeController(0, d->shapeController); d->shapeController->resourceManager()->setGlobalShapeController(d->koShapeController); slotConfigChanged(); } KisDocument::KisDocument(const KisDocument &rhs) : QObject(), d(new Private(*rhs.d, this)) { copyFromDocumentImpl(rhs, CONSTRUCT); } KisDocument::~KisDocument() { // wait until all the pending operations are in progress waitForSavingToComplete(); /** * Push a timebomb, which will try to release the memory after * the document has been deleted */ KisPaintDevice::createMemoryReleaseObject()->deleteLater(); d->autoSaveTimer->disconnect(this); d->autoSaveTimer->stop(); delete d->importExportManager; // Despite being QObject they needs to be deleted before the image delete d->shapeController; delete d->koShapeController; if (d->image) { d->image->notifyAboutToBeDeleted(); /** * WARNING: We should wait for all the internal image jobs to * finish before entering KisImage's destructor. The problem is, * while execution of KisImage::~KisImage, all the weak shared * pointers pointing to the image enter an inconsistent * state(!). The shared counter is already zero and destruction * has started, but the weak reference doesn't know about it, * because KisShared::~KisShared hasn't been executed yet. So all * the threads running in background and having weak pointers will * enter the KisImage's destructor as well. */ d->image->requestStrokeCancellation(); d->image->waitForDone(); // clear undo commands that can still point to the image d->undoStack->clear(); d->image->waitForDone(); KisImageWSP sanityCheckPointer = d->image; Q_UNUSED(sanityCheckPointer); // The following line trigger the deletion of the image d->image.clear(); // check if the image has actually been deleted KIS_SAFE_ASSERT_RECOVER_NOOP(!sanityCheckPointer.isValid()); } if (d->ownsPaletteList) { qDeleteAll(d->paletteList); } delete d; } bool KisDocument::reload() { // XXX: reimplement! return false; } KisDocument *KisDocument::clone() { return new KisDocument(*this); } bool KisDocument::exportDocumentImpl(const KritaUtils::ExportFileJob &job, KisPropertiesConfigurationSP exportConfiguration) { QFileInfo filePathInfo(job.filePath); if (filePathInfo.exists() && !filePathInfo.isWritable()) { slotCompleteSavingDocument(job, ImportExportCodes::NoAccessToWrite, i18n("%1 cannot be written to. Please save under a different name.", job.filePath)); //return ImportExportCodes::NoAccessToWrite; return false; } KisConfig cfg(true); if (cfg.backupFile() && filePathInfo.exists()) { QString backupDir; switch(cfg.readEntry("backupfilelocation", 0)) { case 1: backupDir = QStandardPaths::writableLocation(QStandardPaths::HomeLocation); break; case 2: backupDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); break; default: // Do nothing: the empty string is user file location break; } int numOfBackupsKept = cfg.readEntry("numberofbackupfiles", 1); QString suffix = cfg.readEntry("backupfilesuffix", "~"); if (numOfBackupsKept == 1) { KBackup::simpleBackupFile(job.filePath, backupDir, suffix); } else if (numOfBackupsKept > 2) { KBackup::numberedBackupFile(job.filePath, backupDir, suffix, numOfBackupsKept); } } //KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!job.mimeType.isEmpty(), false); if (job.mimeType.isEmpty()) { KisImportExportErrorCode error = ImportExportCodes::FileFormatIncorrect; slotCompleteSavingDocument(job, error, error.errorMessage()); return false; } const QString actionName = job.flags & KritaUtils::SaveIsExporting ? i18n("Exporting Document...") : i18n("Saving Document..."); bool started = initiateSavingInBackground(actionName, this, SLOT(slotCompleteSavingDocument(KritaUtils::ExportFileJob, KisImportExportErrorCode ,QString)), job, exportConfiguration); if (!started) { emit canceled(QString()); } return started; } bool KisDocument::exportDocument(const QUrl &url, const QByteArray &mimeType, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { using namespace KritaUtils; SaveFlags flags = SaveIsExporting; if (showWarnings) { flags |= SaveShowWarnings; } KisUsageLogger::log(QString("Exporting Document: %1 as %2. %3 * %4 pixels, %5 layers, %6 frames, %7 framerate. Export configuration: %8") .arg(url.toLocalFile()) .arg(QString::fromLatin1(mimeType)) .arg(d->image->width()) .arg(d->image->height()) .arg(d->image->nlayers()) .arg(d->image->animationInterface()->totalLength()) .arg(d->image->animationInterface()->framerate()) .arg(exportConfiguration ? exportConfiguration->toXML() : "No configuration")); return exportDocumentImpl(KritaUtils::ExportFileJob(url.toLocalFile(), mimeType, flags), exportConfiguration); } bool KisDocument::saveAs(const QUrl &_url, const QByteArray &mimeType, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { using namespace KritaUtils; KisUsageLogger::log(QString("Saving Document %9 as %1 (mime: %2). %3 * %4 pixels, %5 layers. %6 frames, %7 framerate. Export configuration: %8") .arg(_url.toLocalFile()) .arg(QString::fromLatin1(mimeType)) .arg(d->image->width()) .arg(d->image->height()) .arg(d->image->nlayers()) .arg(d->image->animationInterface()->totalLength()) .arg(d->image->animationInterface()->framerate()) .arg(exportConfiguration ? exportConfiguration->toXML() : "No configuration") .arg(url().toLocalFile())); return exportDocumentImpl(ExportFileJob(_url.toLocalFile(), mimeType, showWarnings ? SaveShowWarnings : SaveNone), exportConfiguration); } bool KisDocument::save(bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { return saveAs(url(), mimeType(), showWarnings, exportConfiguration); } QByteArray KisDocument::serializeToNativeByteArray() { QByteArray byteArray; QBuffer buffer(&byteArray); QScopedPointer filter(KisImportExportManager::filterForMimeType(nativeFormatMimeType(), KisImportExportManager::Export)); filter->setBatchMode(true); filter->setMimeType(nativeFormatMimeType()); Private::StrippedSafeSavingLocker locker(&d->savingMutex, d->image); if (!locker.successfullyLocked()) { return byteArray; } d->savingImage = d->image; if (!filter->convert(this, &buffer).isOk()) { qWarning() << "serializeToByteArray():: Could not export to our native format"; } return byteArray; } void KisDocument::slotCompleteSavingDocument(const KritaUtils::ExportFileJob &job, KisImportExportErrorCode status, const QString &errorMessage) { if (status.isCancelled()) return; const QString fileName = QFileInfo(job.filePath).fileName(); if (!status.isOk()) { emit statusBarMessage(i18nc("%1 --- failing file name, %2 --- error message", "Error during saving %1: %2", fileName, exportErrorToUserMessage(status, errorMessage)), errorMessageTimeout); if (!fileBatchMode()) { const QString filePath = job.filePath; QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("Could not save %1\nReason: %2", filePath, exportErrorToUserMessage(status, errorMessage))); } } else { if (!(job.flags & KritaUtils::SaveIsExporting)) { const QString existingAutoSaveBaseName = localFilePath(); const bool wasRecovered = isRecovered(); setUrl(QUrl::fromLocalFile(job.filePath)); setLocalFilePath(job.filePath); setMimeType(job.mimeType); updateEditingTime(true); if (!d->modifiedWhileSaving) { /** * If undo stack is already clean/empty, it doesn't emit any * signals, so we might forget update document modified state * (which was set, e.g. while recovering an autosave file) */ if (d->undoStack->isClean()) { setModified(false); } else { d->undoStack->setClean(); } } setRecovered(false); removeAutoSaveFiles(existingAutoSaveBaseName, wasRecovered); } emit completed(); emit sigSavingFinished(); emit statusBarMessage(i18n("Finished saving %1", fileName), successMessageTimeout); } } QByteArray KisDocument::mimeType() const { return d->mimeType; } void KisDocument::setMimeType(const QByteArray & mimeType) { d->mimeType = mimeType; } bool KisDocument::fileBatchMode() const { return d->batchMode; } void KisDocument::setFileBatchMode(const bool batchMode) { d->batchMode = batchMode; } KisDocument* KisDocument::lockAndCloneForSaving() { // force update of all the asynchronous nodes before cloning QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); KisLayerUtils::forceAllDelayedNodesUpdate(d->image->root()); KisMainWindow *window = KisPart::instance()->currentMainwindow(); if (window) { if (window->viewManager()) { if (!window->viewManager()->blockUntilOperationsFinished(d->image)) { return 0; } } } Private::StrippedSafeSavingLocker locker(&d->savingMutex, d->image); if (!locker.successfullyLocked()) { return 0; } return new KisDocument(*this); } KisDocument *KisDocument::lockAndCreateSnapshot() { KisDocument *doc = lockAndCloneForSaving(); if (doc) { // clone palette list doc->d->paletteList = doc->d->clonePaletteList(doc->d->paletteList); doc->d->ownsPaletteList = true; } return doc; } void KisDocument::copyFromDocument(const KisDocument &rhs) { copyFromDocumentImpl(rhs, REPLACE); } void KisDocument::copyFromDocumentImpl(const KisDocument &rhs, CopyPolicy policy) { if (policy == REPLACE) { d->copyFrom(*(rhs.d), this); d->undoStack->clear(); } else { // in CONSTRUCT mode, d should be already initialized connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotConfigChanged())); connect(d->undoStack, SIGNAL(cleanChanged(bool)), this, SLOT(slotUndoStackCleanChanged(bool))); connect(d->autoSaveTimer, SIGNAL(timeout()), this, SLOT(slotAutoSave())); d->shapeController = new KisShapeController(this, d->nserver); d->koShapeController = new KoShapeController(0, d->shapeController); d->shapeController->resourceManager()->setGlobalShapeController(d->koShapeController); } setObjectName(rhs.objectName()); slotConfigChanged(); if (rhs.d->image) { if (policy == REPLACE) { d->image->barrierLock(/* readOnly = */ false); rhs.d->image->barrierLock(/* readOnly = */ true); d->image->copyFromImage(*(rhs.d->image)); d->image->unlock(); rhs.d->image->unlock(); setCurrentImage(d->image, /* forceInitialUpdate = */ true); } else { // clone the image with keeping the GUIDs of the layers intact // NOTE: we expect the image to be locked! setCurrentImage(rhs.image()->clone(/* exactCopy = */ true), /* forceInitialUpdate = */ false); } } if (rhs.d->preActivatedNode) { QQueue linearizedNodes; KisLayerUtils::recursiveApplyNodes(rhs.d->image->root(), [&linearizedNodes](KisNodeSP node) { linearizedNodes.enqueue(node); }); KisLayerUtils::recursiveApplyNodes(d->image->root(), [&linearizedNodes, &rhs, this](KisNodeSP node) { KisNodeSP refNode = linearizedNodes.dequeue(); if (rhs.d->preActivatedNode.data() == refNode.data()) { d->preActivatedNode = node; } }); } - KisNodeSP foundNode = KisLayerUtils::recursiveFindNode(image()->rootLayer(), [](KisNodeSP node) -> bool { return dynamic_cast(node.data()); }); - KisReferenceImagesLayer *refLayer = dynamic_cast(foundNode.data()); - setReferenceImagesLayer(refLayer, /* updateImage = */ false); + // reinitialize references' signal connection + KisReferenceImagesLayerSP referencesLayer = this->referenceImagesLayer(); + setReferenceImagesLayer(referencesLayer, false); KisDecorationsWrapperLayerSP decorationsLayer = KisLayerUtils::findNodeByType(d->image->root()); if (decorationsLayer) { decorationsLayer->setDocument(this); } if (policy == REPLACE) { setModified(true); } } bool KisDocument::exportDocumentSync(const QUrl &url, const QByteArray &mimeType, KisPropertiesConfigurationSP exportConfiguration) { { /** * The caller guarantees that noone else uses the document (usually, * it is a temporary docuent created specifically for exporting), so * we don't need to copy or lock the document. Instead we should just * ensure the barrier lock is synced and then released. */ Private::StrippedSafeSavingLocker locker(&d->savingMutex, d->image); if (!locker.successfullyLocked()) { return false; } } d->savingImage = d->image; const QString fileName = url.toLocalFile(); KisImportExportErrorCode status = d->importExportManager-> exportDocument(fileName, fileName, mimeType, false, exportConfiguration); d->savingImage = 0; return status.isOk(); } bool KisDocument::initiateSavingInBackground(const QString actionName, const QObject *receiverObject, const char *receiverMethod, const KritaUtils::ExportFileJob &job, KisPropertiesConfigurationSP exportConfiguration) { return initiateSavingInBackground(actionName, receiverObject, receiverMethod, job, exportConfiguration, std::unique_ptr()); } bool KisDocument::initiateSavingInBackground(const QString actionName, const QObject *receiverObject, const char *receiverMethod, const KritaUtils::ExportFileJob &job, KisPropertiesConfigurationSP exportConfiguration, std::unique_ptr &&optionalClonedDocument) { KIS_ASSERT_RECOVER_RETURN_VALUE(job.isValid(), false); QScopedPointer clonedDocument; if (!optionalClonedDocument) { clonedDocument.reset(lockAndCloneForSaving()); } else { clonedDocument.reset(optionalClonedDocument.release()); } // we block saving until the current saving is finished! if (!clonedDocument || !d->savingMutex.tryLock()) { return false; } auto waitForImage = [] (KisImageSP image) { KisMainWindow *window = KisPart::instance()->currentMainwindow(); if (window) { if (window->viewManager()) { window->viewManager()->blockUntilOperationsFinishedForced(image); } } }; { KisNodeSP newRoot = clonedDocument->image()->root(); KIS_SAFE_ASSERT_RECOVER(!KisLayerUtils::hasDelayedNodeWithUpdates(newRoot)) { KisLayerUtils::forceAllDelayedNodesUpdate(newRoot); waitForImage(clonedDocument->image()); } } KIS_SAFE_ASSERT_RECOVER(clonedDocument->image()->isIdle()) { waitForImage(clonedDocument->image()); } KIS_ASSERT_RECOVER_RETURN_VALUE(!d->backgroundSaveDocument, false); KIS_ASSERT_RECOVER_RETURN_VALUE(!d->backgroundSaveJob.isValid(), false); d->backgroundSaveDocument.reset(clonedDocument.take()); d->backgroundSaveJob = job; d->modifiedWhileSaving = false; if (d->backgroundSaveJob.flags & KritaUtils::SaveInAutosaveMode) { d->backgroundSaveDocument->d->isAutosaving = true; } connect(d->backgroundSaveDocument.data(), SIGNAL(sigBackgroundSavingFinished(KisImportExportErrorCode, QString)), this, SLOT(slotChildCompletedSavingInBackground(KisImportExportErrorCode, QString))); connect(this, SIGNAL(sigCompleteBackgroundSaving(KritaUtils::ExportFileJob, KisImportExportErrorCode, QString)), receiverObject, receiverMethod, Qt::UniqueConnection); bool started = d->backgroundSaveDocument->startExportInBackground(actionName, job.filePath, job.filePath, job.mimeType, job.flags & KritaUtils::SaveShowWarnings, exportConfiguration); if (!started) { // the state should have been deinitialized in slotChildCompletedSavingInBackground() KIS_SAFE_ASSERT_RECOVER (!d->backgroundSaveDocument && !d->backgroundSaveJob.isValid()) { d->backgroundSaveDocument.take()->deleteLater(); d->savingMutex.unlock(); d->backgroundSaveJob = KritaUtils::ExportFileJob(); } } return started; } void KisDocument::slotChildCompletedSavingInBackground(KisImportExportErrorCode status, const QString &errorMessage) { KIS_ASSERT_RECOVER_RETURN(isSaving()); KIS_ASSERT_RECOVER(d->backgroundSaveDocument) { d->savingMutex.unlock(); return; } if (d->backgroundSaveJob.flags & KritaUtils::SaveInAutosaveMode) { d->backgroundSaveDocument->d->isAutosaving = false; } d->backgroundSaveDocument.take()->deleteLater(); KIS_ASSERT_RECOVER(d->backgroundSaveJob.isValid()) { d->savingMutex.unlock(); return; } const KritaUtils::ExportFileJob job = d->backgroundSaveJob; d->backgroundSaveJob = KritaUtils::ExportFileJob(); // unlock at the very end d->savingMutex.unlock(); KisUsageLogger::log(QString("Completed saving %1 (mime: %2). Result: %3") .arg(job.filePath) .arg(QString::fromLatin1(job.mimeType)) .arg(!status.isOk() ? exportErrorToUserMessage(status, errorMessage) : "OK")); emit sigCompleteBackgroundSaving(job, status, errorMessage); } void KisDocument::slotAutoSaveImpl(std::unique_ptr &&optionalClonedDocument) { if (!d->modified || !d->modifiedAfterAutosave) return; const QString autoSaveFileName = generateAutoSaveFileName(localFilePath()); emit statusBarMessage(i18n("Autosaving... %1", autoSaveFileName), successMessageTimeout); const bool hadClonedDocument = bool(optionalClonedDocument); bool started = false; if (d->image->isIdle() || hadClonedDocument) { started = initiateSavingInBackground(i18n("Autosaving..."), this, SLOT(slotCompleteAutoSaving(KritaUtils::ExportFileJob, KisImportExportErrorCode, QString)), KritaUtils::ExportFileJob(autoSaveFileName, nativeFormatMimeType(), KritaUtils::SaveIsExporting | KritaUtils::SaveInAutosaveMode), 0, std::move(optionalClonedDocument)); } else { emit statusBarMessage(i18n("Autosaving postponed: document is busy..."), errorMessageTimeout); } if (!started && !hadClonedDocument && d->autoSaveFailureCount >= 3) { KisCloneDocumentStroke *stroke = new KisCloneDocumentStroke(this); connect(stroke, SIGNAL(sigDocumentCloned(KisDocument*)), this, SLOT(slotInitiateAsyncAutosaving(KisDocument*)), Qt::BlockingQueuedConnection); KisStrokeId strokeId = d->image->startStroke(stroke); d->image->endStroke(strokeId); setInfiniteAutoSaveInterval(); } else if (!started) { setEmergencyAutoSaveInterval(); } else { d->modifiedAfterAutosave = false; } } void KisDocument::slotAutoSave() { slotAutoSaveImpl(std::unique_ptr()); } void KisDocument::slotInitiateAsyncAutosaving(KisDocument *clonedDocument) { slotAutoSaveImpl(std::unique_ptr(clonedDocument)); } void KisDocument::slotCompleteAutoSaving(const KritaUtils::ExportFileJob &job, KisImportExportErrorCode status, const QString &errorMessage) { Q_UNUSED(job); const QString fileName = QFileInfo(job.filePath).fileName(); if (!status.isOk()) { setEmergencyAutoSaveInterval(); emit statusBarMessage(i18nc("%1 --- failing file name, %2 --- error message", "Error during autosaving %1: %2", fileName, exportErrorToUserMessage(status, errorMessage)), errorMessageTimeout); } else { KisConfig cfg(true); d->autoSaveDelay = cfg.autoSaveInterval(); if (!d->modifiedWhileSaving) { d->autoSaveTimer->stop(); // until the next change d->autoSaveFailureCount = 0; } else { setNormalAutoSaveInterval(); } emit statusBarMessage(i18n("Finished autosaving %1", fileName), successMessageTimeout); } } bool KisDocument::startExportInBackground(const QString &actionName, const QString &location, const QString &realLocation, const QByteArray &mimeType, bool showWarnings, KisPropertiesConfigurationSP exportConfiguration) { d->savingImage = d->image; KisMainWindow *window = KisPart::instance()->currentMainwindow(); if (window) { if (window->viewManager()) { d->savingUpdater = window->viewManager()->createThreadedUpdater(actionName); d->importExportManager->setUpdater(d->savingUpdater); } } KisImportExportErrorCode initializationStatus(ImportExportCodes::OK); d->childSavingFuture = d->importExportManager->exportDocumentAsyc(location, realLocation, mimeType, initializationStatus, showWarnings, exportConfiguration); if (!initializationStatus.isOk()) { if (d->savingUpdater) { d->savingUpdater->cancel(); } d->savingImage.clear(); emit sigBackgroundSavingFinished(initializationStatus, initializationStatus.errorMessage()); return false; } typedef QFutureWatcher StatusWatcher; StatusWatcher *watcher = new StatusWatcher(); watcher->setFuture(d->childSavingFuture); connect(watcher, SIGNAL(finished()), SLOT(finishExportInBackground())); connect(watcher, SIGNAL(finished()), watcher, SLOT(deleteLater())); return true; } void KisDocument::finishExportInBackground() { KIS_SAFE_ASSERT_RECOVER(d->childSavingFuture.isFinished()) { emit sigBackgroundSavingFinished(ImportExportCodes::InternalError, ""); return; } KisImportExportErrorCode status = d->childSavingFuture.result(); const QString errorMessage = status.errorMessage(); d->savingImage.clear(); d->childSavingFuture = QFuture(); d->lastErrorMessage.clear(); if (d->savingUpdater) { d->savingUpdater->setProgress(100); } emit sigBackgroundSavingFinished(status, errorMessage); } void KisDocument::setReadWrite(bool readwrite) { d->readwrite = readwrite; setNormalAutoSaveInterval(); Q_FOREACH (KisMainWindow *mainWindow, KisPart::instance()->mainWindows()) { mainWindow->setReadWrite(readwrite); } } void KisDocument::setAutoSaveDelay(int delay) { if (isReadWrite() && delay > 0) { d->autoSaveTimer->start(delay * 1000); } else { d->autoSaveTimer->stop(); } } void KisDocument::setNormalAutoSaveInterval() { setAutoSaveDelay(d->autoSaveDelay); d->autoSaveFailureCount = 0; } void KisDocument::setEmergencyAutoSaveInterval() { const int emergencyAutoSaveInterval = 10; /* sec */ setAutoSaveDelay(emergencyAutoSaveInterval); d->autoSaveFailureCount++; } void KisDocument::setInfiniteAutoSaveInterval() { setAutoSaveDelay(-1); } KoDocumentInfo *KisDocument::documentInfo() const { return d->docInfo; } bool KisDocument::isModified() const { return d->modified; } QPixmap KisDocument::generatePreview(const QSize& size) { KisImageSP image = d->image; if (d->savingImage) image = d->savingImage; if (image) { QRect bounds = image->bounds(); QSize newSize = bounds.size(); newSize.scale(size, Qt::KeepAspectRatio); QPixmap px = QPixmap::fromImage(image->convertToQImage(newSize, 0)); if (px.size() == QSize(0,0)) { px = QPixmap(newSize); QPainter gc(&px); QBrush checkBrush = QBrush(KisCanvasWidgetBase::createCheckersImage(newSize.width() / 5)); gc.fillRect(px.rect(), checkBrush); gc.end(); } return px; } return QPixmap(size); } QString KisDocument::generateAutoSaveFileName(const QString & path) const { QString retval; // Using the extension allows to avoid relying on the mime magic when opening const QString extension (".kra"); QString prefix = KisConfig(true).readEntry("autosavefileshidden") ? QString(".") : QString(); QRegularExpression autosavePattern1("^\\..+-autosave.kra$"); QRegularExpression autosavePattern2("^.+-autosave.kra$"); QFileInfo fi(path); QString dir = fi.absolutePath(); QString filename = fi.fileName(); if (path.isEmpty() || autosavePattern1.match(filename).hasMatch() || autosavePattern2.match(filename).hasMatch() || !fi.isWritable()) { // Never saved? #ifdef Q_OS_WIN // On Windows, use the temp location (https://bugs.kde.org/show_bug.cgi?id=314921) retval = QString("%1%2%7%3-%4-%5-autosave%6").arg(QDir::tempPath()).arg(QDir::separator()).arg("krita").arg(qApp->applicationPid()).arg(objectName()).arg(extension).arg(prefix); #else // On Linux, use a temp file in $HOME then. Mark it with the pid so two instances don't overwrite each other's autosave file retval = QString("%1%2%7%3-%4-%5-autosave%6").arg(QDir::homePath()).arg(QDir::separator()).arg("krita").arg(qApp->applicationPid()).arg(objectName()).arg(extension).arg(prefix); #endif } else { retval = QString("%1%2%5%3-autosave%4").arg(dir).arg(QDir::separator()).arg(filename).arg(extension).arg(prefix); } //qDebug() << "generateAutoSaveFileName() for path" << path << ":" << retval; return retval; } bool KisDocument::importDocument(const QUrl &_url) { bool ret; dbgUI << "url=" << _url.url(); // open... ret = openUrl(_url); // reset url & m_file (kindly? set by KisParts::openUrl()) to simulate a // File --> Import if (ret) { dbgUI << "success, resetting url"; resetURL(); setTitleModified(); } return ret; } bool KisDocument::openUrl(const QUrl &_url, OpenFlags flags) { if (!_url.isLocalFile()) { return false; } dbgUI << "url=" << _url.url(); d->lastErrorMessage.clear(); // Reimplemented, to add a check for autosave files and to improve error reporting if (!_url.isValid()) { d->lastErrorMessage = i18n("Malformed URL\n%1", _url.url()); // ## used anywhere ? return false; } QUrl url(_url); bool autosaveOpened = false; if (url.isLocalFile() && !fileBatchMode()) { QString file = url.toLocalFile(); QString asf = generateAutoSaveFileName(file); if (QFile::exists(asf)) { KisApplication *kisApp = static_cast(qApp); kisApp->hideSplashScreen(); //dbgUI <<"asf=" << asf; // ## TODO compare timestamps ? int res = QMessageBox::warning(0, i18nc("@title:window", "Krita"), i18n("An autosaved file exists for this document.\nDo you want to open the autosaved file instead?"), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::Yes); switch (res) { case QMessageBox::Yes : url.setPath(asf); autosaveOpened = true; break; case QMessageBox::No : QFile::remove(asf); break; default: // Cancel return false; } } } bool ret = openUrlInternal(url); if (autosaveOpened || flags & RecoveryFile) { setReadWrite(true); // enable save button setModified(true); setRecovered(true); } else { if (ret) { if (!(flags & DontAddToRecent)) { KisPart::instance()->addRecentURLToAllMainWindows(_url); } // Detect readonly local-files; remote files are assumed to be writable QFileInfo fi(url.toLocalFile()); setReadWrite(fi.isWritable()); } setRecovered(false); } return ret; } class DlgLoadMessages : public KoDialog { public: DlgLoadMessages(const QString &title, const QString &message, const QStringList &warnings) { setWindowTitle(title); setWindowIcon(KisIconUtils::loadIcon("warning")); QWidget *page = new QWidget(this); QVBoxLayout *layout = new QVBoxLayout(page); QHBoxLayout *hlayout = new QHBoxLayout(); QLabel *labelWarning= new QLabel(); labelWarning->setPixmap(KisIconUtils::loadIcon("warning").pixmap(32, 32)); hlayout->addWidget(labelWarning); hlayout->addWidget(new QLabel(message)); layout->addLayout(hlayout); QTextBrowser *browser = new QTextBrowser(); QString warning = "

"; if (warnings.size() == 1) { warning += " Reason:

"; } else { warning += " Reasons:

"; } warning += "

    "; Q_FOREACH(const QString &w, warnings) { warning += "\n
  • " + w + "
  • "; } warning += "
"; browser->setHtml(warning); browser->setMinimumHeight(200); browser->setMinimumWidth(400); layout->addWidget(browser); setMainWidget(page); setButtons(KoDialog::Ok); resize(minimumSize()); } }; bool KisDocument::openFile() { //dbgUI <<"for" << localFilePath(); if (!QFile::exists(localFilePath()) && !fileBatchMode()) { QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("File %1 does not exist.", localFilePath())); return false; } QString filename = localFilePath(); QString typeName = mimeType(); if (typeName.isEmpty()) { typeName = KisMimeDatabase::mimeTypeForFile(filename); } //qDebug() << "mimetypes 4:" << typeName; // Allow to open backup files, don't keep the mimetype application/x-trash. if (typeName == "application/x-trash") { QString path = filename; while (path.length() > 0) { path.chop(1); typeName = KisMimeDatabase::mimeTypeForFile(path); //qDebug() << "\t" << path << typeName; if (!typeName.isEmpty()) { break; } } //qDebug() << "chopped" << filename << "to" << path << "Was trash, is" << typeName; } dbgUI << localFilePath() << "type:" << typeName; KisMainWindow *window = KisPart::instance()->currentMainwindow(); KoUpdaterPtr updater; if (window && window->viewManager()) { updater = window->viewManager()->createUnthreadedUpdater(i18n("Opening document")); d->importExportManager->setUpdater(updater); } KisImportExportErrorCode status = d->importExportManager->importDocument(localFilePath(), typeName); if (!status.isOk()) { if (window && window->viewManager()) { updater->cancel(); } QString msg = status.errorMessage(); if (!msg.isEmpty() && !fileBatchMode()) { DlgLoadMessages dlg(i18nc("@title:window", "Krita"), i18n("Could not open %2.\nReason: %1.", msg, prettyPathOrUrl()), errorMessage().split("\n") + warningMessage().split("\n")); dlg.exec(); } return false; } else if (!warningMessage().isEmpty() && !fileBatchMode()) { DlgLoadMessages dlg(i18nc("@title:window", "Krita"), i18n("There were problems opening %1.", prettyPathOrUrl()), warningMessage().split("\n")); dlg.exec(); setUrl(QUrl()); } setMimeTypeAfterLoading(typeName); d->syncDecorationsWrapperLayerState(); emit sigLoadingFinished(); undoStack()->clear(); return true; } // shared between openFile and koMainWindow's "create new empty document" code void KisDocument::setMimeTypeAfterLoading(const QString& mimeType) { d->mimeType = mimeType.toLatin1(); d->outputMimeType = d->mimeType; } bool KisDocument::loadNativeFormat(const QString & file_) { return openUrl(QUrl::fromLocalFile(file_)); } void KisDocument::setModified(bool mod) { if (mod) { updateEditingTime(false); } if (d->isAutosaving) // ignore setModified calls due to autosaving return; if ( !d->readwrite && d->modified ) { errKrita << "Can't set a read-only document to 'modified' !" << endl; return; } //dbgUI<<" url:" << url.path(); //dbgUI<<" mod="<docInfo->aboutInfo("editing-time").toInt() + d->firstMod.secsTo(d->lastMod))); d->firstMod = now; } else if (firstModDelta > 60 || forceStoreElapsed) { d->docInfo->setAboutInfo("editing-time", QString::number(d->docInfo->aboutInfo("editing-time").toInt() + firstModDelta)); d->firstMod = now; } d->lastMod = now; } QString KisDocument::prettyPathOrUrl() const { QString _url(url().toDisplayString()); #ifdef Q_OS_WIN if (url().isLocalFile()) { _url = QDir::toNativeSeparators(_url); } #endif return _url; } // Get caption from document info (title(), in about page) QString KisDocument::caption() const { QString c; const QString _url(url().fileName()); // if URL is empty...it is probably an unsaved file if (_url.isEmpty()) { c = " [" + i18n("Not Saved") + "] "; } else { c = _url; // Fall back to document URL } return c; } void KisDocument::setTitleModified() { emit titleModified(caption(), isModified()); } QDomDocument KisDocument::createDomDocument(const QString& tagName, const QString& version) const { return createDomDocument("krita", tagName, version); } //static QDomDocument KisDocument::createDomDocument(const QString& appName, const QString& tagName, const QString& version) { QDomImplementation impl; QString url = QString("http://www.calligra.org/DTD/%1-%2.dtd").arg(appName).arg(version); QDomDocumentType dtype = impl.createDocumentType(tagName, QString("-//KDE//DTD %1 %2//EN").arg(appName).arg(version), url); // The namespace URN doesn't need to include the version number. QString namespaceURN = QString("http://www.calligra.org/DTD/%1").arg(appName); QDomDocument doc = impl.createDocument(namespaceURN, tagName, dtype); doc.insertBefore(doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\""), doc.documentElement()); return doc; } bool KisDocument::isNativeFormat(const QByteArray& mimetype) const { if (mimetype == nativeFormatMimeType()) return true; return extraNativeMimeTypes().contains(mimetype); } void KisDocument::setErrorMessage(const QString& errMsg) { d->lastErrorMessage = errMsg; } QString KisDocument::errorMessage() const { return d->lastErrorMessage; } void KisDocument::setWarningMessage(const QString& warningMsg) { d->lastWarningMessage = warningMsg; } QString KisDocument::warningMessage() const { return d->lastWarningMessage; } void KisDocument::removeAutoSaveFiles(const QString &autosaveBaseName, bool wasRecovered) { //qDebug() << "removeAutoSaveFiles"; // Eliminate any auto-save file QString asf = generateAutoSaveFileName(autosaveBaseName); // the one in the current dir //qDebug() << "\tfilename:" << asf << "exists:" << QFile::exists(asf); if (QFile::exists(asf)) { //qDebug() << "\tremoving autosavefile" << asf; QFile::remove(asf); } asf = generateAutoSaveFileName(QString()); // and the one in $HOME //qDebug() << "Autsavefile in $home" << asf; if (QFile::exists(asf)) { //qDebug() << "\tremoving autsavefile 2" << asf; QFile::remove(asf); } QList expressions; expressions << QRegularExpression("^\\..+-autosave.kra$") << QRegularExpression("^.+-autosave.kra$"); Q_FOREACH(const QRegularExpression &rex, expressions) { if (wasRecovered && !autosaveBaseName.isEmpty() && rex.match(QFileInfo(autosaveBaseName).fileName()).hasMatch() && QFile::exists(autosaveBaseName)) { QFile::remove(autosaveBaseName); } } } KoUnit KisDocument::unit() const { return d->unit; } void KisDocument::setUnit(const KoUnit &unit) { if (d->unit != unit) { d->unit = unit; emit unitChanged(unit); } } KUndo2Stack *KisDocument::undoStack() { return d->undoStack; } KisImportExportManager *KisDocument::importExportManager() const { return d->importExportManager; } void KisDocument::addCommand(KUndo2Command *command) { if (command) d->undoStack->push(command); } void KisDocument::beginMacro(const KUndo2MagicString & text) { d->undoStack->beginMacro(text); } void KisDocument::endMacro() { d->undoStack->endMacro(); } void KisDocument::slotUndoStackCleanChanged(bool value) { setModified(!value); } void KisDocument::slotConfigChanged() { KisConfig cfg(true); if (d->undoStack->undoLimit() != cfg.undoStackLimit()) { if (!d->undoStack->isClean()) { d->undoStack->clear(); } d->undoStack->setUndoLimit(cfg.undoStackLimit()); } d->autoSaveDelay = cfg.autoSaveInterval(); setNormalAutoSaveInterval(); } void KisDocument::slotImageRootChanged() { d->syncDecorationsWrapperLayerState(); } void KisDocument::clearUndoHistory() { d->undoStack->clear(); } KisGridConfig KisDocument::gridConfig() const { return d->gridConfig; } void KisDocument::setGridConfig(const KisGridConfig &config) { if (d->gridConfig != config) { d->gridConfig = config; d->syncDecorationsWrapperLayerState(); emit sigGridConfigChanged(config); } } QList &KisDocument::paletteList() { return d->paletteList; } void KisDocument::setPaletteList(const QList &paletteList, bool emitSignal) { if (d->paletteList != paletteList) { QList oldPaletteList = d->paletteList; d->paletteList = paletteList; if (emitSignal) { emit sigPaletteListChanged(oldPaletteList, paletteList); } } } const KisGuidesConfig& KisDocument::guidesConfig() const { return d->guidesConfig; } void KisDocument::setGuidesConfig(const KisGuidesConfig &data) { if (d->guidesConfig == data) return; d->guidesConfig = data; d->syncDecorationsWrapperLayerState(); emit sigGuidesConfigChanged(d->guidesConfig); } const KisMirrorAxisConfig& KisDocument::mirrorAxisConfig() const { return d->mirrorAxisConfig; } void KisDocument::setMirrorAxisConfig(const KisMirrorAxisConfig &config) { if (d->mirrorAxisConfig == config) { return; } d->mirrorAxisConfig = config; setModified(true); emit sigMirrorAxisConfigChanged(); } void KisDocument::resetURL() { setUrl(QUrl()); setLocalFilePath(QString()); } KoDocumentInfoDlg *KisDocument::createDocumentInfoDialog(QWidget *parent, KoDocumentInfo *docInfo) const { return new KoDocumentInfoDlg(parent, docInfo); } bool KisDocument::isReadWrite() const { return d->readwrite; } QUrl KisDocument::url() const { return d->m_url; } bool KisDocument::closeUrl(bool promptToSave) { if (promptToSave) { if ( isReadWrite() && isModified()) { Q_FOREACH (KisView *view, KisPart::instance()->views()) { if (view && view->document() == this) { if (!view->queryClose()) { return false; } } } } } // Not modified => ok and delete temp file. d->mimeType = QByteArray(); // It always succeeds for a read-only part, // but the return value exists for reimplementations // (e.g. pressing cancel for a modified read-write part) return true; } void KisDocument::setUrl(const QUrl &url) { d->m_url = url; } QString KisDocument::localFilePath() const { return d->m_file; } void KisDocument::setLocalFilePath( const QString &localFilePath ) { d->m_file = localFilePath; } bool KisDocument::openUrlInternal(const QUrl &url) { if ( !url.isValid() ) { return false; } if (d->m_bAutoDetectedMime) { d->mimeType = QByteArray(); d->m_bAutoDetectedMime = false; } QByteArray mimetype = d->mimeType; if ( !closeUrl() ) { return false; } d->mimeType = mimetype; setUrl(url); d->m_file.clear(); if (d->m_url.isLocalFile()) { d->m_file = d->m_url.toLocalFile(); bool ret; // set the mimetype only if it was not already set (for example, by the host application) if (d->mimeType.isEmpty()) { // get the mimetype of the file // using findByUrl() to avoid another string -> url conversion QString mime = KisMimeDatabase::mimeTypeForFile(d->m_url.toLocalFile()); d->mimeType = mime.toLocal8Bit(); d->m_bAutoDetectedMime = true; } setUrl(d->m_url); ret = openFile(); if (ret) { emit completed(); } else { emit canceled(QString()); } return ret; } return false; } bool KisDocument::newImage(const QString& name, qint32 width, qint32 height, const KoColorSpace* cs, const KoColor &bgColor, KisConfig::BackgroundStyle bgStyle, int numberOfLayers, const QString &description, const double imageResolution) { Q_ASSERT(cs); KisImageSP image; if (!cs) return false; QApplication::setOverrideCursor(Qt::BusyCursor); image = new KisImage(createUndoStore(), width, height, cs, name); Q_CHECK_PTR(image); connect(image, SIGNAL(sigImageModified()), this, SLOT(setImageModified()), Qt::UniqueConnection); image->setResolution(imageResolution, imageResolution); image->assignImageProfile(cs->profile()); documentInfo()->setAboutInfo("title", name); documentInfo()->setAboutInfo("abstract", description); KisLayerSP layer; if (bgStyle == KisConfig::RASTER_LAYER || bgStyle == KisConfig::FILL_LAYER) { KoColor strippedAlpha = bgColor; strippedAlpha.setOpacity(OPACITY_OPAQUE_U8); if (bgStyle == KisConfig::RASTER_LAYER) { layer = new KisPaintLayer(image.data(), "Background", OPACITY_OPAQUE_U8, cs);; layer->paintDevice()->setDefaultPixel(strippedAlpha); } else if (bgStyle == KisConfig::FILL_LAYER) { KisFilterConfigurationSP filter_config = KisGeneratorRegistry::instance()->get("color")->defaultConfiguration(); filter_config->setProperty("color", strippedAlpha.toQColor()); layer = new KisGeneratorLayer(image.data(), "Background Fill", filter_config, image->globalSelection()); } layer->setOpacity(bgColor.opacityU8()); if (numberOfLayers > 1) { //Lock bg layer if others are present. layer->setUserLocked(true); } } else { // KisConfig::CANVAS_COLOR (needs an unlocked starting layer). image->setDefaultProjectionColor(bgColor); layer = new KisPaintLayer(image.data(), image->nextLayerName(), OPACITY_OPAQUE_U8, cs); } Q_CHECK_PTR(layer); image->addNode(layer.data(), image->rootLayer().data()); layer->setDirty(QRect(0, 0, width, height)); setCurrentImage(image); for(int i = 1; i < numberOfLayers; ++i) { KisPaintLayerSP layer = new KisPaintLayer(image, image->nextLayerName(), OPACITY_OPAQUE_U8, cs); image->addNode(layer, image->root(), i); layer->setDirty(QRect(0, 0, width, height)); } KisConfig cfg(false); cfg.defImageWidth(width); cfg.defImageHeight(height); cfg.defImageResolution(imageResolution); cfg.defColorModel(image->colorSpace()->colorModelId().id()); cfg.setDefaultColorDepth(image->colorSpace()->colorDepthId().id()); cfg.defColorProfile(image->colorSpace()->profile()->name()); KisUsageLogger::log(i18n("Created image \"%1\", %2 * %3 pixels, %4 dpi. Color model: %6 %5 (%7). Layers: %8" , name , width, height , imageResolution * 72.0 , image->colorSpace()->colorModelId().name(), image->colorSpace()->colorDepthId().name() , image->colorSpace()->profile()->name() , numberOfLayers)); QApplication::restoreOverrideCursor(); return true; } bool KisDocument::isSaving() const { const bool result = d->savingMutex.tryLock(); if (result) { d->savingMutex.unlock(); } return !result; } void KisDocument::waitForSavingToComplete() { if (isSaving()) { KisAsyncActionFeedback f(i18nc("progress dialog message when the user closes the document that is being saved", "Waiting for saving to complete..."), 0); f.waitForMutex(&d->savingMutex); } } KoShapeControllerBase *KisDocument::shapeController() const { return d->shapeController; } KoShapeLayer* KisDocument::shapeForNode(KisNodeSP layer) const { return d->shapeController->shapeForNode(layer); } QList KisDocument::assistants() const { return d->assistants; } void KisDocument::setAssistants(const QList &value) { if (d->assistants != value) { d->assistants = value; d->syncDecorationsWrapperLayerState(); emit sigAssistantsChanged(); } } -KisSharedPtr KisDocument::referenceImagesLayer() const +KisReferenceImagesLayerSP KisDocument::referenceImagesLayer() const { - return d->referenceImagesLayer.data(); + if (!d->image) return KisReferenceImagesLayerSP(); + + KisReferenceImagesLayerSP referencesLayer = + KisLayerUtils::findNodeByType(d->image->root()); + + return referencesLayer; } void KisDocument::setReferenceImagesLayer(KisSharedPtr layer, bool updateImage) { - if (d->referenceImagesLayer == layer) { + KisReferenceImagesLayerSP currentReferenceLayer = referenceImagesLayer(); + + if (currentReferenceLayer == layer) { return; } - if (d->referenceImagesLayer) { - d->referenceImagesLayer->disconnect(this); + if (currentReferenceLayer) { + currentReferenceLayer->disconnect(this); } if (updateImage) { + if (currentReferenceLayer) { + d->image->removeNode(currentReferenceLayer); + } + if (layer) { d->image->addNode(layer); - } else { - d->image->removeNode(d->referenceImagesLayer); } } - d->referenceImagesLayer = layer; + currentReferenceLayer = layer; - if (d->referenceImagesLayer) { - connect(d->referenceImagesLayer, SIGNAL(sigUpdateCanvas(QRectF)), + if (currentReferenceLayer) { + connect(currentReferenceLayer, SIGNAL(sigUpdateCanvas(QRectF)), this, SIGNAL(sigReferenceImagesChanged())); } + emit sigReferenceImagesLayerChanged(layer); } void KisDocument::setPreActivatedNode(KisNodeSP activatedNode) { d->preActivatedNode = activatedNode; } KisNodeSP KisDocument::preActivatedNode() const { return d->preActivatedNode; } KisImageWSP KisDocument::image() const { return d->image; } KisImageSP KisDocument::savingImage() const { return d->savingImage; } void KisDocument::setCurrentImage(KisImageSP image, bool forceInitialUpdate) { if (d->image) { // Disconnect existing sig/slot connections d->image->setUndoStore(new KisDumbUndoStore()); d->image->disconnect(this); d->shapeController->setImage(0); d->image = 0; } if (!image) return; d->setImageAndInitIdleWatcher(image); d->image->setUndoStore(new KisDocumentUndoStore(this)); d->shapeController->setImage(image); setModified(false); connect(d->image, SIGNAL(sigImageModified()), this, SLOT(setImageModified()), Qt::UniqueConnection); connect(d->image, SIGNAL(sigLayersChangedAsync()), this, SLOT(slotImageRootChanged())); if (forceInitialUpdate) { d->image->initialRefreshGraph(); } } void KisDocument::hackPreliminarySetImage(KisImageSP image) { KIS_SAFE_ASSERT_RECOVER_RETURN(!d->image); d->setImageAndInitIdleWatcher(image); d->shapeController->setImage(image); } void KisDocument::setImageModified() { // we only set as modified if undo stack is not at clean state setModified(!d->undoStack->isClean()); } KisUndoStore* KisDocument::createUndoStore() { return new KisDocumentUndoStore(this); } bool KisDocument::isAutosaving() const { return d->isAutosaving; } QString KisDocument::exportErrorToUserMessage(KisImportExportErrorCode status, const QString &errorMessage) { return errorMessage.isEmpty() ? status.errorMessage() : errorMessage; } void KisDocument::setAssistantsGlobalColor(QColor color) { d->globalAssistantsColor = color; } QColor KisDocument::assistantsGlobalColor() { return d->globalAssistantsColor; } QRectF KisDocument::documentBounds() const { QRectF bounds = d->image->bounds(); - if (d->referenceImagesLayer) { - bounds |= d->referenceImagesLayer->boundingImageRect(); + KisReferenceImagesLayerSP referenceImagesLayer = this->referenceImagesLayer(); + + if (referenceImagesLayer) { + bounds |= referenceImagesLayer->boundingImageRect(); } return bounds; } diff --git a/libs/ui/KisInMemoryFrameCacheSwapper.cpp b/libs/ui/KisInMemoryFrameCacheSwapper.cpp index 299ebdfa49..f5ecaab208 100644 --- a/libs/ui/KisInMemoryFrameCacheSwapper.cpp +++ b/libs/ui/KisInMemoryFrameCacheSwapper.cpp @@ -1,82 +1,84 @@ /* * Copyright (c) 2018 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 "KisInMemoryFrameCacheSwapper.h" #include #include struct KRITAUI_NO_EXPORT KisInMemoryFrameCacheSwapper::Private { QMap framesMap; }; KisInMemoryFrameCacheSwapper::KisInMemoryFrameCacheSwapper() : m_d(new Private) { } KisInMemoryFrameCacheSwapper::~KisInMemoryFrameCacheSwapper() { } void KisInMemoryFrameCacheSwapper::saveFrame(int frameId, KisOpenGLUpdateInfoSP info, const QRect &imageBounds) { Q_UNUSED(imageBounds); KIS_SAFE_ASSERT_RECOVER_NOOP(!m_d->framesMap.contains(frameId)); m_d->framesMap.insert(frameId, info); } KisOpenGLUpdateInfoSP KisInMemoryFrameCacheSwapper::loadFrame(int frameId) { KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->framesMap.contains(frameId)); return m_d->framesMap.value(frameId, KisOpenGLUpdateInfoSP()); } void KisInMemoryFrameCacheSwapper::moveFrame(int srcFrameId, int dstFrameId) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->framesMap.contains(srcFrameId)); KIS_SAFE_ASSERT_RECOVER_NOOP(!m_d->framesMap.contains(dstFrameId)); m_d->framesMap[dstFrameId] = m_d->framesMap[srcFrameId]; m_d->framesMap.remove(srcFrameId); } void KisInMemoryFrameCacheSwapper::forgetFrame(int frameId) { KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->framesMap.contains(frameId)); m_d->framesMap.remove(frameId); } bool KisInMemoryFrameCacheSwapper::hasFrame(int frameId) const { return m_d->framesMap.contains(frameId); } int KisInMemoryFrameCacheSwapper::frameLevelOfDetail(int frameId) const { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(m_d->framesMap.contains(frameId), 0); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!m_d->framesMap[frameId].isNull(), 0); return m_d->framesMap[frameId]->levelOfDetail(); } QRect KisInMemoryFrameCacheSwapper::frameDirtyRect(int frameId) const { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(m_d->framesMap.contains(frameId), QRect()); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!m_d->framesMap[frameId].isNull(), QRect()); return m_d->framesMap[frameId]->dirtyImageRect(); } diff --git a/libs/ui/canvas/kis_grid_config.cpp b/libs/ui/canvas/kis_grid_config.cpp index e5186087ee..726bb8295b 100644 --- a/libs/ui/canvas/kis_grid_config.cpp +++ b/libs/ui/canvas/kis_grid_config.cpp @@ -1,125 +1,125 @@ /* * 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_grid_config.h" #include #include "kis_config.h" #include "kis_dom_utils.h" #include "kis_algebra_2d.h" struct KisGridConfigStaticRegistrar { KisGridConfigStaticRegistrar() { qRegisterMetaType("KisGridConfig"); } }; static KisGridConfigStaticRegistrar __registrar; Q_GLOBAL_STATIC(KisGridConfig, staticDefaultObject) const KisGridConfig& KisGridConfig::defaultGrid() { staticDefaultObject->loadStaticData(); return *staticDefaultObject; } void KisGridConfig::transform(const QTransform &transform) { if (transform.type() >= QTransform::TxShear) return; KisAlgebra2D::DecomposedMatix m(transform); - if (m_gridType == 0) { + if (m_gridType == GRID_RECTANGULAR) { QTransform t = m.scaleTransform(); const qreal eps = 1e-3; const qreal wrappedRotation = KisAlgebra2D::wrapValue(m.angle, 90.0); if (wrappedRotation <= eps || wrappedRotation >= 90.0 - eps) { t *= m.rotateTransform(); } m_spacing = KisAlgebra2D::abs(t.map(m_spacing)); } else { if (qFuzzyCompare(m.scaleX, m.scaleY)) { m_cellSpacing = qRound(qAbs(m_cellSpacing * m.scaleX)); } } m_offset = KisAlgebra2D::wrapValue(transform.map(m_offset), m_spacing); } void KisGridConfig::loadStaticData() { KisConfig cfg(true); m_lineTypeMain = LineTypeInternal(cfg.getGridMainStyle()); m_lineTypeSubdivision = LineTypeInternal(cfg.getGridSubdivisionStyle()); m_colorMain = cfg.getGridMainColor(); m_colorSubdivision = cfg.getGridSubdivisionColor(); } void KisGridConfig::saveStaticData() const { KisConfig cfg(false); cfg.setGridMainStyle(m_lineTypeMain); cfg.setGridSubdivisionStyle(m_lineTypeSubdivision); cfg.setGridMainColor(m_colorMain); cfg.setGridSubdivisionColor(m_colorSubdivision); } QDomElement KisGridConfig::saveDynamicDataToXml(QDomDocument& doc, const QString &tag) const { QDomElement gridElement = doc.createElement(tag); KisDomUtils::saveValue(&gridElement, "showGrid", m_showGrid); KisDomUtils::saveValue(&gridElement, "snapToGrid", m_snapToGrid); KisDomUtils::saveValue(&gridElement, "offset", m_offset); KisDomUtils::saveValue(&gridElement, "spacing", m_spacing); KisDomUtils::saveValue(&gridElement, "offsetAspectLocked", m_offsetAspectLocked); KisDomUtils::saveValue(&gridElement, "spacingAspectLocked", m_spacingAspectLocked); KisDomUtils::saveValue(&gridElement, "subdivision", m_subdivision); KisDomUtils::saveValue(&gridElement, "angleLeft", m_angleLeft); KisDomUtils::saveValue(&gridElement, "angleRight", m_angleRight); KisDomUtils::saveValue(&gridElement, "cellSpacing", m_cellSpacing); KisDomUtils::saveValue(&gridElement, "gridType", m_gridType); return gridElement; } bool KisGridConfig::loadDynamicDataFromXml(const QDomElement &gridElement) { bool result = true; result &= KisDomUtils::loadValue(gridElement, "showGrid", &m_showGrid); result &= KisDomUtils::loadValue(gridElement, "snapToGrid", &m_snapToGrid); result &= KisDomUtils::loadValue(gridElement, "offset", &m_offset); result &= KisDomUtils::loadValue(gridElement, "spacing", &m_spacing); result &= KisDomUtils::loadValue(gridElement, "offsetAspectLocked", &m_offsetAspectLocked); result &= KisDomUtils::loadValue(gridElement, "spacingAspectLocked", &m_spacingAspectLocked); result &= KisDomUtils::loadValue(gridElement, "subdivision", &m_subdivision); result &= KisDomUtils::loadValue(gridElement, "angleLeft", &m_angleLeft); result &= KisDomUtils::loadValue(gridElement, "angleRight", &m_angleRight); - result &= KisDomUtils::loadValue(gridElement, "gridType", &m_gridType); + result &= KisDomUtils::loadValue(gridElement, "gridType", (int*)(&m_gridType)); return result; } diff --git a/libs/ui/canvas/kis_grid_config.h b/libs/ui/canvas/kis_grid_config.h index d590d1dec8..9042fba49a 100644 --- a/libs/ui/canvas/kis_grid_config.h +++ b/libs/ui/canvas/kis_grid_config.h @@ -1,249 +1,257 @@ /* * 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_GRID_CONFIG_H #define __KIS_GRID_CONFIG_H #include #include #include #include #include "kritaui_export.h" class QDomElement; class QDomDocument; class KRITAUI_EXPORT KisGridConfig : boost::equality_comparable { public: enum LineTypeInternal { LINE_SOLID = 0, LINE_DASHED, LINE_DOTTED }; + + enum GridType { + GRID_RECTANGULAR = 0, + GRID_ISOMETRIC + }; + public: KisGridConfig() : m_showGrid(false), m_snapToGrid(false), m_spacing(20,20), m_offsetAspectLocked(true), m_spacingAspectLocked(true), m_angleLeft(45), m_angleRight(45), m_cellSpacing(30), - m_gridType(0), + m_gridType(GRID_RECTANGULAR), m_subdivision(2), m_lineTypeMain(LINE_SOLID), m_lineTypeSubdivision(LINE_DOTTED), m_colorMain(200, 200, 200, 200), m_colorSubdivision(200, 200, 200, 150) { loadStaticData(); } bool operator==(const KisGridConfig &rhs) const { return m_showGrid == rhs.m_showGrid && m_snapToGrid == rhs.m_snapToGrid && m_spacing == rhs.m_spacing && m_offset == rhs.m_offset && m_offsetAspectLocked == rhs.m_offsetAspectLocked && m_spacingAspectLocked == rhs.m_spacingAspectLocked && m_angleRight == rhs.m_angleRight && m_angleLeft == rhs.m_angleLeft && m_gridType == rhs.m_gridType && m_cellSpacing == rhs.m_cellSpacing && m_subdivision == rhs.m_subdivision && m_lineTypeMain == rhs.m_lineTypeMain && m_lineTypeSubdivision == rhs.m_lineTypeSubdivision && m_colorMain == rhs.m_colorMain && m_colorSubdivision == rhs.m_colorSubdivision; } bool showGrid() const { return m_showGrid; } void setShowGrid(bool value) { m_showGrid = value; } bool snapToGrid() const { return m_snapToGrid; } void setSnapToGrid(bool value) { m_snapToGrid = value; } QPoint offset() const { return m_offset; } void setOffset(const QPoint &value) { m_offset = value; } QPoint spacing() const { return m_spacing; } void setSpacing(const QPoint &value) { m_spacing = value; } int subdivision() const { return m_subdivision; } void setSubdivision(int value) { m_subdivision = value; } int angleLeft() const { return m_angleLeft; } void setAngleLeft(int angle) { m_angleLeft = angle; } int angleRight() const { return m_angleRight; } void setAngleRight(int angle) { m_angleRight = angle; } int cellSpacing() const { return m_cellSpacing; } void setCellSpacing(int spacing) { m_cellSpacing = spacing; } - int gridType() const { + GridType gridType() const { return m_gridType; } - void setGridType(int type) { + void setGridType(GridType type) { m_gridType = type; } bool offsetAspectLocked() const { return m_offsetAspectLocked; } void setOffsetAspectLocked(bool value) { m_offsetAspectLocked = value; } bool spacingAspectLocked() const { return m_spacingAspectLocked; } void setSpacingAspectLocked(bool value) { m_spacingAspectLocked = value; } LineTypeInternal lineTypeMain() const { return m_lineTypeMain; } void setLineTypeMain(LineTypeInternal value) { m_lineTypeMain = value; } LineTypeInternal lineTypeSubdivision() const { return m_lineTypeSubdivision; } void setLineTypeSubdivision(LineTypeInternal value) { m_lineTypeSubdivision = value; } QColor colorMain() const { return m_colorMain; } void setColorMain(const QColor &value) { m_colorMain = value; } QColor colorSubdivision() const { return m_colorSubdivision; } void setColorSubdivision(const QColor &value) { m_colorSubdivision = value; } QPen penMain() const { return QPen(m_colorMain, 0, toPenStyle(m_lineTypeMain)); } QPen penSubdivision() const { return QPen(m_colorSubdivision, 0, toPenStyle(m_lineTypeSubdivision)); } void loadStaticData(); void saveStaticData() const; QDomElement saveDynamicDataToXml(QDomDocument& doc, const QString &tag) const; bool loadDynamicDataFromXml(const QDomElement &parent); static const KisGridConfig& defaultGrid(); bool isDefault() const { return *this == defaultGrid(); } /// Transform the grids using the given \p tranform. Please note that \p transform /// should be in 'image' coordinate system. /// Used with image-wide transformations. void transform(const QTransform &transform); private: static Qt::PenStyle toPenStyle(LineTypeInternal type) { return type == LINE_SOLID ? Qt::SolidLine : type == LINE_DASHED ? Qt::DashLine : type == LINE_DOTTED ? Qt::DotLine : Qt::DashDotDotLine; } private: // Dynamic data. Stored in KisDocument. bool m_showGrid; bool m_snapToGrid; QPoint m_spacing; bool m_offsetAspectLocked; bool m_spacingAspectLocked; int m_angleLeft; int m_angleRight; int m_cellSpacing; - int m_gridType; // 0 is rectangle, 1 is isometric + + + GridType m_gridType; int m_subdivision; QPoint m_offset; // Static data. Stored in the Krita config. LineTypeInternal m_lineTypeMain; LineTypeInternal m_lineTypeSubdivision; QColor m_colorMain; QColor m_colorSubdivision; }; Q_DECLARE_METATYPE(KisGridConfig) #endif /* __KIS_GRID_CONFIG_H */ diff --git a/libs/ui/canvas/kis_grid_decoration.cpp b/libs/ui/canvas/kis_grid_decoration.cpp index b82a257c4d..3491cda446 100644 --- a/libs/ui/canvas/kis_grid_decoration.cpp +++ b/libs/ui/canvas/kis_grid_decoration.cpp @@ -1,225 +1,225 @@ /* * This file is part of Krita * * Copyright (c) 2006 Cyrille Berger * Copyright (c) 2014 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_grid_decoration.h" #include #include #include #include #include #include "kis_grid_config.h" #include "kis_coordinates_converter.h" struct KisGridDecoration::Private { KisGridConfig config; }; KisGridDecoration::KisGridDecoration(KisView* parent) : KisCanvasDecoration("grid", parent), m_d(new Private) { setPriority(0); } KisGridDecoration::~KisGridDecoration() { } void KisGridDecoration::setGridConfig(const KisGridConfig &config) { m_d->config = config; } void KisGridDecoration::drawDecoration(QPainter& gc, const QRectF& updateArea, const KisCoordinatesConverter* converter, KisCanvas2* canvas) { if (!m_d->config.showGrid()) return; Q_UNUSED(canvas); QTransform transform = converter->imageToWidgetTransform(); const qreal scale = KoUnit::approxTransformScale(transform); const int minWidgetSize = 3; const int effectiveSize = qMin(m_d->config.spacing().x(), m_d->config.spacing().y()); int scaleCoeff = 1; quint32 subdivision = m_d->config.subdivision(); while (qFloor(scale * scaleCoeff * effectiveSize) <= minWidgetSize) { if (subdivision > 1) { scaleCoeff = subdivision; subdivision = 1; } else { scaleCoeff *= 2; } if (scaleCoeff > 32768) { qWarning() << "WARNING: Grid Scale Coeff is too high! That is surely a bug!"; return; } } const QPen mainPen = m_d->config.penMain(); const QPen subdivisionPen = m_d->config.penSubdivision(); gc.save(); gc.setTransform(transform); gc.setRenderHints(QPainter::Antialiasing, false); gc.setRenderHints(QPainter::HighQualityAntialiasing, false); qreal x1, y1, x2, y2; QRectF imageRect = converter->documentToImage(updateArea) & converter->imageRectInImagePixels(); imageRect.getCoords(&x1, &y1, &x2, &y2); // for angles. This will later be a combobox to select different types of options // also add options to hide specific lines (vertical, horizonta, angle 1, etc - int gridType = m_d->config.gridType(); + KisGridConfig::GridType gridType = m_d->config.gridType(); - if (gridType == 0) { // rectangle + if (gridType == KisGridConfig::GRID_RECTANGULAR) { { // vertical lines const int offset = m_d->config.offset().x(); const int step = scaleCoeff * m_d->config.spacing().x(); const int lineIndexFirst = qCeil((x1 - offset) / step); const int lineIndexLast = qFloor((x2 - offset) / step); for (int i = lineIndexFirst; i <= lineIndexLast; i++) { int w = offset + i * step; gc.setPen(i % subdivision == 0 ? mainPen : subdivisionPen); gc.drawLine(QPointF(w, y1),QPointF(w, y2)); } } { // horizontal lines const int offset = m_d->config.offset().y(); const int step = scaleCoeff * m_d->config.spacing().y(); const int lineIndexFirst = qCeil((y1 - offset) / step); const int lineIndexLast = qFloor((y2 - offset) / step); for (int i = lineIndexFirst; i <= lineIndexLast; i++) { int w = offset + i * step; gc.setPen(i % subdivision == 0 ? mainPen : subdivisionPen); gc.drawLine(QPointF(x1, w),QPointF(x2, w)); } } } - if (gridType== 1) { // isometric + if (gridType == KisGridConfig::GRID_ISOMETRIC) { const int offset = m_d->config.offset().x(); const int offsetY = m_d->config.offset().y(); const int step = scaleCoeff * m_d->config.spacing().y(); const int lineIndexFirst = qCeil((y1 - offset) / step); const int cellSpacing = m_d->config.cellSpacing(); gc.setClipping(true); gc.setClipRect(imageRect, Qt::IntersectClip); // left angle { int gridXAngle = m_d->config.angleLeft(); int counter = lineIndexFirst; int bottomRightOfImageY = y2; // this should be the height of the image int finalY = 0; // figure out the spacing based off the angle. The spacing needs to be perpendicular to the angle, // so we need to do a bit of trig to get the correct spacing. float correctedAngleSpacing = cellSpacing; if (gridXAngle > 0) { correctedAngleSpacing = cellSpacing / qCos( qDegreesToRadians((float)gridXAngle)); } while (finalY < bottomRightOfImageY) { int w = (counter * correctedAngleSpacing) - offsetY; gc.setPen(mainPen); // calculate where the ending point will be based off the angle int startingY = w; int horizontalDistance = x2; int length2 = qTan( qDegreesToRadians((float)gridXAngle)) * x2; // qTan takes radians, so convert first before sending it finalY = startingY - length2; gc.drawLine(QPointF(x1, w),QPointF(horizontalDistance, finalY)); counter = counter +1; } } // right angle (almost the same thing, except starting the lines on the right side) { int gridXAngle = m_d->config.angleRight(); // TODO: add another angle property int counter = lineIndexFirst; int bottomLeftOfImageY = y2; int finalY = 0; // figure out the spacing based off the angle float correctedAngleSpacing = cellSpacing; if (gridXAngle > 0) { correctedAngleSpacing = cellSpacing / qCos( qDegreesToRadians((float)gridXAngle)); } while (finalY < bottomLeftOfImageY) { int w = (counter * correctedAngleSpacing) - offsetY - offset; gc.setPen(mainPen); // calculate where the ending point will be based off the angle int startingY = w; int horizontalDistance = x2; // distance is the same (width of the image) int length2 = qTan( qDegreesToRadians((float)gridXAngle)) * horizontalDistance; // qTan takes radians, so convert first before sending it finalY = startingY - length2; gc.drawLine(QPointF(x2, w),QPointF(0, finalY)); counter = counter +1; } } } gc.restore(); } diff --git a/libs/ui/dialogs/kis_dlg_import_image_sequence.cpp b/libs/ui/dialogs/kis_dlg_import_image_sequence.cpp index 696b025fa4..3080238307 100644 --- a/libs/ui/dialogs/kis_dlg_import_image_sequence.cpp +++ b/libs/ui/dialogs/kis_dlg_import_image_sequence.cpp @@ -1,163 +1,174 @@ /* * Copyright (c) 2016 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_dlg_import_image_sequence.h" #include "KisDocument.h" #include "KisMainWindow.h" #include "kis_image.h" #include "kis_image_animation_interface.h" #include "KisImportExportManager.h" #include "KoFileDialog.h" #include +#include class KisDlgImportImageSequence::ListItem : QListWidgetItem { public: ListItem(const QString &text, QListWidget *view, QCollator *collator) - : QListWidgetItem(text, view), - collator(collator) + : QListWidgetItem(text, view) + , collator(collator) {} bool operator <(const QListWidgetItem &other) const override { - int cmp = collator->compare(this->text(), other.text()); - return cmp < 0; + if (collator->numericMode()) { + const QRegExp rx(QLatin1Literal("[^0-9]+")); + QStringList ours = text().split(rx, QString::SkipEmptyParts); + QStringList theirs = other.text().split(rx, QString::SkipEmptyParts); + + // Let's compare the last numbers -- they are most likely to be the serial numbers + if (ours.size() > 0 && theirs.size() > 0) { + return (ours.last().toInt() < theirs.last().toInt()); + } + + } + return collator->compare(this->text(), other.text()) < 0; } private: QCollator *collator; }; KisDlgImportImageSequence::KisDlgImportImageSequence(KisMainWindow *mainWindow, KisDocument *document) : KoDialog(mainWindow), m_mainWindow(mainWindow), m_document(document) { setButtons(Ok | Cancel); setDefaultButton(Ok); QWidget * page = new QWidget(this); m_ui.setupUi(page); setMainWidget(page); enableButtonOk(false); m_ui.cmbOrder->addItem(i18n("Ascending"), Ascending); m_ui.cmbOrder->addItem(i18n("Descending"), Descending); m_ui.cmbOrder->setCurrentIndex(0); m_ui.cmbSortMode->addItem(i18n("Alphabetical"), Natural); m_ui.cmbSortMode->addItem(i18n("Numerical"), Numerical); m_ui.cmbSortMode->setCurrentIndex(1); m_ui.lstFiles->setSelectionMode(QAbstractItemView::ExtendedSelection); connect(m_ui.btnAddImages, &QAbstractButton::clicked, this, &KisDlgImportImageSequence::slotAddFiles); connect(m_ui.btnRemove, &QAbstractButton::clicked, this, &KisDlgImportImageSequence::slotRemoveFiles); connect(m_ui.spinStep, SIGNAL(valueChanged(int)), this, SLOT(slotSkipChanged(int))); connect(m_ui.cmbOrder, SIGNAL(currentIndexChanged(int)), this, SLOT(slotOrderOptionsChanged(int))); connect(m_ui.cmbSortMode, SIGNAL(currentIndexChanged(int)), this, SLOT(slotOrderOptionsChanged(int))); // cold initialization of the controls slotSkipChanged(m_ui.spinStep->value()); slotOrderOptionsChanged(m_ui.cmbOrder->currentIndex()); slotOrderOptionsChanged(m_ui.cmbSortMode->currentIndex()); } QStringList KisDlgImportImageSequence::files() { QStringList list; for (int i=0; i < m_ui.lstFiles->count(); i++) { list.append(m_ui.lstFiles->item(i)->text()); } return list; } int KisDlgImportImageSequence::firstFrame() { return m_ui.spinFirstFrame->value(); } int KisDlgImportImageSequence::step() { return m_ui.spinStep->value(); } void KisDlgImportImageSequence::slotAddFiles() { QStringList urls = showOpenFileDialog(); if (!urls.isEmpty()) { Q_FOREACH(QString url, urls) { new ListItem(url, m_ui.lstFiles, &m_collator); } sortFileList(); } enableButtonOk(m_ui.lstFiles->count() > 0); } QStringList KisDlgImportImageSequence::showOpenFileDialog() { KoFileDialog dialog(this, KoFileDialog::ImportFiles, "OpenDocument"); dialog.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)); dialog.setMimeTypeFilters(KisImportExportManager::supportedMimeTypes(KisImportExportManager::Import)); dialog.setCaption(i18n("Import Images")); return dialog.filenames(); } void KisDlgImportImageSequence::slotRemoveFiles() { QList selected = m_ui.lstFiles->selectedItems(); Q_FOREACH(QListWidgetItem *item, selected) { delete item; } enableButtonOk(m_ui.lstFiles->count() > 0); } void KisDlgImportImageSequence::slotSkipChanged(int) { int documentFps = m_document->image()->animationInterface()->framerate(); float sourceFps = 1.0f * documentFps / m_ui.spinStep->value(); m_ui.lblFramerate->setText(i18n("Source fps: %1", sourceFps)); } void KisDlgImportImageSequence::slotOrderOptionsChanged(int) { sortFileList(); } void KisDlgImportImageSequence::sortFileList() { int order = m_ui.cmbOrder->currentData().toInt(); bool numeric = m_ui.cmbSortMode->currentData().toInt() == Numerical; m_collator.setNumericMode(numeric); m_ui.lstFiles->sortItems((order == Ascending) ? Qt::AscendingOrder : Qt::DescendingOrder); } diff --git a/libs/ui/flake/KisReferenceImagesLayer.h b/libs/ui/flake/KisReferenceImagesLayer.h index 302f4fe7f7..a8b8eb4a94 100644 --- a/libs/ui/flake/KisReferenceImagesLayer.h +++ b/libs/ui/flake/KisReferenceImagesLayer.h @@ -1,72 +1,74 @@ /* * Copyright (C) 2017 Jouni Pentikäinen * * 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 KRITA_KISREFERENCEIMAGESLAYER_H #define KRITA_KISREFERENCEIMAGESLAYER_H #include "kis_shape_layer.h" #include class KisDocument; class KRITAUI_EXPORT KisReferenceImagesLayer : public KisShapeLayer { Q_OBJECT public: KisReferenceImagesLayer(KoShapeControllerBase* shapeController, KisImageWSP image); KisReferenceImagesLayer(const KisReferenceImagesLayer &rhs); static KUndo2Command * addReferenceImages(KisDocument *document, QList referenceImages); KUndo2Command * removeReferenceImages(KisDocument *document, QList referenceImages); QVector referenceImages() const; QRectF boundingImageRect() const; QColor getPixel(QPointF position) const; void paintReferences(QPainter &painter); bool allowAsChild(KisNodeSP) const override; bool accept(KisNodeVisitor&) override; void accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter) override; KisNodeSP clone() const override { return new KisReferenceImagesLayer(*this); } bool isFakeNode() const override; Q_SIGNALS: /** * The content of the layer has changed, and the canvas decoration * needs to update. */ void sigUpdateCanvas(const QRectF &rect); private: void signalUpdate(const QRectF &rect); friend struct AddReferenceImagesCommand; friend struct RemoveReferenceImagesCommand; friend class ReferenceImagesCanvas; }; +typedef KisSharedPtr KisReferenceImagesLayerSP; + #endif //KRITA_KISREFERENCEIMAGESLAYER_H diff --git a/libs/ui/forms/wdgimportimagesequence.ui b/libs/ui/forms/wdgimportimagesequence.ui index 7526c61bec..8b94c4cf26 100644 --- a/libs/ui/forms/wdgimportimagesequence.ui +++ b/libs/ui/forms/wdgimportimagesequence.ui @@ -1,129 +1,133 @@ WdgImportImageSequence 0 0 575 428 Dialog - + + + true + + Add images... Remove Order Timing Start at The frame number for the first image 9999 Step Number of frames between images 1 999 Source FPS: KisIntParseSpinBox QSpinBox
kis_int_parse_spin_box.h
diff --git a/libs/ui/input/kis_zoom_and_rotate_action.cpp b/libs/ui/input/kis_zoom_and_rotate_action.cpp index 088ff7a701..889879fdd3 100644 --- a/libs/ui/input/kis_zoom_and_rotate_action.cpp +++ b/libs/ui/input/kis_zoom_and_rotate_action.cpp @@ -1,81 +1,85 @@ /* * This file is part of the KDE project * Copyright (C) 2019 Sharaf Zaman * * 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_zoom_and_rotate_action.h" #include "kis_zoom_action.h" #include "kis_rotate_canvas_action.h" class KisZoomAndRotateAction::Private { public: Private(): zoomAction(new KisZoomAction), rotateAction(new KisRotateCanvasAction) {} - KisZoomAction *zoomAction; - KisRotateCanvasAction *rotateAction; + QScopedPointer zoomAction; + QScopedPointer rotateAction; }; KisZoomAndRotateAction::KisZoomAndRotateAction() : KisAbstractInputAction ("Zoom and Rotate Canvas") , d(new Private) { setName(i18n("Zoom and Rotate Canvas")); } +KisZoomAndRotateAction::~KisZoomAndRotateAction() +{ +} + int KisZoomAndRotateAction::priority() const { // if rotation is disabled, we set the lowest priority, so that, this action isn't triggered return KisConfig(true).disableTouchRotation() ? 0 : 5; } void KisZoomAndRotateAction::activate(int shortcut) { d->zoomAction->activate(shortcut); d->rotateAction->activate(shortcut); } void KisZoomAndRotateAction::deactivate(int shortcut) { d->zoomAction->deactivate(shortcut); d->rotateAction->deactivate(shortcut); } void KisZoomAndRotateAction::begin(int shortcut, QEvent *event) { d->zoomAction->begin(shortcut, event); d->rotateAction->begin(shortcut, event); } void KisZoomAndRotateAction::cursorMovedAbsolute(const QPointF &lastPos, const QPointF &pos) { d->zoomAction->cursorMovedAbsolute(lastPos, pos); d->rotateAction->cursorMovedAbsolute(lastPos, pos); } void KisZoomAndRotateAction::inputEvent(QEvent *event) { d->zoomAction->inputEvent(event); d->rotateAction->inputEvent(event); } KisInputActionGroup KisZoomAndRotateAction::inputActionGroup(int shortcut) const { Q_UNUSED(shortcut); return ViewTransformActionGroup; } diff --git a/libs/ui/input/kis_zoom_and_rotate_action.h b/libs/ui/input/kis_zoom_and_rotate_action.h index 73da68bf9e..b0aa049436 100644 --- a/libs/ui/input/kis_zoom_and_rotate_action.h +++ b/libs/ui/input/kis_zoom_and_rotate_action.h @@ -1,48 +1,50 @@ /* * This file is part of the KDE project * Copyright (C) 2019 Sharaf Zaman * * 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_ZOOM_AND_ROTATE_ACTION_H #define KIS_ZOOM_AND_ROTATE_ACTION_H #include "kis_abstract_input_action.h" - +#include /** * @brief This class merely deligates the actions to KisZoomAction * _and_ KisRotateCanvasAction at the same time. */ class KisZoomAndRotateAction : public KisAbstractInputAction { public: KisZoomAndRotateAction(); + ~KisZoomAndRotateAction(); + int priority() const override; void activate(int shortcut) override; void deactivate(int shortcut) override; void begin(int shortcut, QEvent *event) override; void cursorMovedAbsolute(const QPointF &lastPos, const QPointF &pos) override; void inputEvent(QEvent* event) override; KisInputActionGroup inputActionGroup(int shortcut) const override; private: class Private; - const Private *d; + const QScopedPointer d; }; #endif // KIS_ZOOM_AND_ROTATE_ACTION_H diff --git a/libs/ui/widgets/kis_cie_tongue_widget.cpp b/libs/ui/widgets/kis_cie_tongue_widget.cpp index dbb1f2628f..a88c8c363b 100644 --- a/libs/ui/widgets/kis_cie_tongue_widget.cpp +++ b/libs/ui/widgets/kis_cie_tongue_widget.cpp @@ -1,735 +1,734 @@ /* * Copyright (C) 2015 by Wolthera van Hövell tot Westerflier * * Based on the Digikam CIE Tongue widget * Copyright (C) 2006-2013 by Gilles Caulier * * Any source code are inspired from lprof project and * Copyright (C) 1998-2001 Marti Maria * * 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, 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. **/ /** The following table gives the CIE color matching functions \f$\bar{x}(\lambda)\f$, \f$\bar{y}(\lambda)\f$, and \f$\bar{z}(\lambda)\f$, for wavelengths \f$\lambda\f$ at 5 nanometer increments from 380 nm through 780 nm. This table is used in conjunction with Planck's law for the energy spectrum of a black body at a given temperature to plot the black body curve on the CIE chart. The following table gives the spectral chromaticity co-ordinates \f$x(\lambda)\f$ and \f$y(\lambda)\f$ for wavelengths in 5 nanometer increments from 380 nm through 780 nm. These coordinates represent the position in the CIE x-y space of pure spectral colors of the given wavelength, and thus define the outline of the CIE "tongue" diagram. */ #include #include #include #include #include #include #include #include #include #include #include #include "kis_cie_tongue_widget.h" static const double spectral_chromaticity[81][3] = { { 0.1741, 0.0050 }, // 380 nm { 0.1740, 0.0050 }, { 0.1738, 0.0049 }, { 0.1736, 0.0049 }, { 0.1733, 0.0048 }, { 0.1730, 0.0048 }, { 0.1726, 0.0048 }, { 0.1721, 0.0048 }, { 0.1714, 0.0051 }, { 0.1703, 0.0058 }, { 0.1689, 0.0069 }, { 0.1669, 0.0086 }, { 0.1644, 0.0109 }, { 0.1611, 0.0138 }, { 0.1566, 0.0177 }, { 0.1510, 0.0227 }, { 0.1440, 0.0297 }, { 0.1355, 0.0399 }, { 0.1241, 0.0578 }, { 0.1096, 0.0868 }, { 0.0913, 0.1327 }, { 0.0687, 0.2007 }, { 0.0454, 0.2950 }, { 0.0235, 0.4127 }, { 0.0082, 0.5384 }, { 0.0039, 0.6548 }, { 0.0139, 0.7502 }, { 0.0389, 0.8120 }, { 0.0743, 0.8338 }, { 0.1142, 0.8262 }, { 0.1547, 0.8059 }, { 0.1929, 0.7816 }, { 0.2296, 0.7543 }, { 0.2658, 0.7243 }, { 0.3016, 0.6923 }, { 0.3373, 0.6589 }, { 0.3731, 0.6245 }, { 0.4087, 0.5896 }, { 0.4441, 0.5547 }, { 0.4788, 0.5202 }, { 0.5125, 0.4866 }, { 0.5448, 0.4544 }, { 0.5752, 0.4242 }, { 0.6029, 0.3965 }, { 0.6270, 0.3725 }, { 0.6482, 0.3514 }, { 0.6658, 0.3340 }, { 0.6801, 0.3197 }, { 0.6915, 0.3083 }, { 0.7006, 0.2993 }, { 0.7079, 0.2920 }, { 0.7140, 0.2859 }, { 0.7190, 0.2809 }, { 0.7230, 0.2770 }, { 0.7260, 0.2740 }, { 0.7283, 0.2717 }, { 0.7300, 0.2700 }, { 0.7311, 0.2689 }, { 0.7320, 0.2680 }, { 0.7327, 0.2673 }, { 0.7334, 0.2666 }, { 0.7340, 0.2660 }, { 0.7344, 0.2656 }, { 0.7346, 0.2654 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 }, { 0.7347, 0.2653 } // 780 nm }; class Q_DECL_HIDDEN KisCIETongueWidget::Private { public: Private() : profileDataAvailable(false), needUpdatePixmap(false), cieTongueNeedsUpdate(true), uncalibratedColor(false), xBias(0), yBias(0), pxcols(0), pxrows(0), progressCount(0), gridside(0), progressTimer(0), Primaries(9), whitePoint(3) { progressPix = KPixmapSequence("process-working", KisIconUtils::SizeSmallMedium); } bool profileDataAvailable; bool needUpdatePixmap; bool cieTongueNeedsUpdate; bool uncalibratedColor; int xBias; int yBias; int pxcols; int pxrows; int progressCount; // Position of animation during loading/calculation. double gridside; QPainter painter; QTimer* progressTimer; QPixmap pixmap; QPixmap cietongue; QPixmap gamutMap; KPixmapSequence progressPix; QVector Primaries; QVector whitePoint; QPolygonF gamut; model colorModel; }; KisCIETongueWidget::KisCIETongueWidget(QWidget *parent) : QWidget(parent), d(new Private) { d->progressTimer = new QTimer(this); setAttribute(Qt::WA_DeleteOnClose); d->Primaries.resize(9); d->Primaries.fill(0.0); d->whitePoint.resize(3); d->whitePoint<<0.34773<<0.35952<<1.0; d->gamut = QPolygonF(); connect(d->progressTimer, SIGNAL(timeout()), this, SLOT(slotProgressTimerDone())); } KisCIETongueWidget::~KisCIETongueWidget() { delete d; } int KisCIETongueWidget::grids(double val) const { return (int) floor(val * d->gridside + 0.5); } void KisCIETongueWidget::setProfileData(QVector p, QVector w, bool profileData) { d->profileDataAvailable = profileData; if (profileData){ d->Primaries= p; d->whitePoint = w; d->needUpdatePixmap = true; } else { return; } } void KisCIETongueWidget::setGamut(QPolygonF gamut) { d->gamut=gamut; } void KisCIETongueWidget::setRGBData(QVector whitepoint, QVector colorants) { if (colorants.size()==9){ d->Primaries= colorants; d->whitePoint = whitepoint; d->needUpdatePixmap = true; d->colorModel = KisCIETongueWidget::RGBA; d->profileDataAvailable = true; } else { return; } } void KisCIETongueWidget::setCMYKData(QVector whitepoint) { if (whitepoint.size()==3){ //d->Primaries= colorants; d->whitePoint = whitepoint; d->needUpdatePixmap = true; d->colorModel = KisCIETongueWidget::CMYKA; d->profileDataAvailable = true; } else { return; } } void KisCIETongueWidget::setXYZData(QVector whitepoint) { if (whitepoint.size()==3){ d->whitePoint = whitepoint; d->needUpdatePixmap = true; d->colorModel = KisCIETongueWidget::XYZA; d->profileDataAvailable = true; } else { return; } } void KisCIETongueWidget::setGrayData(QVector whitepoint) { if (whitepoint.size()==3){ d->whitePoint = whitepoint; d->needUpdatePixmap = true; d->colorModel = KisCIETongueWidget::GRAYA; d->profileDataAvailable = true; } else { return; } } void KisCIETongueWidget::setLABData(QVector whitepoint) { if (whitepoint.size()==3){ d->whitePoint = whitepoint; d->needUpdatePixmap = true; d->colorModel = KisCIETongueWidget::LABA; d->profileDataAvailable = true; } else { return; } } void KisCIETongueWidget::setYCbCrData(QVector whitepoint) { if (whitepoint.size()==3){ d->whitePoint = whitepoint; d->needUpdatePixmap = true; d->colorModel = KisCIETongueWidget::YCbCrA; d->profileDataAvailable = true; } else { return; } } void KisCIETongueWidget::setProfileDataAvailable(bool dataAvailable) { d->profileDataAvailable = dataAvailable; } void KisCIETongueWidget::mapPoint(int& icx, int& icy, QPointF xy) { icx = (int) floor((xy.x() * (d->pxcols - 1)) + .5); icy = (int) floor(((d->pxrows - 1) - xy.y() * (d->pxrows - 1)) + .5); } void KisCIETongueWidget::biasedLine(int x1, int y1, int x2, int y2) { d->painter.drawLine(x1 + d->xBias, y1, x2 + d->xBias, y2); } void KisCIETongueWidget::biasedText(int x, int y, const QString& txt) { d->painter.drawText(QPoint(d->xBias + x, y), txt); } QRgb KisCIETongueWidget::colorByCoord(double x, double y) { // Get xyz components scaled from coordinates double cx = ((double) x) / (d->pxcols - 1); double cy = 1.0 - ((double) y) / (d->pxrows - 1); double cz = 1.0 - cx - cy; // Project xyz to XYZ space. Note that in this // particular case we are substituting XYZ with xyz //Need to use KoColor here. const KoColorSpace* xyzColorSpace = KoColorSpaceRegistry::instance()->colorSpace("XYZA", "U8"); quint8 data[4]; data[0]= cx*255; data[1]= cy*255; data[2]= cz*255; data[3]= 1.0*255; KoColor colXYZ(data, xyzColorSpace); QColor colRGB = colXYZ.toQColor(); return qRgb(colRGB.red(), colRGB.green(), colRGB.blue()); } void KisCIETongueWidget::outlineTongue() { int lx=0, ly=0; int fx=0, fy=0; for (int x = 380; x <= 700; x += 5) { int ix = (x - 380) / 5; QPointF * p = new QPointF(spectral_chromaticity[ix][0], spectral_chromaticity[ix][1]); int icx, icy; mapPoint(icx, icy, * p); if (x > 380) { biasedLine(lx, ly, icx, icy); } else { fx = icx; fy = icy; } lx = icx; ly = icy; } biasedLine(lx, ly, fx, fy); } void KisCIETongueWidget::fillTongue() { QImage Img = d->cietongue.toImage(); int x; for (int y = 0; y < d->pxrows; ++y) { int xe = 0; // Find horizontal extents of tongue on this line. for (x = 0; x < d->pxcols; ++x) { if (QColor(Img.pixel(x + d->xBias, y)) != QColor(Qt::black)) { for (xe = d->pxcols - 1; xe >= x; --xe) { if (QColor(Img.pixel(xe + d->xBias, y)) != QColor(Qt::black)) { break; } } break; } } if (x < d->pxcols) { for ( ; x <= xe; ++x) { QRgb Color = colorByCoord(x, y); Img.setPixel(x + d->xBias, y, Color); } } } d->cietongue = QPixmap::fromImage(Img, Qt::AvoidDither); } void KisCIETongueWidget::drawTongueAxis() { QFont font; font.setPointSize(6); d->painter.setFont(font); d->painter.setPen(qRgb(255, 255, 255)); biasedLine(0, 0, 0, d->pxrows - 1); biasedLine(0, d->pxrows-1, d->pxcols-1, d->pxrows - 1); for (int y = 1; y <= 9; y += 1) { QString s; int xstart = (y * (d->pxcols - 1)) / 10; int ystart = (y * (d->pxrows - 1)) / 10; s.sprintf("0.%d", y); biasedLine(xstart, d->pxrows - grids(1), xstart, d->pxrows - grids(4)); biasedText(xstart - grids(11), d->pxrows + grids(15), s); s.sprintf("0.%d", 10 - y); biasedLine(0, ystart, grids(3), ystart); biasedText(grids(-25), ystart + grids(5), s); } } void KisCIETongueWidget::drawTongueGrid() { d->painter.setPen(qRgb(128, 128, 128)); d->painter.setOpacity(0.5); for (int y = 1; y <= 9; y += 1) { int xstart = (y * (d->pxcols - 1)) / 10; int ystart = (y * (d->pxrows - 1)) / 10; biasedLine(xstart, grids(4), xstart, d->pxrows - grids(4) - 1); biasedLine(grids(7), ystart, d->pxcols-1-grids(7), ystart); } d->painter.setOpacity(1.0); } void KisCIETongueWidget::drawLabels() { QFont font; font.setPointSize(5); d->painter.setFont(font); for (int x = 450; x <= 650; x += (x > 470 && x < 600) ? 5 : 10) { QString wl; int bx = 0, by = 0, tx, ty; if (x < 520) { bx = grids(-22); by = grids(2); } else if (x < 535) { bx = grids(-8); by = grids(-6); } else { bx = grids(4); } int ix = (x - 380) / 5; - QPointF * p = new QPointF(spectral_chromaticity[ix][0], - spectral_chromaticity[ix][1]); + QPointF p(spectral_chromaticity[ix][0], spectral_chromaticity[ix][1]); int icx, icy; - mapPoint(icx, icy, * p); + mapPoint(icx, icy, p); tx = icx + ((x < 520) ? grids(-2) : ((x >= 535) ? grids(2) : 0)); ty = icy + ((x < 520) ? 0 : ((x >= 535) ? grids(-1) : grids(-2))); d->painter.setPen(qRgb(255, 255, 255)); biasedLine(icx, icy, tx, ty); QRgb Color = colorByCoord(icx, icy); d->painter.setPen(Color); wl.sprintf("%d", x); biasedText(icx+bx, icy+by, wl); } } void KisCIETongueWidget::drawSmallEllipse(QPointF xy, int r, int g, int b, int sz) { int icx, icy; mapPoint(icx, icy, xy); d->painter.save(); d->painter.setRenderHint(QPainter::Antialiasing); d->painter.setPen(qRgb(r, g, b)); d->painter.drawEllipse(icx + d->xBias- sz/2, icy-sz/2, sz, sz); d->painter.setPen(qRgb(r/2, g/2, b/2)); int sz2 = sz-2; d->painter.drawEllipse(icx + d->xBias- sz2/2, icy-sz2/2, sz2, sz2); d->painter.restore(); } void KisCIETongueWidget::drawColorantTriangle() { d->painter.save(); d->painter.setPen(qRgb(80, 80, 80)); d->painter.setRenderHint(QPainter::Antialiasing); if (d->colorModel ==KisCIETongueWidget::RGBA) { drawSmallEllipse((QPointF(d->Primaries[0],d->Primaries[1])), 255, 128, 128, 6); drawSmallEllipse((QPointF(d->Primaries[3],d->Primaries[4])), 128, 255, 128, 6); drawSmallEllipse((QPointF(d->Primaries[6],d->Primaries[7])), 128, 128, 255, 6); int x1, y1, x2, y2, x3, y3; mapPoint(x1, y1, (QPointF(d->Primaries[0],d->Primaries[1])) ); mapPoint(x2, y2, (QPointF(d->Primaries[3],d->Primaries[4])) ); mapPoint(x3, y3, (QPointF(d->Primaries[6],d->Primaries[7])) ); biasedLine(x1, y1, x2, y2); biasedLine(x2, y2, x3, y3); biasedLine(x3, y3, x1, y1); } /*else if (d->colorModel ==CMYK){ for (i=0; iPrimaries.size();i+++){ drawSmallEllipse((QPointF(d->Primaries[0],d->Primaries[1])), 160, 160, 160, 6);//greyscale for now //int x1, y1, x2, y2; //mapPoint(x1, y1, (QPointF(d->Primaries[i],d->Primaries[i+1])) ); //mapPoint(x2, y2, (QPointF(d->Primaries[i+3],d->Primaries[i+4])) ); //biasedLine(x1, y1, x2, y2); } } */ d->painter.restore(); } void KisCIETongueWidget::drawWhitePoint() { drawSmallEllipse(QPointF (d->whitePoint[0],d->whitePoint[1]), 255, 255, 255, 8); } void KisCIETongueWidget::drawGamut() { d->gamutMap=QPixmap(size()); d->gamutMap.fill(Qt::black); QPainter gamutPaint; gamutPaint.begin(&d->gamutMap); QPainterPath path; //gamutPaint.setCompositionMode(QPainter::CompositionMode_Clear); gamutPaint.setRenderHint(QPainter::Antialiasing); path.setFillRule(Qt::WindingFill); gamutPaint.setBrush(Qt::white); gamutPaint.setPen(Qt::white); int x, y = 0; if (!d->gamut.empty()) { gamutPaint.setOpacity(0.5); if (d->colorModel == KisCIETongueWidget::RGBA) { mapPoint(x, y, (QPointF(d->Primaries[0],d->Primaries[1])) ); path.moveTo(QPointF(x + d->xBias,y)); mapPoint(x, y, (QPointF(d->Primaries[3],d->Primaries[4])) ); path.lineTo(QPointF(x + d->xBias,y)); mapPoint(x, y, (QPointF(d->Primaries[6],d->Primaries[7])) ); path.lineTo(QPointF(x + d->xBias,y)); mapPoint(x, y, (QPointF(d->Primaries[0],d->Primaries[1])) ); path.lineTo(QPointF(x + d->xBias,y)); } gamutPaint.drawPath(path); gamutPaint.setOpacity(1.0); foreach (QPointF Point, d->gamut) { mapPoint(x, y, Point); gamutPaint.drawEllipse(x + d->xBias- 2, y-2, 4, 4); //Point.setX(x); //Point.setY(y); //path.lineTo(Point); } } gamutPaint.end(); d->painter.save(); d->painter.setOpacity(0.5); d->painter.setCompositionMode(QPainter::CompositionMode_Multiply); QRect area(d->xBias, 0, d->pxcols, d->pxrows); d->painter.drawPixmap(area,d->gamutMap, area); d->painter.setOpacity(1.0); d->painter.restore(); } void KisCIETongueWidget::updatePixmap() { d->needUpdatePixmap = false; d->pixmap = QPixmap(size()); if (d->cieTongueNeedsUpdate){ // Draw the CIE tongue curve. I don't see why we need to redraw it every time the whitepoint and such changes so we cache it. d->cieTongueNeedsUpdate = false; d->cietongue = QPixmap(size()); d->cietongue.fill(Qt::black); d->painter.begin(&d->cietongue); int pixcols = d->pixmap.width(); int pixrows = d->pixmap.height(); d->gridside = (qMin(pixcols, pixrows)) / 512.0; d->xBias = grids(32); d->yBias = grids(20); d->pxcols = pixcols - d->xBias; d->pxrows = pixrows - d->yBias; d->painter.setBackground(QBrush(qRgb(0, 0, 0))); d->painter.setPen(qRgb(255, 255, 255)); outlineTongue(); d->painter.end(); fillTongue(); d->painter.begin(&d->cietongue); drawTongueAxis(); drawLabels(); drawTongueGrid(); d->painter.end(); } d->pixmap = d->cietongue; d->painter.begin(&d->pixmap); //draw whitepoint and colorants if (d->whitePoint[2] > 0.0) { drawWhitePoint(); } if (d->Primaries[2] != 0.0) { drawColorantTriangle(); } drawGamut(); d->painter.end(); } void KisCIETongueWidget::paintEvent(QPaintEvent*) { QPainter p(this); // Widget is disable : drawing grayed frame. if ( !isEnabled() ) { p.fillRect(0, 0, width(), height(), palette().color(QPalette::Disabled, QPalette::Background)); QPen pen(palette().color(QPalette::Disabled, QPalette::Foreground)); pen.setStyle(Qt::SolidLine); pen.setWidth(1); p.setPen(pen); p.drawRect(0, 0, width(), height()); return; } // No profile data to show, or RAW file if (!d->profileDataAvailable) { p.fillRect(0, 0, width(), height(), palette().color(QPalette::Active, QPalette::Background)); QPen pen(palette().color(QPalette::Active, QPalette::Text)); pen.setStyle(Qt::SolidLine); pen.setWidth(1); p.setPen(pen); p.drawRect(0, 0, width(), height()); if (d->uncalibratedColor) { p.drawText(0, 0, width(), height(), Qt::AlignCenter, i18n("Uncalibrated color space")); } else { p.setPen(Qt::red); p.drawText(0, 0, width(), height(), Qt::AlignCenter, i18n("No profile available...")); } return; } // Create CIE tongue if needed if (d->needUpdatePixmap) { updatePixmap(); } // draw prerendered tongue p.drawPixmap(0, 0, d->pixmap); } void KisCIETongueWidget::resizeEvent(QResizeEvent* event) { Q_UNUSED(event); setMinimumHeight(width()); setMaximumHeight(width()); d->needUpdatePixmap = true; d->cieTongueNeedsUpdate = true; } void KisCIETongueWidget::slotProgressTimerDone() { update(); d->progressTimer->start(200); } diff --git a/libs/ui/widgets/kis_multi_double_filter_widget.cc b/libs/ui/widgets/kis_multi_double_filter_widget.cc index 704351653e..452d393cb7 100644 --- a/libs/ui/widgets/kis_multi_double_filter_widget.cc +++ b/libs/ui/widgets/kis_multi_double_filter_widget.cc @@ -1,122 +1,122 @@ /* * 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 "widgets/kis_multi_double_filter_widget.h" #include #include #include #include #include #include #include #include KisDelayedActionDoubleInput::KisDelayedActionDoubleInput(QWidget * parent, const QString & name) : KisDoubleParseSpinBox(parent) { setObjectName(name); m_timer = new QTimer(this); m_timer->setObjectName(name); m_timer->setSingleShot(true); connect(m_timer, SIGNAL(timeout()), SLOT(slotValueChanged())); connect(this, SIGNAL(valueChanged(double)), SLOT(slotTimeToUpdate())); } void KisDelayedActionDoubleInput::slotTimeToUpdate() { m_timer->start(50); } void KisDelayedActionDoubleInput::slotValueChanged() { emit valueChangedDelayed(value()); } void KisDelayedActionDoubleInput::cancelDelayedSignal() { m_timer->stop(); } KisDoubleWidgetParam::KisDoubleWidgetParam(double nmin, double nmax, double ninitvalue, const QString & nlabel, const QString & nname) : min(nmin), max(nmax), initvalue(ninitvalue), label(nlabel), name(nname) { } KisMultiDoubleFilterWidget::KisMultiDoubleFilterWidget(const QString & filterid, QWidget * parent, const QString & caption, vKisDoubleWidgetParam dwparam) : KisConfigWidget(parent), m_filterid(filterid) { m_nbdoubleWidgets = dwparam.size(); this->setWindowTitle(caption); QGridLayout *widgetLayout = new QGridLayout(this); widgetLayout->setColumnStretch(1, 1); widgetLayout->setContentsMargins(0,0,0,0); widgetLayout->setHorizontalSpacing(0); - m_doubleWidgets = new KisDelayedActionDoubleInput*[ m_nbdoubleWidgets ]; + m_doubleWidgets.resize(m_nbdoubleWidgets); for (qint32 i = 0; i < m_nbdoubleWidgets; ++i) { m_doubleWidgets[i] = new KisDelayedActionDoubleInput(this, dwparam[i].name); m_doubleWidgets[i]->setRange(dwparam[i].min, dwparam[i].max); m_doubleWidgets[i]->setValue(dwparam[i].initvalue); m_doubleWidgets[i]->cancelDelayedSignal(); connect(m_doubleWidgets[i], SIGNAL(valueChangedDelayed(double)), SIGNAL(sigConfigurationItemChanged())); QLabel* lbl = new QLabel(dwparam[i].label + ':', this); widgetLayout->addWidget(lbl, i , 0); widgetLayout->addWidget(m_doubleWidgets[i], i , 1); } widgetLayout->setRowStretch(m_nbdoubleWidgets,1); QSpacerItem * sp = new QSpacerItem(1, 1); widgetLayout->addItem(sp, m_nbdoubleWidgets, 0); } void KisMultiDoubleFilterWidget::setConfiguration(const KisPropertiesConfigurationSP config) { if (!config) return; for (int i = 0; i < m_nbdoubleWidgets ; ++i) { KisDelayedActionDoubleInput * w = m_doubleWidgets[i]; if (w) { double val = config->getDouble(m_doubleWidgets[i]->objectName()); m_doubleWidgets[i]->setValue(val); m_doubleWidgets[i]->cancelDelayedSignal(); } } } KisPropertiesConfigurationSP KisMultiDoubleFilterWidget::configuration() const { KisFilterConfigurationSP config = new KisFilterConfiguration(m_filterid, 0); for (int i = 0; i < nbValues(); ++i) { config->setProperty(m_doubleWidgets[i]->objectName(), m_doubleWidgets[i]->value()); } return config; } diff --git a/libs/ui/widgets/kis_multi_double_filter_widget.h b/libs/ui/widgets/kis_multi_double_filter_widget.h index 18d84eee82..3735f79401 100644 --- a/libs/ui/widgets/kis_multi_double_filter_widget.h +++ b/libs/ui/widgets/kis_multi_double_filter_widget.h @@ -1,84 +1,84 @@ /* * 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_MULTI_DOUBLE_FILTER_WIDGET_H_ #define _KIS_MULTI_DOUBLE_FILTER_WIDGET_H_ #include #include #include #include "kritaui_export.h" #include "kis_double_parse_spin_box.h" class KisDelayedActionDoubleInput : public KisDoubleParseSpinBox { Q_OBJECT public: KisDelayedActionDoubleInput(QWidget * parent, const QString & name); void cancelDelayedSignal(); private Q_SLOTS: void slotValueChanged(); void slotTimeToUpdate(); Q_SIGNALS: void valueChangedDelayed(double value); private: QTimer * m_timer; }; struct KRITAUI_EXPORT KisDoubleWidgetParam { KisDoubleWidgetParam(double nmin, double nmax, double ninitvalue, const QString & label, const QString & nname); double min; double max; double initvalue; QString label; QString name; }; typedef std::vector vKisDoubleWidgetParam; class KRITAUI_EXPORT KisMultiDoubleFilterWidget : public KisConfigWidget { Q_OBJECT public: KisMultiDoubleFilterWidget(const QString & filterid, QWidget * parent, const QString & caption, vKisDoubleWidgetParam dwparam); void setConfiguration(const KisPropertiesConfigurationSP cfg) override; KisPropertiesConfigurationSP configuration() const override; public: inline qint32 nbValues() const { return m_nbdoubleWidgets; } inline double valueAt(qint32 i) { return m_doubleWidgets[i]->value(); } private: - KisDelayedActionDoubleInput** m_doubleWidgets; + QVector m_doubleWidgets; qint32 m_nbdoubleWidgets; QString m_filterid; }; #endif diff --git a/libs/ui/widgets/kis_widget_chooser.cpp b/libs/ui/widgets/kis_widget_chooser.cpp index abc69561b6..02305e048d 100644 --- a/libs/ui/widgets/kis_widget_chooser.cpp +++ b/libs/ui/widgets/kis_widget_chooser.cpp @@ -1,287 +1,282 @@ /* * 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_widget_chooser.h" #include #include #include #include #include #include #include #include #include #include #include #include "kis_config.h" KisWidgetChooser::KisWidgetChooser(int id, QWidget* parent) : QFrame(parent) , m_chooserid(id) { // QFrame::setFrameStyle(QFrame::StyledPanel|QFrame::Raised); m_acceptIcon = KisIconUtils::loadIcon("list-add"); m_buttons = new QButtonGroup(); m_popup = new QFrame(0, Qt::Popup); m_arrowButton = new QToolButton(); m_popup->setFrameStyle(QFrame::Panel|QFrame::Raised); m_arrowButton->setFixedWidth(m_arrowButton->sizeHint().height()/2); m_arrowButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum); m_arrowButton->setAutoRaise(true); updateArrowIcon(); connect(m_arrowButton, SIGNAL(clicked(bool)), SLOT(slotButtonPressed())); } KisWidgetChooser::~KisWidgetChooser() { delete m_buttons; delete m_popup; delete m_arrowButton; } void KisWidgetChooser::updateArrowIcon() { QImage image(16, 16, QImage::Format_ARGB32); image.fill(0); QStylePainter painter(&image, this); QStyleOption option; option.rect = image.rect(); option.palette = palette(); option.state = QStyle::State_Enabled; option.palette.setBrush(QPalette::ButtonText, option.palette.text()); painter.setBrush(option.palette.text().color()); painter.setPen(option.palette.text().color()); painter.drawPrimitive(QStyle::PE_IndicatorArrowDown, option); m_arrowButton->setIcon(QIcon(QPixmap::fromImage(image))); } -void KisWidgetChooser::addWidget(const QString& id, const QString& label, QWidget* widget) +void KisWidgetChooser::addLabelWidget(const QString& id, const QString& label, QWidget* widget) { - if(id.isEmpty()) { - delete widget; - return; - } - removeWidget(id); if (label.isEmpty()) { m_widgets.push_back(Data(id, widget, 0)); } else { m_widgets.push_back(Data(id, widget, new QLabel(label))); } delete m_popup->layout(); m_popup->setLayout(createPopupLayout()); m_popup->adjustSize(); delete QWidget::layout(); QWidget::setLayout(createLayout()); } QLayout* KisWidgetChooser::createLayout() { QHBoxLayout* layout = new QHBoxLayout(); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); for(Iterator i=m_widgets.begin(); i!=m_widgets.end(); ++i) { if(i->chosen) { if (i->label) { layout->addWidget(i->label); } layout->addWidget(i->widget); break; } } layout->addWidget(m_arrowButton); return layout; } QLayout* KisWidgetChooser::createPopupLayout() { QGridLayout* layout = new QGridLayout(); int row = 0; int idx = 0; layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); QButtonGroup* group = new QButtonGroup(); QList buttons = m_buttons->buttons(); for(Iterator i=m_widgets.begin(); i!=m_widgets.end(); ++i) { if(!i->chosen) { if(row == buttons.size()) { QToolButton* bn = new QToolButton(); m_acceptIcon = KisIconUtils::loadIcon("list-add"); bn->setIcon(m_acceptIcon); bn->setAutoRaise(true); buttons.push_back(bn); } if (i->label) { layout->addWidget(i->label , row, 0); layout->addWidget(i->widget , row, 1); layout->addWidget(buttons[row], row, 2); } else { layout->addWidget(i->widget , row, 0); layout->addWidget(buttons[row], row, 1); } group->addButton(buttons[row], idx); ++row; } ++idx; } for(int i=row; ichosen) { delete m_popup->layout(); m_popup->setLayout(createPopupLayout()); m_popup->adjustSize(); } else delete QWidget::layout(); if (data->label) { delete data->label; } delete data->widget; m_widgets.erase(data); } } QWidget* KisWidgetChooser::chooseWidget(const QString& id) { QWidget* chosenWidget = 0; for(Iterator i=m_widgets.begin(); i!=m_widgets.end(); ++i) { if(i->id == id) { chosenWidget = i->widget; i->chosen = true; } else i->chosen = false; } delete m_popup->layout(); m_popup->setLayout(createPopupLayout()); m_popup->adjustSize(); delete QWidget::layout(); QWidget::setLayout(createLayout()); KisConfig cfg(false); cfg.setToolbarSlider(m_chooserid, id); return chosenWidget; } QWidget* KisWidgetChooser::getWidget(const QString& id) const { ConstIterator data = std::find(m_widgets.begin(), m_widgets.end(), Data(id)); if(data != m_widgets.end()) return data->widget; return 0; } void KisWidgetChooser::showPopupWidget() { QSize popSize = m_popup->size(); QRect popupRect(QFrame::mapToGlobal(QPoint(-1, QFrame::height())), popSize); // Get the available geometry of the screen which contains this KisPopupButton QRect screenRect = QApplication::desktop()->availableGeometry(this); // Make sure the popup is not drawn outside the screen area if(popupRect.right() > screenRect.right()) popupRect.translate(screenRect.right() - popupRect.right(), 0); if(popupRect.left() < screenRect.left()) popupRect.translate(screenRect.left() - popupRect.left(), 0); if(popupRect.bottom() > screenRect.bottom()) popupRect.translate(0, -popupRect.height()); m_popup->setGeometry(popupRect); m_popup->show(); } void KisWidgetChooser::updateThemedIcons() { for (int i = 0; i < m_buttons->buttons().length(); i++) { if ( m_buttons->button(i)) { m_buttons->button(i)->setIcon(KisIconUtils::loadIcon("list-add")); } } } void KisWidgetChooser::slotButtonPressed() { showPopupWidget(); } void KisWidgetChooser::slotWidgetChosen(int index) { chooseWidget(m_widgets[index].id); m_popup->hide(); } void KisWidgetChooser::changeEvent(QEvent *e) { QFrame::changeEvent(e); switch (e->type()) { case QEvent::StyleChange: case QEvent::PaletteChange: case QEvent::EnabledChange: updateArrowIcon(); break; default: ; } } diff --git a/libs/ui/widgets/kis_widget_chooser.h b/libs/ui/widgets/kis_widget_chooser.h index 30c222e357..e8bfc6f519 100644 --- a/libs/ui/widgets/kis_widget_chooser.h +++ b/libs/ui/widgets/kis_widget_chooser.h @@ -1,106 +1,110 @@ /* * 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 H_KIS_WIDGET_CHOOSER_H_ #define H_KIS_WIDGET_CHOOSER_H_ #include #include #include #include class QToolButton; class QLabel; class QButtonGroup; class KRITAUI_EXPORT KisWidgetChooser: public QFrame { Q_OBJECT struct Data { Data(const QString& ID): id(ID), widget(0), label(0), chosen(false) { } Data(const Data& d): id(d.id), widget(d.widget), label(d.label), chosen(d.chosen) { } Data(const QString& ID, QWidget* w, QLabel* l): id(ID), widget(w), label(l), chosen(false) { } friend bool operator == (const Data& a, const Data& b) { return a.id == b.id; } QString id; QWidget* widget; QLabel* label; bool chosen; }; typedef QList::iterator Iterator; typedef QList::const_iterator ConstIterator; public: KisWidgetChooser(int id, QWidget* parent=0); ~KisWidgetChooser() override; QWidget* chooseWidget(const QString& id); - void addWidget(const QString& id, const QString& label, QWidget* widget); + void addLabelWidget(const QString& id, const QString& label, QWidget* widget); QWidget* getWidget(const QString& id) const; template TWidget* addWidget(const QString& id, const QString& label = "") { + if (id.isEmpty()) { + return 0; + } + TWidget* widget = new TWidget(); - addWidget(id, label, widget); + addLabelWidget(id, label, widget); return widget; } template TWidget* getWidget(const QString& id) const { return dynamic_cast(getWidget(id)); } public Q_SLOTS: void showPopupWidget(); void updateThemedIcons(); private: void removeWidget(const QString& id); QLayout* createPopupLayout(); QLayout* createLayout(); void updateArrowIcon(); protected Q_SLOTS: void slotButtonPressed(); void slotWidgetChosen(int index); // QWidget interface protected: void changeEvent(QEvent *e) override; private: int m_chooserid; QIcon m_acceptIcon; QToolButton* m_arrowButton; QButtonGroup* m_buttons; QFrame* m_popup; QString m_chosenID; QList m_widgets; }; #endif // H_KIS_WIDGET_CHOOSER_H_ diff --git a/libs/widgets/KoTagFilterWidget.cpp b/libs/widgets/KoTagFilterWidget.cpp index 654bec1548..36448d79bf 100644 --- a/libs/widgets/KoTagFilterWidget.cpp +++ b/libs/widgets/KoTagFilterWidget.cpp @@ -1,140 +1,134 @@ /* * This file is part of the KDE project * Copyright (c) 2002 Patrick Julien * Copyright (c) 2007 Jan Hambrecht * Copyright (c) 2007 Sven Langkamp * Copyright (C) 2011 Srikanth Tiyyagura * Copyright (c) 2011 José Luis Vergara * Copyright (c) 2013 Sascha Suelzer * * 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 "KoTagFilterWidget.h" #include #include #include #include #include #include class KoTagFilterWidget::Private { public: QString tagSearchBarTooltip_saving_disabled; QString tagSearchBarTooltip_saving_enabled; QLineEdit* tagSearchLineEdit; QPushButton* tagSearchSaveButton; QGridLayout* filterBarLayout; }; KoTagFilterWidget::KoTagFilterWidget(QWidget* parent): QWidget(parent) ,d( new Private()) { - d->tagSearchBarTooltip_saving_disabled = i18nc ( + QString searchTooltipMaintext = i18nc( "@info:tooltip", - "Entering search terms here will add to, or remove resources from the current tag view." + "

Enter search terms here to add resources to, or remove them from, the current tag view.

" "

To filter based on the partial, case insensitive name of a resource:
" - "partialname or !partialname.

" - "

In-/exclusion of other tag sets:
" - "[Tagname] or ![Tagname].

" - "

Case sensitive and full name matching in-/exclusion:
" - "\"ExactMatch\" or !\"ExactMatch\".

" - "Filter results cannot be saved for the All Presets view.
" - "In this view, pressing Enter or clearing the filter box will restore all items.
" - "Create and/or switch to a different tag if you want to save filtered resources into named sets." - ); - - d->tagSearchBarTooltip_saving_enabled = i18nc ( + "partialname or !partialname

" + "

To include or exclude other tag sets:
" + "[Tagname] or ![Tagname]

" + "

For case sensitive and full name matching in-/exclusion:
" + "\"ExactMatch\" or !\"ExactMatch\"

"); + + d->tagSearchBarTooltip_saving_disabled = searchTooltipMaintext + i18nc( "@info:tooltip", - "Entering search terms here will add to, or remove resources from the current tag view." - "

To filter based on the partial, case insensitive name of a resource:
" - "partialname or !partialname.

" - "

In-/exclusion of other tag sets:
" - "[Tagname] or ![Tagname].

" - "

Case sensitive and full name matching in-/exclusion:
" - "\"ExactMatch\" or !\"ExactMatch\".

" - "Pressing Enter or clicking the Save button will save the changes." - ); + "

Filter results cannot be saved for the All Presets view. " + "In this view, pressing Enter or clearing the filter box will restore all items. " + "Create and/or switch to a different tag if you want to save filtered resources into named sets.

"); + + d->tagSearchBarTooltip_saving_enabled = searchTooltipMaintext + i18nc( + "@info:tooltip", + "

Pressing Enter or clicking the Save button will save the changes.

"); QGridLayout* filterBarLayout = new QGridLayout; d->tagSearchLineEdit = new QLineEdit(this); d->tagSearchLineEdit->setClearButtonEnabled(true); d->tagSearchLineEdit->setPlaceholderText(i18n("Search")); d->tagSearchLineEdit->setToolTip(d->tagSearchBarTooltip_saving_disabled); d->tagSearchLineEdit->setEnabled(true); filterBarLayout->setSpacing(0); filterBarLayout->setMargin(0); filterBarLayout->setColumnStretch(0, 1); filterBarLayout->addWidget(d->tagSearchLineEdit, 0, 0); d->tagSearchSaveButton = new QPushButton(this); d->tagSearchSaveButton->setIcon(koIcon("media-floppy")); d->tagSearchSaveButton->setToolTip(i18nc("@info:tooltip", "Save the currently filtered set as the new members of the current tag.")); d->tagSearchSaveButton->setEnabled(false); filterBarLayout->addWidget(d->tagSearchSaveButton, 0, 1); connect(d->tagSearchSaveButton, SIGNAL(pressed()), this, SLOT(onSaveButtonClicked())); connect(d->tagSearchLineEdit, SIGNAL(returnPressed()), this, SLOT(onSaveButtonClicked())); connect(d->tagSearchLineEdit, SIGNAL(textChanged(QString)), this, SLOT(onTextChanged(QString))); allowSave(false); this->setLayout(filterBarLayout); } KoTagFilterWidget::~KoTagFilterWidget() { delete d; } void KoTagFilterWidget::allowSave(bool allow) { if (allow) { d->tagSearchSaveButton->show(); d->tagSearchLineEdit->setToolTip(d->tagSearchBarTooltip_saving_enabled); } else { d->tagSearchSaveButton->hide(); d->tagSearchLineEdit->setToolTip(d->tagSearchBarTooltip_saving_disabled); } } void KoTagFilterWidget::clear() { d->tagSearchLineEdit->clear(); d->tagSearchSaveButton->setEnabled(false); } void KoTagFilterWidget::onTextChanged(const QString& lineEditText) { d->tagSearchSaveButton->setEnabled(!lineEditText.isEmpty()); emit filterTextChanged(lineEditText); } void KoTagFilterWidget::onSaveButtonClicked() { emit saveButtonClicked(); clear(); } diff --git a/libs/widgetutils/kis_action_registry.cpp b/libs/widgetutils/kis_action_registry.cpp index 5e51b78d52..56bcc8742c 100644 --- a/libs/widgetutils/kis_action_registry.cpp +++ b/libs/widgetutils/kis_action_registry.cpp @@ -1,419 +1,423 @@ /* * 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 3 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 #include "kis_debug.h" #include "KoResourcePaths.h" #include "kis_icon_utils.h" #include "kis_action_registry.h" #include "kshortcutschemeshelper_p.h" namespace { /** * We associate several pieces of information with each shortcut. The first * piece of information is a QDomElement, containing the raw data from the * .action XML file. The second and third are QKeySequences, the first of * which is the default shortcut, the last of which is any custom shortcut. * The last two are the KActionCollection and KActionCategory used to * organize the shortcut editor. */ struct ActionInfoItem { QDomElement xmlData; QString collectionName; QString categoryName; inline QList defaultShortcuts() const { return m_defaultShortcuts; } inline void setDefaultShortcuts(const QList &value) { m_defaultShortcuts = value; } inline QList customShortcuts() const { return m_customShortcuts; } inline void setCustomShortcuts(const QList &value, bool explicitlyReset) { m_customShortcuts = value; m_explicitlyReset = explicitlyReset; } inline QList effectiveShortcuts() const { return m_customShortcuts.isEmpty() && !m_explicitlyReset ? m_defaultShortcuts : m_customShortcuts; } private: QList m_defaultShortcuts; QList m_customShortcuts; bool m_explicitlyReset = false; }; // Convenience macros to extract text of a child node. QString getChildContent(QDomElement xml, QString node) { return xml.firstChildElement(node).text(); } // Use Krita debug logging categories instead of KDE's default qDebug() for // harmless empty strings and translations QString quietlyTranslate(const QString &s) { if (s.isEmpty()) { return s; } QString translatedString = i18nc("action", s.toUtf8()); if (translatedString == s) { translatedString = i18n(s.toUtf8()); } if (translatedString.isEmpty()) { dbgAction << "No translation found for" << s; return s; } return translatedString; } } class Q_DECL_HIDDEN KisActionRegistry::Private { public: Private(KisActionRegistry *_q) : q(_q) {} // This is the main place containing ActionInfoItems. QMap actionInfoList; void loadActionFiles(); void loadCustomShortcuts(QString filename = QStringLiteral("kritashortcutsrc")); // XXX: this adds a default item for the given name to the list of actioninfo objects! ActionInfoItem &actionInfo(const QString &name) { if (!actionInfoList.contains(name)) { dbgAction << "Tried to look up info for unknown action" << name; } return actionInfoList[name]; } KisActionRegistry *q; QSet sanityPropertizedShortcuts; }; Q_GLOBAL_STATIC(KisActionRegistry, s_instance) KisActionRegistry *KisActionRegistry::instance() { if (!s_instance.exists()) { dbgRegistry << "initializing KoActionRegistry"; } return s_instance; } bool KisActionRegistry::hasAction(const QString &name) const { return d->actionInfoList.contains(name); } KisActionRegistry::KisActionRegistry() : d(new KisActionRegistry::Private(this)) { KConfigGroup cg = KSharedConfig::openConfig()->group("Shortcut Schemes"); QString schemeName = cg.readEntry("Current Scheme", "Default"); loadShortcutScheme(schemeName); loadCustomShortcuts(); } +KisActionRegistry::~KisActionRegistry() +{ +} + KisActionRegistry::ActionCategory KisActionRegistry::fetchActionCategory(const QString &name) const { if (!d->actionInfoList.contains(name)) return ActionCategory(); const ActionInfoItem info = d->actionInfoList.value(name); return ActionCategory(info.collectionName, info.categoryName); } void KisActionRegistry::notifySettingsUpdated() { d->loadCustomShortcuts(); } void KisActionRegistry::loadCustomShortcuts() { d->loadCustomShortcuts(); } void KisActionRegistry::loadShortcutScheme(const QString &schemeName) { // Load scheme file if (schemeName != QStringLiteral("Default")) { QString schemeFileName = KShortcutSchemesHelper::schemeFileLocations().value(schemeName); if (schemeFileName.isEmpty()) { return; } KConfig schemeConfig(schemeFileName, KConfig::SimpleConfig); applyShortcutScheme(&schemeConfig); } else { // Apply default scheme, updating KisActionRegistry data applyShortcutScheme(); } } QAction * KisActionRegistry::makeQAction(const QString &name, QObject *parent) { QAction * a = new QAction(parent); if (!d->actionInfoList.contains(name)) { qWarning() << "Warning: requested data for unknown action" << name; a->setObjectName(name); return a; } propertizeAction(name, a); return a; } void KisActionRegistry::settingsPageSaved() { // For now, custom shortcuts are dealt with by writing to file and reloading. loadCustomShortcuts(); // Announce UI should reload current shortcuts. emit shortcutsUpdated(); } void KisActionRegistry::applyShortcutScheme(const KConfigBase *config) { // First, update the things in KisActionRegistry d->actionInfoList.clear(); d->loadActionFiles(); if (config == 0) { // Use default shortcut scheme. Simplest just to reload everything. loadCustomShortcuts(); } else { const auto schemeEntries = config->group(QStringLiteral("Shortcuts")).entryMap(); // Load info item for each shortcut, reset custom shortcuts auto it = schemeEntries.constBegin(); while (it != schemeEntries.end()) { ActionInfoItem &info = d->actionInfo(it.key()); info.setDefaultShortcuts(QKeySequence::listFromString(it.value())); it++; } } } void KisActionRegistry::updateShortcut(const QString &name, QAction *action) { const ActionInfoItem &info = d->actionInfo(name); action->setShortcuts(info.effectiveShortcuts()); action->setProperty("defaultShortcuts", qVariantFromValue(info.defaultShortcuts())); d->sanityPropertizedShortcuts.insert(name); } bool KisActionRegistry::sanityCheckPropertized(const QString &name) { return d->sanityPropertizedShortcuts.contains(name); } QList KisActionRegistry::registeredShortcutIds() const { return d->actionInfoList.keys(); } bool KisActionRegistry::propertizeAction(const QString &name, QAction * a) { if (!d->actionInfoList.contains(name)) { warnAction << "No XML data found for action" << name; return false; } const ActionInfoItem info = d->actionInfo(name); QDomElement actionXml = info.xmlData; if (!actionXml.text().isEmpty()) { // i18n requires converting format from QString. auto getChildContent_i18n = [=](QString node){return quietlyTranslate(getChildContent(actionXml, node));}; // Note: the fields in the .action documents marked for translation are determined by extractrc. QString icon = getChildContent(actionXml, "icon"); QString text = getChildContent_i18n("text"); QString whatsthis = getChildContent_i18n("whatsThis"); QString toolTip = getChildContent_i18n("toolTip"); QString statusTip = getChildContent_i18n("statusTip"); QString iconText = getChildContent_i18n("iconText"); bool isCheckable = getChildContent(actionXml, "isCheckable") == QString("true"); a->setObjectName(name); // This is helpful, should be added more places in Krita if (!icon.isEmpty()) { a->setIcon(KisIconUtils::loadIcon(icon.toLatin1())); } a->setText(text); a->setObjectName(name); a->setWhatsThis(whatsthis); a->setToolTip(toolTip); a->setStatusTip(statusTip); a->setIconText(iconText); a->setCheckable(isCheckable); } updateShortcut(name, a); return true; } QString KisActionRegistry::getActionProperty(const QString &name, const QString &property) { ActionInfoItem info = d->actionInfo(name); QDomElement actionXml = info.xmlData; if (actionXml.text().isEmpty()) { dbgAction << "No XML data found for action" << name; return QString(); } return getChildContent(actionXml, property); } void KisActionRegistry::Private::loadActionFiles() { QStringList actionDefinitions = KoResourcePaths::findAllResources("kis_actions", "*.action", KoResourcePaths::Recursive); // Extract actions all XML .action files. Q_FOREACH (const QString &actionDefinition, actionDefinitions) { QDomDocument doc; QFile f(actionDefinition); f.open(QFile::ReadOnly); doc.setContent(f.readAll()); QDomElement base = doc.documentElement(); // "ActionCollection" outer group QString collectionName = base.attribute("name"); QString version = base.attribute("version"); if (version != "2") { errAction << ".action XML file" << actionDefinition << "has incorrect version; skipping."; continue; } // Loop over nodes. Each of these corresponds to a // KActionCategory, producing a group of actions in the shortcut dialog. QDomElement actions = base.firstChild().toElement(); while (!actions.isNull()) { // field QDomElement categoryTextNode = actions.firstChild().toElement(); QString categoryName = quietlyTranslate(categoryTextNode.text()); // tags QDomElement actionXml = categoryTextNode.nextSiblingElement(); // Loop over individual actions while (!actionXml.isNull()) { if (actionXml.tagName() == "Action") { // Read name from format QString name = actionXml.attribute("name"); // Bad things if (name.isEmpty()) { errAction << "Unnamed action in definitions file " << actionDefinition; } else if (actionInfoList.contains(name)) { qWarning() << "NOT COOL: Duplicated action name from xml data: " << name; } else { ActionInfoItem info; info.xmlData = actionXml; // Use empty list to signify no shortcut QString shortcutText = getChildContent(actionXml, "shortcut"); if (!shortcutText.isEmpty()) { info.setDefaultShortcuts(QKeySequence::listFromString(shortcutText)); } info.categoryName = categoryName; info.collectionName = collectionName; actionInfoList.insert(name,info); } } actionXml = actionXml.nextSiblingElement(); } actions = actions.nextSiblingElement(); } } } void KisActionRegistry::Private::loadCustomShortcuts(QString filename) { const KConfigGroup localShortcuts(KSharedConfig::openConfig(filename), QStringLiteral("Shortcuts")); if (!localShortcuts.exists()) { return; } // Distinguish between two "null" states for custom shortcuts. for (auto i = actionInfoList.begin(); i != actionInfoList.end(); ++i) { if (localShortcuts.hasKey(i.key())) { QString entry = localShortcuts.readEntry(i.key(), QString()); if (entry == QStringLiteral("none")) { i.value().setCustomShortcuts(QList(), true); } else { i.value().setCustomShortcuts(QKeySequence::listFromString(entry), false); } } else { i.value().setCustomShortcuts(QList(), false); } } } KisActionRegistry::ActionCategory::ActionCategory() { } KisActionRegistry::ActionCategory::ActionCategory(const QString &_componentName, const QString &_categoryName) : componentName(_componentName), categoryName(_categoryName), m_isValid(true) { } bool KisActionRegistry::ActionCategory::isValid() const { return m_isValid && !categoryName.isEmpty() && !componentName.isEmpty(); } diff --git a/libs/widgetutils/kis_action_registry.h b/libs/widgetutils/kis_action_registry.h index fecb591eee..5126259b50 100644 --- a/libs/widgetutils/kis_action_registry.h +++ b/libs/widgetutils/kis_action_registry.h @@ -1,150 +1,151 @@ /* * 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 3 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_ACTION_REGISTRY_H #define KIS_ACTION_REGISTRY_H #include #include #include #include #include #include "kritawidgetutils_export.h" class KActionCollection; class QDomElement; class KConfigBase; class KisShortcutsDialog; /** * KisActionRegistry is intended to manage the global action configuration data * for Krita. The data come from four sources: * - .action files, containing static action configuration data in XML format, * - .rc configuration files, originally from XMLGUI and now in WidgetUtils, * - kritashortcutsrc, containing temporary shortcut configuration, and * - .shortcuts scheme files providing sets of default shortcuts, also from XMLGUI * * This class can be used as a factory by calling makeQAction. It can be used to * add standard properties such as default shortcuts and default tooltip to an * existing action with propertizeAction. If you have a custom action class * which needs to add other properties, you can use propertizeAction to add any * sort of data you wish to the .action configuration file. * * This class is also in charge of displaying the shortcut configuration dialog. * The interplay between this class, KActionCollection, KisShortcutsEditor and * so on can be complex, and is sometimes synchronized by file I/O by reading * and writing the configuration files mentioned above. * * It is a global static. Grab an ::instance(). */ class KRITAWIDGETUTILS_EXPORT KisActionRegistry : public QObject { Q_OBJECT public: static KisActionRegistry *instance(); /** * @return true if the given action exists */ bool hasAction(const QString &name) const; /** * @return value @p property for an action @p name. * * Allow flexible info structure for KisActions, etc. */ QString getActionProperty(const QString &name, const QString &property); /** * Produces a new QAction based on the .action data files. * * N.B. this action will not be saved in the registry. */ QAction *makeQAction(const QString &name, QObject *parent = 0); /** * Fills the standard QAction properties of an action. * * @return true if the action was loaded successfully. */ bool propertizeAction(const QString &name, QAction *a); /** * Called when "OK" button is pressed in settings dialog. */ void settingsPageSaved(); /** * Reload custom shortcuts from kritashortcutsrc */ void loadCustomShortcuts(); /** * Call after settings are changed. */ void notifySettingsUpdated(); // If config == 0, reload defaults void applyShortcutScheme(const KConfigBase *config = 0); struct ActionCategory { ActionCategory(); ActionCategory(const QString &_componentName, const QString &_categoryName); QString componentName; QString categoryName; bool isValid() const; private: bool m_isValid = false; }; ActionCategory fetchActionCategory(const QString &name) const; /** * Constructor. Please don't touch! */ KisActionRegistry(); + ~KisActionRegistry(); /** * @brief loadShortcutScheme * @param schemeName */ void loadShortcutScheme(const QString &schemeName); // Undocumented void updateShortcut(const QString &name, QAction *ac); bool sanityCheckPropertized(const QString &name); QList registeredShortcutIds() const; Q_SIGNALS: void shortcutsUpdated(); private: class Private; - Private * const d; + const QScopedPointer d; }; #endif /* KIS_ACTION_REGISTRY_H */ diff --git a/plugins/assistants/Assistants/kis_assistant_tool.cc b/plugins/assistants/Assistants/kis_assistant_tool.cc index a9af2756e8..bc1820bbd0 100644 --- a/plugins/assistants/Assistants/kis_assistant_tool.cc +++ b/plugins/assistants/Assistants/kis_assistant_tool.cc @@ -1,1061 +1,1070 @@ /* * Copyright (c) 2008 Cyrille Berger * Copyright (c) 2010 Geoffry Song * Copyright (c) 2017 Scott Petrovic * * 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 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 #include #include #include #include #include #include + #include #include #include #include "kis_dom_utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_global.h" #include "VanishingPointAssistant.h" #include "EditAssistantsCommand.h" #include #include KisAssistantTool::KisAssistantTool(KoCanvasBase * canvas) : KisTool(canvas, KisCursor::arrowCursor()), m_canvas(dynamic_cast(canvas)), m_assistantDrag(0), m_newAssistant(0), m_optionsWidget(0) { Q_ASSERT(m_canvas); setObjectName("tool_assistanttool"); } KisAssistantTool::~KisAssistantTool() { } void KisAssistantTool::activate(ToolActivation toolActivation, const QSet &shapes) { KisTool::activate(toolActivation, shapes); m_canvas->paintingAssistantsDecoration()->activateAssistantsEditor(); m_handles = m_canvas->paintingAssistantsDecoration()->handles(); m_handleDrag = 0; m_internalMode = MODE_CREATION; m_assistantHelperYOffset = 10; m_handleSize = 17; m_canvas->paintingAssistantsDecoration()->setHandleSize(m_handleSize); if (m_optionsWidget) { m_canvas->paintingAssistantsDecoration()->deselectAssistant(); updateToolOptionsUI(); } m_canvas->updateCanvas(); } void KisAssistantTool::deactivate() { m_canvas->paintingAssistantsDecoration()->deactivateAssistantsEditor(); m_canvas->updateCanvas(); KisTool::deactivate(); } void KisAssistantTool::beginPrimaryAction(KoPointerEvent *event) { setMode(KisTool::PAINT_MODE); m_origAssistantList = KisPaintingAssistant::cloneAssistantList(m_canvas->paintingAssistantsDecoration()->assistants()); bool newAssistantAllowed = true; KisPaintingAssistantsDecorationSP canvasDecoration = m_canvas->paintingAssistantsDecoration(); if (m_newAssistant) { m_internalMode = MODE_CREATION; *m_newAssistant->handles().back() = canvasDecoration->snapToGuide(event, QPointF(), false); if (m_newAssistant->handles().size() == m_newAssistant->numHandles()) { addAssistant(); } else { m_newAssistant->addHandle(new KisPaintingAssistantHandle(canvasDecoration->snapToGuide(event, QPointF(), false)), HandleType::NORMAL); } m_canvas->updateCanvas(); return; } m_handleDrag = 0; double minDist = 81.0; QPointF mousePos = m_canvas->viewConverter()->documentToView(canvasDecoration->snapToGuide(event, QPointF(), false));//m_canvas->viewConverter()->documentToView(event->point); // syncs the assistant handles to the handles reference we store in this tool // they can get out of sync with the way the actions and paintevents occur // we probably need to stop storing a reference in m_handles and call the assistants directly m_handles = m_canvas->paintingAssistantsDecoration()->handles(); Q_FOREACH (KisPaintingAssistantSP assistant, m_canvas->paintingAssistantsDecoration()->assistants()) { // find out which handle on all assistants is closest to the mouse position // vanishing points have "side handles", so make sure to include that { QList allAssistantHandles; allAssistantHandles.append(assistant->handles()); allAssistantHandles.append(assistant->sideHandles()); Q_FOREACH (const KisPaintingAssistantHandleSP handle, allAssistantHandles) { double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*handle)); if (dist < minDist) { minDist = dist; m_handleDrag = handle; assistantSelected(assistant); // whatever handle is the closest contains the selected assistant } } } if(m_handleDrag && assistant->id() == "perspective") { // Look for the handle which was pressed if (m_handleDrag == assistant->topLeft()) { double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*m_handleDrag)); if (dist < minDist) { minDist = dist; } m_dragStart = QPointF(assistant->topRight().data()->x(),assistant->topRight().data()->y()); m_internalMode = MODE_DRAGGING_NODE; } else if (m_handleDrag == assistant->topRight()) { double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*m_handleDrag)); if (dist < minDist) { minDist = dist; } m_internalMode = MODE_DRAGGING_NODE; m_dragStart = QPointF(assistant->topLeft().data()->x(),assistant->topLeft().data()->y()); } else if (m_handleDrag == assistant->bottomLeft()) { double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*m_handleDrag)); if (dist < minDist) { minDist = dist; } m_internalMode = MODE_DRAGGING_NODE; m_dragStart = QPointF(assistant->bottomRight().data()->x(),assistant->bottomRight().data()->y()); } else if (m_handleDrag == assistant->bottomRight()) { double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*m_handleDrag)); if (dist < minDist) { minDist = dist; } m_internalMode = MODE_DRAGGING_NODE; m_dragStart = QPointF(assistant->bottomLeft().data()->x(),assistant->bottomLeft().data()->y()); } else if (m_handleDrag == assistant->leftMiddle()) { m_internalMode = MODE_DRAGGING_TRANSLATING_TWONODES; m_dragStart = QPointF((assistant->bottomLeft().data()->x()+assistant->topLeft().data()->x())*0.5, (assistant->bottomLeft().data()->y()+assistant->topLeft().data()->y())*0.5); m_selectedNode1 = new KisPaintingAssistantHandle(assistant->topLeft().data()->x(),assistant->topLeft().data()->y()); m_selectedNode2 = new KisPaintingAssistantHandle(assistant->bottomLeft().data()->x(),assistant->bottomLeft().data()->y()); m_newAssistant = toQShared(KisPaintingAssistantFactoryRegistry::instance()->get("perspective")->createPaintingAssistant()); m_newAssistant->addHandle(assistant->topLeft(), HandleType::NORMAL ); m_newAssistant->addHandle(m_selectedNode1, HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode2, HandleType::NORMAL); m_newAssistant->addHandle(assistant->bottomLeft(), HandleType::NORMAL); m_dragEnd = event->point; m_handleDrag = 0; m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas return; } else if (m_handleDrag == assistant->rightMiddle()) { m_dragStart = QPointF((assistant->topRight().data()->x()+assistant->bottomRight().data()->x())*0.5, (assistant->topRight().data()->y()+assistant->bottomRight().data()->y())*0.5); m_internalMode = MODE_DRAGGING_TRANSLATING_TWONODES; m_selectedNode1 = new KisPaintingAssistantHandle(assistant->topRight().data()->x(),assistant->topRight().data()->y()); m_selectedNode2 = new KisPaintingAssistantHandle(assistant->bottomRight().data()->x(),assistant->bottomRight().data()->y()); m_newAssistant = toQShared(KisPaintingAssistantFactoryRegistry::instance()->get("perspective")->createPaintingAssistant()); m_newAssistant->addHandle(assistant->topRight(), HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode1, HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode2, HandleType::NORMAL); m_newAssistant->addHandle(assistant->bottomRight(), HandleType::NORMAL); m_dragEnd = event->point; m_handleDrag = 0; m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas return; } else if (m_handleDrag == assistant->topMiddle()) { m_dragStart = QPointF((assistant->topLeft().data()->x()+assistant->topRight().data()->x())*0.5, (assistant->topLeft().data()->y()+assistant->topRight().data()->y())*0.5); m_internalMode = MODE_DRAGGING_TRANSLATING_TWONODES; m_selectedNode1 = new KisPaintingAssistantHandle(assistant->topLeft().data()->x(),assistant->topLeft().data()->y()); m_selectedNode2 = new KisPaintingAssistantHandle(assistant->topRight().data()->x(),assistant->topRight().data()->y()); m_newAssistant = toQShared(KisPaintingAssistantFactoryRegistry::instance()->get("perspective")->createPaintingAssistant()); m_newAssistant->addHandle(m_selectedNode1, HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode2, HandleType::NORMAL); m_newAssistant->addHandle(assistant->topRight(), HandleType::NORMAL); m_newAssistant->addHandle(assistant->topLeft(), HandleType::NORMAL); m_dragEnd = event->point; m_handleDrag = 0; m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas return; } else if (m_handleDrag == assistant->bottomMiddle()) { m_dragStart = QPointF((assistant->bottomLeft().data()->x()+assistant->bottomRight().data()->x())*0.5, (assistant->bottomLeft().data()->y()+assistant->bottomRight().data()->y())*0.5); m_internalMode = MODE_DRAGGING_TRANSLATING_TWONODES; m_selectedNode1 = new KisPaintingAssistantHandle(assistant->bottomLeft().data()->x(),assistant->bottomLeft().data()->y()); m_selectedNode2 = new KisPaintingAssistantHandle(assistant->bottomRight().data()->x(),assistant->bottomRight().data()->y()); m_newAssistant = toQShared(KisPaintingAssistantFactoryRegistry::instance()->get("perspective")->createPaintingAssistant()); m_newAssistant->addHandle(assistant->bottomLeft(), HandleType::NORMAL); m_newAssistant->addHandle(assistant->bottomRight(), HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode2, HandleType::NORMAL); m_newAssistant->addHandle(m_selectedNode1, HandleType::NORMAL); m_dragEnd = event->point; m_handleDrag = 0; m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas return; } m_snapIsRadial = false; } else if (m_handleDrag && assistant->handles().size()>1 && (assistant->id() == "ruler" || assistant->id() == "parallel ruler" || assistant->id() == "infinite ruler" || assistant->id() == "spline")){ if (m_handleDrag == assistant->handles()[0]) { m_dragStart = *assistant->handles()[1]; } else if (m_handleDrag == assistant->handles()[1]) { m_dragStart = *assistant->handles()[0]; } else if(assistant->handles().size()==4){ if (m_handleDrag == assistant->handles()[2]) { m_dragStart = *assistant->handles()[0]; } else if (m_handleDrag == assistant->handles()[3]) { m_dragStart = *assistant->handles()[1]; } } m_snapIsRadial = false; } else if (m_handleDrag && assistant->handles().size()>2 && (assistant->id() == "ellipse" || assistant->id() == "concentric ellipse" || assistant->id() == "fisheye-point")){ m_snapIsRadial = false; if (m_handleDrag == assistant->handles()[0]) { m_dragStart = *assistant->handles()[1]; } else if (m_handleDrag == assistant->handles()[1]) { m_dragStart = *assistant->handles()[0]; } else if (m_handleDrag == assistant->handles()[2]) { m_dragStart = assistant->getEditorPosition(); m_radius = QLineF(m_dragStart, *assistant->handles()[0]); m_snapIsRadial = true; } } else { m_dragStart = assistant->getEditorPosition(); m_snapIsRadial = false; } } if (m_handleDrag) { // TODO: Shift-press should now be handled using the alternate actions // if (event->modifiers() & Qt::ShiftModifier) { // m_handleDrag->uncache(); // m_handleDrag = m_handleDrag->split()[0]; // m_handles = m_canvas->view()->paintingAssistantsDecoration()->handles(); // } m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas return; } m_assistantDrag.clear(); Q_FOREACH (KisPaintingAssistantSP assistant, m_canvas->paintingAssistantsDecoration()->assistants()) { AssistantEditorData editorShared; // shared position data between assistant tool and decoration const KisCoordinatesConverter *converter = m_canvas->coordinatesConverter(); // This code contains the click event behavior. QTransform initialTransform = converter->documentToWidgetTransform(); QPointF actionsPosition = initialTransform.map(assistant->viewportConstrainedEditorPosition(converter, editorShared.boundingSize)); // for UI editor widget controls with move, show, and delete -- disregard document transforms like rotating and mirroring. // otherwise the UI controls get awkward to use when they are at 45 degree angles or the order of controls gets flipped backwards QPointF uiMousePosition = initialTransform.map(canvasDecoration->snapToGuide(event, QPointF(), false)); QPointF iconMovePosition(actionsPosition + editorShared.moveIconPosition); QPointF iconSnapPosition(actionsPosition + editorShared.snapIconPosition); QPointF iconDeletePosition(actionsPosition + editorShared.deleteIconPosition); QRectF deleteRect(iconDeletePosition, QSizeF(editorShared.deleteIconSize, editorShared.deleteIconSize)); QRectF visibleRect(iconSnapPosition, QSizeF(editorShared.snapIconSize, editorShared.snapIconSize)); QRectF moveRect(iconMovePosition, QSizeF(editorShared.moveIconSize, editorShared.moveIconSize)); if (moveRect.contains(uiMousePosition)) { m_assistantDrag = assistant; m_cursorStart = event->point; m_currentAdjustment = QPointF(); m_internalMode = MODE_EDITING; assistantSelected(assistant); // whatever handle is the closest contains the selected assistant return; } if (deleteRect.contains(uiMousePosition)) { removeAssistant(assistant); if(m_canvas->paintingAssistantsDecoration()->assistants().isEmpty()) { m_internalMode = MODE_CREATION; } else m_internalMode = MODE_EDITING; m_canvas->updateCanvas(); return; } if (visibleRect.contains(uiMousePosition)) { newAssistantAllowed = false; assistant->setSnappingActive(!assistant->isSnappingActive()); // toggle assistant->uncache();//this updates the chache of the assistant, very important. assistantSelected(assistant); // whatever handle is the closest contains the selected assistant } } if (newAssistantAllowed==true){//don't make a new assistant when I'm just toogling visibility// QString key = m_options.availableAssistantsComboBox->model()->index( m_options.availableAssistantsComboBox->currentIndex(), 0 ).data(Qt::UserRole).toString(); m_newAssistant = toQShared(KisPaintingAssistantFactoryRegistry::instance()->get(key)->createPaintingAssistant()); m_internalMode = MODE_CREATION; m_newAssistant->addHandle(new KisPaintingAssistantHandle(canvasDecoration->snapToGuide(event, QPointF(), false)), HandleType::NORMAL); if (m_newAssistant->numHandles() <= 1) { addAssistant(); } else { m_newAssistant->addHandle(new KisPaintingAssistantHandle(canvasDecoration->snapToGuide(event, QPointF(), false)), HandleType::NORMAL); } } if (m_newAssistant) { m_newAssistant->setAssistantGlobalColorCache(m_canvas->paintingAssistantsDecoration()->globalAssistantsColor()); } m_canvas->updateCanvas(); } void KisAssistantTool::continuePrimaryAction(KoPointerEvent *event) { KisPaintingAssistantsDecorationSP canvasDecoration = m_canvas->paintingAssistantsDecoration(); if (m_handleDrag) { *m_handleDrag = event->point; //ported from the gradient tool... we need to think about this more in the future. if (event->modifiers() == Qt::ShiftModifier && m_snapIsRadial) { QLineF dragRadius = QLineF(m_dragStart, event->point); dragRadius.setLength(m_radius.length()); *m_handleDrag = dragRadius.p2(); } else if (event->modifiers() == Qt::ShiftModifier ) { QPointF move = snapToClosestAxis(event->point - m_dragStart); *m_handleDrag = m_dragStart + move; } else { *m_handleDrag = canvasDecoration->snapToGuide(event, QPointF(), false); } m_handleDrag->uncache(); m_handleCombine = 0; if (!(event->modifiers() & Qt::ShiftModifier)) { double minDist = 49.0; QPointF mousePos = m_canvas->viewConverter()->documentToView(event->point); Q_FOREACH (const KisPaintingAssistantHandleSP handle, m_handles) { if (handle == m_handleDrag) continue; double dist = KisPaintingAssistant::norm2(mousePos - m_canvas->viewConverter()->documentToView(*handle)); if (dist < minDist) { minDist = dist; m_handleCombine = handle; } } } m_canvas->updateCanvas(); } else if (m_assistantDrag) { QPointF newAdjustment = canvasDecoration->snapToGuide(event, QPointF(), false) - m_cursorStart; if (event->modifiers() == Qt::ShiftModifier ) { newAdjustment = snapToClosestAxis(newAdjustment); } Q_FOREACH (KisPaintingAssistantHandleSP handle, m_assistantDrag->handles()) { *handle += (newAdjustment - m_currentAdjustment); } if (m_assistantDrag->id()== "vanishing point"){ Q_FOREACH (KisPaintingAssistantHandleSP handle, m_assistantDrag->sideHandles()) { *handle += (newAdjustment - m_currentAdjustment); } } m_currentAdjustment = newAdjustment; m_canvas->updateCanvas(); } else { event->ignore(); } bool wasHiglightedNode = m_higlightedNode != 0; QPointF mousep = m_canvas->viewConverter()->documentToView(event->point); QList pAssistant= m_canvas->paintingAssistantsDecoration()->assistants(); Q_FOREACH (KisPaintingAssistantSP assistant, pAssistant) { if(assistant->id() == "perspective") { if ((m_higlightedNode = assistant->closestCornerHandleFromPoint(mousep))) { if (m_higlightedNode == m_selectedNode1 || m_higlightedNode == m_selectedNode2) { m_higlightedNode = 0; } else { m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas break; } } } //this following bit sets the translations for the vanishing-point handles. if(m_handleDrag && assistant->id() == "vanishing point" && assistant->sideHandles().size()==4) { //for inner handles, the outer handle gets translated. if (m_handleDrag == assistant->sideHandles()[0]) { QLineF perspectiveline = QLineF(*assistant->handles()[0], *assistant->sideHandles()[0]); qreal length = QLineF(*assistant->sideHandles()[0], *assistant->sideHandles()[1]).length(); if (length < 2.0){ length = 2.0; } length += perspectiveline.length(); perspectiveline.setLength(length); *assistant->sideHandles()[1] = perspectiveline.p2(); } else if (m_handleDrag == assistant->sideHandles()[2]){ QLineF perspectiveline = QLineF(*assistant->handles()[0], *assistant->sideHandles()[2]); qreal length = QLineF(*assistant->sideHandles()[2], *assistant->sideHandles()[3]).length(); if (length<2.0){ length=2.0; } length += perspectiveline.length(); perspectiveline.setLength(length); *assistant->sideHandles()[3] = perspectiveline.p2(); } // for outer handles, only the vanishing point is translated, but only if there's an intersection. else if (m_handleDrag == assistant->sideHandles()[1]|| m_handleDrag == assistant->sideHandles()[3]){ QPointF vanishingpoint(0,0); QLineF perspectiveline = QLineF(*assistant->sideHandles()[0], *assistant->sideHandles()[1]); QLineF perspectiveline2 = QLineF(*assistant->sideHandles()[2], *assistant->sideHandles()[3]); if (QLineF(perspectiveline2).intersect(QLineF(perspectiveline), &vanishingpoint) != QLineF::NoIntersection){ *assistant->handles()[0] = vanishingpoint; } }// and for the vanishing point itself, only the outer handles get translated. else if (m_handleDrag == assistant->handles()[0]){ QLineF perspectiveline = QLineF(*assistant->handles()[0], *assistant->sideHandles()[0]); QLineF perspectiveline2 = QLineF(*assistant->handles()[0], *assistant->sideHandles()[2]); qreal length = QLineF(*assistant->sideHandles()[0], *assistant->sideHandles()[1]).length(); qreal length2 = QLineF(*assistant->sideHandles()[2], *assistant->sideHandles()[3]).length(); if (length < 2.0) { length = 2.0; } if (length2 < 2.0) { length2=2.0; } length += perspectiveline.length(); length2 += perspectiveline2.length(); perspectiveline.setLength(length); perspectiveline2.setLength(length2); *assistant->sideHandles()[1] = perspectiveline.p2(); *assistant->sideHandles()[3] = perspectiveline2.p2(); } } } if (wasHiglightedNode && !m_higlightedNode) { m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas } } void KisAssistantTool::endPrimaryAction(KoPointerEvent *event) { setMode(KisTool::HOVER_MODE); if (m_handleDrag || m_assistantDrag) { if (m_handleDrag) { if (!(event->modifiers() & Qt::ShiftModifier) && m_handleCombine) { m_handleCombine->mergeWith(m_handleDrag); m_handleCombine->uncache(); m_handles = m_canvas->paintingAssistantsDecoration()->handles(); } m_handleDrag = m_handleCombine = 0; } else { m_assistantDrag.clear(); } dbgUI << "creating undo command..."; KUndo2Command *command = new EditAssistantsCommand(m_canvas, m_origAssistantList, KisPaintingAssistant::cloneAssistantList(m_canvas->paintingAssistantsDecoration()->assistants())); m_canvas->viewManager()->undoAdapter()->addCommand(command); dbgUI << "done"; } else if(m_internalMode == MODE_DRAGGING_TRANSLATING_TWONODES) { addAssistant(); m_internalMode = MODE_CREATION; } else { event->ignore(); } m_canvas->updateCanvas(); // TODO update only the relevant part of the canvas } void KisAssistantTool::addAssistant() { m_canvas->paintingAssistantsDecoration()->addAssistant(m_newAssistant); KisAbstractPerspectiveGrid* grid = dynamic_cast(m_newAssistant.data()); if (grid) { m_canvas->viewManager()->canvasResourceProvider()->addPerspectiveGrid(grid); } QList assistants = m_canvas->paintingAssistantsDecoration()->assistants(); KUndo2Command *addAssistantCmd = new EditAssistantsCommand(m_canvas, m_origAssistantList, KisPaintingAssistant::cloneAssistantList(assistants), EditAssistantsCommand::ADD, assistants.indexOf(m_newAssistant)); m_canvas->viewManager()->undoAdapter()->addCommand(addAssistantCmd); m_handles = m_canvas->paintingAssistantsDecoration()->handles(); m_canvas->paintingAssistantsDecoration()->setSelectedAssistant(m_newAssistant); updateToolOptionsUI(); // vanishing point assistant will get an extra option m_newAssistant.clear(); } void KisAssistantTool::removeAssistant(KisPaintingAssistantSP assistant) { QList assistants = m_canvas->paintingAssistantsDecoration()->assistants(); KisAbstractPerspectiveGrid* grid = dynamic_cast(assistant.data()); if (grid) { m_canvas->viewManager()->canvasResourceProvider()->removePerspectiveGrid(grid); } m_canvas->paintingAssistantsDecoration()->removeAssistant(assistant); KUndo2Command *removeAssistantCmd = new EditAssistantsCommand(m_canvas, m_origAssistantList, KisPaintingAssistant::cloneAssistantList(m_canvas->paintingAssistantsDecoration()->assistants()), EditAssistantsCommand::REMOVE, assistants.indexOf(assistant)); m_canvas->viewManager()->undoAdapter()->addCommand(removeAssistantCmd); m_handles = m_canvas->paintingAssistantsDecoration()->handles(); m_canvas->paintingAssistantsDecoration()->deselectAssistant(); updateToolOptionsUI(); } void KisAssistantTool::assistantSelected(KisPaintingAssistantSP assistant) { m_canvas->paintingAssistantsDecoration()->setSelectedAssistant(assistant); updateToolOptionsUI(); } void KisAssistantTool::updateToolOptionsUI() { KisPaintingAssistantSP m_selectedAssistant = m_canvas->paintingAssistantsDecoration()->selectedAssistant(); bool hasActiveAssistant = m_selectedAssistant ? true : false; if (m_selectedAssistant) { bool isVanishingPointAssistant = m_selectedAssistant->id() == "vanishing point"; m_options.vanishingPointAngleSpinbox->setVisible(isVanishingPointAssistant); if (isVanishingPointAssistant) { QSharedPointer assis = qSharedPointerCast(m_selectedAssistant); m_options.vanishingPointAngleSpinbox->setValue(assis->referenceLineDensity()); } // load custom color settings from assistant (this happens when changing assistant m_options.useCustomAssistantColor->setChecked(m_selectedAssistant->useCustomColor()); m_options.customAssistantColorButton->setColor(m_selectedAssistant->assistantCustomColor()); - float opacity = (float)m_selectedAssistant->assistantCustomColor().alpha()/255.0 * 100.0 ; - m_options.customColorOpacitySlider->setValue(opacity); + + + double opacity = (double)m_selectedAssistant->assistantCustomColor().alpha()/(double)255.00 * (double)100.00 ; + opacity = ceil(opacity); // helps keep the 0-100% slider from shifting + + m_options.customColorOpacitySlider->blockSignals(true); + m_options.customColorOpacitySlider->setValue((double)opacity); + m_options.customColorOpacitySlider->blockSignals(false); + + } else { m_options.vanishingPointAngleSpinbox->setVisible(false); // } // show/hide elements if an assistant is selected or not m_options.assistantsGlobalOpacitySlider->setVisible(hasActiveAssistant); m_options.assistantsColor->setVisible(hasActiveAssistant); m_options.globalColorLabel->setVisible(hasActiveAssistant); m_options.useCustomAssistantColor->setVisible(hasActiveAssistant); // hide custom color options if use custom color is not selected bool showCustomColorSettings = m_options.useCustomAssistantColor->isChecked() && hasActiveAssistant; m_options.customColorOpacitySlider->setVisible(showCustomColorSettings); m_options.customAssistantColorButton->setVisible(showCustomColorSettings); // disable global color settings if we are using the custom color m_options.assistantsGlobalOpacitySlider->setEnabled(!showCustomColorSettings); m_options.assistantsColor->setEnabled(!showCustomColorSettings); m_options.globalColorLabel->setEnabled(!showCustomColorSettings); } void KisAssistantTool::slotChangeVanishingPointAngle(double value) { if ( m_canvas->paintingAssistantsDecoration()->assistants().length() == 0) { return; } // get the selected assistant and change the angle value KisPaintingAssistantSP m_selectedAssistant = m_canvas->paintingAssistantsDecoration()->selectedAssistant(); if (m_selectedAssistant) { bool isVanishingPointAssistant = m_selectedAssistant->id() == "vanishing point"; if (isVanishingPointAssistant) { QSharedPointer assis = qSharedPointerCast(m_selectedAssistant); assis->setReferenceLineDensity((float)value); } } m_canvas->canvasWidget()->update(); } void KisAssistantTool::mouseMoveEvent(KoPointerEvent *event) { if (m_newAssistant && m_internalMode == MODE_CREATION) { *m_newAssistant->handles().back() = event->point; } else if (m_newAssistant && m_internalMode == MODE_DRAGGING_TRANSLATING_TWONODES) { QPointF translate = event->point - m_dragEnd; m_dragEnd = event->point; m_selectedNode1.data()->operator = (QPointF(m_selectedNode1.data()->x(),m_selectedNode1.data()->y()) + translate); m_selectedNode2.data()->operator = (QPointF(m_selectedNode2.data()->x(),m_selectedNode2.data()->y()) + translate); } m_canvas->updateCanvas(); } void KisAssistantTool::paint(QPainter& _gc, const KoViewConverter &_converter) { QRectF canvasSize = QRectF(QPointF(0, 0), QSizeF(m_canvas->image()->size())); // show special display while a new assistant is in the process of being created if (m_newAssistant) { QColor assistantColor = m_newAssistant->effectiveAssistantColor(); assistantColor.setAlpha(80); m_newAssistant->drawAssistant(_gc, canvasSize, m_canvas->coordinatesConverter(), false, m_canvas, true, false); Q_FOREACH (const KisPaintingAssistantHandleSP handle, m_newAssistant->handles()) { QPainterPath path; path.addEllipse(QRectF(_converter.documentToView(*handle) - QPointF(m_handleSize * 0.5, m_handleSize * 0.5), QSizeF(m_handleSize, m_handleSize))); _gc.save(); _gc.setPen(Qt::NoPen); _gc.setBrush(assistantColor); _gc.drawPath(path); _gc.restore(); } } Q_FOREACH (KisPaintingAssistantSP assistant, m_canvas->paintingAssistantsDecoration()->assistants()) { QColor assistantColor = assistant->effectiveAssistantColor(); assistantColor.setAlpha(80); Q_FOREACH (const KisPaintingAssistantHandleSP handle, m_handles) { QRectF ellipse(_converter.documentToView(*handle) - QPointF(m_handleSize * 0.5, m_handleSize * 0.5), QSizeF(m_handleSize, m_handleSize)); // render handles differently if it is the one being dragged. if (handle == m_handleDrag || handle == m_handleCombine) { QPen stroke(assistantColor, 4); _gc.save(); _gc.setPen(stroke); _gc.setBrush(Qt::NoBrush); _gc.drawEllipse(ellipse); _gc.restore(); } } } } void KisAssistantTool::removeAllAssistants() { m_canvas->viewManager()->canvasResourceProvider()->clearPerspectiveGrids(); m_canvas->paintingAssistantsDecoration()->removeAll(); m_handles = m_canvas->paintingAssistantsDecoration()->handles(); m_canvas->updateCanvas(); m_canvas->paintingAssistantsDecoration()->deselectAssistant(); updateToolOptionsUI(); } void KisAssistantTool::loadAssistants() { KoFileDialog dialog(m_canvas->viewManager()->mainWindow(), KoFileDialog::OpenFile, "OpenAssistant"); dialog.setCaption(i18n("Select an Assistant")); dialog.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)); dialog.setMimeTypeFilters(QStringList() << "application/x-krita-assistant", "application/x-krita-assistant"); QString filename = dialog.filename(); if (filename.isEmpty()) return; if (!QFileInfo(filename).exists()) return; QFile file(filename); file.open(QIODevice::ReadOnly); QByteArray data = file.readAll(); QXmlStreamReader xml(data); QMap handleMap; KisPaintingAssistantSP assistant; bool errors = false; while (!xml.atEnd()) { switch (xml.readNext()) { case QXmlStreamReader::StartElement: if (xml.name() == "handle") { if (assistant && !xml.attributes().value("ref").isEmpty()) { KisPaintingAssistantHandleSP handle = handleMap.value(xml.attributes().value("ref").toString().toInt()); if (handle) { assistant->addHandle(handle, HandleType::NORMAL); } else { errors = true; } } else { QString strId = xml.attributes().value("id").toString(), strX = xml.attributes().value("x").toString(), strY = xml.attributes().value("y").toString(); if (!strId.isEmpty() && !strX.isEmpty() && !strY.isEmpty()) { int id = strId.toInt(); double x = strX.toDouble(), y = strY.toDouble(); if (!handleMap.contains(id)) { handleMap.insert(id, new KisPaintingAssistantHandle(x, y)); } else { errors = true; } } else { errors = true; } } } else if (xml.name() == "assistant") { const KisPaintingAssistantFactory* factory = KisPaintingAssistantFactoryRegistry::instance()->get(xml.attributes().value("type").toString()); if (factory) { if (assistant) { errors = true; assistant.clear(); } assistant = toQShared(factory->createPaintingAssistant()); } else { errors = true; } // load custom shared assistant properties if ( xml.attributes().hasAttribute("useCustomColor")) { QStringRef useCustomColor = xml.attributes().value("useCustomColor"); bool usingColor = false; if (useCustomColor.toString() == "1") { usingColor = true; } assistant->setUseCustomColor(usingColor); } if ( xml.attributes().hasAttribute("useCustomColor")) { QStringRef customColor = xml.attributes().value("customColor"); assistant->setAssistantCustomColor( KisDomUtils::qStringToQColor(customColor.toString()) ); } } if (assistant) { assistant->loadCustomXml(&xml); } break; case QXmlStreamReader::EndElement: if (xml.name() == "assistant") { if (assistant) { if (assistant->handles().size() == assistant->numHandles()) { if (assistant->id() == "vanishing point"){ //ideally we'd save and load side-handles as well, but this is all I've got// QPointF pos = *assistant->handles()[0]; assistant->addHandle(new KisPaintingAssistantHandle(pos+QPointF(-70,0)), HandleType::SIDE); assistant->addHandle(new KisPaintingAssistantHandle(pos+QPointF(-140,0)), HandleType::SIDE); assistant->addHandle(new KisPaintingAssistantHandle(pos+QPointF(70,0)), HandleType::SIDE); assistant->addHandle(new KisPaintingAssistantHandle(pos+QPointF(140,0)), HandleType::SIDE); } m_canvas->paintingAssistantsDecoration()->addAssistant(assistant); KisAbstractPerspectiveGrid* grid = dynamic_cast(assistant.data()); if (grid) { m_canvas->viewManager()->canvasResourceProvider()->addPerspectiveGrid(grid); } } else { errors = true; } assistant.clear(); } } break; default: break; } } if (assistant) { errors = true; assistant.clear(); } if (xml.hasError()) { QMessageBox::warning(0, i18nc("@title:window", "Krita"), xml.errorString()); } if (errors) { QMessageBox::warning(0, i18nc("@title:window", "Krita"), i18n("Errors were encountered. Not all assistants were successfully loaded.")); } m_handles = m_canvas->paintingAssistantsDecoration()->handles(); m_canvas->updateCanvas(); } void KisAssistantTool::saveAssistants() { if (m_handles.isEmpty()) return; QByteArray data; QXmlStreamWriter xml(&data); xml.writeStartDocument(); xml.writeStartElement("paintingassistant"); xml.writeAttribute("color", KisDomUtils::qColorToQString( m_canvas->paintingAssistantsDecoration()->globalAssistantsColor())); // global color if no custom color used xml.writeStartElement("handles"); QMap handleMap; Q_FOREACH (const KisPaintingAssistantHandleSP handle, m_handles) { int id = handleMap.size(); handleMap.insert(handle, id); xml.writeStartElement("handle"); //xml.writeAttribute("type", handle->handleType()); xml.writeAttribute("id", QString::number(id)); xml.writeAttribute("x", QString::number(double(handle->x()), 'f', 3)); xml.writeAttribute("y", QString::number(double(handle->y()), 'f', 3)); xml.writeEndElement(); } xml.writeEndElement(); xml.writeStartElement("assistants"); Q_FOREACH (const KisPaintingAssistantSP assistant, m_canvas->paintingAssistantsDecoration()->assistants()) { xml.writeStartElement("assistant"); xml.writeAttribute("type", assistant->id()); xml.writeAttribute("useCustomColor", QString::number(assistant->useCustomColor())); xml.writeAttribute("customColor", KisDomUtils::qColorToQString(assistant->assistantCustomColor())); // custom assistant properties like angle density on vanishing point assistant->saveCustomXml(&xml); // handle information xml.writeStartElement("handles"); Q_FOREACH (const KisPaintingAssistantHandleSP handle, assistant->handles()) { xml.writeStartElement("handle"); xml.writeAttribute("ref", QString::number(handleMap.value(handle))); xml.writeEndElement(); } xml.writeEndElement(); xml.writeEndElement(); } xml.writeEndElement(); xml.writeEndElement(); xml.writeEndDocument(); KoFileDialog dialog(m_canvas->viewManager()->mainWindow(), KoFileDialog::SaveFile, "OpenAssistant"); dialog.setCaption(i18n("Save Assistant")); dialog.setDefaultDir(QStandardPaths::writableLocation(QStandardPaths::PicturesLocation)); dialog.setMimeTypeFilters(QStringList() << "application/x-krita-assistant", "application/x-krita-assistant"); QString filename = dialog.filename(); if (filename.isEmpty()) return; QFile file(filename); file.open(QIODevice::WriteOnly); file.write(data); } QWidget *KisAssistantTool::createOptionWidget() { if (!m_optionsWidget) { m_optionsWidget = new QWidget; m_options.setupUi(m_optionsWidget); // See https://bugs.kde.org/show_bug.cgi?id=316896 QWidget *specialSpacer = new QWidget(m_optionsWidget); specialSpacer->setObjectName("SpecialSpacer"); specialSpacer->setFixedSize(0, 0); m_optionsWidget->layout()->addWidget(specialSpacer); m_options.loadAssistantButton->setIcon(KisIconUtils::loadIcon("document-open")); m_options.saveAssistantButton->setIcon(KisIconUtils::loadIcon("document-save")); m_options.deleteAllAssistantsButton->setIcon(KisIconUtils::loadIcon("edit-delete")); QList assistants; Q_FOREACH (const QString& key, KisPaintingAssistantFactoryRegistry::instance()->keys()) { QString name = KisPaintingAssistantFactoryRegistry::instance()->get(key)->name(); assistants << KoID(key, name); } std::sort(assistants.begin(), assistants.end(), KoID::compareNames); Q_FOREACH(const KoID &id, assistants) { m_options.availableAssistantsComboBox->addItem(id.name(), id.id()); } connect(m_options.saveAssistantButton, SIGNAL(clicked()), SLOT(saveAssistants())); connect(m_options.loadAssistantButton, SIGNAL(clicked()), SLOT(loadAssistants())); connect(m_options.deleteAllAssistantsButton, SIGNAL(clicked()), SLOT(removeAllAssistants())); connect(m_options.assistantsColor, SIGNAL(changed(QColor)), SLOT(slotGlobalAssistantsColorChanged(QColor))); connect(m_options.assistantsGlobalOpacitySlider, SIGNAL(valueChanged(int)), SLOT(slotGlobalAssistantOpacityChanged())); connect(m_options.vanishingPointAngleSpinbox, SIGNAL(valueChanged(double)), this, SLOT(slotChangeVanishingPointAngle(double))); //ENTER_FUNCTION() << ppVar(m_canvas) << ppVar(m_canvas && m_canvas->paintingAssistantsDecoration()); // initialize UI elements with existing data if possible if (m_canvas && m_canvas->paintingAssistantsDecoration()) { const QColor color = m_canvas->paintingAssistantsDecoration()->globalAssistantsColor(); QColor opaqueColor = color; opaqueColor.setAlpha(255); //ENTER_FUNCTION() << ppVar(opaqueColor); m_options.assistantsColor->setColor(opaqueColor); m_options.customAssistantColorButton->setColor(opaqueColor); m_options.assistantsGlobalOpacitySlider->setValue(color.alphaF() * 100.0); } else { m_options.assistantsColor->setColor(QColor(176, 176, 176, 255)); // grey default for all assistants m_options.assistantsGlobalOpacitySlider->setValue(100); // 100% } m_options.assistantsGlobalOpacitySlider->setPrefix(i18n("Opacity: ")); m_options.assistantsGlobalOpacitySlider->setSuffix(" %"); // custom color of selected assistant m_options.customColorOpacitySlider->setValue(100); // 100% m_options.customColorOpacitySlider->setPrefix(i18n("Opacity: ")); m_options.customColorOpacitySlider->setSuffix(" %"); connect(m_options.useCustomAssistantColor, SIGNAL(clicked(bool)), this, SLOT(slotUpdateCustomColor())); connect(m_options.customAssistantColorButton, SIGNAL(changed(QColor)), this, SLOT(slotUpdateCustomColor())); connect(m_options.customColorOpacitySlider, SIGNAL(valueChanged(int)), SLOT(slotCustomOpacityChanged())); m_options.vanishingPointAngleSpinbox->setPrefix(i18n("Density: ")); m_options.vanishingPointAngleSpinbox->setSuffix(QChar(Qt::Key_degree)); m_options.vanishingPointAngleSpinbox->setRange(1.0, 180.0); m_options.vanishingPointAngleSpinbox->setSingleStep(0.5); m_options.vanishingPointAngleSpinbox->setVisible(false); } updateToolOptionsUI(); return m_optionsWidget; } void KisAssistantTool::slotGlobalAssistantsColorChanged(const QColor& setColor) { // color and alpha are stored separately, so we need to merge the values before sending it on int oldAlpha = m_canvas->paintingAssistantsDecoration()->globalAssistantsColor().alpha(); QColor newColor = setColor; newColor.setAlpha(oldAlpha); m_canvas->paintingAssistantsDecoration()->setGlobalAssistantsColor(newColor); m_canvas->paintingAssistantsDecoration()->uncache(); m_canvas->canvasWidget()->update(); } void KisAssistantTool::slotGlobalAssistantOpacityChanged() { QColor newColor = m_canvas->paintingAssistantsDecoration()->globalAssistantsColor(); qreal newOpacity = m_options.assistantsGlobalOpacitySlider->value() * 0.01 * 255.0; newColor.setAlpha(int(newOpacity)); m_canvas->paintingAssistantsDecoration()->setGlobalAssistantsColor(newColor); m_canvas->paintingAssistantsDecoration()->uncache(); m_canvas->canvasWidget()->update(); } void KisAssistantTool::slotUpdateCustomColor() { // get the selected assistant and change the angle value KisPaintingAssistantSP m_selectedAssistant = m_canvas->paintingAssistantsDecoration()->selectedAssistant(); if (m_selectedAssistant) { m_selectedAssistant->setUseCustomColor(m_options.useCustomAssistantColor->isChecked()); // changing color doesn't keep alpha, so update that before we send it on QColor newColor = m_options.customAssistantColorButton->color(); newColor.setAlpha(m_selectedAssistant->assistantCustomColor().alpha()); m_selectedAssistant->setAssistantCustomColor(newColor); m_selectedAssistant->uncache(); } updateToolOptionsUI(); m_canvas->canvasWidget()->update(); } void KisAssistantTool::slotCustomOpacityChanged() { KisPaintingAssistantSP m_selectedAssistant = m_canvas->paintingAssistantsDecoration()->selectedAssistant(); if (m_selectedAssistant) { QColor newColor = m_selectedAssistant->assistantCustomColor(); qreal newOpacity = m_options.customColorOpacitySlider->value() * 0.01 * 255.0; newColor.setAlpha(int(newOpacity)); m_selectedAssistant->setAssistantCustomColor(newColor); m_selectedAssistant->uncache(); } // this forces the canvas to refresh to see the changes immediately m_canvas->paintingAssistantsDecoration()->uncache(); m_canvas->canvasWidget()->update(); } diff --git a/plugins/dockers/griddocker/grid_config_widget.cpp b/plugins/dockers/griddocker/grid_config_widget.cpp index 8d5841889e..f58be72233 100644 --- a/plugins/dockers/griddocker/grid_config_widget.cpp +++ b/plugins/dockers/griddocker/grid_config_widget.cpp @@ -1,362 +1,362 @@ /* * Copyright (c) 2016 Dmitry Kazakov * * 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 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 "grid_config_widget.h" #include "ui_grid_config_widget.h" #include "kis_grid_config.h" #include "kis_guides_config.h" #include "kis_debug.h" #include "kis_aspect_ratio_locker.h" #include "kis_int_parse_spin_box.h" #include #include #include #include struct GridConfigWidget::Private { Private() : guiSignalsBlocked(false) {} KisGridConfig gridConfig; KisGuidesConfig guidesConfig; bool guiSignalsBlocked {false}; }; GridConfigWidget::GridConfigWidget(QWidget *parent) : QWidget(parent), ui(new Ui::GridConfigWidget), m_d(new Private) { ui->setupUi(this); ui->colorMain->setAlphaChannelEnabled(true); ui->colorSubdivision->setAlphaChannelEnabled(true); ui->colorGuides->setAlphaChannelEnabled(true); ui->angleLeftSpinbox->setSuffix(QChar(Qt::Key_degree)); ui->angleRightSpinbox->setSuffix(QChar(Qt::Key_degree)); ui->cellSpacingSpinbox->setSuffix(i18n(" px")); ui->gridTypeCombobox->addItem(i18n("Rectangle")); ui->gridTypeCombobox->addItem(i18n("Isometric")); ui->gridTypeCombobox->setCurrentIndex(0); // set to rectangle by default slotGridTypeChanged(); // update the UI to hide any elements we don't need connect(ui->gridTypeCombobox, SIGNAL(currentIndexChanged(int)), SLOT(slotGridTypeChanged())); setGridConfig(m_d->gridConfig); setGuidesConfig(m_d->guidesConfig); // hide offset UI elements if offset is disabled connect(ui->chkOffset, SIGNAL(toggled(bool)), ui->lblXOffset, SLOT(setVisible(bool))); connect(ui->chkOffset, SIGNAL(toggled(bool)), ui->lblYOffset, SLOT(setVisible(bool))); connect(ui->chkOffset, SIGNAL(toggled(bool)), ui->intXOffset, SLOT(setVisible(bool))); connect(ui->chkOffset, SIGNAL(toggled(bool)), ui->intYOffset, SLOT(setVisible(bool))); connect(ui->chkOffset, SIGNAL(toggled(bool)), ui->offsetAspectButton, SLOT(setVisible(bool))); ui->lblXOffset->setVisible(false); ui->lblYOffset->setVisible(false); ui->intXOffset->setVisible(false); ui->intYOffset->setVisible(false); ui->offsetAspectButton->setVisible(false); connect(ui->chkShowGrid, SIGNAL(stateChanged(int)), SLOT(slotGridGuiChanged())); connect(ui->chkSnapToGrid, SIGNAL(stateChanged(int)), SLOT(slotGridGuiChanged())); connect(ui->chkShowGuides, SIGNAL(stateChanged(int)), SLOT(slotGuidesGuiChanged())); connect(ui->chkSnapToGuides, SIGNAL(stateChanged(int)), SLOT(slotGuidesGuiChanged())); connect(ui->chkLockGuides, SIGNAL(stateChanged(int)), SLOT(slotGuidesGuiChanged())); connect(ui->intSubdivision, SIGNAL(valueChanged(int)), SLOT(slotGridGuiChanged())); connect(ui->angleLeftSpinbox, SIGNAL(valueChanged(int)), SLOT(slotGridGuiChanged())); connect(ui->angleRightSpinbox, SIGNAL(valueChanged(int)), SLOT(slotGridGuiChanged())); connect(ui->cellSpacingSpinbox, SIGNAL(valueChanged(int)), SLOT(slotGridGuiChanged())); connect(ui->selectMainStyle, SIGNAL(currentIndexChanged(int)), SLOT(slotGridGuiChanged())); connect(ui->colorMain, SIGNAL(changed(QColor)), SLOT(slotGridGuiChanged())); connect(ui->selectSubdivisionStyle, SIGNAL(currentIndexChanged(int)), SLOT(slotGridGuiChanged())); connect(ui->colorSubdivision, SIGNAL(changed(QColor)), SLOT(slotGridGuiChanged())); connect(ui->selectGuidesStyle, SIGNAL(currentIndexChanged(int)), SLOT(slotGuidesGuiChanged())); connect(ui->colorGuides, SIGNAL(changed(QColor)), SLOT(slotGuidesGuiChanged())); ui->chkOffset->setChecked(false); KisAspectRatioLocker *offsetLocker = new KisAspectRatioLocker(this); offsetLocker->connectSpinBoxes(ui->intXOffset, ui->intYOffset, ui->offsetAspectButton); KisAspectRatioLocker *spacingLocker = new KisAspectRatioLocker(this); spacingLocker->connectSpinBoxes(ui->intHSpacing, ui->intVSpacing, ui->spacingAspectButton); connect(offsetLocker, SIGNAL(sliderValueChanged()), SLOT(slotGridGuiChanged())); connect(offsetLocker, SIGNAL(aspectButtonChanged()), SLOT(slotGridGuiChanged())); connect(spacingLocker, SIGNAL(sliderValueChanged()), SLOT(slotGridGuiChanged())); connect(spacingLocker, SIGNAL(aspectButtonChanged()), SLOT(slotGridGuiChanged())); connect(ui->chkShowRulers,SIGNAL(toggled(bool)),SIGNAL(showRulersChanged(bool))); connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), SLOT(slotPreferencesUpdated())); } GridConfigWidget::~GridConfigWidget() { delete ui; } void GridConfigWidget::setGridConfig(const KisGridConfig &value) { KisGridConfig currentConfig = fetchGuiGridConfig(); if (currentConfig == value) return; setGridConfigImpl(value); } void GridConfigWidget::setGuidesConfig(const KisGuidesConfig &value) { KisGuidesConfig currentConfig = fetchGuiGuidesConfig(); if (currentConfig == value) return; setGuidesConfigImpl(value); } void GridConfigWidget::setGridConfigImpl(const KisGridConfig &value) { m_d->gridConfig = value; m_d->guiSignalsBlocked = true; if (!m_d->gridConfig.offset().isNull()) { ui->chkOffset->setChecked(true); } ui->offsetAspectButton->setKeepAspectRatio(m_d->gridConfig.offsetAspectLocked()); ui->spacingAspectButton->setKeepAspectRatio(m_d->gridConfig.spacingAspectLocked()); ui->chkShowGrid->setChecked(m_d->gridConfig.showGrid()); ui->intHSpacing->setMaximum(std::numeric_limits::max()); ui->intVSpacing->setMaximum(std::numeric_limits::max()); ui->intHSpacing->setValue(m_d->gridConfig.spacing().x()); ui->intVSpacing->setValue(m_d->gridConfig.spacing().y()); ui->intXOffset->setValue(m_d->gridConfig.offset().x()); ui->intYOffset->setValue(m_d->gridConfig.offset().y()); ui->intSubdivision->setValue(m_d->gridConfig.subdivision()); ui->chkSnapToGrid->setChecked(m_d->gridConfig.snapToGrid()); ui->angleLeftSpinbox->setValue(m_d->gridConfig.angleLeft()); ui->angleRightSpinbox->setValue(m_d->gridConfig.angleRight()); ui->cellSpacingSpinbox->setValue(m_d->gridConfig.cellSpacing()); ui->selectMainStyle->setCurrentIndex(int(m_d->gridConfig.lineTypeMain())); ui->selectSubdivisionStyle->setCurrentIndex(int(m_d->gridConfig.lineTypeSubdivision())); - ui->gridTypeCombobox->setCurrentIndex(m_d->gridConfig.gridType()); + ui->gridTypeCombobox->setCurrentIndex(int(m_d->gridConfig.gridType())); ui->colorMain->setColor(m_d->gridConfig.colorMain()); ui->colorSubdivision->setColor(m_d->gridConfig.colorSubdivision()); m_d->guiSignalsBlocked = false; emit gridValueChanged(); } void GridConfigWidget::setGuidesConfigImpl(const KisGuidesConfig &value) { m_d->guidesConfig = value; m_d->guiSignalsBlocked = true; ui->chkShowGuides->setChecked(m_d->guidesConfig.showGuides()); ui->chkSnapToGuides->setChecked(m_d->guidesConfig.snapToGuides()); ui->chkLockGuides->setChecked(m_d->guidesConfig.lockGuides()); ui->selectGuidesStyle->setCurrentIndex(int(m_d->guidesConfig.guidesLineType())); ui->colorGuides->setColor(m_d->guidesConfig.guidesColor()); m_d->guiSignalsBlocked = false; emit guidesValueChanged(); } KisGridConfig GridConfigWidget::gridConfig() const { return m_d->gridConfig; } KisGuidesConfig GridConfigWidget::guidesConfig() const { return m_d->guidesConfig; } KisGridConfig GridConfigWidget::fetchGuiGridConfig() const { KisGridConfig config; config.setShowGrid(ui->chkShowGrid->isChecked()); config.setSnapToGrid(ui->chkSnapToGrid->isChecked()); QPoint pt; pt.rx() = ui->intHSpacing->value(); pt.ry() = ui->intVSpacing->value(); config.setSpacing(pt); pt.rx() = ui->intXOffset->value(); pt.ry() = ui->intYOffset->value(); config.setOffset(pt); config.setSubdivision(ui->intSubdivision->value()); config.setAngleLeft(ui->angleLeftSpinbox->value()); config.setAngleRight(ui->angleRightSpinbox->value()); config.setCellSpacing(ui->cellSpacingSpinbox->value()); - config.setGridType(ui->gridTypeCombobox->currentIndex()); + config.setGridType(KisGridConfig::GridType(ui->gridTypeCombobox->currentIndex())); config.setOffsetAspectLocked(ui->offsetAspectButton->keepAspectRatio()); config.setSpacingAspectLocked(ui->spacingAspectButton->keepAspectRatio()); config.setLineTypeMain(KisGridConfig::LineTypeInternal(ui->selectMainStyle->currentIndex())); config.setLineTypeSubdivision(KisGridConfig::LineTypeInternal(ui->selectSubdivisionStyle->currentIndex())); config.setColorMain(ui->colorMain->color()); config.setColorSubdivision(ui->colorSubdivision->color()); return config; } KisGuidesConfig GridConfigWidget::fetchGuiGuidesConfig() const { KisGuidesConfig config = m_d->guidesConfig; config.setShowGuides(ui->chkShowGuides->isChecked()); config.setSnapToGuides(ui->chkSnapToGuides->isChecked()); config.setLockGuides(ui->chkLockGuides->isChecked()); config.setGuidesLineType(KisGuidesConfig::LineTypeInternal(ui->selectGuidesStyle->currentIndex())); config.setGuidesColor(ui->colorGuides->color()); return config; } void GridConfigWidget::slotGridGuiChanged() { if (m_d->guiSignalsBlocked) return; KisGridConfig currentConfig = fetchGuiGridConfig(); if (currentConfig == m_d->gridConfig) return; setGridConfigImpl(currentConfig); } void GridConfigWidget::slotPreferencesUpdated() { KisConfig cfg(true); enableIsometricGrid(cfg.useOpenGL()); // Isometric view needs OpenGL } void GridConfigWidget::slotGuidesGuiChanged() { if (m_d->guiSignalsBlocked) return; KisGuidesConfig currentConfig = fetchGuiGuidesConfig(); if (currentConfig == m_d->guidesConfig) return; setGuidesConfigImpl(currentConfig); } void GridConfigWidget::slotGridTypeChanged() { bool showRectangleControls = ui->gridTypeCombobox->currentIndex() == 0; // specific rectangle UI controls ui->lblXSpacing->setVisible(showRectangleControls); ui->lblYSpacing->setVisible(showRectangleControls); ui->intHSpacing->setVisible(showRectangleControls); ui->intVSpacing->setVisible(showRectangleControls); ui->spacingAspectButton->setVisible(showRectangleControls); ui->lblSubdivision->setVisible(showRectangleControls); ui->intSubdivision->setVisible(showRectangleControls); ui->lblSubdivisionStyle->setVisible(showRectangleControls); ui->selectSubdivisionStyle->setVisible(showRectangleControls); ui->colorSubdivision->setVisible(showRectangleControls); // specific isometric UI controls ui->leftAngleLabel->setVisible(!showRectangleControls); ui->rightAngleLabel->setVisible(!showRectangleControls); ui->angleLeftSpinbox->setVisible(!showRectangleControls); ui->angleRightSpinbox->setVisible(!showRectangleControls); ui->cellSpacingLabel->setVisible(!showRectangleControls); ui->cellSpacingSpinbox->setVisible(!showRectangleControls); // disable snapping for isometric grid type for now // remember if we had snapping enabled if it was on the rectangule mode if (!showRectangleControls) { m_isGridEnabled = ui->chkSnapToGrid->isChecked(); ui->chkSnapToGrid->setEnabled(false); ui->chkSnapToGrid->setChecked(false); } else { ui->chkSnapToGrid->setEnabled(true); ui->chkSnapToGrid->setChecked(m_isGridEnabled); } slotGridGuiChanged(); } bool GridConfigWidget::showRulers() const { return ui->chkShowRulers->isChecked(); } void GridConfigWidget::enableIsometricGrid(bool value) { m_isIsometricGridEnabled = value; // Isometric grids disabled if OpenGL is disabled QStandardItemModel *model = qobject_cast(ui->gridTypeCombobox->model()); QStandardItem *item = model->item(1); // isometric option // item->setFlags(m_isIsometricGridEnabled ? item->flags() & ~Qt::ItemIsEnabled: // item->flags() | Qt::ItemIsEnabled); item->setEnabled(m_isIsometricGridEnabled); if (m_isIsometricGridEnabled) { item->setText(i18n("Isometric")); } else { item->setText(i18n("Isometric (requires OpenGL)")); // change drop down index to Rectangular in case it was previously set to isometric ui->gridTypeCombobox->setCurrentIndex(0); } } void GridConfigWidget::setShowRulers(bool value) { ui->chkShowRulers->setChecked(value); } diff --git a/plugins/extensions/qmic/kis_qmic_simple_convertor.cpp b/plugins/extensions/qmic/kis_qmic_simple_convertor.cpp index 5c76518fe5..a0e11681fe 100644 --- a/plugins/extensions/qmic/kis_qmic_simple_convertor.cpp +++ b/plugins/extensions/qmic/kis_qmic_simple_convertor.cpp @@ -1,873 +1,875 @@ /* * Copyright (c) 2013 Lukáš Tvrdý #include #include #include #include #include #include #define SCALE_TO_FLOAT( v ) KoColorSpaceMaths< _channel_type_, float>::scaleToA( v ) #define SCALE_FROM_FLOAT( v ) KoColorSpaceMaths< float, _channel_type_>::scaleToA( v ) template class KisColorToFloatConvertor : public KoColorTransformation { typedef traits RGBTrait; typedef typename RGBTrait::Pixel RGBPixel; public: KisColorToFloatConvertor(float gmicUnitValue = 255.0f) : m_gmicUnitValue(gmicUnitValue) {} float m_gmicUnitValue; void transform(const quint8 *src, quint8 *dst, qint32 nPixels) const override { float gmicUnitValue2KritaUnitValue = m_gmicUnitValue / KoColorSpaceMathsTraits::unitValue; const RGBPixel* srcPixel = reinterpret_cast(src); KoRgbF32Traits::Pixel* dstPixel = reinterpret_cast(dst); while(nPixels > 0) { dstPixel->red = SCALE_TO_FLOAT(srcPixel->red) * gmicUnitValue2KritaUnitValue; dstPixel->green = SCALE_TO_FLOAT(srcPixel->green) * gmicUnitValue2KritaUnitValue; dstPixel->blue = SCALE_TO_FLOAT(srcPixel->blue) * gmicUnitValue2KritaUnitValue; dstPixel->alpha = SCALE_TO_FLOAT(srcPixel->alpha) * gmicUnitValue2KritaUnitValue; --nPixels; ++srcPixel; ++dstPixel; } } }; template class KisColorFromFloat : public KoColorTransformation { typedef traits RGBTrait; typedef typename RGBTrait::Pixel RGBPixel; public: KisColorFromFloat(float gmicUnitValue = 255.0f) : m_gmicUnitValue(gmicUnitValue) { } void transform(const quint8 *src, quint8 *dst, qint32 nPixels) const override { const KoRgbF32Traits::Pixel* srcPixel = reinterpret_cast(src); RGBPixel* dstPixel = reinterpret_cast(dst); float gmicUnitValue2KritaUnitValue = KoColorSpaceMathsTraits::unitValue / m_gmicUnitValue; while(nPixels > 0) { dstPixel->red = SCALE_FROM_FLOAT(srcPixel->red * gmicUnitValue2KritaUnitValue); dstPixel->green = SCALE_FROM_FLOAT(srcPixel->green * gmicUnitValue2KritaUnitValue); dstPixel->blue = SCALE_FROM_FLOAT(srcPixel->blue * gmicUnitValue2KritaUnitValue); dstPixel->alpha = SCALE_FROM_FLOAT(srcPixel->alpha * gmicUnitValue2KritaUnitValue); --nPixels; ++srcPixel; ++dstPixel; } } private: float m_gmicUnitValue; }; template class KisColorFromGrayScaleFloat : public KoColorTransformation { typedef traits RGBTrait; typedef typename RGBTrait::Pixel RGBPixel; public: KisColorFromGrayScaleFloat(float gmicUnitValue = 255.0f):m_gmicUnitValue(gmicUnitValue){} void transform(const quint8 *src, quint8 *dst, qint32 nPixels) const override { const KoRgbF32Traits::Pixel* srcPixel = reinterpret_cast(src); RGBPixel* dstPixel = reinterpret_cast(dst); float gmicUnitValue2KritaUnitValue = KoColorSpaceMathsTraits::unitValue / m_gmicUnitValue; // warning: green and blue channels on input contain random data!!! see that we copy only one channel // when gmic image has grayscale colorspace while(nPixels > 0) { dstPixel->red = dstPixel->green = dstPixel->blue = SCALE_FROM_FLOAT(srcPixel->red * gmicUnitValue2KritaUnitValue); dstPixel->alpha = SCALE_FROM_FLOAT(srcPixel->alpha * gmicUnitValue2KritaUnitValue); --nPixels; ++srcPixel; ++dstPixel; } } private: float m_gmicUnitValue; }; template class KisColorFromGrayScaleAlphaFloat : public KoColorTransformation { typedef traits RGBTrait; typedef typename RGBTrait::Pixel RGBPixel; public: KisColorFromGrayScaleAlphaFloat(float gmicUnitValue = 255.0f):m_gmicUnitValue(gmicUnitValue){} void transform(const quint8 *src, quint8 *dst, qint32 nPixels) const override { const KoRgbF32Traits::Pixel* srcPixel = reinterpret_cast(src); RGBPixel* dstPixel = reinterpret_cast(dst); float gmicUnitValue2KritaUnitValue = KoColorSpaceMathsTraits::unitValue / m_gmicUnitValue; // warning: green and blue channels on input contain random data!!! see that we copy only one channel // when gmic image has grayscale colorspace while(nPixels > 0) { dstPixel->red = dstPixel->green = dstPixel->blue = SCALE_FROM_FLOAT(srcPixel->red * gmicUnitValue2KritaUnitValue); dstPixel->alpha = SCALE_FROM_FLOAT(srcPixel->green * gmicUnitValue2KritaUnitValue); --nPixels; ++srcPixel; ++dstPixel; } } private: float m_gmicUnitValue; }; static KoColorTransformation* createTransformationFromGmic(const KoColorSpace* colorSpace, quint32 gmicSpectrum,float gmicUnitValue) { KoColorTransformation * colorTransformation = 0; if (colorSpace->colorModelId() != RGBAColorModelID) { dbgKrita << "Unsupported color space for fast pixel transformation to gmic pixel format" << colorSpace->id(); return 0; } if (colorSpace->colorDepthId() == Float32BitsColorDepthID) { if (gmicSpectrum == 3 || gmicSpectrum == 4) { colorTransformation = new KisColorFromFloat< float, KoRgbTraits < float > >(gmicUnitValue); } else if (gmicSpectrum == 1) { colorTransformation = new KisColorFromGrayScaleFloat >(gmicUnitValue); } else if (gmicSpectrum == 2) { colorTransformation = new KisColorFromGrayScaleAlphaFloat >(gmicUnitValue); } } #ifdef HAVE_OPENEXR else if (colorSpace->colorDepthId() == Float16BitsColorDepthID) { if (gmicSpectrum == 3 || gmicSpectrum == 4) { colorTransformation = new KisColorFromFloat< half, KoRgbTraits < half > >(gmicUnitValue); } else if (gmicSpectrum == 1) { colorTransformation = new KisColorFromGrayScaleFloat< half, KoRgbTraits < half > >(gmicUnitValue); } else if (gmicSpectrum == 2) { colorTransformation = new KisColorFromGrayScaleAlphaFloat< half, KoRgbTraits < half > >(gmicUnitValue); } } #endif else if (colorSpace->colorDepthId() == Integer16BitsColorDepthID) { if (gmicSpectrum == 3 || gmicSpectrum == 4) { colorTransformation = new KisColorFromFloat< quint16, KoBgrTraits < quint16 > >(gmicUnitValue); } else if (gmicSpectrum == 1) { colorTransformation = new KisColorFromGrayScaleFloat< quint16, KoBgrTraits < quint16 > >(gmicUnitValue); } else if (gmicSpectrum == 2) { colorTransformation = new KisColorFromGrayScaleAlphaFloat< quint16, KoBgrTraits < quint16 > >(gmicUnitValue); } } else if (colorSpace->colorDepthId() == Integer8BitsColorDepthID) { if (gmicSpectrum == 3 || gmicSpectrum == 4) { colorTransformation = new KisColorFromFloat< quint8, KoBgrTraits < quint8 > >(gmicUnitValue); } else if (gmicSpectrum == 1) { colorTransformation = new KisColorFromGrayScaleFloat< quint8, KoBgrTraits < quint8 > >(gmicUnitValue); } else if (gmicSpectrum == 2) { colorTransformation = new KisColorFromGrayScaleAlphaFloat< quint8, KoBgrTraits < quint8 > >(gmicUnitValue); } } else { dbgKrita << "Unsupported color space " << colorSpace->id() << " for fast pixel transformation to gmic pixel format"; return 0; } return colorTransformation; } static KoColorTransformation* createTransformation(const KoColorSpace* colorSpace) { KoColorTransformation * colorTransformation = 0; if (colorSpace->colorModelId() != RGBAColorModelID) { dbgKrita << "Unsupported color space for fast pixel transformation to gmic pixel format" << colorSpace->id(); return 0; } if (colorSpace->colorDepthId() == Float32BitsColorDepthID) { colorTransformation = new KisColorToFloatConvertor< float, KoRgbTraits < float > >(); } #ifdef HAVE_OPENEXR else if (colorSpace->colorDepthId() == Float16BitsColorDepthID) { colorTransformation = new KisColorToFloatConvertor< half, KoRgbTraits < half > >(); } #endif else if (colorSpace->colorDepthId() == Integer16BitsColorDepthID) { colorTransformation = new KisColorToFloatConvertor< quint16, KoBgrTraits < quint16 > >(); } else if (colorSpace->colorDepthId() == Integer8BitsColorDepthID) { colorTransformation = new KisColorToFloatConvertor< quint8, KoBgrTraits < quint8 > >(); } else { dbgKrita << "Unsupported color space " << colorSpace->id() << " for fast pixel transformation to gmic pixel format"; return 0; } return colorTransformation; } void KisQmicSimpleConvertor::convertFromGmicFast(gmic_image& gmicImage, KisPaintDeviceSP dst, float gmicUnitValue) { dbgPlugins << "convertFromGmicFast"; const KoColorSpace * dstColorSpace = dst->colorSpace(); KoColorTransformation * gmicToDstPixelFormat = createTransformationFromGmic(dstColorSpace, gmicImage._spectrum, gmicUnitValue); if (gmicToDstPixelFormat == 0) { dbgPlugins << "Fall-back to slow color conversion"; convertFromGmicImage(gmicImage, dst, gmicUnitValue); return; } qint32 x = 0; qint32 y = 0; qint32 width = gmicImage._width; qint32 height = gmicImage._height; width = width < 0 ? 0 : width; height = height < 0 ? 0 : height; const KoColorSpace *rgbaFloat32bitcolorSpace = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), Float32BitsColorDepthID.id(), KoColorSpaceRegistry::instance()->rgb8()->profile()); // this function always convert to rgba or rgb with various color depth quint32 dstNumChannels = rgbaFloat32bitcolorSpace->channelCount(); // number of channels that we will copy quint32 numChannels = gmicImage._spectrum; // gmic image has 4, 3, 2, 1 channel QVector planes(dstNumChannels); int channelOffset = gmicImage._width * gmicImage._height; for (unsigned int channelIndex = 0; channelIndex < gmicImage._spectrum; channelIndex++) { planes[channelIndex] = gmicImage._data + channelOffset * channelIndex; } for (unsigned int channelIndex = gmicImage._spectrum; channelIndex < dstNumChannels; channelIndex++) { planes[channelIndex] = 0; //turn off } qint32 dataY = 0; qint32 imageY = y; qint32 rowsRemaining = height; const qint32 floatPixelSize = rgbaFloat32bitcolorSpace->pixelSize(); KisRandomAccessorSP it = dst->createRandomAccessorNG(dst->x(), dst->y()); // 0,0 int tileWidth = it->numContiguousColumns(dst->x()); int tileHeight = it->numContiguousRows(dst->y()); Q_ASSERT(tileWidth == 64); Q_ASSERT(tileHeight == 64); quint8 *convertedTile = new quint8[rgbaFloat32bitcolorSpace->pixelSize() * tileWidth * tileHeight]; // grayscale and rgb case does not have alpha, so let's fill 4th channel of rgba tile with opacity opaque if (gmicImage._spectrum == 1 || gmicImage._spectrum == 3) { quint32 nPixels = tileWidth * tileHeight; quint32 pixelIndex = 0; KoRgbF32Traits::Pixel* srcPixel = reinterpret_cast(convertedTile); while (pixelIndex < nPixels) { srcPixel->alpha = gmicUnitValue; ++srcPixel; ++pixelIndex; } } while (rowsRemaining > 0) { qint32 dataX = 0; qint32 imageX = x; qint32 columnsRemaining = width; qint32 numContiguousImageRows = it->numContiguousRows(imageY); qint32 rowsToWork = qMin(numContiguousImageRows, rowsRemaining); while (columnsRemaining > 0) { qint32 numContiguousImageColumns = it->numContiguousColumns(imageX); qint32 columnsToWork = qMin(numContiguousImageColumns, columnsRemaining); const qint32 dataIdx = dataX + dataY * width; const qint32 tileRowStride = (tileWidth - columnsToWork) * floatPixelSize; quint8 *tileItStart = convertedTile; // copy gmic channels to float tile qint32 channelSize = sizeof(float); for(quint32 i=0; imoveTo(imageX, imageY); quint8 *dstTileItStart = it->rawData(); tileItStart = convertedTile; // back to the start of the converted tile // copy float tile to dst colorspace based on input colorspace (rgb or grayscale) for (qint32 row = 0; row < rowsToWork; row++) { gmicToDstPixelFormat->transform(tileItStart, dstTileItStart, columnsToWork); dstTileItStart += dstColorSpace->pixelSize() * tileWidth; tileItStart += floatPixelSize * tileWidth; } imageX += columnsToWork; dataX += columnsToWork; columnsRemaining -= columnsToWork; } imageY += rowsToWork; dataY += rowsToWork; rowsRemaining -= rowsToWork; } delete [] convertedTile; delete gmicToDstPixelFormat; } void KisQmicSimpleConvertor::convertToGmicImageFast(KisPaintDeviceSP dev, gmic_image *gmicImage, QRect rc) { KoColorTransformation * pixelToGmicPixelFormat = createTransformation(dev->colorSpace()); if (pixelToGmicPixelFormat == 0) { dbgPlugins << "Fall-back to slow color conversion method"; convertToGmicImage(dev, gmicImage, rc); return; } if (rc.isEmpty()) { dbgPlugins << "Image rectangle is empty! Using supplied gmic layer dimension"; rc = QRect(0, 0, gmicImage->_width, gmicImage->_height); } qint32 x = rc.x(); qint32 y = rc.y(); qint32 width = rc.width(); qint32 height = rc.height(); width = width < 0 ? 0 : width; height = height < 0 ? 0 : height; const qint32 numChannels = 4; int greenOffset = gmicImage->_width * gmicImage->_height; int blueOffset = greenOffset * 2; int alphaOffset = greenOffset * 3; QVector planes; planes.append(gmicImage->_data); planes.append(gmicImage->_data + greenOffset); planes.append(gmicImage->_data + blueOffset); planes.append(gmicImage->_data + alphaOffset); KisRandomConstAccessorSP it = dev->createRandomConstAccessorNG(dev->x(), dev->y()); int tileWidth = it->numContiguousColumns(dev->x()); int tileHeight = it->numContiguousRows(dev->y()); Q_ASSERT(tileWidth == 64); Q_ASSERT(tileHeight == 64); const KoColorSpace *rgbaFloat32bitcolorSpace = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), Float32BitsColorDepthID.id(), KoColorSpaceRegistry::instance()->rgb8()->profile()); Q_CHECK_PTR(rgbaFloat32bitcolorSpace); const qint32 dstPixelSize = rgbaFloat32bitcolorSpace->pixelSize(); const qint32 srcPixelSize = dev->pixelSize(); quint8 * dstTile = new quint8[dstPixelSize * tileWidth * tileHeight]; qint32 dataY = 0; qint32 imageX = x; qint32 imageY = y; it->moveTo(imageX, imageY); qint32 rowsRemaining = height; while (rowsRemaining > 0) { qint32 dataX = 0; imageX = x; qint32 columnsRemaining = width; qint32 numContiguousImageRows = it->numContiguousRows(imageY); qint32 rowsToWork = qMin(numContiguousImageRows, rowsRemaining); qint32 convertedTileY = tileHeight - rowsToWork; Q_ASSERT(convertedTileY >= 0); while (columnsRemaining > 0) { qint32 numContiguousImageColumns = it->numContiguousColumns(imageX); qint32 columnsToWork = qMin(numContiguousImageColumns, columnsRemaining); qint32 convertedTileX = tileWidth - columnsToWork; Q_ASSERT(convertedTileX >= 0); const qint32 dataIdx = dataX + dataY * width; const qint32 dstTileIndex = convertedTileX + convertedTileY * tileWidth; const qint32 tileRowStride = (tileWidth - columnsToWork) * dstPixelSize; const qint32 srcTileRowStride = (tileWidth - columnsToWork) * srcPixelSize; it->moveTo(imageX, imageY); quint8 *tileItStart = dstTile + dstTileIndex * dstPixelSize; // transform tile row by row quint8 *dstTileIt = tileItStart; quint8 *srcTileIt = const_cast(it->rawDataConst()); qint32 row = rowsToWork; while (row > 0) { pixelToGmicPixelFormat->transform(srcTileIt, dstTileIt , columnsToWork); srcTileIt += columnsToWork * srcPixelSize; srcTileIt += srcTileRowStride; dstTileIt += columnsToWork * dstPixelSize; dstTileIt += tileRowStride; row--; } // here we want to copy floats to dstTile, so tileItStart has to point to float buffer qint32 channelSize = sizeof(float); for(qint32 i = 0; i< numChannels; i++) { float * planeIt = planes[i] + dataIdx; qint32 dataStride = (width - columnsToWork); quint8* tileIt = tileItStart; for (qint32 row = 0; row < rowsToWork; row++) { for (int col = 0; col < columnsToWork; col++) { memcpy(planeIt, tileIt, channelSize); tileIt += dstPixelSize; planeIt += 1; } tileIt += tileRowStride; planeIt += dataStride; } // skip channel in tile: red, green, blue, alpha tileItStart += channelSize; } imageX += columnsToWork; dataX += columnsToWork; columnsRemaining -= columnsToWork; } imageY += rowsToWork; dataY += rowsToWork; rowsRemaining -= rowsToWork; } delete [] dstTile; delete pixelToGmicPixelFormat; } // gmic assumes float rgba in 0.0 - 255.0 void KisQmicSimpleConvertor::convertToGmicImage(KisPaintDeviceSP dev, gmic_image *gmicImage, QRect rc) { Q_ASSERT(!dev.isNull()); Q_ASSERT(gmicImage->_spectrum == 4); // rgba if (rc.isEmpty()) { rc = QRect(0, 0, gmicImage->_width, gmicImage->_height); } const KoColorSpace *rgbaFloat32bitcolorSpace = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), Float32BitsColorDepthID.id(), KoColorSpaceRegistry::instance()->rgb8()->profile()); Q_CHECK_PTR(rgbaFloat32bitcolorSpace); KoColorTransformation *pixelToGmicPixelFormat = createTransformation(rgbaFloat32bitcolorSpace); int greenOffset = gmicImage->_width * gmicImage->_height; int blueOffset = greenOffset * 2; int alphaOffset = greenOffset * 3; KoColorConversionTransformation::Intent renderingIntent = KoColorConversionTransformation::internalRenderingIntent(); KoColorConversionTransformation::ConversionFlags conversionFlags = KoColorConversionTransformation::internalConversionFlags(); const KoColorSpace * colorSpace = dev->colorSpace(); KisRandomConstAccessorSP it = dev->createRandomConstAccessorNG(0,0); int optimalBufferSize = 64; // most common numContiguousColumns, tile size? - quint8 * floatRGBApixel = new quint8[rgbaFloat32bitcolorSpace->pixelSize() * optimalBufferSize]; + QScopedArrayPointer floatRGBApixelStorage(new quint8[rgbaFloat32bitcolorSpace->pixelSize() * optimalBufferSize]); + quint8 *floatRGBApixel = floatRGBApixelStorage.data(); + quint32 pixelSize = rgbaFloat32bitcolorSpace->pixelSize(); int pos = 0; for (int y = 0; y < rc.height(); y++) { int x = 0; while (x < rc.width()) { it->moveTo(rc.x() + x, rc.y() + y); qint32 numContiguousColumns = qMin(it->numContiguousColumns(rc.x() + x), optimalBufferSize); numContiguousColumns = qMin(numContiguousColumns, rc.width() - x); colorSpace->convertPixelsTo(it->rawDataConst(), floatRGBApixel, rgbaFloat32bitcolorSpace, numContiguousColumns, renderingIntent, conversionFlags); pixelToGmicPixelFormat->transform(floatRGBApixel, floatRGBApixel, numContiguousColumns); pos = y * gmicImage->_width + x; for (qint32 bx = 0; bx < numContiguousColumns; bx++) { memcpy(gmicImage->_data + pos ,floatRGBApixel + bx * pixelSize , 4); memcpy(gmicImage->_data + pos + greenOffset ,floatRGBApixel + bx * pixelSize + 4, 4); memcpy(gmicImage->_data + pos + blueOffset ,floatRGBApixel + bx * pixelSize + 8, 4); memcpy(gmicImage->_data + pos + alphaOffset ,floatRGBApixel + bx * pixelSize + 12, 4); pos++; } x += numContiguousColumns; } } - delete [] floatRGBApixel; delete pixelToGmicPixelFormat; } void KisQmicSimpleConvertor::convertFromGmicImage(gmic_image& gmicImage, KisPaintDeviceSP dst, float gmicMaxChannelValue) { dbgPlugins << "convertFromGmicSlow"; Q_ASSERT(!dst.isNull()); const KoColorSpace *rgbaFloat32bitcolorSpace = KoColorSpaceRegistry::instance()->colorSpace(RGBAColorModelID.id(), Float32BitsColorDepthID.id(), KoColorSpaceRegistry::instance()->rgb8()->profile()); const KoColorSpace *dstColorSpace = dst->colorSpace(); KisPaintDeviceSP dev = dst; int greenOffset = gmicImage._width * gmicImage._height; int blueOffset = greenOffset * 2; int alphaOffset = greenOffset * 3; QRect rc(0,0,gmicImage._width, gmicImage._height); KisRandomAccessorSP it = dev->createRandomAccessorNG(0,0); int pos; float r,g,b,a; int optimalBufferSize = 64; // most common numContiguousColumns, tile size? - quint8 * floatRGBApixel = new quint8[rgbaFloat32bitcolorSpace->pixelSize() * optimalBufferSize]; + QScopedArrayPointer floatRGBApixelStorage(new quint8[rgbaFloat32bitcolorSpace->pixelSize() * optimalBufferSize]); + quint8 * floatRGBApixel = floatRGBApixelStorage.data(); quint32 pixelSize = rgbaFloat32bitcolorSpace->pixelSize(); KoColorConversionTransformation::Intent renderingIntent = KoColorConversionTransformation::internalRenderingIntent(); KoColorConversionTransformation::ConversionFlags conversionFlags = KoColorConversionTransformation::internalConversionFlags(); // Krita needs rgba in 0.0...1.0 float multiplied = KoColorSpaceMathsTraits::unitValue / gmicMaxChannelValue; switch (gmicImage._spectrum) { case 1: { // convert grayscale to rgba for (int y = 0; y < rc.height(); y++) { int x = 0; while (x < rc.width()) { it->moveTo(x, y); qint32 numContiguousColumns = qMin(it->numContiguousColumns(x), optimalBufferSize); numContiguousColumns = qMin(numContiguousColumns, rc.width() - x); pos = y * gmicImage._width + x; for (qint32 bx = 0; bx < numContiguousColumns; bx++) { r = g = b = gmicImage._data[pos] * multiplied; a = KoColorSpaceMathsTraits::unitValue; memcpy(floatRGBApixel + bx * pixelSize, &r,4); memcpy(floatRGBApixel + bx * pixelSize + 4, &g,4); memcpy(floatRGBApixel + bx * pixelSize + 8, &b,4); memcpy(floatRGBApixel + bx * pixelSize + 12, &a,4); pos++; } rgbaFloat32bitcolorSpace->convertPixelsTo(floatRGBApixel, it->rawData(), dstColorSpace, numContiguousColumns,renderingIntent, conversionFlags); x += numContiguousColumns; } } break; } case 2: { // convert grayscale alpha to rgba for (int y = 0; y < rc.height(); y++) { int x = 0; while (x < rc.width()) { it->moveTo(x, y); qint32 numContiguousColumns = qMin(it->numContiguousColumns(x), optimalBufferSize); numContiguousColumns = qMin(numContiguousColumns, rc.width() - x); pos = y * gmicImage._width + x; for (qint32 bx = 0; bx < numContiguousColumns; bx++) { r = g = b = gmicImage._data[pos] * multiplied; a = gmicImage._data[pos + greenOffset] * multiplied; memcpy(floatRGBApixel + bx * pixelSize, &r,4); memcpy(floatRGBApixel + bx * pixelSize + 4, &g,4); memcpy(floatRGBApixel + bx * pixelSize + 8, &b,4); memcpy(floatRGBApixel + bx * pixelSize + 12, &a,4); pos++; } rgbaFloat32bitcolorSpace->convertPixelsTo(floatRGBApixel, it->rawData(), dstColorSpace, numContiguousColumns,renderingIntent, conversionFlags); x += numContiguousColumns; } } break; } case 3: { // convert rgb -> rgba for (int y = 0; y < rc.height(); y++) { int x = 0; while (x < rc.width()) { it->moveTo(x, y); qint32 numContiguousColumns = qMin(it->numContiguousColumns(x), optimalBufferSize); numContiguousColumns = qMin(numContiguousColumns, rc.width() - x); pos = y * gmicImage._width + x; for (qint32 bx = 0; bx < numContiguousColumns; bx++) { r = gmicImage._data[pos] * multiplied; g = gmicImage._data[pos + greenOffset] * multiplied; b = gmicImage._data[pos + blueOffset ] * multiplied; a = gmicMaxChannelValue * multiplied; memcpy(floatRGBApixel + bx * pixelSize, &r,4); memcpy(floatRGBApixel + bx * pixelSize + 4, &g,4); memcpy(floatRGBApixel + bx * pixelSize + 8, &b,4); memcpy(floatRGBApixel + bx * pixelSize + 12, &a,4); pos++; } rgbaFloat32bitcolorSpace->convertPixelsTo(floatRGBApixel, it->rawData(), dstColorSpace, numContiguousColumns,renderingIntent, conversionFlags); x += numContiguousColumns; } } break; } case 4: { for (int y = 0; y < rc.height(); y++) { int x = 0; while (x < rc.width()) { it->moveTo(x, y); qint32 numContiguousColumns = qMin(it->numContiguousColumns(x), optimalBufferSize); numContiguousColumns = qMin(numContiguousColumns, rc.width() - x); pos = y * gmicImage._width + x; for (qint32 bx = 0; bx < numContiguousColumns; bx++) { r = gmicImage._data[pos] * multiplied; g = gmicImage._data[pos + greenOffset] * multiplied; b = gmicImage._data[pos + blueOffset ] * multiplied; a = gmicImage._data[pos + alphaOffset] * multiplied; memcpy(floatRGBApixel + bx * pixelSize, &r,4); memcpy(floatRGBApixel + bx * pixelSize + 4, &g,4); memcpy(floatRGBApixel + bx * pixelSize + 8, &b,4); memcpy(floatRGBApixel + bx * pixelSize + 12, &a,4); pos++; } rgbaFloat32bitcolorSpace->convertPixelsTo(floatRGBApixel, it->rawData(), dstColorSpace, numContiguousColumns,renderingIntent, conversionFlags); x += numContiguousColumns; } } break; } default: { dbgPlugins << "Unsupported gmic output format : " << gmicImage._width << gmicImage._height << gmicImage._depth << gmicImage._spectrum; } } } QImage KisQmicSimpleConvertor::convertToQImage(gmic_image& gmicImage, float gmicActualMaxChannelValue) { QImage image = QImage(gmicImage._width, gmicImage._height, QImage::Format_ARGB32); dbgPlugins << image.format() <<"first pixel:"<< gmicImage._data[0] << gmicImage._width << gmicImage._height << gmicImage._spectrum; int greenOffset = gmicImage._width * gmicImage._height; int blueOffset = greenOffset * 2; int pos = 0; // always put 255 to qimage float multiplied = 255.0f / gmicActualMaxChannelValue; for (unsigned int y = 0; y < gmicImage._height; y++) { QRgb *pixel = reinterpret_cast(image.scanLine(y)); for (unsigned int x = 0; x < gmicImage._width; x++) { pos = y * gmicImage._width + x; float r = gmicImage._data[pos] * multiplied; float g = gmicImage._data[pos + greenOffset] * multiplied; float b = gmicImage._data[pos + blueOffset] * multiplied; pixel[x] = qRgb(int(r),int(g), int(b)); } } return image; } void KisQmicSimpleConvertor::convertFromQImage(const QImage &image, gmic_image *gmicImage, float gmicUnitValue) { int greenOffset = gmicImage->_width * gmicImage->_height; int blueOffset = greenOffset * 2; int alphaOffset = greenOffset * 3; int pos = 0; // QImage has 0..255 float qimageUnitValue = 255.0f; float multiplied = gmicUnitValue / qimageUnitValue; Q_ASSERT(image.width() == int(gmicImage->_width)); Q_ASSERT(image.height() == int(gmicImage->_height)); Q_ASSERT(image.format() == QImage::Format_ARGB32); switch (gmicImage->_spectrum) { case 1: { for (int y = 0; y < image.height(); y++) { const QRgb *pixel = reinterpret_cast(image.scanLine(y)); for (int x = 0; x < image.width(); x++) { pos = y * gmicImage->_width + x; gmicImage->_data[pos] = qGray(pixel[x]) * multiplied; } } break; } case 2: { for (int y = 0; y < image.height(); y++) { const QRgb *pixel = reinterpret_cast(image.scanLine(y)); for (int x = 0; x < image.width(); x++) { pos = y * gmicImage->_width + x; gmicImage->_data[pos] = qGray(pixel[x]) * multiplied; gmicImage->_data[pos + greenOffset] = qAlpha(pixel[x]) * multiplied; } } break; } case 3: { for (int y = 0; y < image.height(); y++) { const QRgb *pixel = reinterpret_cast(image.scanLine(y)); for (int x = 0; x < image.width(); x++) { pos = y * gmicImage->_width + x; gmicImage->_data[pos] = qRed(pixel[x]) * multiplied; gmicImage->_data[pos + greenOffset] = qGreen(pixel[x]) * multiplied; gmicImage->_data[pos + blueOffset] = qBlue(pixel[x]) * multiplied; } } break; } case 4: { for (int y = 0; y < image.height(); y++) { const QRgb *pixel = reinterpret_cast(image.scanLine(y)); for (int x = 0; x < image.width(); x++) { pos = y * gmicImage->_width + x; gmicImage->_data[pos] = qRed(pixel[x]) * multiplied; gmicImage->_data[pos + greenOffset] = qGreen(pixel[x]) * multiplied; gmicImage->_data[pos + blueOffset] = qBlue(pixel[x]) * multiplied; gmicImage->_data[pos + alphaOffset] = qAlpha(pixel[x]) * multiplied; } } break; } default: { Q_ASSERT(false); dbgKrita << "Unexpected gmic image format"; break; } } } diff --git a/plugins/filters/gaussianhighpass/gaussianhighpass.h b/plugins/filters/gaussianhighpass/gaussianhighpass.h index d714b9fdd7..d2c9ddcafe 100644 --- a/plugins/filters/gaussianhighpass/gaussianhighpass.h +++ b/plugins/filters/gaussianhighpass/gaussianhighpass.h @@ -1,35 +1,35 @@ /* * This file is part of Krita * * Copyright (c) 2019 Miguel Lopez * * 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 _GUASSIANHIGHPASS_PLUGIN_H_ -#define _GUASSIANHIGHPASS_PLUGIN_H_ +#ifndef _GAUSSIANHIGHPASS_PLUGIN_H_ +#define _GAUSSIANHIGHPASS_PLUGIN_H_ #include #include class GaussianHighPassPlugin : public QObject { Q_OBJECT public: GaussianHighPassPlugin(QObject *parent, const QVariantList &); ~GaussianHighPassPlugin() override; }; #endif diff --git a/plugins/filters/gaussianhighpass/gaussianhighpass_filter.h b/plugins/filters/gaussianhighpass/gaussianhighpass_filter.h index 418c3dae24..b772b97156 100644 --- a/plugins/filters/gaussianhighpass/gaussianhighpass_filter.h +++ b/plugins/filters/gaussianhighpass/gaussianhighpass_filter.h @@ -1,54 +1,54 @@ /* * This file is part of Krita * * Copyright (c) 2019 Miguel Lopez * * 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_GUASSIANHIGHPASS_FILTER_H -#define KIS_GUASSIANHIGHPASS_FILTER_H +#ifndef KIS_GAUSSIANHIGHPASS_FILTER_H +#define KIS_GAUSSIANHIGHPASS_FILTER_H #include "filter/kis_filter.h" #include "kis_cached_paint_device.h" class KisGaussianHighPassFilter : public KisFilter { public: KisGaussianHighPassFilter(); void processImpl(KisPaintDeviceSP device, const QRect& applyRect, const KisFilterConfigurationSP config, KoUpdater * ) const override; static inline KoID id() { return KoID("gaussianhighpass", i18n("Gaussian High Pass")); } KisConfigWidget * createConfigurationWidget(QWidget* parent, const KisPaintDeviceSP dev, bool useForMasks) const override; KisFilterConfigurationSP factoryConfiguration() const override; QRect changedRect(const QRect & rect, const KisFilterConfigurationSP _config, int lod) const override; QRect neededRect(const QRect & rect, const KisFilterConfigurationSP _config, int lod) const override; private: mutable KisCachedPaintDevice m_cachedPaintDevice; }; #endif diff --git a/plugins/filters/gaussianhighpass/wdg_gaussianhighpass.h b/plugins/filters/gaussianhighpass/wdg_gaussianhighpass.h index 21985c221d..bfe21debaa 100644 --- a/plugins/filters/gaussianhighpass/wdg_gaussianhighpass.h +++ b/plugins/filters/gaussianhighpass/wdg_gaussianhighpass.h @@ -1,43 +1,43 @@ /* * This file is part of Krita * * Copyright (c) 2019 Miguel Lopez * * 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_WDG_GUASSIANHIGHPASS_H_ -#define _KIS_WDG_GUASSIANHIGHPASS_H_ +#ifndef _KIS_WDG_GAUSSIANHIGHPASS_H_ +#define _KIS_WDG_GAUSSIANHIGHPASS_H_ #include class Ui_WdgGaussianHighPass; class KisWdgGaussianHighPass : public KisConfigWidget { Q_OBJECT public: KisWdgGaussianHighPass(QWidget * parent); ~KisWdgGaussianHighPass() override; inline const Ui_WdgGaussianHighPass* widget() const { return m_widget; } void setConfiguration(const KisPropertiesConfigurationSP) override; KisPropertiesConfigurationSP configuration() const override; private: Ui_WdgGaussianHighPass* m_widget; }; #endif diff --git a/plugins/filters/gradientmap/krita_filter_gradient_map.cpp b/plugins/filters/gradientmap/krita_filter_gradient_map.cpp index 4b8b6eda03..4e6aa773ef 100644 --- a/plugins/filters/gradientmap/krita_filter_gradient_map.cpp +++ b/plugins/filters/gradientmap/krita_filter_gradient_map.cpp @@ -1,136 +1,138 @@ /* * This file is part of the KDE project * * Copyright (c) 2016 Spencer Brown * 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_filter_gradient_map.h" #include #include #include #include #include #include #include "kis_config_widget.h" #include #include #include #include #include #include "gradientmap.h" #include #include KritaFilterGradientMap::KritaFilterGradientMap() : KisFilter(id(), FiltersCategoryMapId, i18n("&Gradient Map...")) { setColorSpaceIndependence(FULLY_INDEPENDENT); setShowConfigurationWidget(true); setSupportsLevelOfDetail(true); setSupportsPainting(true); setSupportsAdjustmentLayers(true); setSupportsThreading(true); } void KritaFilterGradientMap::processImpl(KisPaintDeviceSP device, const QRect& applyRect, const KisFilterConfigurationSP config, KoUpdater *progressUpdater) const { Q_ASSERT(!device.isNull()); QDomDocument doc; if (config->version()==1) { QDomElement elt = doc.createElement("gradient"); KoAbstractGradient *gradientAb = KoResourceServerProvider::instance()->gradientServer()->resourceByName(config->getString("gradientName")); if (!gradientAb) { qWarning() << "Could not find gradient" << config->getString("gradientName"); } gradientAb = KoResourceServerProvider::instance()->gradientServer()->resources().first(); - KoStopGradient::fromQGradient(gradientAb->toQGradient())->toXML(doc, elt); + QScopedPointer qGradient(gradientAb->toQGradient()); + KoStopGradient::fromQGradient(qGradient.data())->toXML(doc, elt); doc.appendChild(elt); } else { doc.setContent(config->getString("gradientXML", "")); } KoStopGradient gradient = KoStopGradient::fromXML(doc.firstChildElement()); const ColorMode colorMode = ColorMode(config->getInt("colorMode")); KisDitherUtil ditherUtil; if (colorMode == ColorMode::Dither) ditherUtil.setConfiguration(*config, "dither/"); KoColor outColor(Qt::white, device->colorSpace()); KisSequentialIteratorProgress it(device, applyRect, progressUpdater); qreal grey; const int pixelSize = device->colorSpace()->pixelSize(); while (it.nextPixel()) { grey = qreal(device->colorSpace()->intensity8(it.oldRawData())) / 255; if (colorMode == ColorMode::Nearest) { KoGradientStop leftStop, rightStop; if (!gradient.stopsAt(leftStop, rightStop, grey)) continue; if (std::abs(grey - leftStop.first) < std::abs(grey - rightStop.first)) { outColor = leftStop.second; } else { outColor = rightStop.second; } } else if (colorMode == ColorMode::Dither) { KoGradientStop leftStop, rightStop; if (!gradient.stopsAt(leftStop, rightStop, grey)) continue; qreal localT = (grey - leftStop.first) / (rightStop.first - leftStop.first); if (localT < ditherUtil.threshold(QPoint(it.x(), it.y()))) { outColor = leftStop.second; } else { outColor = rightStop.second; } } else { gradient.colorAt(outColor, grey); } outColor.setOpacity(qMin(KoColor(it.oldRawData(), device->colorSpace()).opacityF(), outColor.opacityF())); outColor.convertTo(device->colorSpace()); memcpy(it.rawData(), outColor.data(), pixelSize); } } KisFilterConfigurationSP KritaFilterGradientMap::factoryConfiguration() const { KisFilterConfigurationSP config = new KisFilterConfiguration("gradientmap", 2); KoAbstractGradient *gradient = KoResourceServerProvider::instance()->gradientServer()->resources().first(); KoStopGradient stopGradient; - stopGradient.fromQGradient(gradient->toQGradient()); + QScopedPointer qGradient(gradient->toQGradient()); + stopGradient.fromQGradient(qGradient.data()); QDomDocument doc; QDomElement elt = doc.createElement("gradient"); stopGradient.toXML(doc, elt); doc.appendChild(elt); config->setProperty("gradientXML", doc.toString()); config->setProperty("colorMode", false); KisDitherWidget::factoryConfiguration(*config, "dither/"); return config; } KisConfigWidget * KritaFilterGradientMap::createConfigurationWidget(QWidget * parent, const KisPaintDeviceSP dev, bool) const { return new KritaGradientMapConfigWidget(parent, dev); } diff --git a/plugins/impex/psd/psd_resource_section.cpp b/plugins/impex/psd/psd_resource_section.cpp index 1783505243..e7e63eca82 100644 --- a/plugins/impex/psd/psd_resource_section.cpp +++ b/plugins/impex/psd/psd_resource_section.cpp @@ -1,228 +1,228 @@ /* * 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. */ #include "psd_resource_section.h" #include #include #include #include "psd_utils.h" #include "psd_resource_block.h" PSDImageResourceSection::PSDImageResourceSection() { } PSDImageResourceSection::~PSDImageResourceSection() { resources.clear(); } bool PSDImageResourceSection::read(QIODevice* io) { quint32 resourceSectionLength = 0; if (!psdread(io, &resourceSectionLength)) { error = "Could not read image resource section length"; return false; } dbgFile << "Image Resource Sectionlength:" << resourceSectionLength << ", starts at:" << io->pos(); QByteArray ba = io->read(resourceSectionLength); if ((quint32)ba.size() != resourceSectionLength) { error = "Could not read all resources"; return false; } QBuffer buf; buf.setBuffer(&ba); buf.open(QBuffer::ReadOnly); while (!buf.atEnd()) { - PSDResourceBlock* block = new PSDResourceBlock(); + QScopedPointer block(new PSDResourceBlock()); if (!block->read(&buf)) { error = "Error reading block: " + block->error; dbgFile << error << ", skipping."; continue; } dbgFile << "resource block created. Type:" << block->identifier << "name" << block->name << "size" << block->dataSize << "," << buf.bytesAvailable() << "bytes to go"; - resources[(PSDResourceID)block->identifier] = block; + resources[(PSDResourceID)block->identifier] = block.take(); } dbgFile << "Read" << resources.size() << "Image Resource Blocks"; return valid(); } bool PSDImageResourceSection::write(QIODevice* io) { Q_UNUSED(io); if (!valid()) { error = "Resource Section is Invalid"; return false; } // First write all the sections QByteArray ba; QBuffer buf; buf.setBuffer(&ba); buf.open(QBuffer::WriteOnly); Q_FOREACH (PSDResourceBlock* block, resources) { if (!block->write(&buf)) { error = block->error; return false; } } buf.close(); // Then get the size quint32 resourceBlockLength = ba.size(); dbgFile << "resource section has size" << resourceBlockLength; psdwrite(io, resourceBlockLength); // and write the whole buffer; return (io->write(ba.constData(), ba.size()) == resourceBlockLength); } bool PSDImageResourceSection::valid() { return true; } QString PSDImageResourceSection::idToString(PSDImageResourceSection::PSDResourceID id) { switch(id) { case UNKNOWN: return "Unknown"; case PS2_IMAGE_INFO : return "0x03e8 - Obsolete - ps 2.0 image info"; case MAC_PRINT_INFO : return "0x03e9 - Optional - Mac print manager print info record"; case PS2_COLOR_TAB : return "0x03eb - Obsolete - ps 2.0 indexed colour table"; case RESN_INFO : return "0x03ed - ResolutionInfo structure"; case ALPHA_NAMES : return "0x03ee - Alpha channel names"; case DISPLAY_INFO : return "0x03ef - DisplayInfo structure"; case CAPTION : return "0x03f0 - Optional - Caption string"; case BORDER_INFO : return "0x03f1 - Border info"; case BACKGROUND_COL : return "0x03f2 - Background colour"; case PRINT_FLAGS : return "0x03f3 - Print flags"; case GREY_HALFTONE : return "0x03f4 - Greyscale and multichannel halftoning info"; case COLOR_HALFTONE : return "0x03f5 - Colour halftoning info"; case DUOTONE_HALFTONE : return "0x03f6 - Duotone halftoning info"; case GREY_XFER : return "0x03f7 - Greyscale and multichannel transfer functions"; case COLOR_XFER : return "0x03f8 - Colour transfer functions"; case DUOTONE_XFER : return "0x03f9 - Duotone transfer functions"; case DUOTONE_INFO : return "0x03fa - Duotone image information"; case EFFECTIVE_BW : return "0x03fb - Effective black & white values for dot range"; case OBSOLETE_01 : return "0x03fc - Obsolete"; case EPS_OPT : return "0x03fd - EPS options"; case QUICK_MASK : return "0x03fe - Quick mask info"; case OBSOLETE_02 : return "0x03ff - Obsolete"; case LAYER_STATE : return "0x0400 - Layer state info"; case WORKING_PATH : return "0x0401 - Working path (not saved)"; case LAYER_GROUP : return "0x0402 - Layers group info"; case OBSOLETE_03 : return "0x0403 - Obsolete"; case IPTC_NAA_DATA : return "0x0404 - IPTC-NAA record (IMV4.pdf)"; case IMAGE_MODE_RAW : return "0x0405 - Image mode for raw format files"; case JPEG_QUAL : return "0x0406 - JPEG quality"; case GRID_GUIDE : return "0x0408 - Grid & guide info"; case THUMB_RES : return "0x0409 - Thumbnail resource"; case COPYRIGHT_FLG : return "0x040a - Copyright flag"; case URL : return "0x040b - URL string"; case THUMB_RES2 : return "0x040c - Thumbnail resource"; case GLOBAL_ANGLE : return "0x040d - Global angle"; case COLOR_SAMPLER : return "0x040e - Colour samplers resource"; case ICC_PROFILE : return "0x040f - ICC Profile"; case WATERMARK : return "0x0410 - Watermark"; case ICC_UNTAGGED : return "0x0411 - Do not use ICC profile flag"; case EFFECTS_VISIBLE : return "0x0412 - Show / hide all effects layers"; case SPOT_HALFTONE : return "0x0413 - Spot halftone"; case DOC_IDS : return "0x0414 - Document specific IDs"; case ALPHA_NAMES_UNI : return "0x0415 - Unicode alpha names"; case IDX_COL_TAB_CNT : return "0x0416 - Indexed colour table count"; case IDX_TRANSPARENT : return "0x0417 - Index of transparent colour (if any)"; case GLOBAL_ALT : return "0x0419 - Global altitude"; case SLICES : return "0x041a - Slices"; case WORKFLOW_URL_UNI : return "0x041b - Workflow URL - Unicode string"; case JUMP_TO_XPEP : return "0x041c - Jump to XPEP (?)"; case ALPHA_ID : return "0x041d - Alpha IDs"; case URL_LIST_UNI : return "0x041e - URL list - unicode"; case VERSION_INFO : return "0x0421 - Version info"; case EXIF_DATA : return "0x0422 - (Photoshop 7.0) EXIF data 1. See http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf"; case EXIF_DATA_3 : return "0x0423 - (Photoshop 7.0) EXIF data 3. See http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf"; case XMP_DATA : return "0x0424 - XMP data block"; case CAPTION_DIGEST : return "0x0425 - (Photoshop 7.0) Caption digest. 16 bytes: RSA Data Security, MD5 message-digest algorithm"; case PRINT_SCALE : return "0x0426 - (Photoshop 7.0) Print scale. 2 bytes style (0 = centered, 1 = size to fit, 2 = user defined). 4 bytes x location (floating point). 4 bytes y location (floating point). 4 bytes scale (floating point)"; case PIXEL_ASPECT_RATION : return "0x0428 - (Photoshop CS) Pixel Aspect Ratio. 4 bytes (version = 1 or 2), 8 bytes double, x / y of a pixel. Version 2, attempting to correct values for NTSC and PAL, previously off by a factor of approx. 5%."; case LAYER_COMPS : return "0x0429 - (Photoshop CS) Layer Comps. 4 bytes (descriptor version = 16), Descriptor (see See Descriptor structure)"; case ALTERNATE_DUOTONE : return "0x042A - (Photoshop CS) Alternate Duotone Colors. 2 bytes (version = 1), 2 bytes count, following is repeated for each count: [ Color: 2 bytes for space followed by 4 * 2 byte color component ], following this is another 2 byte count, usually 256, followed by Lab colors one byte each for L, a, b. This resource is not read or used by Photoshop."; case ALTERNATE_SPOT : return "0x042B - (Photoshop CS)Alternate Spot Colors. 2 bytes (version = 1), 2 bytes channel count, following is repeated for each count: 4 bytes channel ID, Color: 2 bytes for space followed by 4 * 2 byte color component. This resource is not read or used by Photoshop."; case LAYER_SELECTION_ID : return "0x042D - (Photoshop CS2) Layer Selection ID(s). 2 bytes count, following is repeated for each count: 4 bytes layer ID"; case HDR_TONING : return "0x042E - (Photoshop CS2) HDR Toning information"; case CS2_PRINT_INFO : return "0x042F - (Photoshop CS2) Print info"; case LAYER_GROUP_ENABLED_ID: return "0x0430 - (Photoshop CS2) Layer Group(s) Enabled ID. 1 byte for each layer in the document, repeated by length of the resource. NOTE: Layer groups have start and end markers"; case COLOR_SAMPLERS : return "0x0431 - (Photoshop CS3) Color samplers resource. Also see ID 1038 for old format. See See Color samplers resource format."; case MEASUREMENT_SCALE : return "0x0432 - (Photoshop CS3) Measurement Scale. 4 bytes (descriptor version = 16), Descriptor (see See Descriptor structure)"; case TIMELINE_INFO : return "0x0433 - (Photoshop CS3) Timeline Information. 4 bytes (descriptor version = 16), Descriptor (see See Descriptor structure)"; case SHEET_DISCLOSURE : return "0x0434 - (Photoshop CS3) Sheet Disclosure. 4 bytes (descriptor version = 16), Descriptor (see See Descriptor structure)"; case CS3_DISPLAY_INFO : return "0x0435 - (Photoshop CS3) DisplayInfo structure to support floating point clors. Also see ID 1007. See Appendix A in Photoshop API Guide.pdf ."; case ONION_SKINS : return "0x0436 - (Photoshop CS3) Onion Skins. 4 bytes (descriptor version = 16), Descriptor (see See Descriptor structure)"; case COUNT_INFO : return "0x0438 - (Photoshop CS4) Count Information. 4 bytes (descriptor version = 16), Descriptor (see See Descriptor structure) Information about the count in the document. See the Count Tool."; case CS5_PRINT_INFO : return "0x043A - (Photoshop CS5) Print Information. 4 bytes (descriptor version = 16), Descriptor (see See Descriptor structure) Information about the current print settings in the document. The color management options."; case CS5_PRINT_STYLE : return "0x043B - (Photoshop CS5) Print Style. 4 bytes (descriptor version = 16), Descriptor (see See Descriptor structure) Information about the current print style in the document. The printing marks, labels, ornaments, etc."; case CS5_NSPrintInfo : return "0x043C - (Photoshop CS5) Macintosh NSPrintInfo. Variable OS specific info for Macintosh. NSPrintInfo. It is recommended that you do not interpret or use this data."; case CS5_WIN_DEVMODE : return "0x043D - (Photoshop CS5) Windows DEVMODE. Variable OS specific info for Windows. DEVMODE. It is recommended that you do not interpret or use this data."; case CS6_AUTOSAVE_FILE_PATH : return "0x043E - (Photoshop CS6) Auto Save File Path. Unicode string. It is recommended that you do not interpret or use this data."; case CS6_AUTOSAVE_FORMAT : return "0x043F - (Photoshop CS6) Auto Save Format. Unicode string. It is recommended that you do not interpret or use this data."; case CC_PATH_SELECTION_SATE : return "0x0440 - (Photoshop CC) Path Selection State. 4 bytes (descriptor version = 16), Descriptor (see See Descriptor structure) Information about the current path selection state."; case PATH_INFO_FIRST : return "0x07d0 - First path info block"; case PATH_INFO_LAST : return "0x0bb6 - Last path info block"; case CLIPPING_PATH : return "0x0bb7 - Name of clipping path"; case CC_ORIGIN_PATH_INFO : return "0x0BB8 (Photoshop CC) Origin Path Info. 4 bytes (descriptor version = 16), Descriptor (see See Descriptor structure) Information about the origin path data."; case PLUGIN_RESOURCE_START : return "0x0FA0-0x1387 Plug-In resource(s). Resources added by a plug-in. See the plug-in API found in the SDK documentation "; case PLUGIN_RESOURCE_END : return "Last plug-in resource"; case IMAGE_READY_VARS : return "0x1B58 Image Ready variables. XML representation of variables definition"; case IMAGE_READY_DATA_SETS : return "0x1B59 Image Ready data sets"; case LIGHTROOM_WORKFLOW : return "0x1F40 (Photoshop CS3) Lightroom workflow, if present the document is in the middle of a Lightroom workflow."; case PRINT_FLAGS_2 : return "0x2710 - Print flags"; default: { if (id > PATH_INFO_FIRST && id < PATH_INFO_LAST) return "Path Info Block"; if (id > PLUGIN_RESOURCE_START && id < PLUGIN_RESOURCE_END) return "Plug-In Resource"; } }; return QString("Unknown Resource Block: %1").arg(id); } diff --git a/plugins/impex/xcf/3rdparty/xcftools/utils.c b/plugins/impex/xcf/3rdparty/xcftools/utils.c index d76b3dd812..aa8ba263f9 100644 --- a/plugins/impex/xcf/3rdparty/xcftools/utils.c +++ b/plugins/impex/xcf/3rdparty/xcftools/utils.c @@ -1,169 +1,178 @@ /* Generic support functions for Xcftools * * This file was written by Henning Makholm * It is hereby in the public domain. * * In jurisdictions that do not recognise grants of copyright to the * public domain: I, the author and (presumably, in those jurisdictions) * copyright holder, hereby permit anyone to distribute and use this code, * in source code or binary form, with or without modifications. This * permission is world-wide and irrevocable. * * Of course, I will not be liable for any errors or shortcomings in the * code, since I give it away without asking any compenstations. * * If you use or distribute this code, I would appreciate receiving * credit for writing it, in whichever way you find proper and customary. */ #include "xcftools.h" #include #include #include #include const char *progname = "$0" ; int verboseFlag = 0 ; void -vFatalGeneric(int status,const char *format,va_list args) +vFatalGeneric(int status,const char *format, va_list args) { (void) status; /* mark as unused */ if( format ) { if( *format == '!' ) { vfprintf(stderr,format+1,args); fprintf(stderr,": %s\n",strerror(errno)); } else { vfprintf(stderr,format,args); fputc('\n',stderr); } } /* don't exit here - Krita can't handle errors otherwise */ /* exit(status); */ } void FatalGeneric(int status,const char* format,...) { - va_list v; va_start(v,format); + va_list v; + va_start(v,format); if( format ) fprintf(stderr,"%s: ",progname); vFatalGeneric(status,format,v); + va_end(v); } void FatalUnexpected(const char* format,...) { - va_list v; va_start(v,format); + va_list v; + va_start(v, format); fprintf(stderr,"%s: ",progname); - vFatalGeneric(127,format,v) ; + vFatalGeneric(127, format, v); + va_end(v); } void FatalBadXCF(const char* format,...) { va_list v; va_start(v,format); fprintf(stderr,"%s: %s:\n ",progname,_("Corrupted or malformed XCF file")); vFatalGeneric(125,format,v) ; + va_end(v); } int xcfCheckspace(uint32_t addr,int spaceafter,const char *format,...) { if( xcf_length < spaceafter || addr > xcf_length - spaceafter ) { - va_list v; va_start(v,format); + va_list v; + va_start(v,format); fprintf(stderr,"%s: %s\n ",progname,_("Corrupted or truncated XCF file")); fprintf(stderr,"(0x%" PRIXPTR " bytes): ",(uintptr_t)xcf_length); vFatalGeneric(125,format,v) ; + va_end(v); return XCF_ERROR; } return XCF_OK; } void FatalUnsupportedXCF(const char* format,...) { - va_list v; va_start(v,format); + va_list v; + va_start(v,format); fprintf(stderr,"%s: %s\n ",progname, _("The image contains features not understood by this program:")); vFatalGeneric(123,format,v) ; + va_end(v); } void gpl_blurb(void) { fprintf(stderr,PACKAGE_STRING "\n"); fprintf(stderr, _("Type \"%s -h\" to get an option summary.\n"),progname); /* don't exit here - Krita will close otherwise */ /* exit(1) ; */ } /* ******************************************************* */ void * xcfmalloc(size_t size) { void *ptr = malloc(size); if( !ptr ) { FatalUnexpected(_("Out of memory")); return XCF_PTR_EMPTY; } return ptr ; } void xcffree(void *block) { if( xcf_file && (uint8_t*)block >= xcf_file && (uint8_t*)block < xcf_file + xcf_length ) ; else free(block); } /* ******************************************************* */ FILE * openout(const char *name) { FILE *newfile ; if( strcmp(name,"-") == 0 ) return stdout ; newfile = fopen(name,"wb") ; if( newfile == NULL ) { FatalUnexpected(_("!Cannot create file %s"),name); return XCF_PTR_EMPTY; } return newfile ; } int closeout(FILE *f,const char *name) { if( f == NULL ) return XCF_OK; if( fflush(f) == 0 ) { errno = 0 ; if( !ferror(f) ) { if( fclose(f) == 0 ) return XCF_OK; } else if( errno == 0 ) { /* Attempt to coax a valid errno out of the standard library, * following an idea by Bruno Haible * http://lists.gnu.org/archive/html/bug-gnulib/2003-09/msg00157.html */ if( fputc('\0', f) != EOF && fflush(f) == 0 ) errno = EIO ; /* Argh, everything succedes. Just call it an I/O error */ } } FatalUnexpected(_("!Error writing file %s"),name); return XCF_ERROR; } diff --git a/plugins/tools/basictools/kis_tool_measure.cc b/plugins/tools/basictools/kis_tool_measure.cc index f5c0e3edca..714e26e641 100644 --- a/plugins/tools/basictools/kis_tool_measure.cc +++ b/plugins/tools/basictools/kis_tool_measure.cc @@ -1,219 +1,219 @@ /* * * 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_tool_measure.h" #include #include #include #include #include #include #include #include #include "kis_image.h" #include "kis_cursor.h" #include "KoPointerEvent.h" #include "KoCanvasBase.h" #include #include "krita_utils.h" #define INNER_RADIUS 50 KisToolMeasureOptionsWidget::KisToolMeasureOptionsWidget(QWidget* parent, double resolution) : QWidget(parent), m_resolution(resolution), m_unit(KoUnit::Pixel) { m_distance = 0.0; QGridLayout* optionLayout = new QGridLayout(this); Q_CHECK_PTR(optionLayout); optionLayout->setMargin(0); optionLayout->addWidget(new QLabel(i18n("Distance:"), this), 0, 0); optionLayout->addWidget(new QLabel(i18n("Angle:"), this), 1, 0); m_distanceLabel = new QLabel(this); m_distanceLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); optionLayout->addWidget(m_distanceLabel, 0, 1); m_angleLabel = new QLabel(this); m_angleLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter); optionLayout->addWidget(m_angleLabel, 1, 1); KComboBox* unitBox = new KComboBox(this); unitBox->addItems(KoUnit::listOfUnitNameForUi(KoUnit::ListAll)); connect(unitBox, SIGNAL(currentIndexChanged(int)), this, SLOT(slotUnitChanged(int))); unitBox->setCurrentIndex(m_unit.indexInListForUi(KoUnit::ListAll)); optionLayout->addWidget(unitBox, 0, 2); optionLayout->addItem(new QSpacerItem(1, 1, QSizePolicy::Fixed, QSizePolicy::Expanding), 2, 0, 1, 2); } void KisToolMeasureOptionsWidget::slotSetDistance(double distance) { m_distance = distance / m_resolution; updateDistance(); } void KisToolMeasureOptionsWidget::slotSetAngle(double angle) { m_angleLabel->setText(i18nc("angle value in degrees", "%1°", KritaUtils::prettyFormatReal(angle))); } void KisToolMeasureOptionsWidget::slotUnitChanged(int index) { m_unit = KoUnit::fromListForUi(index, KoUnit::ListAll, m_resolution); updateDistance(); } void KisToolMeasureOptionsWidget::updateDistance() { - m_distanceLabel->setText(QString("%1").arg(m_unit.toUserValue(m_distance), 5, 'f', 1)); + m_distanceLabel->setText(KritaUtils::prettyFormatReal(m_unit.toUserValue(m_distance))); } KisToolMeasure::KisToolMeasure(KoCanvasBase * canvas) : KisTool(canvas, KisCursor::crossCursor()) { } KisToolMeasure::~KisToolMeasure() { } void KisToolMeasure::paint(QPainter& gc, const KoViewConverter &converter) { qreal sx, sy; converter.zoom(&sx, &sy); gc.scale(sx / currentImage()->xRes(), sy / currentImage()->yRes()); QPen old = gc.pen(); QPen pen(Qt::SolidLine); gc.setPen(pen); gc.drawLine(m_startPos, m_endPos); if (deltaX() >= 0) gc.drawLine(QPointF(m_startPos.x(), m_startPos.y()), QPointF(m_startPos.x() + INNER_RADIUS, m_startPos.y())); else gc.drawLine(QPointF(m_startPos.x(), m_startPos.y()), QPointF(m_startPos.x() - INNER_RADIUS, m_startPos.y())); if (distance() >= INNER_RADIUS) { QRectF rectangle(m_startPos.x() - INNER_RADIUS, m_startPos.y() - INNER_RADIUS, 2*INNER_RADIUS, 2*INNER_RADIUS); int startAngle = (deltaX() >= 0) ? 0 : 180 * 16; int spanAngle; if ((deltaY() >= 0 && deltaX() >= 0) || (deltaY() < 0 && deltaX() < 0)) spanAngle = static_cast(angle() * 16); else spanAngle = static_cast(-angle() * 16); gc.drawArc(rectangle, startAngle, spanAngle); } gc.setPen(old); } void KisToolMeasure::beginPrimaryAction(KoPointerEvent *event) { setMode(KisTool::PAINT_MODE); // Erase old temporary lines canvas()->updateCanvas(convertToPt(boundingRect())); m_startPos = convertToPixelCoord(event); m_endPos = m_startPos; emit sigDistanceChanged(0.0); emit sigAngleChanged(0.0); } void KisToolMeasure::continuePrimaryAction(KoPointerEvent *event) { CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE); // Erase old temporary lines canvas()->updateCanvas(convertToPt(boundingRect())); QPointF pos = convertToPixelCoord(event); if (event->modifiers() == Qt::AltModifier) { QPointF trans = pos - m_endPos; m_startPos += trans; m_endPos += trans; } else { m_endPos = pos; } canvas()->updateCanvas(convertToPt(boundingRect())); emit sigDistanceChanged(distance()); emit sigAngleChanged(angle()); } void KisToolMeasure::endPrimaryAction(KoPointerEvent *event) { CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE); Q_UNUSED(event); setMode(KisTool::HOVER_MODE); } QWidget* KisToolMeasure::createOptionWidget() { if (!currentImage()) return 0; m_optionsWidget = new KisToolMeasureOptionsWidget(0, currentImage()->xRes()); // See https://bugs.kde.org/show_bug.cgi?id=316896 QWidget *specialSpacer = new QWidget(m_optionsWidget); specialSpacer->setObjectName("SpecialSpacer"); specialSpacer->setFixedSize(0, 0); m_optionsWidget->layout()->addWidget(specialSpacer); m_optionsWidget->setObjectName(toolId() + " option widget"); connect(this, SIGNAL(sigDistanceChanged(double)), m_optionsWidget, SLOT(slotSetDistance(double))); connect(this, SIGNAL(sigAngleChanged(double)), m_optionsWidget, SLOT(slotSetAngle(double))); m_optionsWidget->setFixedHeight(m_optionsWidget->sizeHint().height()); return m_optionsWidget; } double KisToolMeasure::angle() { return atan(qAbs(deltaY()) / qAbs(deltaX())) / (2*M_PI)*360; } double KisToolMeasure::distance() { return sqrt(deltaX()*deltaX() + deltaY()*deltaY()); } QRectF KisToolMeasure::boundingRect() { QRectF bound; bound.setTopLeft(m_startPos); bound.setBottomRight(m_endPos); bound = bound.united(QRectF(m_startPos.x() - INNER_RADIUS, m_startPos.y() - INNER_RADIUS, 2 * INNER_RADIUS, 2 * INNER_RADIUS)); return bound.normalized(); } diff --git a/plugins/tools/selectiontools/kis_tool_select_outline.cc b/plugins/tools/selectiontools/kis_tool_select_outline.cc index 87763bd16c..80108ef9ab 100644 --- a/plugins/tools/selectiontools/kis_tool_select_outline.cc +++ b/plugins/tools/selectiontools/kis_tool_select_outline.cc @@ -1,280 +1,288 @@ /* * 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) { } 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) { if (selectionDragInProgress()) return; m_lastCursorPos = convertToPixelCoord(event); if (m_continuedMode && mode() != PAINT_MODE) { updateContinuedMode(); + } else { + KisToolSelect::mouseMoveEvent(event); } KisToolSelect::mouseMoveEvent(event); } void KisToolSelectOutline::beginPrimaryAction(KoPointerEvent *event) { - if (selectionDragInProgress()) return; + KisToolSelectBase::beginPrimaryAction(event); + if (selectionDragInProgress()) { + return; + } 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) -{ - if (selectionDragInProgress()) return; +{ + if (selectionDragInProgress()) { + KisToolSelectBase::continuePrimaryAction(event); + return; + } CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE); QPointF point = convertToPixelCoord(event); m_paintPath.lineTo(pixelToView(point)); m_points.append(point); updateFeedback(); } void KisToolSelectOutline::endPrimaryAction(KoPointerEvent *event) { - Q_UNUSED(event); - const bool hadMoveInProgress = selectionDragInProgress(); - - if (hadMoveInProgress) return; + if (selectionDragInProgress()) { + KisToolSelectBase::endPrimaryAction(event); + return; + } CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE); setMode(KisTool::HOVER_MODE); if (!m_continuedMode) { finishSelectionAction(); m_points.clear(); // ensure points are always cleared } } void KisToolSelectOutline::finishSelectionAction() { KisCanvas2 * kisCanvas = dynamic_cast(canvas()); KIS_ASSERT_RECOVER_RETURN(kisCanvas); kisCanvas->updateCanvas(); const QRectF boundingRect = KisAlgebra2D::accumulateBounds(m_points); const QRectF boundingViewRect = pixelToView(boundingRect); KisSelectionToolHelper helper(kisCanvas, kundo2_i18n("Select by Outline")); if (helper.tryDeselectCurrentSelection(boundingViewRect, selectionAction())) { return; } if (m_points.count() > 2) { QApplication::setOverrideCursor(KisCursor::waitCursor()); const SelectionMode mode = helper.tryOverrideSelectionMode(kisCanvas->viewManager()->selection(), selectionMode(), selectionAction()); if (mode == PIXEL_SELECTION) { KisPixelSelectionSP tmpSel = KisPixelSelectionSP(new KisPixelSelection()); KisPainter painter(tmpSel); painter.setPaintColor(KoColor(Qt::black, tmpSel->colorSpace())); 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, selectionAction()); } 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::resetCursorStyle() { if (selectionAction() == SELECTION_ADD) { useCursor(KisCursor::load("tool_outline_selection_cursor_add.png", 6, 6)); } else if (selectionAction() == SELECTION_SUBTRACT) { useCursor(KisCursor::load("tool_outline_selection_cursor_sub.png", 6, 6)); } else { KisToolSelect::resetCursorStyle(); } }