diff --git a/libs/flake/KoPathShape.cpp b/libs/flake/KoPathShape.cpp index 3aa73dfbc3..8bdde8dd04 100644 --- a/libs/flake/KoPathShape.cpp +++ b/libs/flake/KoPathShape.cpp @@ -1,1682 +1,1686 @@ /* 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()); } static const qreal DefaultMarkerWidth = 3.0; KoPathShapePrivate::KoPathShapePrivate(KoPathShape *q) : KoTosContainerPrivate(q), fillRule(Qt::OddEvenFill), autoFillMarkers(false) { } KoPathShapePrivate::KoPathShapePrivate(const KoPathShapePrivate &rhs, KoPathShape *q) : KoTosContainerPrivate(rhs, q), fillRule(rhs.fillRule), markersNew(rhs.markersNew), autoFillMarkers(rhs.autoFillMarkers) { Q_FOREACH (KoSubpath *subPath, rhs.subpaths) { KoSubpath *clonedSubPath = new KoSubpath(); Q_FOREACH (KoPathPoint *point, *subPath) { *clonedSubPath << new KoPathPoint(*point, q); } subpaths << clonedSubPath; } } QRectF KoPathShapePrivate::handleRect(const QPointF &p, qreal radius) const { return QRectF(p.x() - radius, p.y() - radius, 2*radius, 2*radius); } void KoPathShapePrivate::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(new KoPathShapePrivate(this)) { } KoPathShape::KoPathShape(KoPathShapePrivate *dd) : KoTosContainer(dd) { } KoPathShape::KoPathShape(const KoPathShape &rhs) : KoTosContainer(new KoPathShapePrivate(*rhs.d_func(), this)) { } KoPathShape::~KoPathShape() { clear(); } KoShape *KoPathShape::cloneShape() const { return new KoPathShape(*this); } void KoPathShape::saveContourOdf(KoShapeSavingContext &context, const QSizeF &scaleFactor) const { Q_D(const KoPathShape); 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().addAttributePt("svg:width", size().width()); context.xmlWriter().addAttributePt("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 { Q_D(const KoPathShape); 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) { Q_D(KoPathShape); // 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) { Q_D(KoPathShape); 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 { Q_D(const KoPathShape); 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) { Q_D(KoPathShape); 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_D(KoPathShape); Q_FOREACH (KoSubpath *subpath, d->subpaths) { Q_FOREACH (KoPathPoint *point, *subpath) delete point; delete subpath; } d->subpaths.clear(); } void KoPathShape::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_D(KoPathShape); + + 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 KoPathShapePrivate::paintDebug(QPainter &painter) { Q_Q(KoPathShape); 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 KoPathShapePrivate::debugPath() const { Q_Q(const KoPathShape); 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) { Q_D(KoPathShape); 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 { Q_D(const KoPathShape); 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 reccursion return outlineRect().size(); } void KoPathShape::setSize(const QSizeF &newSize) { Q_D(KoPathShape); 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) { Q_D(KoPathShape); KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StartSubpath | KoPathPoint::StopSubpath); KoSubpath * path = new KoSubpath; path->push_back(point); d->subpaths.push_back(path); return point; } KoPathPoint * KoPathShape::lineTo(const QPointF &p) { Q_D(KoPathShape); if (d->subpaths.empty()) { moveTo(QPointF(0, 0)); } KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); KoPathPoint * lastPoint = d->subpaths.last()->last(); d->updateLast(&lastPoint); d->subpaths.last()->push_back(point); return point; } KoPathPoint * KoPathShape::curveTo(const QPointF &c1, const QPointF &c2, const QPointF &p) { Q_D(KoPathShape); if (d->subpaths.empty()) { moveTo(QPointF(0, 0)); } KoPathPoint * lastPoint = d->subpaths.last()->last(); d->updateLast(&lastPoint); lastPoint->setControlPoint2(c1); KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); point->setControlPoint1(c2); d->subpaths.last()->push_back(point); return point; } KoPathPoint * KoPathShape::curveTo(const QPointF &c, const QPointF &p) { Q_D(KoPathShape); if (d->subpaths.empty()) moveTo(QPointF(0, 0)); KoPathPoint * lastPoint = d->subpaths.last()->last(); d->updateLast(&lastPoint); lastPoint->setControlPoint2(c); KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); d->subpaths.last()->push_back(point); return point; } KoPathPoint * KoPathShape::arcTo(qreal rx, qreal ry, qreal startAngle, qreal sweepAngle) { Q_D(KoPathShape); 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 aproximation 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() { Q_D(KoPathShape); if (d->subpaths.empty()) { return; } d->closeSubpath(d->subpaths.last()); } void KoPathShape::closeMerge() { Q_D(KoPathShape); if (d->subpaths.empty()) { return; } d->closeMergeSubpath(d->subpaths.last()); } QPointF KoPathShape::normalize() { Q_D(KoPathShape); 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()); d->shapeChanged(ContentChanged); return tl; } void KoPathShapePrivate::map(const QTransform &matrix) { Q_Q(KoPathShape); 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); } } } void KoPathShapePrivate::updateLast(KoPathPoint **lastPoint) { Q_Q(KoPathShape); // 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 = subpaths.last()->first(); // clone the first point of the subpath... KoPathPoint * newLastPoint = new KoPathPoint(*subpathStart, q); // ... 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); 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 { Q_D(const KoPathShape); 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 { Q_D(const KoPathShape); 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 { Q_D(const KoPathShape); 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 { Q_D(const KoPathShape); 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 { Q_D(const KoPathShape); 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 { Q_D(const KoPathShape); 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 { Q_D(const KoPathShape); return d->subpaths.count(); } int KoPathShape::subpathPointCount(int subpathIndex) const { Q_D(const KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); if (subpath == 0) return -1; return subpath->size(); } bool KoPathShape::isClosedSubpath(int subpathIndex) const { Q_D(const KoPathShape); 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) { Q_D(KoPathShape); 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); return true; } KoPathPoint * KoPathShape::removePoint(const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); 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); } } return point; } bool KoPathShape::breakAfter(const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); 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); return true; } bool KoPathShape::join(int subpathIndex) { Q_D(KoPathShape); 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; return true; } bool KoPathShape::moveSubpath(int oldSubpathIndex, int newSubpathIndex) { Q_D(KoPathShape); 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); return true; } KoPathPointIndex KoPathShape::openSubpath(const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); 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); return pathPointIndex(oldStartPoint); } KoPathPointIndex KoPathShape::closeSubpath(const KoPathPointIndex &pointIndex) { Q_D(KoPathShape); 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); d->closeSubpath(subpath); return pathPointIndex(oldStartPoint); } bool KoPathShape::reverseSubpath(int subpathIndex) { Q_D(KoPathShape); 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); return true; } KoSubpath * KoPathShape::removeSubpath(int subpathIndex) { Q_D(KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); if (subpath != 0) { Q_FOREACH (KoPathPoint* point, *subpath) { point->setParent(this); } d->subpaths.removeAt(subpathIndex); } return subpath; } bool KoPathShape::addSubpath(KoSubpath * subpath, int subpathIndex) { Q_D(KoPathShape); if (subpathIndex < 0 || subpathIndex > d->subpaths.size()) return false; Q_FOREACH (KoPathPoint* point, *subpath) { point->setParent(this); } d->subpaths.insert(subpathIndex, subpath); return true; } int KoPathShape::combine(KoPathShape *path) { Q_D(KoPathShape); int insertSegmentPosition = -1; if (!path) return insertSegmentPosition; QTransform pathMatrix = path->absoluteTransformation(0); QTransform myMatrix = absoluteTransformation(0).inverted(); Q_FOREACH (KoSubpath* subpath, path->d_func()->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(); return insertSegmentPosition; } bool KoPathShape::separate(QList & separatedPaths) { Q_D(KoPathShape); if (! d->subpaths.size()) return false; QTransform myMatrix = absoluteTransformation(0); Q_FOREACH (KoSubpath* subpath, d->subpaths) { KoPathShape *shape = new KoPathShape(); if (! shape) continue; shape->setStroke(stroke()); shape->setShapeId(shapeId()); KoSubpath *newSubpath = new KoSubpath(); Q_FOREACH (KoPathPoint* point, *subpath) { KoPathPoint *newPoint = new KoPathPoint(*point, shape); newPoint->map(myMatrix); newSubpath->append(newPoint); } shape->d_func()->subpaths.append(newSubpath); shape->normalize(); separatedPaths.append(shape); } return true; } void KoPathShapePrivate::closeSubpath(KoSubpath *subpath) { if (! subpath) return; subpath->last()->setProperty(KoPathPoint::CloseSubpath); subpath->first()->setProperty(KoPathPoint::CloseSubpath); } void KoPathShapePrivate::closeMergeSubpath(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); } else { closeSubpath(subpath); } } KoSubpath *KoPathShapePrivate::subPath(int subpathIndex) const { Q_Q(const KoPathShape); 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 { Q_D(const KoPathShape); 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); // 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 KoPathShapePrivate::nodeTypes() const { Q_Q(const KoPathShape); 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 KoPathShapePrivate::loadNodeTypes(const KoXmlElement &element) { Q_Q(KoPathShape); 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 { Q_D(const KoPathShape); return d->fillRule; } void KoPathShape::setFillRule(Qt::FillRule fillRule) { Q_D(KoPathShape); 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->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) { Q_D(KoPathShape); if (!marker && d->markersNew.contains(pos)) { d->markersNew.remove(pos); } else { d->markersNew[pos] = marker; } } KoMarker *KoPathShape::marker(KoFlake::MarkerPosition pos) const { Q_D(const KoPathShape); return d->markersNew[pos].data(); } bool KoPathShape::hasMarkers() const { Q_D(const KoPathShape); return !d->markersNew.isEmpty(); } bool KoPathShape::autoFillMarkers() const { Q_D(const KoPathShape); return d->autoFillMarkers; } void KoPathShape::setAutoFillMarkers(bool value) { Q_D(KoPathShape); d->autoFillMarkers = value; } QPainterPath KoPathShape::pathStroke(const QPen &pen) const { Q_D(const KoPathShape); if (d->subpaths.isEmpty()) { return QPainterPath(); } QPainterPath pathOutline; QPainterPathStroker stroker; stroker.setWidth(0); stroker.setJoinStyle(Qt::MiterJoin); QPair firstSegments; QPair lastSegments; 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; } diff --git a/libs/flake/KoShapeManager.cpp b/libs/flake/KoShapeManager.cpp index 28b05e0857..1a1c5d5195 100644 --- a/libs/flake/KoShapeManager.cpp +++ b/libs/flake/KoShapeManager.cpp @@ -1,605 +1,614 @@ /* This file is part of the KDE project Copyright (C) 2006-2008 Thorsten Zachmann Copyright (C) 2006-2010 Thomas Zander Copyright (C) 2009-2010 Jan Hambrecht 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 "KoShapeManager.h" #include "KoShapeManager_p.h" #include "KoSelection.h" #include "KoToolManager.h" #include "KoPointerEvent.h" #include "KoShape.h" #include "KoShape_p.h" #include "KoCanvasBase.h" #include "KoShapeContainer.h" #include "KoShapeStrokeModel.h" #include "KoShapeGroup.h" #include "KoToolProxy.h" #include "KoShapeShadow.h" #include "KoShapeLayer.h" #include "KoFilterEffect.h" #include "KoFilterEffectStack.h" #include "KoFilterEffectRenderContext.h" #include "KoShapeBackground.h" #include #include "KoClipPath.h" #include "KoClipMaskPainter.h" #include "KoShapePaintingContext.h" #include "KoViewConverter.h" #include "KisQPainterStateSaver.h" #include "KoSvgTextChunkShape.h" #include "KoSvgTextShape.h" #include #include #include #include "kis_painting_tweaks.h" bool KoShapeManager::Private::shapeUsedInRenderingTree(KoShape *shape) { // FIXME: make more general! return !dynamic_cast(shape) && !dynamic_cast(shape) && !(dynamic_cast(shape) && !dynamic_cast(shape)); } void KoShapeManager::Private::updateTree() { // for detecting collisions between shapes. DetectCollision detector; bool selectionModified = false; bool anyModified = false; Q_FOREACH (KoShape *shape, aggregate4update) { if (shapeIndexesBeforeUpdate.contains(shape)) detector.detect(tree, shape, shapeIndexesBeforeUpdate[shape]); selectionModified = selectionModified || selection->isSelected(shape); anyModified = true; } foreach (KoShape *shape, aggregate4update) { if (!shapeUsedInRenderingTree(shape)) continue; tree.remove(shape); QRectF br(shape->boundingRect()); tree.insert(br, shape); } // do it again to see which shapes we intersect with _after_ moving. foreach (KoShape *shape, aggregate4update) { detector.detect(tree, shape, shapeIndexesBeforeUpdate[shape]); } aggregate4update.clear(); shapeIndexesBeforeUpdate.clear(); detector.fireSignals(); if (selectionModified) { emit q->selectionContentChanged(); } if (anyModified) { emit q->contentChanged(); } } void KoShapeManager::Private::paintGroup(KoShapeGroup *group, QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { QList shapes = group->shapes(); qSort(shapes.begin(), shapes.end(), KoShape::compareShapeZIndex); Q_FOREACH (KoShape *child, shapes) { // we paint recursively here, so we do not have to check recursively for visibility if (!child->isVisible()) continue; KoShapeGroup *childGroup = dynamic_cast(child); if (childGroup) { paintGroup(childGroup, painter, converter, paintContext); } else { painter.save(); KoShapeManager::renderSingleShape(child, painter, converter, paintContext); painter.restore(); } } } KoShapeManager::KoShapeManager(KoCanvasBase *canvas, const QList &shapes) : d(new Private(this, canvas)) { Q_ASSERT(d->canvas); // not optional. connect(d->selection, SIGNAL(selectionChanged()), this, SIGNAL(selectionChanged())); setShapes(shapes); } KoShapeManager::KoShapeManager(KoCanvasBase *canvas) : d(new Private(this, canvas)) { Q_ASSERT(d->canvas); // not optional. connect(d->selection, SIGNAL(selectionChanged()), this, SIGNAL(selectionChanged())); } KoShapeManager::~KoShapeManager() { Q_FOREACH (KoShape *shape, d->shapes) { shape->priv()->removeShapeManager(this); } Q_FOREACH (KoShape *shape, d->additionalShapes) { shape->priv()->removeShapeManager(this); } delete d; } void KoShapeManager::setShapes(const QList &shapes, Repaint repaint) { //clear selection d->selection->deselectAll(); Q_FOREACH (KoShape *shape, d->shapes) { shape->priv()->removeShapeManager(this); } d->aggregate4update.clear(); d->tree.clear(); d->shapes.clear(); Q_FOREACH (KoShape *shape, shapes) { addShape(shape, repaint); } } void KoShapeManager::addShape(KoShape *shape, Repaint repaint) { if (d->shapes.contains(shape)) return; shape->priv()->addShapeManager(this); d->shapes.append(shape); if (d->shapeUsedInRenderingTree(shape)) { QRectF br(shape->boundingRect()); d->tree.insert(br, shape); } if (repaint == PaintShapeOnAdd) { shape->update(); } // add the children of a KoShapeContainer KoShapeContainer *container = dynamic_cast(shape); if (container) { foreach (KoShape *containerShape, container->shapes()) { addShape(containerShape, repaint); } } Private::DetectCollision detector; detector.detect(d->tree, shape, shape->zIndex()); detector.fireSignals(); } void KoShapeManager::remove(KoShape *shape) { Private::DetectCollision detector; detector.detect(d->tree, shape, shape->zIndex()); detector.fireSignals(); shape->update(); shape->priv()->removeShapeManager(this); d->selection->deselect(shape); d->aggregate4update.remove(shape); if (d->shapeUsedInRenderingTree(shape)) { d->tree.remove(shape); } d->shapes.removeAll(shape); // remove the children of a KoShapeContainer KoShapeContainer *container = dynamic_cast(shape); if (container) { foreach (KoShape *containerShape, container->shapes()) { remove(containerShape); } } } KoShapeManager::ShapeInterface::ShapeInterface(KoShapeManager *_q) : q(_q) { } void KoShapeManager::ShapeInterface::notifyShapeDestructed(KoShape *shape) { q->d->selection->deselect(shape); q->d->aggregate4update.remove(shape); // we cannot access RTTI of the semi-destructed shape, so just // unlink it lazily if (q->d->tree.contains(shape)) { q->d->tree.remove(shape); } q->d->shapes.removeAll(shape); } KoShapeManager::ShapeInterface *KoShapeManager::shapeInterface() { return &d->shapeInterface; } void KoShapeManager::paint(QPainter &painter, const KoViewConverter &converter, bool forPrint) { d->updateTree(); painter.setPen(Qt::NoPen); // painters by default have a black stroke, lets turn that off. painter.setBrush(Qt::NoBrush); QList unsortedShapes; if (painter.hasClipping()) { QRectF rect = converter.viewToDocument(KisPaintingTweaks::safeClipBoundingRect(painter)); unsortedShapes = d->tree.intersects(rect); } else { unsortedShapes = shapes(); warnFlake << "KoShapeManager::paint Painting with a painter that has no clipping will lead to too much being painted!"; } // filter all hidden shapes from the list // also filter shapes with a parent which has filter effects applied QList sortedShapes; foreach (KoShape *shape, unsortedShapes) { if (!shape->isVisible(true)) continue; bool addShapeToList = true; // check if one of the shapes ancestors have filter effects KoShapeContainer *parent = shape->parent(); while (parent) { // parent must be part of the shape manager to be taken into account if (!d->shapes.contains(parent)) break; if (parent->filterEffectStack() && !parent->filterEffectStack()->isEmpty()) { addShapeToList = false; break; } parent = parent->parent(); } if (addShapeToList) { sortedShapes.append(shape); } else if (parent) { sortedShapes.append(parent); } } qSort(sortedShapes.begin(), sortedShapes.end(), KoShape::compareShapeZIndex); KoShapePaintingContext paintContext(d->canvas, forPrint); //FIXME foreach (KoShape *shape, sortedShapes) { renderSingleShape(shape, painter, converter, paintContext); } #ifdef CALLIGRA_RTREE_DEBUG // paint tree qreal zx = 0; qreal zy = 0; converter.zoom(&zx, &zy); painter.save(); painter.scale(zx, zy); d->tree.paint(painter); painter.restore(); #endif if (! forPrint) { KoShapePaintingContext paintContext(d->canvas, forPrint); //FIXME d->selection->paint(painter, converter, paintContext); } } void KoShapeManager::renderSingleShape(KoShape *shape, QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { KisQPainterStateSaver saver(&painter); // apply shape clipping KoClipPath::applyClipping(shape, painter, converter); // apply transformation painter.setTransform(shape->absoluteTransformation(&converter) * painter.transform()); // paint the shape paintShape(shape, painter, converter, paintContext); } void KoShapeManager::paintShape(KoShape *shape, QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { qreal transparency = shape->transparency(true); if (transparency > 0.0) { painter.setOpacity(1.0-transparency); } if (shape->shadow()) { painter.save(); shape->shadow()->paint(shape, painter, converter); painter.restore(); } if (!shape->filterEffectStack() || shape->filterEffectStack()->isEmpty()) { QScopedPointer clipMaskPainter; QPainter *shapePainter = &painter; KoClipMask *clipMask = shape->clipMask(); if (clipMask) { clipMaskPainter.reset(new KoClipMaskPainter(&painter, shape->boundingRect())); shapePainter = clipMaskPainter->shapePainter(); } - shapePainter->save(); + /** + * We expect the shape to save/restore the painter's state itself. Such design was not + * not always here, so we need a period of sanity checks to ensure all the shapes are + * ported correctly. + */ + const QTransform sanityCheckTransformSaved = shapePainter->transform(); + shape->paint(*shapePainter, converter, paintContext); shape->paintStroke(*shapePainter, converter, paintContext); - shapePainter->restore(); + + KIS_SAFE_ASSERT_RECOVER(shapePainter->transform() == sanityCheckTransformSaved) { + shapePainter->setTransform(sanityCheckTransformSaved); + } if (clipMask) { shape->clipMask()->drawMask(clipMaskPainter->maskPainter(), shape); clipMaskPainter->renderOnGlobalPainter(); } } else { // TODO: clipping mask is not implemented for this case! // There are filter effects, then we need to prerender the shape on an image, to filter it QRectF shapeBound(QPointF(), shape->size()); // First step, compute the rectangle used for the image QRectF clipRegion = shape->filterEffectStack()->clipRectForBoundingRect(shapeBound); // convert clip region to view coordinates QRectF zoomedClipRegion = converter.documentToView(clipRegion); // determine the offset of the clipping rect from the shapes origin QPointF clippingOffset = zoomedClipRegion.topLeft(); // Initialize the buffer image QImage sourceGraphic(zoomedClipRegion.size().toSize(), QImage::Format_ARGB32_Premultiplied); sourceGraphic.fill(qRgba(0,0,0,0)); QHash imageBuffers; QSet requiredStdInputs = shape->filterEffectStack()->requiredStandarsInputs(); if (requiredStdInputs.contains("SourceGraphic") || requiredStdInputs.contains("SourceAlpha")) { // Init the buffer painter QPainter imagePainter(&sourceGraphic); imagePainter.translate(-1.0f*clippingOffset); imagePainter.setPen(Qt::NoPen); imagePainter.setBrush(Qt::NoBrush); imagePainter.setRenderHint(QPainter::Antialiasing, painter.testRenderHint(QPainter::Antialiasing)); // Paint the shape on the image KoShapeGroup *group = dynamic_cast(shape); if (group) { // the childrens matrix contains the groups matrix as well // so we have to compensate for that before painting the children imagePainter.setTransform(group->absoluteTransformation(&converter).inverted(), true); Private::paintGroup(group, imagePainter, converter, paintContext); } else { imagePainter.save(); shape->paint(imagePainter, converter, paintContext); shape->paintStroke(imagePainter, converter, paintContext); imagePainter.restore(); imagePainter.end(); } } if (requiredStdInputs.contains("SourceAlpha")) { QImage sourceAlpha = sourceGraphic; sourceAlpha.fill(qRgba(0,0,0,255)); sourceAlpha.setAlphaChannel(sourceGraphic.alphaChannel()); imageBuffers.insert("SourceAlpha", sourceAlpha); } if (requiredStdInputs.contains("FillPaint")) { QImage fillPaint = sourceGraphic; if (shape->background()) { QPainter fillPainter(&fillPaint); QPainterPath fillPath; fillPath.addRect(fillPaint.rect().adjusted(-1,-1,1,1)); shape->background()->paint(fillPainter, converter, paintContext, fillPath); } else { fillPaint.fill(qRgba(0,0,0,0)); } imageBuffers.insert("FillPaint", fillPaint); } imageBuffers.insert("SourceGraphic", sourceGraphic); imageBuffers.insert(QString(), sourceGraphic); KoFilterEffectRenderContext renderContext(converter); renderContext.setShapeBoundingBox(shapeBound); QImage result; QList filterEffects = shape->filterEffectStack()->filterEffects(); // Filter foreach (KoFilterEffect *filterEffect, filterEffects) { QRectF filterRegion = filterEffect->filterRectForBoundingRect(shapeBound); filterRegion = converter.documentToView(filterRegion); QRect subRegion = filterRegion.translated(-clippingOffset).toRect(); // set current filter region renderContext.setFilterRegion(subRegion & sourceGraphic.rect()); if (filterEffect->maximalInputCount() <= 1) { QList inputs = filterEffect->inputs(); QString input = inputs.count() ? inputs.first() : QString(); // get input image from image buffers and apply the filter effect QImage image = imageBuffers.value(input); if (!image.isNull()) { result = filterEffect->processImage(imageBuffers.value(input), renderContext); } } else { QList inputImages; Q_FOREACH (const QString &input, filterEffect->inputs()) { QImage image = imageBuffers.value(input); if (!image.isNull()) inputImages.append(imageBuffers.value(input)); } // apply the filter effect if (filterEffect->inputs().count() == inputImages.count()) result = filterEffect->processImages(inputImages, renderContext); } // store result of effect imageBuffers.insert(filterEffect->output(), result); } KoFilterEffect *lastEffect = filterEffects.last(); // Paint the result painter.save(); painter.drawImage(clippingOffset, imageBuffers.value(lastEffect->output())); painter.restore(); } } KoShape *KoShapeManager::shapeAt(const QPointF &position, KoFlake::ShapeSelection selection, bool omitHiddenShapes) { d->updateTree(); QList sortedShapes(d->tree.contains(position)); qSort(sortedShapes.begin(), sortedShapes.end(), KoShape::compareShapeZIndex); KoShape *firstUnselectedShape = 0; for (int count = sortedShapes.count() - 1; count >= 0; count--) { KoShape *shape = sortedShapes.at(count); if (omitHiddenShapes && ! shape->isVisible(true)) continue; if (! shape->hitTest(position)) continue; switch (selection) { case KoFlake::ShapeOnTop: if (shape->isSelectable()) return shape; case KoFlake::Selected: if (d->selection->isSelected(shape)) return shape; break; case KoFlake::Unselected: if (! d->selection->isSelected(shape)) return shape; break; case KoFlake::NextUnselected: // we want an unselected shape if (d->selection->isSelected(shape)) continue; // memorize the first unselected shape if (! firstUnselectedShape) firstUnselectedShape = shape; // check if the shape above is selected if (count + 1 < sortedShapes.count() && d->selection->isSelected(sortedShapes.at(count + 1))) return shape; break; } } // if we want the next unselected below a selected but there was none selected, // return the first found unselected shape if (selection == KoFlake::NextUnselected && firstUnselectedShape) return firstUnselectedShape; if (d->selection->hitTest(position)) return d->selection; return 0; // missed everything } QList KoShapeManager::shapesAt(const QRectF &rect, bool omitHiddenShapes, bool containedMode) { d->updateTree(); QList shapes(containedMode ? d->tree.contained(rect) : d->tree.intersects(rect)); for (int count = shapes.count() - 1; count >= 0; count--) { KoShape *shape = shapes.at(count); if (omitHiddenShapes && !shape->isVisible(true)) { shapes.removeAt(count); } else { const QPainterPath outline = shape->absoluteTransformation(0).map(shape->outline()); if (!containedMode && !outline.intersects(rect) && !outline.contains(rect)) { shapes.removeAt(count); } else if (containedMode) { QPainterPath containingPath; containingPath.addRect(rect); if (!containingPath.contains(outline)) { shapes.removeAt(count); } } } } return shapes; } void KoShapeManager::update(const QRectF &rect, const KoShape *shape, bool selectionHandles) { d->canvas->updateCanvas(rect); if (selectionHandles && d->selection->isSelected(shape)) { if (d->canvas->toolProxy()) d->canvas->toolProxy()->repaintDecorations(); } } void KoShapeManager::notifyShapeChanged(KoShape *shape) { Q_ASSERT(shape); if (d->aggregate4update.contains(shape) || d->additionalShapes.contains(shape)) { return; } const bool wasEmpty = d->aggregate4update.isEmpty(); d->aggregate4update.insert(shape); d->shapeIndexesBeforeUpdate.insert(shape, shape->zIndex()); KoShapeContainer *container = dynamic_cast(shape); if (container) { Q_FOREACH (KoShape *child, container->shapes()) notifyShapeChanged(child); } if (wasEmpty && !d->aggregate4update.isEmpty()) QTimer::singleShot(100, this, SLOT(updateTree())); emit shapeChanged(shape); } QList KoShapeManager::shapes() const { return d->shapes; } QList KoShapeManager::topLevelShapes() const { QList shapes; // get all toplevel shapes Q_FOREACH (KoShape *shape, d->shapes) { if (shape->parent() == 0) { shapes.append(shape); } } return shapes; } KoSelection *KoShapeManager::selection() const { return d->selection; } KoCanvasBase *KoShapeManager::canvas() { return d->canvas; } //have to include this because of Q_PRIVATE_SLOT #include "moc_KoShapeManager.cpp" diff --git a/libs/flake/KoShapeStroke.cpp b/libs/flake/KoShapeStroke.cpp index e75a4ca5f7..9731518b4e 100644 --- a/libs/flake/KoShapeStroke.cpp +++ b/libs/flake/KoShapeStroke.cpp @@ -1,416 +1,420 @@ /* This file is part of the KDE project * * Copyright (C) 2006-2007 Thomas Zander * Copyright (C) 2006-2008 Jan Hambrecht * Copyright (C) 2007,2009 Thorsten Zachmann * Copyright (C) 2012 Inge Wallin * * 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. */ // Own #include "KoShapeStroke.h" // Posix #include // Qt #include #include // Calligra #include #include // Flake #include "KoViewConverter.h" #include "KoShape.h" #include "KoShapeSavingContext.h" #include "KoPathShape.h" #include "KoMarker.h" #include "KoInsets.h" #include #include #include +#include "KisQPainterStateSaver.h" #include "kis_global.h" class Q_DECL_HIDDEN KoShapeStroke::Private { public: Private(KoShapeStroke *_q) : q(_q) {} KoShapeStroke *q; void paintBorder(KoShape *shape, QPainter &painter, const QPen &pen) const; QColor color; QPen pen; QBrush brush; }; namespace { QPair anglesForSegment(KoPathSegment segment) { const qreal eps = 1e-6; if (segment.degree() < 3) { segment = segment.toCubic(); } QList points = segment.controlPoints(); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(points.size() == 4, qMakePair(0.0, 0.0)); QPointF vec1 = points[1] - points[0]; QPointF vec2 = points[3] - points[2]; if (vec1.manhattanLength() < eps) { points[1] = segment.pointAt(eps); vec1 = points[1] - points[0]; } if (vec2.manhattanLength() < eps) { points[2] = segment.pointAt(1.0 - eps); vec2 = points[3] - points[2]; } const qreal angle1 = std::atan2(vec1.y(), vec1.x()); const qreal angle2 = std::atan2(vec2.y(), vec2.x()); return qMakePair(angle1, angle2); } } void KoShapeStroke::Private::paintBorder(KoShape *shape, QPainter &painter, const QPen &pen) const { if (!pen.isCosmetic() && pen.style() != Qt::NoPen) { KoPathShape *pathShape = dynamic_cast(shape); if (pathShape) { QPainterPath path = pathShape->pathStroke(pen); painter.fillPath(path, pen.brush()); if (!pathShape->hasMarkers()) return; const bool autoFillMarkers = pathShape->autoFillMarkers(); KoMarker *startMarker = pathShape->marker(KoFlake::StartMarker); KoMarker *midMarker = pathShape->marker(KoFlake::MidMarker); KoMarker *endMarker = pathShape->marker(KoFlake::EndMarker); for (int i = 0; i < pathShape->subpathCount(); i++) { const int numSubPoints = pathShape->subpathPointCount(i); if (numSubPoints < 2) continue; const bool isClosedSubpath = pathShape->isClosedSubpath(i); qreal firstAngle = 0.0; { KoPathSegment segment = pathShape->segmentByIndex(KoPathPointIndex(i, 0)); firstAngle= anglesForSegment(segment).first; } const int numSegments = isClosedSubpath ? numSubPoints : numSubPoints - 1; qreal lastAngle = 0.0; { KoPathSegment segment = pathShape->segmentByIndex(KoPathPointIndex(i, numSegments - 1)); lastAngle = anglesForSegment(segment).second; } qreal previousAngle = 0.0; for (int j = 0; j < numSegments; j++) { KoPathSegment segment = pathShape->segmentByIndex(KoPathPointIndex(i, j)); QPair angles = anglesForSegment(segment); const qreal angle1 = angles.first; const qreal angle2 = angles.second; if (j == 0 && startMarker) { const qreal angle = isClosedSubpath ? bisectorAngle(firstAngle, lastAngle) : firstAngle; if (autoFillMarkers) { startMarker->applyShapeStroke(shape, q, segment.first()->point(), pen.widthF(), angle); } startMarker->paintAtPosition(&painter, segment.first()->point(), pen.widthF(), angle); } if (j > 0 && midMarker) { const qreal angle = bisectorAngle(previousAngle, angle1); if (autoFillMarkers) { midMarker->applyShapeStroke(shape, q, segment.first()->point(), pen.widthF(), angle); } midMarker->paintAtPosition(&painter, segment.first()->point(), pen.widthF(), angle); } if (j == numSegments - 1 && endMarker) { const qreal angle = isClosedSubpath ? bisectorAngle(firstAngle, lastAngle) : lastAngle; if (autoFillMarkers) { endMarker->applyShapeStroke(shape, q, segment.second()->point(), pen.widthF(), angle); } endMarker->paintAtPosition(&painter, segment.second()->point(), pen.widthF(), angle); } previousAngle = angle2; } } return; } painter.strokePath(shape->outline(), pen); } } KoShapeStroke::KoShapeStroke() : d(new Private(this)) { d->color = QColor(Qt::black); // we are not rendering stroke with zero width anymore // so lets use a default width of 1.0 d->pen.setWidthF(1.0); } KoShapeStroke::KoShapeStroke(const KoShapeStroke &other) : KoShapeStrokeModel(), d(new Private(this)) { d->color = other.d->color; d->pen = other.d->pen; d->brush = other.d->brush; } KoShapeStroke::KoShapeStroke(qreal lineWidth, const QColor &color) : d(new Private(this)) { d->pen.setWidthF(qMax(qreal(0.0), lineWidth)); d->pen.setJoinStyle(Qt::MiterJoin); d->color = color; } KoShapeStroke::~KoShapeStroke() { delete d; } KoShapeStroke &KoShapeStroke::operator = (const KoShapeStroke &rhs) { if (this == &rhs) return *this; d->pen = rhs.d->pen; d->color = rhs.d->color; d->brush = rhs.d->brush; return *this; } void KoShapeStroke::fillStyle(KoGenStyle &style, KoShapeSavingContext &context) const { QPen pen = d->pen; if (d->brush.gradient()) pen.setBrush(d->brush); else pen.setColor(d->color); KoOdfGraphicStyles::saveOdfStrokeStyle(style, context.mainStyles(), pen); } void KoShapeStroke::strokeInsets(const KoShape *shape, KoInsets &insets) const { Q_UNUSED(shape); // '0.5' --- since we draw a line half inside, and half outside the object. qreal extent = 0.5 * (d->pen.widthF() >= 0 ? d->pen.widthF() : 1.0); // if we have square cap, we need a little more space // -> sqrt((0.5*penWidth)^2 + (0.5*penWidth)^2) if (capStyle() == Qt::SquareCap) { extent *= M_SQRT2; } if (joinStyle() == Qt::MiterJoin) { // miter limit in Qt is normalized by the line width (and not half-width) extent = qMax(extent, d->pen.widthF() * miterLimit()); } insets.top = extent; insets.bottom = extent; insets.left = extent; insets.right = extent; } qreal KoShapeStroke::strokeMaxMarkersInset(const KoShape *shape) const { qreal result = 0.0; const KoPathShape *pathShape = dynamic_cast(shape); if (pathShape && pathShape->hasMarkers()) { const qreal lineWidth = d->pen.widthF(); QVector markers; markers << pathShape->marker(KoFlake::StartMarker); markers << pathShape->marker(KoFlake::MidMarker); markers << pathShape->marker(KoFlake::EndMarker); Q_FOREACH (const KoMarker *marker, markers) { if (marker) { result = qMax(result, marker->maxInset(lineWidth)); } } } return result; } bool KoShapeStroke::hasTransparency() const { return d->color.alpha() > 0; } QPen KoShapeStroke::resultLinePen() const { QPen pen = d->pen; if (d->brush.gradient()) { pen.setBrush(d->brush); } else { pen.setColor(d->color); } return pen; } void KoShapeStroke::paint(KoShape *shape, QPainter &painter, const KoViewConverter &converter) { + KisQPainterStateSaver saver(&painter); + + // TODO: move apply conversion to some centralized place KoShape::applyConversion(painter, converter); d->paintBorder(shape, painter, resultLinePen()); } bool KoShapeStroke::compareFillTo(const KoShapeStrokeModel *other) { if (!other) return false; const KoShapeStroke *stroke = dynamic_cast(other); if (!stroke) return false; return (d->brush.gradient() && d->brush == stroke->d->brush) || (!d->brush.gradient() && d->color == stroke->d->color); } bool KoShapeStroke::compareStyleTo(const KoShapeStrokeModel *other) { if (!other) return false; const KoShapeStroke *stroke = dynamic_cast(other); if (!stroke) return false; QPen pen1 = d->pen; QPen pen2 = stroke->d->pen; // just a random color top avoid comparison of that property pen1.setColor(Qt::magenta); pen2.setColor(Qt::magenta); return pen1 == pen2; } bool KoShapeStroke::isVisible() const { return d->pen.widthF() > 0 && (d->brush.gradient() || d->color.alpha() > 0); } void KoShapeStroke::setCapStyle(Qt::PenCapStyle style) { d->pen.setCapStyle(style); } Qt::PenCapStyle KoShapeStroke::capStyle() const { return d->pen.capStyle(); } void KoShapeStroke::setJoinStyle(Qt::PenJoinStyle style) { d->pen.setJoinStyle(style); } Qt::PenJoinStyle KoShapeStroke::joinStyle() const { return d->pen.joinStyle(); } void KoShapeStroke::setLineWidth(qreal lineWidth) { d->pen.setWidthF(qMax(qreal(0.0), lineWidth)); } qreal KoShapeStroke::lineWidth() const { return d->pen.widthF(); } void KoShapeStroke::setMiterLimit(qreal miterLimit) { d->pen.setMiterLimit(miterLimit); } qreal KoShapeStroke::miterLimit() const { return d->pen.miterLimit(); } QColor KoShapeStroke::color() const { return d->color; } void KoShapeStroke::setColor(const QColor &color) { d->color = color; } void KoShapeStroke::setLineStyle(Qt::PenStyle style, const QVector &dashes) { if (style < Qt::CustomDashLine) { d->pen.setStyle(style); } else { d->pen.setDashPattern(dashes); } } Qt::PenStyle KoShapeStroke::lineStyle() const { return d->pen.style(); } QVector KoShapeStroke::lineDashes() const { return d->pen.dashPattern(); } void KoShapeStroke::setDashOffset(qreal dashOffset) { d->pen.setDashOffset(dashOffset); } qreal KoShapeStroke::dashOffset() const { return d->pen.dashOffset(); } void KoShapeStroke::setLineBrush(const QBrush &brush) { d->brush = brush; } QBrush KoShapeStroke::lineBrush() const { return d->brush; } diff --git a/libs/flake/KoUnavailShape.cpp b/libs/flake/KoUnavailShape.cpp index 1c85d0f7b2..b4c87d9e30 100644 --- a/libs/flake/KoUnavailShape.cpp +++ b/libs/flake/KoUnavailShape.cpp @@ -1,628 +1,629 @@ /* This file is part of the KDE project * * Copyright (C) 2010-2011 Inge Wallin * * 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. */ // Own #include "KoUnavailShape.h" // Qt #include #include #include #include #include #include #include #include #include // Calligra #include #include #include #include #include #include #include #include #include "KoShapeLoadingContext.h" #include "KoShapeSavingContext.h" #include "SimpleShapeContainerModel.h" #include "KoShapeBackground.h" +#include "KisQPainterStateSaver.h" #include // The XML of a frame looks something like this: // // 1. // 2. // 3. // 4. // // or // // 1. // 2. ...inline xml here... // 3. // 4. // // We define each Xml statement on lines 2 and 3 above as an "object". // (Strictly only the first child element is an object in the ODF sense, // but we have to have some terminology here.) // // In an ODF frame, only the first line, i.e. the first object // contains the real contents. All the rest of the objects are used / // shown if we cannot handle the first one. The most common cases are // that there is only one object inside the frame OR that there are 2 // and the 2nd is a picture. // // Sometimes, e.g. in the case of an embedded document, the reference // points not to a file but to a directory structure inside the ODF // store. // // When we load and save in the UnavailShape, we have to be general // enough to cover all possible cases of references and inline XML, // embedded files and embedded directory structures. // // We also have to be careful because we cannot reuse the object names // that are in the original files when saving. Instead we need to // create new object names because the ones that were used in the // original file may already be used by other embedded files/objects // that are saved by other shapes. // // FIXME: There should only be ONE place where new object / file names // are generated, not 2(?) like there are now: // KoEmbeddedDocumentSaver and the KoImageCollection. // // An ObjectEntry is used to store information about objects in the // frame, as defined above. struct ObjectEntry { QByteArray objectXmlContents; // the XML tree in the object QString objectName; // object name in the frame without "./" // This is extracted from objectXmlContents. bool isDir; KoOdfManifestEntry *manifestEntry; // manifest entry for the above. }; // A FileEntry is used to store information about embedded files // inside (i.e. referred to by) an object. struct FileEntry { QString path; // Normalized filename, i.e. without "./". QString mimeType; bool isDir; QByteArray contents; }; class KoUnavailShape::Private { public: Private(KoUnavailShape* qq); ~Private(); void draw(QPainter &painter) const; void drawNull(QPainter &painter) const; void storeObjects(const KoXmlElement &element); void storeXmlRecursive(const KoXmlElement &el, KoXmlWriter &writer, ObjectEntry *object, QHash &unknownNamespaces); void storeFile(const QString &filename, KoShapeLoadingContext &context); QByteArray loadFile(const QString &filename, KoShapeLoadingContext &context); // Objects inside the frame. For each file, we store: // - The XML code for the object // - Any embedded files (names, contents) that are referenced by xlink:href // - Whether they are directories, i.e. if they contain a file tree and not just one file. // - The manifest entries QList objectEntries; // Embedded files QList embeddedFiles; // List of embedded files. // Some cached values. QPixmap questionMark; QPixmap pixmapPreview; QSvgRenderer *scalablePreview; KoUnavailShape* q; }; KoUnavailShape::Private::Private(KoUnavailShape* qq) : scalablePreview(new QSvgRenderer()) , q(qq) { // Get the question mark "icon". questionMark.load(":/questionmark.png"); } KoUnavailShape::Private::~Private() { qDeleteAll(objectEntries); qDeleteAll(embeddedFiles); // It's a QObject, but we haven't parented it. delete(scalablePreview); } // ---------------------------------------------------------------- // The main class KoUnavailShape::KoUnavailShape() : KoFrameShape( "", "" ) , KoShapeContainer(new SimpleShapeContainerModel()) , d(new Private(this)) { setShapeId(KoUnavailShape_SHAPEID); // Default size of the shape. KoShape::setSize( QSizeF( CM_TO_POINT( 5 ), CM_TO_POINT( 3 ) ) ); } KoUnavailShape::~KoUnavailShape() { delete d; } - void KoUnavailShape::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { + KisQPainterStateSaver saver(&painter); applyConversion(painter, converter); // If the frame is empty, just draw a background. debugFlake << "Number of objects:" << d->objectEntries.size(); if (d->objectEntries.isEmpty()) { // But... only try to draw the background if there's one such if (background()) { QPainterPath p; p.addRect(QRectF(QPointF(), size())); background()->paint(painter, converter, paintContext, p); } } else { if(shapes().isEmpty()) { d->draw(painter); } } } void KoUnavailShape::paintComponent(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &) { Q_UNUSED(painter); Q_UNUSED(converter); } void KoUnavailShape::Private::draw(QPainter &painter) const { painter.save(); painter.setRenderHint(QPainter::Antialiasing); // Run through the previews in order of preference. Draw a placeholder // questionmark if there is no preview available for rendering. if (scalablePreview->isValid()) { QRect bounds(0, 0, q->boundingRect().width(), q->boundingRect().height()); scalablePreview->render(&painter, bounds); } else if (!pixmapPreview.isNull()) { QRect bounds(0, 0, q->boundingRect().width(), q->boundingRect().height()); painter.setRenderHint(QPainter::SmoothPixmapTransform); painter.drawPixmap(bounds, pixmapPreview); } else if (q->shapes().isEmpty()) { // Draw a nice question mark with a frame around it if there // is no other preview image. If there is a contained image // shape, we don't need to draw anything. // Get the question mark "icon". // FIXME: We should be able to use d->questionMark here. QPixmap questionMark; questionMark.load(":/questionmark.png"); // The size of the image is: // - the size of the shape if shapesize < 2cm // - 2 cm if 2cm <= shapesize <= 8cm // - shapesize / 4 if shapesize > 8cm qreal width = q->size().width(); qreal height = q->size().height(); qreal picSize = CM_TO_POINT(2); // Default size is 2 cm. if (width < CM_TO_POINT(2) || height < CM_TO_POINT(2)) picSize = qMin(width, height); else if (width > CM_TO_POINT(8) && height > CM_TO_POINT(8)) picSize = qMin(width, height) / qreal(4.0); painter.drawPixmap((width - picSize) / qreal(2.0), (height - picSize) / qreal(2.0), picSize, picSize, questionMark); // Draw a gray rectangle around the shape. painter.setPen(QPen(QColor(172, 196, 206), 0)); painter.drawRect(QRectF(QPointF(0,0), q->size())); } painter.restore(); } void KoUnavailShape::Private::drawNull(QPainter &painter) const { QRectF rect(QPointF(0,0), q->size()); painter.save(); // Draw a simple cross in a rectangle just to indicate that there is something here. painter.drawLine(rect.topLeft(), rect.bottomRight()); painter.drawLine(rect.bottomLeft(), rect.topRight()); painter.restore(); } // ---------------------------------------------------------------- // Loading and Saving void KoUnavailShape::saveOdf(KoShapeSavingContext & context) const { debugFlake << "START SAVING ##################################################"; KoEmbeddedDocumentSaver &fileSaver = context.embeddedSaver(); KoXmlWriter &writer = context.xmlWriter(); writer.startElement("draw:frame"); // See also loadOdf() in loadOdfAttributes. saveOdfAttributes( context, OdfAllAttributes ); // Write the stored XML to the file, but don't reuse object names. int lap = 0; QString newName; foreach (const ObjectEntry *object, d->objectEntries) { QByteArray xmlArray(object->objectXmlContents); QString objectName(object->objectName); // Possibly empty. KoOdfManifestEntry *manifestEntry(object->manifestEntry); // Create a name for this object. If this is not the first // object, i.e. a replacement object (most likely a picture), // then reuse the name but put it in ReplacementObjects. if (++lap == 1) { // The first lap in the loop is the actual object. All // other laps are replacement objects. newName = fileSaver.getFilename("Object "); } else if (lap == 2) { newName = "ObjectReplacements/" + newName; } else // FIXME: what should replacement 2 and onwards be called? newName = newName + "_"; // If there was a previous object name, replace it with the new one. if (!objectName.isEmpty() && manifestEntry) { // FIXME: We must make a copy of the byte array here because // otherwise we won't be able to save > 1 time. xmlArray.replace(objectName.toLatin1(), newName.toLatin1()); } writer.addCompleteElement(xmlArray.data()); // If the objectName is empty, this may be inline XML. // If so, we are done now. if (objectName.isEmpty() || !manifestEntry) { continue; } // Save embedded files for this object. foreach (FileEntry *entry, d->embeddedFiles) { QString fileName(entry->path); // If we found a file for this object, we need to write it // but with the new object name instead of the old one. if (!fileName.startsWith(objectName)) continue; debugFlake << "Object name: " << objectName << "newName: " << newName << "filename: " << fileName << "isDir: " << entry->isDir; fileName.replace(objectName, newName); fileName.prepend("./"); debugFlake << "New filename: " << fileName; // FIXME: Check if we need special treatment of directories. fileSaver.saveFile(fileName, entry->mimeType.toLatin1(), entry->contents); } // Write the manifest entry for the object itself. If it's a // file, the manifest is already written by saveFile, so skip // it here. if (object->isDir) { fileSaver.saveManifestEntry(newName + '/', manifestEntry->mediaType(), manifestEntry->version()); } } writer.endElement(); // draw:frame } bool KoUnavailShape::loadOdf(const KoXmlElement &frameElement, KoShapeLoadingContext &context) { debugFlake << "START LOADING ##################################################"; //debugFlake << "Loading ODF frame in the KoUnavailShape. Element = " // << frameElement.tagName(); loadOdfAttributes(frameElement, context, OdfAllAttributes); // NOTE: We cannot use loadOdfFrame() because we want to save all // the things inside the frame, not just one of them, like // loadOdfFrame() provides. // Get the manifest. QList manifest = context.odfLoadingContext().manifestEntries(); #if 0 // Enable to show all manifest entries. debugFlake << "MANIFEST: "; foreach (KoOdfManifestEntry *entry, manifest) { debugFlake << entry->mediaType() << entry->fullPath() << entry->version(); } #endif // 1. Get the XML contents of the objects from the draw:frame. As // a side effect, this extracts the object names from all // xlink:href and stores them into d->objectNames. The saved // xml contents itself is saved into d->objectXmlContents // (QByteArray) so we can save it back from saveOdf(). d->storeObjects(frameElement); #if 1 // Debug only debugFlake << "----------------------------------------------------------------"; debugFlake << "After storeObjects():"; foreach (ObjectEntry *object, d->objectEntries) { debugFlake << "objectXmlContents: " << object->objectXmlContents << "objectName: " << object->objectName; // Note: at this point, isDir and manifestEntry are not set. #endif } // 2. Loop through the objects that were found in the frame and // save all the files associated with them. Some of the // objects are files, and some are directories. The // directories are searched and the files within are saved as // well. // // In this loop, isDir and manifestEntry of each ObjectEntry are set. bool foundPreview = false; foreach (ObjectEntry *object, d->objectEntries) { QString objectName = object->objectName; if (objectName.isEmpty()) continue; debugFlake << "Storing files for object named:" << objectName; // Try to find out if the entry is a directory. // If the object is a directory, then save all the files // inside it, otherwise save the file as it is. QString dirName = objectName + '/'; bool isDir = !context.odfLoadingContext().mimeTypeForPath(dirName).isEmpty(); if (isDir) { // A directory: the files can be found in the manifest. foreach (KoOdfManifestEntry *entry, manifest) { if (entry->fullPath() == dirName) continue; if (entry->fullPath().startsWith(dirName)) { d->storeFile(entry->fullPath(), context); } } } else { // A file: save it. d->storeFile(objectName, context); } // Get the manifest entry for this object. KoOdfManifestEntry *entry = 0; QString entryName = isDir ? dirName : objectName; for (int j = 0; j < manifest.size(); ++j) { KoOdfManifestEntry *temp = manifest.value(j); if (temp->fullPath() == entryName) { entry = new KoOdfManifestEntry(*temp); break; } } object->isDir = isDir; object->manifestEntry = entry; // If we have not already found a preview in previous times // through the loop, then see if this one may be a preview. if (!foundPreview) { debugFlake << "Attempting to load preview from " << objectName; QByteArray previewData = d->loadFile(objectName, context); // Check to see if we know the mimetype for this entry. Specifically: // 1. Check to see if the item is a loadable SVG file // FIXME: Perhaps check in the manifest first? But this // seems to work well. d->scalablePreview->load(previewData); if (d->scalablePreview->isValid()) { debugFlake << "Found scalable preview image!"; d->scalablePreview->setViewBox(d->scalablePreview->boundsOnElement("svg")); foundPreview = true; continue; } // 2. Otherwise check to see if it's a loadable pixmap file d->pixmapPreview.loadFromData(previewData); if (!d->pixmapPreview.isNull()) { debugFlake << "Found pixel based preview image!"; foundPreview = true; } } } #if 0 // Enable to get more detailed debug messages debugFlake << "Object manifest entries:"; for (int i = 0; i < d->manifestEntries.size(); ++i) { KoOdfManifestEntry *entry = d->manifestEntries.value(i); debugFlake << i << ":" << entry; if (entry) debugFlake << entry->fullPath() << entry->mediaType() << entry->version(); else debugFlake << "--"; } debugFlake << "END LOADING ####################################################"; #endif return true; } // Load the actual contents inside the frame. bool KoUnavailShape::loadOdfFrameElement(const KoXmlElement & /*element*/, KoShapeLoadingContext &/*context*/) { return true; } // ---------------------------------------------------------------- // Private functions void KoUnavailShape::Private::storeObjects(const KoXmlElement &element) { // Loop through all the child elements of the draw:frame and save them. KoXmlNode n = element.firstChild(); for (; !n.isNull(); n = n.nextSibling()) { debugFlake << "In draw:frame, node =" << n.nodeName(); // This disregards #text, but that's not in the spec anyway so // it doesn't need to be saved. if (!n.isElement()) continue; KoXmlElement el = n.toElement(); ObjectEntry *object = new ObjectEntry; QByteArray contentsTmp; QBuffer buffer(&contentsTmp); // the member KoXmlWriter writer(&buffer); // 1. Find out the objectName // Save the normalized filename, i.e. without a starting "./". // An empty string is saved if no name is found. QString name = el.attributeNS(KoXmlNS::xlink, "href", QString()); if (name.startsWith(QLatin1String("./"))) name.remove(0, 2); object->objectName = name; // 2. Copy the XML code. QHash unknownNamespaces; storeXmlRecursive(el, writer, object, unknownNamespaces); object->objectXmlContents = contentsTmp; // 3, 4: the isDir and manifestEntry members are not set here, // but initialize them anyway. . object->isDir = false; // Has to be initialized to something. object->manifestEntry = 0; objectEntries.append(object); } } void KoUnavailShape::Private::storeXmlRecursive(const KoXmlElement &el, KoXmlWriter &writer, ObjectEntry *object, QHash &unknownNamespaces) { // Start the element; // keep the name in a QByteArray so that it stays valid until end element is called. const QByteArray name(el.nodeName().toLatin1()); writer.startElement(name.constData()); // Child elements // Loop through all the child elements of the draw:frame. KoXmlNode n = el.firstChild(); for (; !n.isNull(); n = n.nextSibling()) { if (n.isElement()) { storeXmlRecursive(n.toElement(), writer, object, unknownNamespaces); } else if (n.isText()) { writer.addTextNode(n.toText().data()/*.toUtf8()*/); } } // End the element writer.endElement(); } /** * This function stores the embedded file in an internal store - it does not save files to disk, * and thus it is named in this manner, to avoid the function being confused with functions which * save files to disk. */ void KoUnavailShape::Private::storeFile(const QString &fileName, KoShapeLoadingContext &context) { debugFlake << "Saving file: " << fileName; // Directories need to be saved too, but they don't have any file contents. if (fileName.endsWith('/')) { FileEntry *entry = new FileEntry; entry->path = fileName; entry->mimeType = context.odfLoadingContext().mimeTypeForPath(entry->path); entry->isDir = true; embeddedFiles.append(entry); } QByteArray fileContent = loadFile(fileName, context); if (fileContent.isNull()) return; // Actually store the file in the list. FileEntry *entry = new FileEntry; entry->path = fileName; if (entry->path.startsWith(QLatin1String("./"))) entry->path.remove(0, 2); entry->mimeType = context.odfLoadingContext().mimeTypeForPath(entry->path); entry->isDir = false; entry->contents = fileContent; embeddedFiles.append(entry); debugFlake << "File length: " << fileContent.size(); } QByteArray KoUnavailShape::Private::loadFile(const QString &fileName, KoShapeLoadingContext &context) { // Can't load a file which is a directory, return an invalid QByteArray if (fileName.endsWith('/')) return QByteArray(); KoStore *store = context.odfLoadingContext().store(); QByteArray fileContent; if (!store->open(fileName)) { store->close(); return QByteArray(); } int fileSize = store->size(); fileContent = store->read(fileSize); store->close(); //debugFlake << "File content: " << fileContent; return fileContent; } diff --git a/plugins/flake/artistictextshape/ArtisticTextShape.cpp b/plugins/flake/artistictextshape/ArtisticTextShape.cpp index 00da7dc75f..d657411ff1 100644 --- a/plugins/flake/artistictextshape/ArtisticTextShape.cpp +++ b/plugins/flake/artistictextshape/ArtisticTextShape.cpp @@ -1,1379 +1,1382 @@ /* This file is part of the KDE project * Copyright (C) 2007-2009,2011 Jan Hambrecht * Copyright (C) 2008 Rob Buis * * 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 "ArtisticTextShape.h" #include "ArtisticTextLoadingContext.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include +#include #include #include #include #include #include #include ArtisticTextShape::ArtisticTextShape() : m_path(0) , m_startOffset(0.0) , m_textAnchor(AnchorStart) , m_textUpdateCounter(0) , m_defaultFont("ComicSans", 20) { setShapeId(ArtisticTextShapeID); cacheGlyphOutlines(); updateSizeAndPosition(); } ArtisticTextShape::~ArtisticTextShape() { if (m_path) { m_path->removeDependee(this); } } void ArtisticTextShape::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { + KisQPainterStateSaver saver(&painter); + applyConversion(painter, converter); if (background()) { background()->paint(painter, converter, paintContext, outline()); } } void ArtisticTextShape::saveOdf(KoShapeSavingContext &context) const { SvgWriter svgWriter(QList() << const_cast(this)); QByteArray fileContent; QBuffer fileContentDevice(&fileContent); if (!fileContentDevice.open(QIODevice::WriteOnly)) { return; } if (!svgWriter.save(fileContentDevice, size())) { qWarning() << "Could not write svg content"; return; } const QString fileName = context.embeddedSaver().getFilename("SvgImages/Image"); const QString mimeType = "image/svg+xml"; context.xmlWriter().startElement("draw:frame"); context.embeddedSaver().embedFile(context.xmlWriter(), "draw:image", fileName, mimeType.toLatin1(), fileContent); context.xmlWriter().endElement(); // draw:frame } bool ArtisticTextShape::loadOdf(const KoXmlElement &/*element*/, KoShapeLoadingContext &/*context*/) { return false; } QSizeF ArtisticTextShape::size() const { if (m_ranges.isEmpty()) { return nullBoundBox().size(); } else { return outline().boundingRect().size(); } } void ArtisticTextShape::setSize(const QSizeF &newSize) { QSizeF oldSize = size(); if (!oldSize.isNull()) { qreal zoomX = newSize.width() / oldSize.width(); qreal zoomY = newSize.height() / oldSize.height(); QTransform matrix(zoomX, 0, 0, zoomY, 0, 0); update(); applyTransformation(matrix); update(); } KoShape::setSize(newSize); } QPainterPath ArtisticTextShape::outline() const { return m_outline; } QRectF ArtisticTextShape::nullBoundBox() const { QFontMetrics metrics(defaultFont()); QPointF tl(0.0, -metrics.ascent()); QPointF br(metrics.averageCharWidth(), metrics.descent()); return QRectF(tl, br); } QFont ArtisticTextShape::defaultFont() const { return m_defaultFont; } qreal baselineShiftForFontSize(const ArtisticTextRange &range, qreal fontSize) { switch (range.baselineShift()) { case ArtisticTextRange::Sub: return fontSize / 3.; // taken from wikipedia case ArtisticTextRange::Super: return -fontSize / 3.; // taken from wikipedia case ArtisticTextRange::Percent: return range.baselineShiftValue() * fontSize; case ArtisticTextRange::Length: return range.baselineShiftValue(); default: return 0.0; } } QVector ArtisticTextShape::calculateAbstractCharacterPositions() { const int totalTextLength = plainText().length(); QVector charPositions; // one more than the number of characters for position after the last character charPositions.resize(totalTextLength + 1); // the character index within the text shape int globalCharIndex = 0; QPointF charPos(0, 0); QPointF advance(0, 0); const bool attachedToPath = isOnPath(); Q_FOREACH (const ArtisticTextRange &range, m_ranges) { QFontMetricsF metrics(QFont(range.font(), &m_paintDevice)); const QString textRange = range.text(); const qreal letterSpacing = range.letterSpacing(); const int localTextLength = textRange.length(); const bool absoluteXOffset = range.xOffsetType() == ArtisticTextRange::AbsoluteOffset; const bool absoluteYOffset = range.yOffsetType() == ArtisticTextRange::AbsoluteOffset; // set baseline shift const qreal baselineShift = baselineShiftForFontSize(range, defaultFont().pointSizeF()); for (int localCharIndex = 0; localCharIndex < localTextLength; ++localCharIndex, ++globalCharIndex) { // apply offset to character if (range.hasXOffset(localCharIndex)) { if (absoluteXOffset) { charPos.rx() = range.xOffset(localCharIndex); } else { charPos.rx() += range.xOffset(localCharIndex); } } else { charPos.rx() += advance.x(); } if (range.hasYOffset(localCharIndex)) { if (absoluteYOffset) { // when attached to a path, absolute y-offsets are ignored if (!attachedToPath) { charPos.ry() = range.yOffset(localCharIndex); } } else { charPos.ry() += range.yOffset(localCharIndex); } } else { charPos.ry() += advance.y(); } // apply baseline shift charPos.ry() += baselineShift; // save character position of current character charPositions[globalCharIndex] = charPos; // advance character position advance = QPointF(metrics.width(textRange[localCharIndex]) + letterSpacing, 0.0); charPos.ry() -= baselineShift; } } charPositions[globalCharIndex] = charPos + advance; return charPositions; } void ArtisticTextShape::createOutline() { // reset relevant data m_outline = QPainterPath(); m_charPositions.clear(); m_charOffsets.clear(); // calculate character positions in baseline coordinates m_charPositions = calculateAbstractCharacterPositions(); // the character index within the text shape int globalCharIndex = 0; if (isOnPath()) { // one more than the number of characters for offset after the last character m_charOffsets.insert(0, m_charPositions.size(), -1); // the current character position qreal startCharOffset = m_startOffset * m_baseline.length(); // calculate total text width qreal totalTextWidth = 0.0; foreach (const ArtisticTextRange &range, m_ranges) { QFontMetricsF metrics(QFont(range.font(), &m_paintDevice)); totalTextWidth += metrics.width(range.text()); } // adjust starting character position to anchor point if (m_textAnchor == AnchorMiddle) { startCharOffset -= 0.5 * totalTextWidth; } else if (m_textAnchor == AnchorEnd) { startCharOffset -= totalTextWidth; } QPointF pathPoint; qreal rotation = 0.0; qreal charOffset; foreach (const ArtisticTextRange &range, m_ranges) { QFontMetricsF metrics(QFont(range.font(), &m_paintDevice)); const QString localText = range.text(); const int localTextLength = localText.length(); for (int localCharIndex = 0; localCharIndex < localTextLength; ++localCharIndex, ++globalCharIndex) { QPointF charPos = m_charPositions[globalCharIndex]; // apply advance along baseline charOffset = startCharOffset + charPos.x(); const qreal charMidPoint = charOffset + 0.5 * metrics.width(localText[localCharIndex]); // get the normalized position of the middle of the character const qreal midT = m_baseline.percentAtLength(charMidPoint); // is the character midpoint beyond the baseline ends? if (midT <= 0.0 || midT >= 1.0) { if (midT >= 1.0) { pathPoint = m_baseline.pointAtPercent(1.0); for (int i = globalCharIndex; i < m_charPositions.size(); ++i) { m_charPositions[i] = pathPoint; m_charOffsets[i] = 1.0; } break; } else { m_charPositions[globalCharIndex] = m_baseline.pointAtPercent(0.0); m_charOffsets[globalCharIndex] = 0.0; continue; } } // get the percent value of the actual char position qreal t = m_baseline.percentAtLength(charOffset); // get the path point of the given path position pathPoint = m_baseline.pointAtPercent(t); // save character offset as fraction of baseline length m_charOffsets[globalCharIndex] = m_baseline.percentAtLength(charOffset); // save character position as point m_charPositions[globalCharIndex] = pathPoint; // get the angle at the given path position const qreal angle = m_baseline.angleAtPercent(midT); if (range.hasRotation(localCharIndex)) { rotation = range.rotation(localCharIndex); } QTransform m; m.translate(pathPoint.x(), pathPoint.y()); m.rotate(360. - angle + rotation); m.translate(0.0, charPos.y()); m_outline.addPath(m.map(m_charOutlines[globalCharIndex])); } } // save offset and position after last character m_charOffsets[globalCharIndex] = m_baseline.percentAtLength(startCharOffset + m_charPositions[globalCharIndex].x()); m_charPositions[globalCharIndex] = m_baseline.pointAtPercent(m_charOffsets[globalCharIndex]); } else { qreal rotation = 0.0; Q_FOREACH (const ArtisticTextRange &range, m_ranges) { const QString textRange = range.text(); const int localTextLength = textRange.length(); for (int localCharIndex = 0; localCharIndex < localTextLength; ++localCharIndex, ++globalCharIndex) { const QPointF &charPos = m_charPositions[globalCharIndex]; if (range.hasRotation(localCharIndex)) { rotation = range.rotation(localCharIndex); } QTransform m; m.translate(charPos.x(), charPos.y()); m.rotate(rotation); m_outline.addPath(m.map(m_charOutlines[globalCharIndex])); } } } } void ArtisticTextShape::setPlainText(const QString &newText) { if (plainText() == newText) { return; } beginTextUpdate(); if (newText.isEmpty()) { // remove all text ranges m_ranges.clear(); } else if (isEmpty()) { // create new text range m_ranges.append(ArtisticTextRange(newText, defaultFont())); } else { // set text to first range m_ranges.first().setText(newText); // remove all ranges except the first while (m_ranges.count() > 1) { m_ranges.pop_back(); } } finishTextUpdate(); } QString ArtisticTextShape::plainText() const { QString allText; Q_FOREACH (const ArtisticTextRange &range, m_ranges) { allText += range.text(); } return allText; } QList ArtisticTextShape::text() const { return m_ranges; } bool ArtisticTextShape::isEmpty() const { return m_ranges.isEmpty(); } void ArtisticTextShape::clear() { beginTextUpdate(); m_ranges.clear(); finishTextUpdate(); } void ArtisticTextShape::setFont(const QFont &newFont) { // no text if (isEmpty()) { return; } const int rangeCount = m_ranges.count(); // only one text range with the same font if (rangeCount == 1 && m_ranges.first().font() == newFont) { return; } beginTextUpdate(); // set font on ranges for (int i = 0; i < rangeCount; ++i) { m_ranges[i].setFont(newFont); } m_defaultFont = newFont; finishTextUpdate(); } void ArtisticTextShape::setFont(int charIndex, int charCount, const QFont &font) { if (isEmpty() || charCount <= 0) { return; } if (charIndex == 0 && charCount == plainText().length()) { setFont(font); return; } CharIndex charPos = indexOfChar(charIndex); if (charPos.first < 0 || charPos.first >= m_ranges.count()) { return; } beginTextUpdate(); int remainingCharCount = charCount; while (remainingCharCount > 0) { ArtisticTextRange &currRange = m_ranges[charPos.first]; // does this range have a different font ? if (currRange.font() != font) { if (charPos.second == 0 && currRange.text().length() < remainingCharCount) { // set font on all characters of this range currRange.setFont(font); remainingCharCount -= currRange.text().length(); } else { ArtisticTextRange changedRange = currRange.extract(charPos.second, remainingCharCount); changedRange.setFont(font); if (charPos.second == 0) { m_ranges.insert(charPos.first, changedRange); } else if (charPos.second >= currRange.text().length()) { m_ranges.insert(charPos.first + 1, changedRange); } else { ArtisticTextRange remainingRange = currRange.extract(charPos.second); m_ranges.insert(charPos.first + 1, changedRange); m_ranges.insert(charPos.first + 2, remainingRange); } charPos.first++; remainingCharCount -= changedRange.text().length(); } } charPos.first++; if (charPos.first >= m_ranges.count()) { break; } charPos.second = 0; } finishTextUpdate(); } QFont ArtisticTextShape::fontAt(int charIndex) const { if (isEmpty()) { return defaultFont(); } if (charIndex < 0) { return m_ranges.first().font(); } const int rangeIndex = indexOfChar(charIndex).first; if (rangeIndex < 0) { return m_ranges.last().font(); } return m_ranges[rangeIndex].font(); } void ArtisticTextShape::setStartOffset(qreal offset) { if (m_startOffset == offset) { return; } update(); m_startOffset = qBound(0.0, offset, 1.0); updateSizeAndPosition(); update(); notifyChanged(); } qreal ArtisticTextShape::startOffset() const { return m_startOffset; } qreal ArtisticTextShape::baselineOffset() const { return m_charPositions.value(0).y(); } void ArtisticTextShape::setTextAnchor(TextAnchor anchor) { if (anchor == m_textAnchor) { return; } qreal totalTextWidth = 0.0; foreach (const ArtisticTextRange &range, m_ranges) { QFontMetricsF metrics(QFont(range.font(), &m_paintDevice)); totalTextWidth += metrics.width(range.text()); } qreal oldOffset = 0.0; if (m_textAnchor == AnchorMiddle) { oldOffset = -0.5 * totalTextWidth; } else if (m_textAnchor == AnchorEnd) { oldOffset = -totalTextWidth; } m_textAnchor = anchor; qreal newOffset = 0.0; if (m_textAnchor == AnchorMiddle) { newOffset = -0.5 * totalTextWidth; } else if (m_textAnchor == AnchorEnd) { newOffset = -totalTextWidth; } update(); updateSizeAndPosition(); if (! isOnPath()) { QTransform m; m.translate(newOffset - oldOffset, 0.0); setTransformation(transformation() * m); } update(); notifyChanged(); } ArtisticTextShape::TextAnchor ArtisticTextShape::textAnchor() const { return m_textAnchor; } bool ArtisticTextShape::putOnPath(KoPathShape *path) { if (! path) { return false; } if (path->outline().isEmpty()) { return false; } if (! path->addDependee(this)) { return false; } update(); m_path = path; // use the paths outline converted to document coordinates as the baseline m_baseline = m_path->absoluteTransformation(0).map(m_path->outline()); // reset transformation setTransformation(QTransform()); updateSizeAndPosition(); // move to correct position setAbsolutePosition(m_outlineOrigin, KoFlake::TopLeft); update(); return true; } bool ArtisticTextShape::putOnPath(const QPainterPath &path) { if (path.isEmpty()) { return false; } update(); if (m_path) { m_path->removeDependee(this); } m_path = 0; m_baseline = path; // reset transformation setTransformation(QTransform()); updateSizeAndPosition(); // move to correct position setAbsolutePosition(m_outlineOrigin, KoFlake::TopLeft); update(); return true; } void ArtisticTextShape::removeFromPath() { update(); if (m_path) { m_path->removeDependee(this); } m_path = 0; m_baseline = QPainterPath(); updateSizeAndPosition(); update(); } bool ArtisticTextShape::isOnPath() const { return (m_path != 0 || ! m_baseline.isEmpty()); } ArtisticTextShape::LayoutMode ArtisticTextShape::layout() const { if (m_path) { return OnPathShape; } else if (! m_baseline.isEmpty()) { return OnPath; } else { return Straight; } } QPainterPath ArtisticTextShape::baseline() const { return m_baseline; } KoPathShape *ArtisticTextShape::baselineShape() const { return m_path; } QList ArtisticTextShape::removeText(int charIndex, int charCount) { QList extractedRanges; if (!charCount) { return extractedRanges; } if (charIndex == 0 && charCount >= plainText().length()) { beginTextUpdate(); extractedRanges = m_ranges; m_ranges.clear(); finishTextUpdate(); return extractedRanges; } CharIndex charPos = indexOfChar(charIndex); if (charPos.first < 0 || charPos.first >= m_ranges.count()) { return extractedRanges; } beginTextUpdate(); int extractedTextLength = 0; while (extractedTextLength < charCount) { ArtisticTextRange r = m_ranges[charPos.first].extract(charPos.second, charCount - extractedTextLength); extractedTextLength += r.text().length(); extractedRanges.append(r); if (extractedTextLength == charCount) { break; } charPos.first++; if (charPos.first >= m_ranges.count()) { break; } charPos.second = 0; } // now remove all empty ranges const int rangeCount = m_ranges.count(); for (int i = charPos.first; i < rangeCount; ++i) { if (m_ranges[charPos.first].text().isEmpty()) { m_ranges.removeAt(charPos.first); } } finishTextUpdate(); return extractedRanges; } QList ArtisticTextShape::copyText(int charIndex, int charCount) { QList extractedRanges; if (!charCount) { return extractedRanges; } CharIndex charPos = indexOfChar(charIndex); if (charPos.first < 0 || charPos.first >= m_ranges.count()) { return extractedRanges; } int extractedTextLength = 0; while (extractedTextLength < charCount) { ArtisticTextRange copy = m_ranges[charPos.first]; ArtisticTextRange r = copy.extract(charPos.second, charCount - extractedTextLength); extractedTextLength += r.text().length(); extractedRanges.append(r); if (extractedTextLength == charCount) { break; } charPos.first++; if (charPos.first >= m_ranges.count()) { break; } charPos.second = 0; } return extractedRanges; } void ArtisticTextShape::insertText(int charIndex, const QString &str) { if (isEmpty()) { appendText(str); return; } CharIndex charPos = indexOfChar(charIndex); if (charIndex < 0) { // insert before first character charPos = CharIndex(0, 0); } else if (charIndex >= plainText().length()) { // insert after last character charPos = CharIndex(m_ranges.count() - 1, m_ranges.last().text().length()); } // check range index, just in case if (charPos.first < 0) { return; } beginTextUpdate(); m_ranges[charPos.first].insertText(charPos.second, str); finishTextUpdate(); } void ArtisticTextShape::insertText(int charIndex, const ArtisticTextRange &textRange) { QList ranges; ranges.append(textRange); insertText(charIndex, ranges); } void ArtisticTextShape::insertText(int charIndex, const QList &textRanges) { if (isEmpty()) { beginTextUpdate(); m_ranges = textRanges; finishTextUpdate(); return; } CharIndex charPos = indexOfChar(charIndex); if (charIndex < 0) { // insert before first character charPos = CharIndex(0, 0); } else if (charIndex >= plainText().length()) { // insert after last character charPos = CharIndex(m_ranges.count() - 1, m_ranges.last().text().length()); } // check range index, just in case if (charPos.first < 0) { return; } beginTextUpdate(); ArtisticTextRange &hitRange = m_ranges[charPos.first]; if (charPos.second == 0) { // insert ranges before the hit range Q_FOREACH (const ArtisticTextRange &range, textRanges) { m_ranges.insert(charPos.first, range); charPos.first++; } } else if (charPos.second == hitRange.text().length()) { // insert ranges after the hit range Q_FOREACH (const ArtisticTextRange &range, textRanges) { m_ranges.insert(charPos.first + 1, range); charPos.first++; } } else { // insert ranges inside hit range ArtisticTextRange right = hitRange.extract(charPos.second, hitRange.text().length()); m_ranges.insert(charPos.first + 1, right); // now insert after the left part of hit range Q_FOREACH (const ArtisticTextRange &range, textRanges) { m_ranges.insert(charPos.first + 1, range); charPos.first++; } } // TODO: merge ranges with same style finishTextUpdate(); } void ArtisticTextShape::appendText(const QString &text) { beginTextUpdate(); if (isEmpty()) { m_ranges.append(ArtisticTextRange(text, defaultFont())); } else { m_ranges.last().appendText(text); } finishTextUpdate(); } void ArtisticTextShape::appendText(const ArtisticTextRange &text) { beginTextUpdate(); m_ranges.append(text); // TODO: merge ranges with same style finishTextUpdate(); } bool ArtisticTextShape::replaceText(int charIndex, int charCount, const ArtisticTextRange &textRange) { QList ranges; ranges.append(textRange); return replaceText(charIndex, charCount, ranges); } bool ArtisticTextShape::replaceText(int charIndex, int charCount, const QList &textRanges) { CharIndex charPos = indexOfChar(charIndex); if (charPos.first < 0 || !charCount) { return false; } beginTextUpdate(); removeText(charIndex, charCount); insertText(charIndex, textRanges); finishTextUpdate(); return true; } qreal ArtisticTextShape::charAngleAt(int charIndex) const { if (isOnPath()) { qreal t = m_charOffsets.value(qBound(0, charIndex, m_charOffsets.size() - 1)); return m_baseline.angleAtPercent(t); } return 0.0; } QPointF ArtisticTextShape::charPositionAt(int charIndex) const { return m_charPositions.value(qBound(0, charIndex, m_charPositions.size() - 1)); } QRectF ArtisticTextShape::charExtentsAt(int charIndex) const { CharIndex charPos = indexOfChar(charIndex); if (charIndex < 0 || isEmpty()) { charPos = CharIndex(0, 0); } else if (charPos.first < 0) { charPos = CharIndex(m_ranges.count() - 1, m_ranges.last().text().length() - 1); } if (charPos.first < m_ranges.size()) { const ArtisticTextRange &range = m_ranges.at(charPos.first); QFontMetrics metrics(range.font()); int w = metrics.charWidth(range.text(), charPos.second); return QRectF(0, 0, w, metrics.height()); } return QRectF(); } void ArtisticTextShape::updateSizeAndPosition(bool global) { QTransform shapeTransform = absoluteTransformation(0); // determine baseline position in document coordinates QPointF oldBaselinePosition = shapeTransform.map(QPointF(0, baselineOffset())); createOutline(); QRectF bbox = m_outline.boundingRect(); if (bbox.isEmpty()) { bbox = nullBoundBox(); } if (isOnPath()) { // calculate the offset we have to apply to keep our position QPointF offset = m_outlineOrigin - bbox.topLeft(); // cache topleft corner of baseline path m_outlineOrigin = bbox.topLeft(); // the outline position is in document coordinates // so we adjust our position QTransform m; m.translate(-offset.x(), -offset.y()); global ? applyAbsoluteTransformation(m) : applyTransformation(m); } else { // determine the new baseline position in document coordinates QPointF newBaselinePosition = shapeTransform.map(QPointF(0, -bbox.top())); // apply a transformation to compensate any translation of // our baseline position QPointF delta = oldBaselinePosition - newBaselinePosition; QTransform m; m.translate(delta.x(), delta.y()); applyAbsoluteTransformation(m); } setSize(bbox.size()); // map outline to shape coordinate system QTransform normalizeMatrix; normalizeMatrix.translate(-bbox.left(), -bbox.top()); m_outline = normalizeMatrix.map(m_outline); const int charCount = m_charPositions.count(); for (int i = 0; i < charCount; ++i) { m_charPositions[i] = normalizeMatrix.map(m_charPositions[i]); } } void ArtisticTextShape::cacheGlyphOutlines() { m_charOutlines.clear(); Q_FOREACH (const ArtisticTextRange &range, m_ranges) { const QString rangeText = range.text(); const QFont rangeFont(range.font(), &m_paintDevice); const int textLength = rangeText.length(); for (int charIdx = 0; charIdx < textLength; ++charIdx) { QPainterPath charOutline; charOutline.addText(QPointF(), rangeFont, rangeText[charIdx]); m_charOutlines.append(charOutline); } } } void ArtisticTextShape::shapeChanged(ChangeType type, KoShape *shape) { if (m_path && shape == m_path) { if (type == KoShape::Deleted) { // baseline shape was deleted m_path = 0; } else if (type == KoShape::ParentChanged && !shape->parent()) { // baseline shape was probably removed from the document m_path->removeDependee(this); m_path = 0; } else { update(); // use the paths outline converted to document coordinates as the baseline m_baseline = m_path->absoluteTransformation(0).map(m_path->outline()); updateSizeAndPosition(true); update(); } } } CharIndex ArtisticTextShape::indexOfChar(int charIndex) const { if (isEmpty()) { return CharIndex(-1, -1); } int rangeIndex = 0; int textLength = 0; Q_FOREACH (const ArtisticTextRange &range, m_ranges) { const int rangeTextLength = range.text().length(); if (static_cast(charIndex) < textLength + rangeTextLength) { return CharIndex(rangeIndex, charIndex - textLength); } textLength += rangeTextLength; rangeIndex++; } return CharIndex(-1, -1); } void ArtisticTextShape::beginTextUpdate() { if (m_textUpdateCounter) { return; } m_textUpdateCounter++; update(); } void ArtisticTextShape::finishTextUpdate() { if (!m_textUpdateCounter) { return; } cacheGlyphOutlines(); updateSizeAndPosition(); update(); notifyChanged(); m_textUpdateCounter--; } bool ArtisticTextShape::saveSvg(SvgSavingContext &context) { context.shapeWriter().startElement("text", false); context.shapeWriter().addAttribute("id", context.getID(this)); SvgStyleWriter::saveSvgStyle(this, context); const QList formattedText = text(); // if we have only a single text range, save the font on the text element const bool hasSingleRange = formattedText.size() == 1; if (hasSingleRange) { saveSvgFont(formattedText.first().font(), context); } qreal anchorOffset = 0.0; if (textAnchor() == ArtisticTextShape::AnchorMiddle) { anchorOffset += 0.5 * this->size().width(); context.shapeWriter().addAttribute("text-anchor", "middle"); } else if (textAnchor() == ArtisticTextShape::AnchorEnd) { anchorOffset += this->size().width(); context.shapeWriter().addAttribute("text-anchor", "end"); } // check if we are set on a path if (layout() == ArtisticTextShape::Straight) { context.shapeWriter().addAttributePt("x", anchorOffset); context.shapeWriter().addAttributePt("y", baselineOffset()); SvgUtil::writeTransformAttributeLazy("transform", transformation(), context.shapeWriter()); Q_FOREACH (const ArtisticTextRange &range, formattedText) { saveSvgTextRange(range, context, !hasSingleRange, baselineOffset()); } } else { KoPathShape *baselineShape = KoPathShape::createShapeFromPainterPath(baseline()); QString id = context.createUID("baseline"); context.styleWriter().startElement("path"); context.styleWriter().addAttribute("id", id); context.styleWriter().addAttribute("d", baselineShape->toString(baselineShape->absoluteTransformation(0) * context.userSpaceTransform())); context.styleWriter().endElement(); context.shapeWriter().startElement("textPath"); context.shapeWriter().addAttribute("xlink:href", QLatin1Char('#') + id); if (startOffset() > 0.0) { context.shapeWriter().addAttribute("startOffset", QString("%1%").arg(startOffset() * 100.0)); } Q_FOREACH (const ArtisticTextRange &range, formattedText) { saveSvgTextRange(range, context, !hasSingleRange, baselineOffset()); } context.shapeWriter().endElement(); delete baselineShape; } context.shapeWriter().endElement(); return true; } void ArtisticTextShape::saveSvgFont(const QFont &font, SvgSavingContext &context) { context.shapeWriter().addAttribute("font-family", font.family()); context.shapeWriter().addAttributePt("font-size", font.pointSizeF()); if (font.bold()) { context.shapeWriter().addAttribute("font-weight", "bold"); } if (font.italic()) { context.shapeWriter().addAttribute("font-style", "italic"); } } void ArtisticTextShape::saveSvgTextRange(const ArtisticTextRange &range, SvgSavingContext &context, bool saveRangeFont, qreal baselineOffset) { context.shapeWriter().startElement("tspan", false); if (range.hasXOffsets()) { const char *attributeName = (range.xOffsetType() == ArtisticTextRange::AbsoluteOffset ? "x" : "dx"); QString attributeValue; int charIndex = 0; while (range.hasXOffset(charIndex)) { if (charIndex) { attributeValue += QLatin1Char(','); } attributeValue += QString("%1").arg(SvgUtil::toUserSpace(range.xOffset(charIndex++))); } context.shapeWriter().addAttribute(attributeName, attributeValue); } if (range.hasYOffsets()) { if (range.yOffsetType() != ArtisticTextRange::AbsoluteOffset) { baselineOffset = 0; } const char *attributeName = (range.yOffsetType() == ArtisticTextRange::AbsoluteOffset ? " y" : " dy"); QString attributeValue; int charIndex = 0; while (range.hasYOffset(charIndex)) { if (charIndex) { attributeValue += QLatin1Char(','); } attributeValue += QString("%1").arg(SvgUtil::toUserSpace(baselineOffset + range.yOffset(charIndex++))); } context.shapeWriter().addAttribute(attributeName, attributeValue); } if (range.hasRotations()) { QString attributeValue; int charIndex = 0; while (range.hasRotation(charIndex)) { if (charIndex) { attributeValue += ','; } attributeValue += QString("%1").arg(range.rotation(charIndex++)); } context.shapeWriter().addAttribute("rotate", attributeValue); } if (range.baselineShift() != ArtisticTextRange::None) { switch (range.baselineShift()) { case ArtisticTextRange::Sub: context.shapeWriter().addAttribute("baseline-shift", "sub"); break; case ArtisticTextRange::Super: context.shapeWriter().addAttribute("baseline-shift", "super"); break; case ArtisticTextRange::Percent: context.shapeWriter().addAttribute("baseline-shift", QString("%1%").arg(range.baselineShiftValue() * 100)); break; case ArtisticTextRange::Length: context.shapeWriter().addAttribute("baseline-shift", QString("%1%").arg(SvgUtil::toUserSpace(range.baselineShiftValue()))); break; default: break; } } if (saveRangeFont) { saveSvgFont(range.font(), context); } context.shapeWriter().addTextNode(range.text()); context.shapeWriter().endElement(); } bool ArtisticTextShape::loadSvg(const KoXmlElement &textElement, SvgLoadingContext &context) { clear(); QString anchor; if (!textElement.attribute("text-anchor").isEmpty()) { anchor = textElement.attribute("text-anchor"); } SvgStyles elementStyles = context.styleParser().collectStyles(textElement); context.styleParser().parseFont(elementStyles); ArtisticTextLoadingContext textContext; textContext.parseCharacterTransforms(textElement, context.currentGC()); KoXmlElement parentElement = textElement; // first check if we have a "textPath" child element for (KoXmlNode n = textElement.firstChild(); !n.isNull(); n = n.nextSibling()) { KoXmlElement e = n.toElement(); if (e.tagName() == "textPath") { parentElement = e; break; } } KoPathShape *path = 0; bool pathInDocument = false; double offset = 0.0; const bool hasTextPathElement = parentElement != textElement && parentElement.hasAttribute("xlink:href"); if (hasTextPathElement) { // create the referenced path shape context.pushGraphicsContext(parentElement); context.styleParser().parseFont(context.styleParser().collectStyles(parentElement)); textContext.pushCharacterTransforms(); textContext.parseCharacterTransforms(parentElement, context.currentGC()); QString href = parentElement.attribute("xlink:href").mid(1); if (context.hasDefinition(href)) { const KoXmlElement &p = context.definition(href); // must be a path element as per svg spec if (p.tagName() == "path") { pathInDocument = false; path = new KoPathShape(); path->clear(); KoPathShapeLoader loader(path); loader.parseSvg(p.attribute("d"), true); path->setPosition(path->normalize()); QPointF newPosition = QPointF(SvgUtil::fromUserSpace(path->position().x()), SvgUtil::fromUserSpace(path->position().y())); QSizeF newSize = QSizeF(SvgUtil::fromUserSpace(path->size().width()), SvgUtil::fromUserSpace(path->size().height())); path->setSize(newSize); path->setPosition(newPosition); path->applyAbsoluteTransformation(context.currentGC()->matrix); } } else { path = dynamic_cast(context.shapeById(href)); if (path) { pathInDocument = true; } } // parse the start offset if (! parentElement.attribute("startOffset").isEmpty()) { QString start = parentElement.attribute("startOffset"); if (start.endsWith('%')) { offset = 0.01 * start.remove('%').toDouble(); } else { const float pathLength = path ? path->outline().length() : 0.0; if (pathLength > 0.0) { offset = start.toDouble() / pathLength; } } } } if (parentElement.hasChildNodes()) { // parse child elements parseTextRanges(parentElement, context, textContext); if (!context.currentGC()->preserveWhitespace) { const QString text = plainText(); if (text.endsWith(' ')) { removeText(text.length() - 1, 1); } } setPosition(textContext.textPosition()); } else { // a single text range appendText(createTextRange(textElement.text(), textContext, context.currentGC())); setPosition(textContext.textPosition()); } if (hasTextPathElement) { if (path) { if (pathInDocument) { putOnPath(path); } else { putOnPath(path->absoluteTransformation(0).map(path->outline())); delete path; } if (offset > 0.0) { setStartOffset(offset); } } textContext.popCharacterTransforms(); context.popGraphicsContext(); } // adjust position by baseline offset if (! isOnPath()) { setPosition(position() - QPointF(0, baselineOffset())); } if (anchor == "middle") { setTextAnchor(ArtisticTextShape::AnchorMiddle); } else if (anchor == "end") { setTextAnchor(ArtisticTextShape::AnchorEnd); } return true; } void ArtisticTextShape::parseTextRanges(const KoXmlElement &element, SvgLoadingContext &context, ArtisticTextLoadingContext &textContext) { for (KoXmlNode n = element.firstChild(); !n.isNull(); n = n.nextSibling()) { KoXmlElement e = n.toElement(); if (e.isNull()) { ArtisticTextRange range = createTextRange(n.toText().data(), textContext, context.currentGC()); appendText(range); } else if (e.tagName() == "tspan") { SvgGraphicsContext *gc = context.pushGraphicsContext(e); context.styleParser().parseFont(context.styleParser().collectStyles(e)); textContext.pushCharacterTransforms(); textContext.parseCharacterTransforms(e, gc); parseTextRanges(e, context, textContext); textContext.popCharacterTransforms(); context.popGraphicsContext(); } else if (e.tagName() == "tref") { if (e.attribute("xlink:href").isEmpty()) { continue; } QString href = e.attribute("xlink:href").mid(1); ArtisticTextShape *refText = dynamic_cast(context.shapeById(href)); if (refText) { foreach (const ArtisticTextRange &range, refText->text()) { appendText(range); } } else if (context.hasDefinition(href)) { const KoXmlElement &p = context.definition(href); SvgGraphicsContext *gc = context.currentGC(); appendText(ArtisticTextRange(textContext.simplifyText(p.text(), gc->preserveWhitespace), gc->font)); } } else { continue; } } } ArtisticTextRange ArtisticTextShape::createTextRange(const QString &text, ArtisticTextLoadingContext &context, SvgGraphicsContext *gc) { ArtisticTextRange range(context.simplifyText(text, gc->preserveWhitespace), gc->font); const int textLength = range.text().length(); switch (context.xOffsetType()) { case ArtisticTextLoadingContext::Absolute: range.setXOffsets(context.xOffsets(textLength), ArtisticTextRange::AbsoluteOffset); break; case ArtisticTextLoadingContext::Relative: range.setXOffsets(context.xOffsets(textLength), ArtisticTextRange::RelativeOffset); break; default: // no x-offsets break; } switch (context.yOffsetType()) { case ArtisticTextLoadingContext::Absolute: range.setYOffsets(context.yOffsets(textLength), ArtisticTextRange::AbsoluteOffset); break; case ArtisticTextLoadingContext::Relative: range.setYOffsets(context.yOffsets(textLength), ArtisticTextRange::RelativeOffset); break; default: // no y-offsets break; } range.setRotations(context.rotations(textLength)); #if 0 range.setLetterSpacing(gc->letterSpacing); range.setWordSpacing(gc->wordSpacing); if (gc->baselineShift == "sub") { range.setBaselineShift(ArtisticTextRange::Sub); } else if (gc->baselineShift == "super") { range.setBaselineShift(ArtisticTextRange::Super); } else if (gc->baselineShift.endsWith('%')) { range.setBaselineShift(ArtisticTextRange::Percent, SvgUtil::fromPercentage(gc->baselineShift)); } else { qreal value = SvgUtil::parseUnitX(gc, gc->baselineShift); if (value != 0.0) { range.setBaselineShift(ArtisticTextRange::Length, value); } } #endif //range.printDebug(); return range; } diff --git a/plugins/flake/imageshape/ImageShape.cpp b/plugins/flake/imageshape/ImageShape.cpp index 8443e3ea1e..aa42692abb 100644 --- a/plugins/flake/imageshape/ImageShape.cpp +++ b/plugins/flake/imageshape/ImageShape.cpp @@ -1,186 +1,187 @@ /* * 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 #include "ImageShape.h" #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include "kis_dom_utils.h" #include +#include "KisQPainterStateSaver.h" + struct Q_DECL_HIDDEN ImageShape::Private { Private() {} Private(const Private &rhs) : image(rhs.image), ratioParser(rhs.ratioParser ? new SvgUtil::PreserveAspectRatioParser(*rhs.ratioParser) : 0), viewBoxTransform(rhs.viewBoxTransform) { } QImage image; QScopedPointer ratioParser; QTransform viewBoxTransform; }; ImageShape::ImageShape() : m_d(new Private) { } ImageShape::ImageShape(const ImageShape &rhs) : KoTosContainer(new KoTosContainerPrivate(*rhs.d_func(), this)), m_d(new Private(*rhs.m_d)) { } ImageShape::~ImageShape() { } KoShape *ImageShape::cloneShape() const { return new ImageShape(*this); } void ImageShape::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { Q_UNUSED(paintContext); + KisQPainterStateSaver saver(&painter); const QRectF myrect(QPointF(), size()); applyConversion(painter, converter); - painter.save(); painter.setRenderHint(QPainter::SmoothPixmapTransform); painter.setClipRect(QRectF(QPointF(), size()), Qt::IntersectClip); painter.setTransform(m_d->viewBoxTransform, true); painter.drawImage(QPoint(), m_d->image); - painter.restore(); } void ImageShape::setSize(const QSizeF &size) { KoTosContainer::setSize(size); } void ImageShape::saveOdf(KoShapeSavingContext &context) const { Q_UNUSED(context); } bool ImageShape::loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context) { Q_UNUSED(element); Q_UNUSED(context); return false; } bool ImageShape::saveSvg(SvgSavingContext &context) { const QString uid = context.createUID("image"); context.shapeWriter().startElement("image"); context.shapeWriter().addAttribute("id", uid); SvgUtil::writeTransformAttributeLazy("transform", transformation(), context.shapeWriter()); context.shapeWriter().addAttribute("width", QString("%1px").arg(KisDomUtils::toString(size().width()))); context.shapeWriter().addAttribute("height", QString("%1px").arg(KisDomUtils::toString(size().height()))); QString aspectString = m_d->ratioParser->toString(); if (!aspectString.isEmpty()) { context.shapeWriter().addAttribute("preserveAspectRatio", aspectString); } QByteArray ba; QBuffer buffer(&ba); buffer.open(QIODevice::WriteOnly); if (m_d->image.save(&buffer, "PNG")) { const QString mimeType = KisMimeDatabase::mimeTypeForSuffix("*.png"); context.shapeWriter().addAttribute("xlink:href", "data:"+ mimeType + ";base64," + ba.toBase64()); } context.shapeWriter().endElement(); // image return true; } bool ImageShape::loadSvg(const KoXmlElement &element, SvgLoadingContext &context) { const qreal x = SvgUtil::parseUnitX(context.currentGC(), element.attribute("x")); const qreal y = SvgUtil::parseUnitY(context.currentGC(), element.attribute("y")); const qreal w = SvgUtil::parseUnitX(context.currentGC(), element.attribute("width")); const qreal h = SvgUtil::parseUnitY(context.currentGC(), element.attribute("height")); setSize(QSizeF(w, h)); setPosition(QPointF(x, y)); if (w == 0.0 || h == 0.0) { setVisible(false); } QString fileName = element.attribute("xlink:href"); QByteArray data; if (fileName.startsWith("data:")) { QRegularExpression re("data:(.+?);base64,(.+)"); QRegularExpressionMatch match = re.match(fileName); data = match.captured(2).toLatin1(); data = QByteArray::fromBase64(data); } else { data = context.fetchExternalFile(fileName); } if (!data.isEmpty()) { QBuffer buffer(&data); m_d->image.load(&buffer, ""); } const QString aspectString = element.attribute("preserveAspectRatio", "xMidYMid meet"); m_d->ratioParser.reset(new SvgUtil::PreserveAspectRatioParser(aspectString)); if (!m_d->image.isNull()) { m_d->viewBoxTransform = QTransform::fromScale(w / m_d->image.width(), h / m_d->image.height()); SvgUtil::parseAspectRatio(*m_d->ratioParser, QRectF(QPointF(), size()), QRect(QPoint(), m_d->image.size()), &m_d->viewBoxTransform); } if (m_d->ratioParser->defer) { // TODO: } return true; }