diff --git a/libs/basicflakes/tools/KoCreatePathTool_p.h b/libs/basicflakes/tools/KoCreatePathTool_p.h index 4eb8e03052..386460403f 100644 --- a/libs/basicflakes/tools/KoCreatePathTool_p.h +++ b/libs/basicflakes/tools/KoCreatePathTool_p.h @@ -1,445 +1,445 @@ /* This file is part of the KDE project * * Copyright (C) 2006 Thorsten Zachmann * Copyright (C) 2008-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. */ #ifndef KOCREATEPATHTOOL_P_H #define KOCREATEPATHTOOL_P_H #include "KoCreatePathTool.h" #include "KoPathPoint.h" #include "KoPathPointData.h" #include "KoPathPointMergeCommand.h" #include "KoParameterShape.h" #include "KoShapeManager.h" #include "KoSnapStrategy.h" #include "KoToolBase_p.h" #include #include "kis_config.h" #include "math.h" class KoStrokeConfigWidget; class KoConverter; /// Small helper to keep track of a path point and its parent path shape struct PathConnectionPoint { PathConnectionPoint() : path(0), point(0) { } // reset state to invalid void reset() { path = 0; point = 0; } PathConnectionPoint& operator =(KoPathPoint * pathPoint) { if (!pathPoint || ! pathPoint->parent()) { reset(); } else { path = pathPoint->parent(); point = pathPoint; } return *this; } bool operator != (const PathConnectionPoint &rhs) const { return rhs.path != path || rhs.point != point; } bool operator == (const PathConnectionPoint &rhs) const { return rhs.path == path && rhs.point == point; } bool isValid() const { return path && point; } // checks if the path and point are still valid void validate(KoCanvasBase *canvas) { // no point in validating an already invalid state if (!isValid()) { return; } // we need canvas to validate if (!canvas) { reset(); return; } - // check if path is still part of the docment + // check if path is still part of the document if (!canvas->shapeManager()->shapes().contains(path)) { reset(); return; } // check if point is still part of the path if (path->pathPointIndex(point) == KoPathPointIndex(-1, -1)) { reset(); return; } } KoPathShape * path; KoPathPoint * point; }; inline qreal squareDistance(const QPointF &p1, const QPointF &p2) { qreal dx = p1.x() - p2.x(); qreal dy = p1.y() - p2.y(); return dx * dx + dy * dy; } class AngleSnapStrategy : public KoSnapStrategy { public: explicit AngleSnapStrategy(qreal angleStep, bool active) : KoSnapStrategy(KoSnapGuide::CustomSnapping), m_angleStep(angleStep), m_active(active) { } void setStartPoint(const QPointF &startPoint) { m_startPoint = startPoint; } void setAngleStep(qreal angleStep) { m_angleStep = qAbs(angleStep); } bool snap(const QPointF &mousePosition, KoSnapProxy * proxy, qreal maxSnapDistance) override { Q_UNUSED(proxy); if (!m_active) return false; QLineF line(m_startPoint, mousePosition); qreal currentAngle = line.angle(); int prevStep = qAbs(currentAngle / m_angleStep); int nextStep = prevStep + 1; qreal prevAngle = prevStep * m_angleStep; qreal nextAngle = nextStep * m_angleStep; if (qAbs(currentAngle - prevAngle) <= qAbs(currentAngle - nextAngle)) { line.setAngle(prevAngle); } else { line.setAngle(nextAngle); } qreal maxSquareSnapDistance = maxSnapDistance * maxSnapDistance; qreal snapDistance = squareDistance(mousePosition, line.p2()); if (snapDistance > maxSquareSnapDistance) return false; setSnappedPosition(line.p2()); return true; } QPainterPath decoration(const KoViewConverter &converter) const override { Q_UNUSED(converter); QPainterPath decoration; decoration.moveTo(m_startPoint); decoration.lineTo(snappedPosition()); return decoration; } void deactivate() { m_active = false; } void activate() { m_active = true; } private: QPointF m_startPoint; qreal m_angleStep; bool m_active; }; class KoCreatePathToolPrivate : public KoToolBasePrivate { KoCreatePathTool * const q; public: KoCreatePathToolPrivate(KoCreatePathTool * const qq, KoCanvasBase* canvas) : KoToolBasePrivate(qq, canvas), q(qq), shape(0), activePoint(0), firstPoint(0), handleRadius(3), mouseOverFirstPoint(false), pointIsDragged(false), finishAfterThisPoint(false), hoveredPoint(0), angleSnapStrategy(0), angleSnappingDelta(15), angleSnapStatus(false), enableClosePathShortcut(true) { } KoPathShape *shape; KoPathPoint *activePoint; KoPathPoint *firstPoint; int handleRadius; bool mouseOverFirstPoint; bool pointIsDragged; bool finishAfterThisPoint; PathConnectionPoint existingStartPoint; ///< an existing path point we started a new path at PathConnectionPoint existingEndPoint; ///< an existing path point we finished a new path at KoPathPoint *hoveredPoint; ///< an existing path end point the mouse is hovering on bool prevPointWasDragged = false; bool autoSmoothCurves = false; QPointF dragStartPoint; AngleSnapStrategy *angleSnapStrategy; int angleSnappingDelta; bool angleSnapStatus; bool enableClosePathShortcut; void repaintActivePoint() const { const bool isFirstPoint = (activePoint == firstPoint); if (!isFirstPoint && !pointIsDragged) return; QRectF rect = activePoint->boundingRect(false); // make sure that we have the second control point inside our // update rect, as KoPathPoint::boundingRect will not include // the second control point of the last path point if the path // is not closed const QPointF &point = activePoint->point(); const QPointF &controlPoint = activePoint->controlPoint2(); rect = rect.united(QRectF(point, controlPoint).normalized()); // when painting the first point we want the // first control point to be painted as well if (isFirstPoint) { const QPointF &controlPoint = activePoint->controlPoint1(); rect = rect.united(QRectF(point, controlPoint).normalized()); } QPointF border = q->canvas()->viewConverter() ->viewToDocument(QPointF(handleRadius, handleRadius)); rect.adjust(-border.x(), -border.y(), border.x(), border.y()); q->canvas()->updateCanvas(rect); } /// returns the nearest existing path point KoPathPoint* endPointAtPosition(const QPointF &position) const { QRectF roi = q->handleGrabRect(position); QList shapes = q->canvas()->shapeManager()->shapesAt(roi); KoPathPoint * nearestPoint = 0; qreal minDistance = HUGE_VAL; uint grabSensitivity = q->grabSensitivity(); qreal maxDistance = q->canvas()->viewConverter()->viewToDocumentX(grabSensitivity); Q_FOREACH(KoShape * s, shapes) { KoPathShape * path = dynamic_cast(s); if (!path) continue; KoParameterShape *paramShape = dynamic_cast(s); if (paramShape && paramShape->isParametricShape()) continue; KoPathPoint * p = 0; uint subpathCount = path->subpathCount(); for (uint i = 0; i < subpathCount; ++i) { if (path->isClosedSubpath(i)) continue; p = path->pointByIndex(KoPathPointIndex(i, 0)); // check start of subpath qreal d = squareDistance(position, path->shapeToDocument(p->point())); if (d < minDistance && d < maxDistance) { nearestPoint = p; minDistance = d; } // check end of subpath p = path->pointByIndex(KoPathPointIndex(i, path->subpathPointCount(i) - 1)); d = squareDistance(position, path->shapeToDocument(p->point())); if (d < minDistance && d < maxDistance) { nearestPoint = p; minDistance = d; } } } return nearestPoint; } /// Connects given path with the ones we hit when starting/finishing bool connectPaths(KoPathShape *pathShape, const PathConnectionPoint &pointAtStart, const PathConnectionPoint &pointAtEnd) const { KoPathShape * startShape = 0; KoPathShape * endShape = 0; KoPathPoint * startPoint = 0; KoPathPoint * endPoint = 0; if (pointAtStart.isValid()) { startShape = pointAtStart.path; startPoint = pointAtStart.point; } if (pointAtEnd.isValid()) { endShape = pointAtEnd.path; endPoint = pointAtEnd.point; } // at least one point must be valid if (!startPoint && !endPoint) return false; // do not allow connecting to the same point twice if (startPoint == endPoint) endPoint = 0; // we have hit an existing path point on start/finish // what we now do is: // 1. combine the new created path with the ones we hit on start/finish // 2. merge the endpoints of the corresponding subpaths uint newPointCount = pathShape->subpathPointCount(0); KoPathPointIndex newStartPointIndex(0, 0); KoPathPointIndex newEndPointIndex(0, newPointCount - 1); KoPathPoint * newStartPoint = pathShape->pointByIndex(newStartPointIndex); KoPathPoint * newEndPoint = pathShape->pointByIndex(newEndPointIndex); // combine with the path we hit on start KoPathPointIndex startIndex(-1, -1); if (startShape && startPoint) { startIndex = startShape->pathPointIndex(startPoint); pathShape->combine(startShape); pathShape->moveSubpath(0, pathShape->subpathCount() - 1); } // combine with the path we hit on finish KoPathPointIndex endIndex(-1, -1); if (endShape && endPoint) { endIndex = endShape->pathPointIndex(endPoint); if (endShape != startShape) { endIndex.first += pathShape->subpathCount(); pathShape->combine(endShape); } } // do we connect twice to a single subpath ? bool connectToSingleSubpath = (startShape == endShape && startIndex.first == endIndex.first); if (startIndex.second == 0 && !connectToSingleSubpath) { pathShape->reverseSubpath(startIndex.first); startIndex.second = pathShape->subpathPointCount(startIndex.first) - 1; } if (endIndex.second > 0 && !connectToSingleSubpath) { pathShape->reverseSubpath(endIndex.first); endIndex.second = 0; } // after combining we have a path where with the subpaths in the following // order: // 1. the subpaths of the pathshape we started the new path at // 2. the subpath we just created // 3. the subpaths of the pathshape we finished the new path at // get the path points we want to merge, as these are not going to // change while merging KoPathPoint * existingStartPoint = pathShape->pointByIndex(startIndex); KoPathPoint * existingEndPoint = pathShape->pointByIndex(endIndex); // merge first two points if (existingStartPoint) { KoPathPointData pd1(pathShape, pathShape->pathPointIndex(existingStartPoint)); KoPathPointData pd2(pathShape, pathShape->pathPointIndex(newStartPoint)); KoPathPointMergeCommand cmd1(pd1, pd2); cmd1.redo(); } // merge last two points if (existingEndPoint) { KoPathPointData pd3(pathShape, pathShape->pathPointIndex(newEndPoint)); KoPathPointData pd4(pathShape, pathShape->pathPointIndex(existingEndPoint)); KoPathPointMergeCommand cmd2(pd3, pd4); cmd2.redo(); } return true; } void addPathShape() { if (!shape) return; if (shape->pointCount() < 2) { cleanUp(); return; } // this is done so that nothing happens when the mouseReleaseEvent for the this event is received KoPathShape *pathShape = shape; shape = 0; q->addPathShape(pathShape); cleanUp(); return; } void cleanUp() { // reset snap guide q->canvas()->updateCanvas(q->canvas()->snapGuide()->boundingRect()); q->canvas()->snapGuide()->reset(); angleSnapStrategy = 0; delete shape; shape = 0; existingStartPoint = 0; existingEndPoint = 0; hoveredPoint = 0; } void angleDeltaChanged(int value) { angleSnappingDelta = value; if (angleSnapStrategy) angleSnapStrategy->setAngleStep(angleSnappingDelta); } void autoSmoothCurvesChanged(bool value) { autoSmoothCurves = value; KisConfig cfg(false); cfg.setAutoSmoothBezierCurves(value); } void loadAutoSmoothValueFromConfig() { KisConfig cfg(true); autoSmoothCurves = cfg.autoSmoothBezierCurves(); emit q->sigUpdateAutoSmoothCurvesGUI(autoSmoothCurves); } void angleSnapChanged(int angleSnap) { angleSnapStatus = ! angleSnapStatus; if (angleSnapStrategy) { if (angleSnap == Qt::Checked) angleSnapStrategy->activate(); else angleSnapStrategy->deactivate(); } } }; #endif // KOCREATEPATHTOOL_P_H diff --git a/libs/brush/kis_abr_translator.h b/libs/brush/kis_abr_translator.h index a525f1ba6a..3fa008c212 100644 --- a/libs/brush/kis_abr_translator.h +++ b/libs/brush/kis_abr_translator.h @@ -1,183 +1,183 @@ /* * Copyright (c) 2010 Lukáš Tvrdý * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_ABR_TRANSLATOR_H #define KIS_ABR_TRANSLATOR_H #include #include const QString ABR_PRESET_START = "START"; const QString ABR_OBJECT = "Objc"; const QString ABR_PRESET_NAME = "Nm "; const QString ABR_BRUSH_DIAMETER = "Dmtr"; const QString ABR_BRUSH_HARDNESS = "Hrdn"; const QString ABR_BRUSH_ANGLE = "Angl"; const QString ABR_BRUSH_ROUNDNESS = "Rndn"; const QString ABR_BRUSH_SPACING = "Spcn"; const QString ABR_BRUSH_INTR = "Intr"; const QString ABR_FLIP_X = "flipX"; const QString ABR_FLIP_Y = "flipY"; const QString ABR_USE_TIP_DYNAMICS = "useTipDynamics"; const QString ABR_TIP_DYNAMICS_MINUMUM_DIAMETER = "minimumDiameter"; const QString ABR_TIP_DYNAMICS_MINUMUM_ROUNDNESS = "minimumRoundness"; const QString ABR_TIP_DYNAMICS_TILT_SCALE = "tiltScale"; const QString ABR_SZVR = "szVr"; // size variance? const QString ABR_DYNAMICS_FADE_STEP = "fStp"; const QString ABR_DYNAMICS_JITTER = "jitter"; const QString ABR_ANGLE_DYNAMICS = "angleDynamics"; const QString ABR_CONTROL = "bVTy"; // control of the attribute const QString ABR_ROUNDNESS_DYNAMICS = "roundnessDynamics"; const QString ABR_USE_SCATTER = "useScatter"; const QString ABR_DUAL_BRUSH = "dualBrush"; const QString ABR_USE_DUAL_BRUSH = "useDualBrush"; const QString ABR_BRUSH_GROUP = "brushGroup"; const QString ABR_USE_BRUSH_GROUP = "useBrushGroup"; const QString ABR_USE_TEXTURE = "useTexture"; const QString ABR_USE_PAINT_DYNAMICS = "usePaintDynamics"; const QString ABR_USE_COLOR_DYNAMICS = "useColorDynamics"; const QString ABR_WET_EDGES = "Wtdg"; const QString ABR_NOISE = "Nose"; const QString ABR_AIRBRUSH = "Rpt "; // repeat -// TODO: if string comaprisons would be slow, convert it to enum +// TODO: if string comparisons would be slow, convert it to enum enum enumObjectTypes {BRUSH_PRESET, BRUSH, SIZE_VARIANCE, ANGLE_DYNAMICS, ROUNDNESS_DYNAMICS, DUAL_BRUSH, COUNT_DYNAMICS, SCATTER_DYNAMICS, BRUSH_GROUP}; const QString OBJECT_NAME_BRUSH_PRESET = ""; const QString OBJECT_NAME_BRUSH = "Brsh"; const QString OBJECT_NAME_SIZE_VARIANCE = "szVr"; const QString OBJECT_NAME_ANGLE_DYNAMICS = "angleDynamics"; const QString OBJECT_NAME_ROUNDNESS_DYNAMICS = "roundnessDynamics"; const QString OBJECT_NAME_DUAL_BRUSH = "dualBrush"; const QString OBJECT_NAME_COUNT_DYNAMICS = "countDynamics"; const QString OBJECT_NAME_SCATTER_DYNAMICS = "scatterDynamics"; const QString OBJECT_NAME_BRUSH_GROUP = "brushGroup"; const QString OBJECT_NAME_FLOW_JITTER = "prVr"; const QString OBJECT_NAME_OPACITY_VARIANCE = "opVr"; const QString BRUSH_TYPE_COMPUTED = "computedBrush"; const QString BRUSH_TYPE_SAMPLED = "sampledBrush"; enum enumAbrControllers { OFF, FADE, PEN_PRESSURE, PEN_TILT, STYLUS_WHEEL, ROTATION, INITIAL_DIRECTION, DIRECTION}; class AbrBrushProperties { public: void toXML(QDomDocument& , QDomElement&) const; void setupProperty(const QString &attributeName, const QString &type, const QString &value); void reset(); private: double m_diameter; double m_hardness; double m_angle; double m_roundness; double m_spacing; bool m_intr; bool m_flipX; bool m_flipY; QString m_brushType; }; class AbrGroupProperties { public: void setupProperty(const QString &attributeName, const QString &type, const QString &value); enumAbrControllers m_bVTy; int m_fadeStep; double m_sizeJitter; }; #include class AbrTipDynamicsProperties { public: AbrTipDynamicsProperties(); void toXML(QDomDocument& , QDomElement&) const; void setupProperty(const QString &attributeName, const QString &type, const QString &value); void reset() { m_groupType.clear(); } private: bool m_useTipDynamics; bool m_flipX; bool m_flipY; double m_minumumDiameter; double m_minumumRoundness; double m_tiltScale; AbrGroupProperties m_sizeVarianceProperties; AbrGroupProperties m_angleProperties; AbrGroupProperties m_RoundnessProperties; QString m_groupType; QHash m_groups; }; class AbrDualBrushProperties { public: bool useDualBrush; void toXML(QDomDocument& , QDomElement&) const; }; class AbrBrushGroupProperties { public: bool useBrushGroup; bool useTexture; bool usePaintDynamics; bool useColorDynamics; bool wetEdges; bool noise; bool repeat; void toXML(QDomDocument& , QDomElement&) const; }; class KisAbrTranslator { public: KisAbrTranslator(); ~KisAbrTranslator(); void addEntry(const QString &attributeName, const QString &type, const QString &value); QString toString(); void finishPreset(); void clean(); private: void init(); void setupObjectType(const QString &type); void setupBrush(const QString &attributeName, const QString &type, const QString &value); private: QDomDocument m_doc; QDomElement m_root; QString m_currentObjectName; AbrBrushProperties m_abrBrushProperties; AbrTipDynamicsProperties m_abrTipDynamics; }; #endif diff --git a/libs/command/kundo2magicstring.h b/libs/command/kundo2magicstring.h index d6a3ffe8f8..a8a6ebe0b0 100644 --- a/libs/command/kundo2magicstring.h +++ b/libs/command/kundo2magicstring.h @@ -1,318 +1,318 @@ /* * Copyright (c) 2014 Dmitry Kazakov * Copyright (c) 2014 Alexander Potashev * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KUNDO2MAGICSTRING_H #define KUNDO2MAGICSTRING_H #include #include #include #include "kritacommand_export.h" /** * \class KUndo2MagicString is a special wrapper for a string that is * going to passed to a KUndo2Command and be later shown in the undo * history and undo action in menu. The strings like that must have * (qtundo-format) context to let translators know that they are * allowed to use magic split in them. * * Magic split is used in some languages to split the message in the * undo history docker (which is either verb or noun in * nominative) and the message in undo/redo actions (which is * usually a noun * in accusative). When the translator needs it he, splits two * translations with '\n' symbol and the magic string will recognize * it. * * \note KUndo2MagicString will never support concatenation operators, * because in many languages you cannot combine words without * knowing the proper case. */ class KRITACOMMAND_EXPORT KUndo2MagicString { public: /** * Construct an empty string. Note that you cannot create a * non-empy string without special functions, all the calls to which * are processed by xgettext. */ KUndo2MagicString(); /** * Fetch the main translated string. That is the one that goes to * undo history and resembles the action name in verb/nominative */ QString toString() const; /** * Fetch the secondary string which will go to the undo/redo * action. This is usually a noun in accusative. If the * translator didn't provide a secondary string, toString() and * toSecondaryString() return the same values. */ QString toSecondaryString() const; /** * \return true if the contained string is empty */ bool isEmpty() const; bool operator==(const KUndo2MagicString &rhs) const; bool operator!=(const KUndo2MagicString &rhs) const; private: /** * Construction of a magic string is allowed only with the means - * of speacial macros which resemble their kde-wide counterparts + * of special macros which resemble their kde-wide counterparts */ explicit KUndo2MagicString(const QString &text); friend KUndo2MagicString kundo2_noi18n(const QString &text); template friend KUndo2MagicString kundo2_noi18n(const char *text, const A1 &a1); template friend KUndo2MagicString kundo2_noi18n(const char *text, const A1 &a1, const A2 &a2); template friend KUndo2MagicString kundo2_noi18n(const char *text, const A1 &a1, const A2 &a2, const A3 &a3); template friend KUndo2MagicString kundo2_noi18n(const char *text, const A1 &a1, const A2 &a2, const A3 &a3, const A4 &a4); friend KUndo2MagicString kundo2_i18n(const char *text); template friend KUndo2MagicString kundo2_i18n(const char *text, const A1 &a1); template friend KUndo2MagicString kundo2_i18n(const char *text, const A1 &a1, const A2 &a2); template friend KUndo2MagicString kundo2_i18n(const char *text, const A1 &a1, const A2 &a2, const A3 &a3); template friend KUndo2MagicString kundo2_i18n(const char *text, const A1 &a1, const A2 &a2, const A3 &a3, const A4 &a4); friend KUndo2MagicString kundo2_i18nc(const char *ctxt, const char *text); template friend KUndo2MagicString kundo2_i18nc(const char *ctxt, const char *text, const A1 &a1); template friend KUndo2MagicString kundo2_i18nc(const char *ctxt, const char *text, const A1 &a1, const A2 &a2); template friend KUndo2MagicString kundo2_i18nc(const char *ctxt, const char *text, const A1 &a1, const A2 &a2, const A3 &a3); template friend KUndo2MagicString kundo2_i18np(const char *sing, const char *plur, const A1 &a1); template friend KUndo2MagicString kundo2_i18np(const char *sing, const char *plur, const A1 &a1, const A2 &a2); template friend KUndo2MagicString kundo2_i18np(const char *sing, const char *plur, const A1 &a1, const A2 &a2, const A3 &a3); template friend KUndo2MagicString kundo2_i18ncp(const char *ctxt, const char *sing, const char *plur, const A1 &a1); template friend KUndo2MagicString kundo2_i18ncp(const char *ctxt, const char *sing, const char *plur, const A1 &a1, const A2 &a2); template friend KUndo2MagicString kundo2_i18ncp(const char *ctxt, const char *sing, const char *plur, const A1 &a1, const A2 &a2, const A3 &a3); private: QString m_text; }; inline QDebug operator<<(QDebug dbg, const KUndo2MagicString &v) { if (v.toString() != v.toSecondaryString()) { dbg.nospace() << v.toString() << "(" << v.toSecondaryString() << ")"; } else { dbg.nospace() << v.toString(); } return dbg.space(); } /** * This is a special wrapper to a string which tells explicitly * that we don't need a translation for a given string. It is used * either in testing or internal commands, which don't go to the * stack directly. */ inline KUndo2MagicString kundo2_noi18n(const QString &text) { return KUndo2MagicString(text); } template inline KUndo2MagicString kundo2_noi18n(const char *text, const A1 &a1) { return KUndo2MagicString(QString(text).arg(a1)); } template inline KUndo2MagicString kundo2_noi18n(const char *text, const A1 &a1, const A2 &a2) { return KUndo2MagicString(QString(text).arg(a1).arg(a2)); } template inline KUndo2MagicString kundo2_noi18n(const char *text, const A1 &a1, const A2 &a2, const A3 &a3) { return KUndo2MagicString(QString(text).arg(a1).arg(a2).arg(a3)); } template inline KUndo2MagicString kundo2_noi18n(const char *text, const A1 &a1, const A2 &a2, const A3 &a3, const A4 &a4) { return KUndo2MagicString(QString(text).arg(a1).arg(a2).arg(a3).arg(a4)); } /** * Same as ki18n, but is supposed to work with strings going to * undo stack */ inline KUndo2MagicString kundo2_i18n(const char *text) { return KUndo2MagicString(i18nc("(qtundo-format)", text)); } template inline KUndo2MagicString kundo2_i18n(const char *text, const A1 &a1) { return KUndo2MagicString(i18nc("(qtundo-format)", text, a1)); } template inline KUndo2MagicString kundo2_i18n(const char *text, const A1 &a1, const A2 &a2) { return KUndo2MagicString(i18nc("(qtundo-format)", text, a1, a2)); } template inline KUndo2MagicString kundo2_i18n(const char *text, const A1 &a1, const A2 &a2, const A3 &a3) { return KUndo2MagicString(i18nc("(qtundo-format)", text, a1, a2, a3)); } template inline KUndo2MagicString kundo2_i18n(const char *text, const A1 &a1, const A2 &a2, const A3 &a3, const A4 &a4) { return KUndo2MagicString(i18nc("(qtundo-format)", text, a1, a2, a3, a4)); } inline QString prependContext(const char *ctxt) { return QString("(qtundo-format) %1").arg(ctxt); } /** * Same as ki18nc, but is supposed to work with strings going to * undo stack */ inline KUndo2MagicString kundo2_i18nc(const char *ctxt, const char *text) { return KUndo2MagicString(i18nc(prependContext(ctxt).toLatin1().data(), text)); } template inline KUndo2MagicString kundo2_i18nc(const char *ctxt, const char *text, const A1 &a1) { return KUndo2MagicString(i18nc(prependContext(ctxt).toLatin1().data(), text, a1)); } template inline KUndo2MagicString kundo2_i18nc(const char *ctxt, const char *text, const A1 &a1, const A2 &a2) { return KUndo2MagicString(i18nc(prependContext(ctxt).toLatin1().data(), text, a1, a2)); } template inline KUndo2MagicString kundo2_i18nc(const char *ctxt, const char *text, const A1 &a1, const A2 &a2, const A3 &a3) { return KUndo2MagicString(i18nc(prependContext(ctxt).toLatin1().data(), text, a1, a2, a3)); } template inline KUndo2MagicString kundo2_i18nc(const char *ctxt, const char *text, const A1 &a1, const A2 &a2, const A3 &a3, const A4 &a4) { return KUndo2MagicString(i18nc(prependContext(ctxt).toLatin1().data(), text, a1, a2, a3, a4)); } /** * Same as ki18np, but is supposed to work with strings going to * undo stack */ template inline KUndo2MagicString kundo2_i18np(const char *sing, const char *plur, const A1 &a1) { return KUndo2MagicString(i18ncp("(qtundo-format)", sing, plur, a1)); } template inline KUndo2MagicString kundo2_i18np(const char *sing, const char *plur, const A1 &a1, const A2 &a2) { return i18ncp("(qtundo-format)", sing, plur, a1, a2); } template inline KUndo2MagicString kundo2_i18np(const char *sing, const char *plur, const A1 &a1, const A2 &a2, const A3 &a3) { return i18ncp("(qtundo-format)", sing, plur, a1, a2, a3); } template inline KUndo2MagicString kundo2_i18np(const char *sing, const char *plur, const A1 &a1, const A2 &a2, const A3 &a3, const A4 &a4) { return i18ncp("(qtundo-format)", sing, plur, a1, a2, a3, a4); } /** * Same as ki18ncp, but is supposed to work with strings going to * undo stack */ template inline KUndo2MagicString kundo2_i18ncp(const char *ctxt, const char *sing, const char *plur, const A1 &a1) { return KUndo2MagicString(i18ncp(prependContext(ctxt).toLatin1().data(), sing, plur, a1)); } template inline KUndo2MagicString kundo2_i18ncp(const char *ctxt, const char *sing, const char *plur, const A1 &a1, const A2 &a2) { return i18ncp(prependContext(ctxt).toLatin1().data(), sing, plur, a1, a2); } template inline KUndo2MagicString kundo2_i18ncp(const char *ctxt, const char *sing, const char *plur, const A1 &a1, const A2 &a2, const A3 &a3) { return i18ncp(prependContext(ctxt).toLatin1().data(), sing, plur, a1, a2, a3); } template inline KUndo2MagicString kundo2_i18ncp(const char *ctxt, const char *sing, const char *plur, const A1 &a1, const A2 &a2, const A3 &a3, const A4 &a4) { return i18ncp(prependContext(ctxt).toLatin1().data(), sing, plur, a1, a2, a3, a4); } #endif /* KUNDO2MAGICSTRING_H */ diff --git a/libs/command/kundo2stack.cpp b/libs/command/kundo2stack.cpp index ca727cf168..8ec09265da 100644 --- a/libs/command/kundo2stack.cpp +++ b/libs/command/kundo2stack.cpp @@ -1,1473 +1,1473 @@ /* * Copyright (c) 2014 Dmitry Kazakov * Copyright (c) 2014 Mohit Goyal * * This library is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ /**************************************************************************** ** ** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies). ** All rights reserved. ** Contact: Nokia Corporation (qt-info@nokia.com) ** ** This file is part of the QtGui module of the Qt Toolkit. ** ** $QT_BEGIN_LICENSE:LGPL$ ** No Commercial Usage ** This file contains pre-release code and may not be distributed. ** You may use this file in accordance with the terms and conditions ** contained in the Technology Preview License Agreement accompanying ** this package. ** ** GNU Lesser General Public License Usage ** Alternatively, this file may be used under the terms of the GNU Lesser ** General Public License version 2.1 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 2.1 requirements ** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. ** ** In addition, as a special exception, Nokia gives you certain additional ** rights. These rights are described in the Nokia Qt LGPL Exception ** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. ** ** If you have questions regarding the use of this file, please contact ** Nokia at qt-info@nokia.com. ** ** ** ** ** ** ** ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include #include #include #include #include "kundo2stack.h" #include "kundo2stack_p.h" #include "kundo2group.h" #include #include #ifndef QT_NO_UNDOCOMMAND /*! \class KUndo2Command \brief The KUndo2Command class is the base class of all commands stored on a KUndo2QStack. \since 4.2 For an overview of Qt's Undo Framework, see the \l{Overview of Qt's Undo Framework}{overview document}. A KUndo2Command represents a single editing action on a document; for example, inserting or deleting a block of text in a text editor. KUndo2Command can apply a change to the document with redo() and undo the change with undo(). The implementations for these functions must be provided in a derived class. \snippet doc/src/snippets/code/src_gui_util_qundostack.cpp 0 A KUndo2Command has an associated text(). This is a short string describing what the command does. It is used to update the text properties of the stack's undo and redo actions; see KUndo2QStack::createUndoAction() and KUndo2QStack::createRedoAction(). KUndo2Command objects are owned by the stack they were pushed on. KUndo2QStack deletes a command if it has been undone and a new command is pushed. For example: \snippet doc/src/snippets/code/src_gui_util_qundostack.cpp 1 In effect, when a command is pushed, it becomes the top-most command on the stack. To support command compression, KUndo2Command has an id() and the virtual function mergeWith(). These functions are used by KUndo2QStack::push(). To support command macros, a KUndo2Command object can have any number of child commands. Undoing or redoing the parent command will cause the child commands to be undone or redone. A command can be assigned to a parent explicitly in the constructor. In this case, the command will be owned by the parent. The parent in this case is usually an empty command, in that it doesn't provide its own implementation of undo() and redo(). Instead, it uses the base implementations of these functions, which simply call undo() or redo() on all its children. The parent should, however, have a meaningful text(). \snippet doc/src/snippets/code/src_gui_util_qundostack.cpp 2 Another way to create macros is to use the convenience functions KUndo2QStack::beginMacro() and KUndo2QStack::endMacro(). \sa KUndo2QStack */ /*! Constructs a KUndo2Command object with the given \a parent and \a text. If \a parent is not 0, this command is appended to parent's child list. The parent command then owns this command and will delete it in its destructor. \sa ~KUndo2Command() */ KUndo2Command::KUndo2Command(const KUndo2MagicString &text, KUndo2Command *parent): m_hasParent(parent != 0), m_timedID(-1), m_endOfCommand(QTime::currentTime()) { d = new KUndo2CommandPrivate; if (parent != 0) { parent->d->child_list.append(this); } setText(text); setTime(); } /*! Constructs a KUndo2Command object with parent \a parent. If \a parent is not 0, this command is appended to parent's child list. The parent command then owns this command and will delete it in its destructor. \sa ~KUndo2Command() */ KUndo2Command::KUndo2Command(KUndo2Command *parent): m_hasParent(parent != 0), m_timedID(-1) { d = new KUndo2CommandPrivate; if (parent != 0) parent->d->child_list.append(this); setTime(); } /*! Destroys the KUndo2Command object and all child commands. \sa KUndo2Command() */ KUndo2Command::~KUndo2Command() { qDeleteAll(d->child_list); delete d; } /*! Returns the ID of this command. A command ID is used in command compression. It must be an integer unique to this command's class, or -1 if the command doesn't support compression. If the command supports compression this function must be overridden in the derived class to return the correct ID. The base implementation returns -1. KUndo2QStack::push() will only try to merge two commands if they have the same ID, and the ID is not -1. \sa mergeWith(), KUndo2QStack::push() */ int KUndo2Command::id() const { return -1; } /*! Attempts to merge this command with \a command. Returns true on success; otherwise returns false. If this function returns true, calling this command's redo() must have the same effect as redoing both this command and \a command. Similarly, calling this command's undo() must have the same effect as undoing \a command and this command. KUndo2QStack will only try to merge two commands if they have the same id, and the id is not -1. The default implementation returns false. \snippet doc/src/snippets/code/src_gui_util_qundostack.cpp 3 \sa id() KUndo2QStack::push() */ bool KUndo2Command::mergeWith(const KUndo2Command *command) { Q_UNUSED(command); return false; } /*! Applies a change to the document. This function must be implemented in the derived class. Calling KUndo2QStack::push(), KUndo2QStack::undo() or KUndo2QStack::redo() from this function leads to - undefined beahavior. + undefined behavior. The default implementation calls redo() on all child commands. \sa undo() */ void KUndo2Command::redo() { for (int i = 0; i < d->child_list.size(); ++i) d->child_list.at(i)->redo(); } /*! Reverts a change to the document. After undo() is called, the state of the document should be the same as before redo() was called. This function must be implemented in the derived class. Calling KUndo2QStack::push(), KUndo2QStack::undo() or KUndo2QStack::redo() from this function leads to - undefined beahavior. + undefined behavior. The default implementation calls undo() on all child commands in reverse order. \sa redo() */ void KUndo2Command::undo() { for (int i = d->child_list.size() - 1; i >= 0; --i) d->child_list.at(i)->undo(); } /*! Returns a short text string describing what this command does; for example, "insert text". The text is used when the text properties of the stack's undo and redo actions are updated. \sa setText(), KUndo2QStack::createUndoAction(), KUndo2QStack::createRedoAction() */ QString KUndo2Command::actionText() const { if(d->actionText!=0) return d->actionText; else return QString(); } /*! Returns a short text string describing what this command does; for example, "insert text". The text is used when the text properties of the stack's undo and redo actions are updated. \sa setText(), KUndo2QStack::createUndoAction(), KUndo2QStack::createRedoAction() */ KUndo2MagicString KUndo2Command::text() const { return d->text; } /*! Sets the command's text to be the \a text specified. The specified text should be a short user-readable string describing what this command does. \sa text() KUndo2QStack::createUndoAction() KUndo2QStack::createRedoAction() */ void KUndo2Command::setText(const KUndo2MagicString &undoText) { d->text = undoText; d->actionText = undoText.toSecondaryString(); } /*! \since 4.4 Returns the number of child commands in this command. \sa child() */ int KUndo2Command::childCount() const { return d->child_list.count(); } /*! \since 4.4 Returns the child command at \a index. \sa childCount(), KUndo2QStack::command() */ const KUndo2Command *KUndo2Command::child(int index) const { if (index < 0 || index >= d->child_list.count()) return 0; return d->child_list.at(index); } bool KUndo2Command::hasParent() { return m_hasParent; } int KUndo2Command::timedId() { return m_timedID; } void KUndo2Command::setTimedID(int value) { m_timedID = value; } bool KUndo2Command::timedMergeWith(KUndo2Command *other) { if(other->timedId() == this->timedId() && other->timedId()!=-1 ) m_mergeCommandsVector.append(other); else return false; return true; } void KUndo2Command::setTime() { m_timeOfCreation = QTime::currentTime(); } QTime KUndo2Command::time() { return m_timeOfCreation; } void KUndo2Command::setEndTime() { m_endOfCommand = QTime::currentTime(); } QTime KUndo2Command::endTime() { return m_endOfCommand; } void KUndo2Command::undoMergedCommands() { undo(); if (!mergeCommandsVector().isEmpty()) { QVectorIterator it(mergeCommandsVector()); it.toFront(); while (it.hasNext()) { KUndo2Command* cmd = it.next(); cmd->undoMergedCommands(); } } } void KUndo2Command::redoMergedCommands() { if (!mergeCommandsVector().isEmpty()) { QVectorIterator it(mergeCommandsVector()); it.toBack(); while (it.hasPrevious()) { KUndo2Command* cmd = it.previous(); cmd->redoMergedCommands(); } } redo(); } QVector KUndo2Command::mergeCommandsVector() { return m_mergeCommandsVector; } bool KUndo2Command::isMerged() { return !m_mergeCommandsVector.isEmpty(); } KUndo2CommandExtraData* KUndo2Command::extraData() const { return d->extraData.data(); } void KUndo2Command::setExtraData(KUndo2CommandExtraData *data) { d->extraData.reset(data); } #endif // QT_NO_UNDOCOMMAND #ifndef QT_NO_UNDOSTACK /*! \class KUndo2QStack \brief The KUndo2QStack class is a stack of KUndo2Command objects. \since 4.2 For an overview of Qt's Undo Framework, see the \l{Overview of Qt's Undo Framework}{overview document}. An undo stack maintains a stack of commands that have been applied to a document. New commands are pushed on the stack using push(). Commands can be undone and redone using undo() and redo(), or by triggering the actions returned by createUndoAction() and createRedoAction(). KUndo2QStack keeps track of the \a current command. This is the command which will be executed by the next call to redo(). The index of this command is returned by index(). The state of the edited object can be rolled forward or back using setIndex(). If the top-most command on the stack has already been redone, index() is equal to count(). KUndo2QStack provides support for undo and redo actions, command compression, command macros, and supports the concept of a \e{clean state}. \section1 Undo and Redo Actions KUndo2QStack provides convenient undo and redo QAction objects, which can be inserted into a menu or a toolbar. When commands are undone or redone, KUndo2QStack updates the text properties of these actions to reflect what change they will trigger. The actions are also disabled when no command is available for undo or redo. These actions are returned by KUndo2QStack::createUndoAction() and KUndo2QStack::createRedoAction(). \section1 Command Compression and Macros Command compression is useful when several commands can be compressed into a single command that can be undone and redone in a single operation. For example, when a user types a character in a text editor, a new command is created. This command inserts the character into the document at the cursor position. However, it is more convenient for the user to be able to undo or redo typing of whole words, sentences, or paragraphs. Command compression allows these single-character commands to be merged into a single command which inserts or deletes sections of text. For more information, see KUndo2Command::mergeWith() and push(). A command macro is a sequence of commands, all of which are undone and redone in one go. Command macros are created by giving a command a list of child commands. Undoing or redoing the parent command will cause the child commands to be undone or redone. Command macros may be created explicitly by specifying a parent in the KUndo2Command constructor, or by using the convenience functions beginMacro() and endMacro(). Although command compression and macros appear to have the same effect to the user, they often have different uses in an application. Commands that perform small changes to a document may be usefully compressed if there is no need to individually record them, and if only larger changes are relevant to the user. However, for commands that need to be recorded individually, or those that cannot be compressed, it is useful to use macros to provide a more convenient user experience while maintaining a record of each command. \section1 Clean State KUndo2QStack supports the concept of a clean state. When the document is saved to disk, the stack can be marked as clean using setClean(). Whenever the stack returns to this state through undoing and redoing commands, it emits the signal cleanChanged(). This signal is also emitted when the stack leaves the clean state. This signal is usually used to enable and disable the save actions in the application, and to update the document's title to reflect that it contains unsaved changes. \sa KUndo2Command, KUndo2View */ #ifndef QT_NO_ACTION KUndo2Action::KUndo2Action(const QString &textTemplate, const QString &defaultText, QObject *parent) : QAction(parent) , m_textTemplate(textTemplate) , m_defaultText(defaultText) { } void KUndo2Action::setPrefixedText(const QString &text) { if (text.isEmpty()) setText(m_defaultText); else setText(m_textTemplate.arg(text)); } #endif // QT_NO_ACTION /*! \internal Sets the current index to \a idx, emitting appropriate signals. If \a clean is true, makes \a idx the clean index as well. */ void KUndo2QStack::setIndex(int idx, bool clean) { bool was_clean = m_index == m_clean_index; if (m_lastMergedIndex <= idx) { m_lastMergedSetCount = idx - m_lastMergedIndex; } else { m_lastMergedSetCount = 1; m_lastMergedIndex = idx-1; } if(idx == 0){ m_lastMergedSetCount = 0; m_lastMergedIndex = 0; } if (idx != m_index) { m_index = idx; emit indexChanged(m_index); emit canUndoChanged(canUndo()); emit undoTextChanged(undoText()); emit canRedoChanged(canRedo()); emit redoTextChanged(redoText()); } if (clean) m_clean_index = m_index; bool is_clean = m_index == m_clean_index; if (is_clean != was_clean) emit cleanChanged(is_clean); } void KUndo2QStack::purgeRedoState() { bool macro = !m_macro_stack.isEmpty(); if (macro) return; bool redoStateChanged = false; bool cleanStateChanged = false; while (m_index < m_command_list.size()) { delete m_command_list.takeLast(); redoStateChanged = true; } if (m_clean_index > m_index) { m_clean_index = -1; // we've deleted the clean state cleanStateChanged = true; } if (redoStateChanged) { emit canRedoChanged(canRedo()); emit redoTextChanged(redoText()); } if (cleanStateChanged) { emit cleanChanged(isClean()); } } /*! \internal - If the number of commands on the stack exceedes the undo limit, deletes commands from + If the number of commands on the stack exceeds the undo limit, deletes commands from the bottom of the stack. Returns true if commands were deleted. */ bool KUndo2QStack::checkUndoLimit() { if (m_undo_limit <= 0 || !m_macro_stack.isEmpty() || m_undo_limit >= m_command_list.count()) return false; int del_count = m_command_list.count() - m_undo_limit; for (int i = 0; i < del_count; ++i) delete m_command_list.takeFirst(); m_index -= del_count; if (m_clean_index != -1) { if (m_clean_index < del_count) m_clean_index = -1; // we've deleted the clean command else m_clean_index -= del_count; } return true; } /*! Constructs an empty undo stack with the parent \a parent. The stack will initially be in the clean state. If \a parent is a KUndo2Group object, the stack is automatically added to the group. \sa push() */ KUndo2QStack::KUndo2QStack(QObject *parent) : QObject(parent), m_index(0), m_clean_index(0), m_group(0), m_undo_limit(0), m_useCumulativeUndoRedo(false), m_lastMergedSetCount(0), m_lastMergedIndex(0) { setTimeT1(5); setTimeT2(1); setStrokesN(2); #ifndef QT_NO_UNDOGROUP if (KUndo2Group *group = qobject_cast(parent)) group->addStack(this); #endif } /*! Destroys the undo stack, deleting any commands that are on it. If the stack is in a KUndo2Group, the stack is automatically removed from the group. \sa KUndo2QStack()The number of last strokes which Krita should store separately */ KUndo2QStack::~KUndo2QStack() { #ifndef QT_NO_UNDOGROUP if (m_group != 0) m_group->removeStack(this); #endif clear(); } /*! Clears the command stack by deleting all commands on it, and returns the stack to the clean state.{ } Commands are not undone or redone; the state of the edited object remains unchanged. This function is usually used when the contents of the document are abandoned. \sa KUndo2QStack() */ void KUndo2QStack::clear() { if (m_command_list.isEmpty()) return; bool was_clean = isClean(); m_macro_stack.clear(); qDeleteAll(m_command_list); m_command_list.clear(); m_index = 0; m_clean_index = 0; emit indexChanged(0); emit canUndoChanged(false); emit undoTextChanged(QString()); emit canRedoChanged(false); emit redoTextChanged(QString()); if (!was_clean) emit cleanChanged(true); } /*! Pushes \a cmd on the stack or merges it with the most recently executed command. In either case, executes \a cmd by calling its redo() function. If \a cmd's id is not -1, and if the id is the same as that of the most recently executed command, KUndo2QStack will attempt to merge the two commands by calling KUndo2Command::mergeWith() on the most recently executed command. If KUndo2Command::mergeWith() returns true, \a cmd is deleted. In all other cases \a cmd is simply pushed on the stack. If commands were undone before \a cmd was pushed, the current command and all commands above it are deleted. Hence \a cmd always ends up being the top-most on the stack. Once a command is pushed, the stack takes ownership of it. There are no getters to return the command, since modifying it after it has been executed will almost always lead to corruption of the document's state. \sa KUndo2Command::id() KUndo2Command::mergeWith() */ void KUndo2QStack::push(KUndo2Command *cmd) { cmd->redoMergedCommands(); cmd->setEndTime(); bool macro = !m_macro_stack.isEmpty(); KUndo2Command *cur = 0; if (macro) { KUndo2Command *macro_cmd = m_macro_stack.last(); if (!macro_cmd->d->child_list.isEmpty()) cur = macro_cmd->d->child_list.last(); } else { if (m_index > 0) cur = m_command_list.at(m_index - 1); while (m_index < m_command_list.size()) delete m_command_list.takeLast(); if (m_clean_index > m_index) m_clean_index = -1; // we've deleted the clean state } bool try_merge = cur != 0 && cur->id() != -1 && cur->id() == cmd->id() && (macro || m_index != m_clean_index); /** * Here we are going to try to merge several commands together using the * QVector field in the commands using 3 parameters. * * N : Number of commands that should remain individual at the top of the * stack. * * T1 : Time lapsed between current command and previously merged command * -- signal to merge throughout the stack. * - * T2 : Time elapsed between two commands signalling both commands belong + * T2 : Time elapsed between two commands signaling both commands belong * of the same set * * Whenever a KUndo2Command is initialized -- it consists of a start-time * and when it is pushed -- an end time. Every time a command is pushed -- * it checks whether the command pushed was pushed after T1 seconds of the * last merged command Then the merging begins with each group depending on * the time in between each command (T2). * * @TODO : Currently it is not able to merge two merged commands together. */ if (!macro && m_command_list.size() > 1 && cmd->timedId() != -1 && m_useCumulativeUndoRedo) { KUndo2Command* lastcmd = m_command_list.last(); if (qAbs(cmd->time().msecsTo(lastcmd->endTime())) < m_timeT2 * 1000) { m_lastMergedSetCount++; } else { m_lastMergedSetCount = 0; m_lastMergedIndex = m_index-1; } if (lastcmd->timedId() == -1){ m_lastMergedSetCount = 0; m_lastMergedIndex = m_index; } { /** * Implement N rule: not more than N strokes with the same timedId * should stay separate in the Undo History */ const int mergeDestinationIndex = m_lastMergedIndex; const int mergeSourceIndex = m_lastMergedIndex + 1; if (m_lastMergedSetCount > m_strokesN && mergeDestinationIndex >= 0 && mergeSourceIndex < m_command_list.size()) { KUndo2Command* mergeDestination = m_command_list.at(mergeDestinationIndex); KUndo2Command* mergeSource = m_command_list.at(mergeSourceIndex); if (mergeDestination && mergeSource) { if(mergeDestination->timedMergeWith(mergeSource)){ m_command_list.removeAt(mergeSourceIndex); } m_lastMergedSetCount--; m_lastMergedIndex = m_command_list.indexOf(mergeDestination); } } } m_index = m_command_list.size(); if (m_lastMergedIndex < m_index) { KUndo2Command* mergeDestination = m_command_list.at(m_lastMergedIndex); if (qAbs(cmd->time().msecsTo(mergeDestination->endTime())) > m_timeT1 * 1000) { //T1 time elapsed QListIterator it(m_command_list); it.toBack(); m_lastMergedSetCount = 1; while (it.hasPrevious()) { KUndo2Command* curr = it.previous(); KUndo2Command* lastCmdInCurrent = curr; if (!lastcmd->mergeCommandsVector().isEmpty()) { if (qAbs(lastcmd->mergeCommandsVector().last()->time().msecsTo(lastCmdInCurrent->endTime())) < int(m_timeT2 * 1000) && lastcmd != lastCmdInCurrent && lastcmd != curr) { if(lastcmd->timedMergeWith(curr)){ if (m_command_list.contains(curr)) { m_command_list.removeOne(curr); } } } else { lastcmd = curr; //end of a merge set } } else { if (qAbs(lastcmd->time().msecsTo(lastCmdInCurrent->endTime())) < int(m_timeT2 * 1000) && lastcmd != lastCmdInCurrent &&lastcmd!=curr) { if(lastcmd->timedMergeWith(curr)){ if (m_command_list.contains(curr)){ m_command_list.removeOne(curr); } } } else { lastcmd = curr; //end of a merge set } } } m_lastMergedIndex = m_command_list.size()-1; } } m_index = m_command_list.size(); } if (try_merge && cur->mergeWith(cmd)) { delete cmd; if (!macro) { emit indexChanged(m_index); emit canUndoChanged(canUndo()); emit undoTextChanged(undoText()); emit canRedoChanged(canRedo()); emit redoTextChanged(redoText()); } } else { if (macro) { m_macro_stack.last()->d->child_list.append(cmd); } else { m_command_list.append(cmd); if(checkUndoLimit()) { m_lastMergedIndex = m_index - m_strokesN; } setIndex(m_index + 1, false); } } } /*! Marks the stack as clean and emits cleanChanged() if the stack was not already clean. Whenever the stack returns to this state through the use of undo/redo commands, it emits the signal cleanChanged(). This signal is also emitted when the stack leaves the clean state. \sa isClean(), cleanIndex() */ void KUndo2QStack::setClean() { if (!m_macro_stack.isEmpty()) { qWarning("KUndo2QStack::setClean(): cannot set clean in the middle of a macro"); return; } setIndex(m_index, true); } /*! If the stack is in the clean state, returns true; otherwise returns false. \sa setClean() cleanIndex() */ bool KUndo2QStack::isClean() const { if (!m_macro_stack.isEmpty()) return false; return m_clean_index == m_index; } /*! Returns the clean index. This is the index at which setClean() was called. A stack may not have a clean index. This happens if a document is saved, some commands are undone, then a new command is pushed. Since push() deletes all the undone commands before pushing the new command, the stack can't return to the clean state again. In this case, this function returns -1. \sa isClean() setClean() */ int KUndo2QStack::cleanIndex() const { return m_clean_index; } /*! Undoes the command below the current command by calling KUndo2Command::undo(). Decrements the current command index. If the stack is empty, or if the bottom command on the stack has already been undone, this function does nothing. \sa redo() index() */ void KUndo2QStack::undo() { if (m_index == 0) return; if (!m_macro_stack.isEmpty()) { qWarning("KUndo2QStack::undo(): cannot undo in the middle of a macro"); return; } int idx = m_index - 1; m_command_list.at(idx)->undoMergedCommands(); setIndex(idx, false); } /*! Redoes the current command by calling KUndo2Command::redo(). Increments the current command index. If the stack is empty, or if the top command on the stack has already been redone, this function does nothing. \sa undo() index() */ void KUndo2QStack::redo() { if (m_index == m_command_list.size()) return; if (!m_macro_stack.isEmpty()) { qWarning("KUndo2QStack::redo(): cannot redo in the middle of a macro"); return; } m_command_list.at(m_index)->redoMergedCommands(); setIndex(m_index + 1, false); } /*! Returns the number of commands on the stack. Macro commands are counted as one command. \sa index() setIndex() command() */ int KUndo2QStack::count() const { return m_command_list.size(); } /*! Returns the index of the current command. This is the command that will be executed on the next call to redo(). It is not always the top-most command on the stack, since a number of commands may have been undone. \sa undo() redo() count() */ int KUndo2QStack::index() const { return m_index; } /*! Repeatedly calls undo() or redo() until the current command index reaches \a idx. This function can be used to roll the state of the document forwards of backwards. indexChanged() is emitted only once. \sa index() count() undo() redo() */ void KUndo2QStack::setIndex(int idx) { if (!m_macro_stack.isEmpty()) { qWarning("KUndo2QStack::setIndex(): cannot set index in the middle of a macro"); return; } if (idx < 0) idx = 0; else if (idx > m_command_list.size()) idx = m_command_list.size(); int i = m_index; while (i < idx) { m_command_list.at(i++)->redoMergedCommands(); notifySetIndexChangedOneCommand(); } while (i > idx) { m_command_list.at(--i)->undoMergedCommands(); notifySetIndexChangedOneCommand(); } setIndex(idx, false); } /** * Called by setIndex after every command execution. It is needed by * Krita to insert barriers between different kind of commands */ void KUndo2QStack::notifySetIndexChangedOneCommand() { } /*! Returns true if there is a command available for undo; otherwise returns false. This function returns false if the stack is empty, or if the bottom command on the stack has already been undone. Synonymous with index() == 0. \sa index() canRedo() */ bool KUndo2QStack::canUndo() const { if (!m_macro_stack.isEmpty()) return false; return m_index > 0; } /*! Returns true if there is a command available for redo; otherwise returns false. This function returns false if the stack is empty or if the top command on the stack has already been redone. Synonymous with index() == count(). \sa index() canUndo() */ bool KUndo2QStack::canRedo() const { if (!m_macro_stack.isEmpty()) return false; return m_index < m_command_list.size(); } /*! Returns the text of the command which will be undone in the next call to undo(). \sa KUndo2Command::text() redoActionText() undoItemText() */ QString KUndo2QStack::undoText() const { if (!m_macro_stack.isEmpty()) { return QString(); } if (m_index > 0 && m_command_list.count() >= m_index -1 && m_command_list.at(m_index - 1) != 0) { return m_command_list.at(m_index - 1)->actionText(); } return QString(); } /*! Returns the text of the command which will be redone in the next call to redo(). \sa KUndo2Command::text() undoActionText() redoItemText() */ QString KUndo2QStack::redoText() const { if (!m_macro_stack.isEmpty()) return QString(); if (m_index < m_command_list.size()) return m_command_list.at(m_index)->actionText(); return QString(); } #ifndef QT_NO_ACTION /*! Creates an undo QAction object with the given \a parent. Triggering this action will cause a call to undo(). The text of this action is the text of the command which will be undone in the next call to undo(), prefixed by the specified \a prefix. If there is no command available for undo, this action will be disabled. If \a prefix is empty, the default prefix "Undo" is used. \sa createRedoAction(), canUndo(), KUndo2Command::text() */ QAction *KUndo2QStack::createUndoAction(QObject *parent) const { KUndo2Action *result = new KUndo2Action(i18n("Undo %1"), i18nc("Default text for undo action", "Undo"), parent); result->setEnabled(canUndo()); result->setPrefixedText(undoText()); connect(this, SIGNAL(canUndoChanged(bool)), result, SLOT(setEnabled(bool))); connect(this, SIGNAL(undoTextChanged(QString)), result, SLOT(setPrefixedText(QString))); connect(result, SIGNAL(triggered()), this, SLOT(undo())); return result; } /*! Creates an redo QAction object with the given \a parent. Triggering this action will cause a call to redo(). The text of this action is the text of the command which will be redone in the next call to redo(), prefixed by the specified \a prefix. If there is no command available for redo, this action will be disabled. If \a prefix is empty, the default prefix "Redo" is used. \sa createUndoAction(), canRedo(), KUndo2Command::text() */ QAction *KUndo2QStack::createRedoAction(QObject *parent) const { KUndo2Action *result = new KUndo2Action(i18n("Redo %1"), i18nc("Default text for redo action", "Redo"), parent); result->setEnabled(canRedo()); result->setPrefixedText(redoText()); connect(this, SIGNAL(canRedoChanged(bool)), result, SLOT(setEnabled(bool))); connect(this, SIGNAL(redoTextChanged(QString)), result, SLOT(setPrefixedText(QString))); connect(result, SIGNAL(triggered()), this, SLOT(redo())); return result; } #endif // QT_NO_ACTION /*! Begins composition of a macro command with the given \a text description. An empty command described by the specified \a text is pushed on the stack. Any subsequent commands pushed on the stack will be appended to the empty command's children until endMacro() is called. Calls to beginMacro() and endMacro() may be nested, but every call to beginMacro() must have a matching call to endMacro(). While a macro is composed, the stack is disabled. This means that: \list \i indexChanged() and cleanChanged() are not emitted, \i canUndo() and canRedo() return false, \i calling undo() or redo() has no effect, \i the undo/redo actions are disabled. \endlist The stack becomes enabled and appropriate signals are emitted when endMacro() is called for the outermost macro. \snippet doc/src/snippets/code/src_gui_util_qundostack.cpp 4 This code is equivalent to: \snippet doc/src/snippets/code/src_gui_util_qundostack.cpp 5 \sa endMacro() */ void KUndo2QStack::beginMacro(const KUndo2MagicString &text) { KUndo2Command *cmd = new KUndo2Command(); cmd->setText(text); if (m_macro_stack.isEmpty()) { while (m_index < m_command_list.size()) delete m_command_list.takeLast(); if (m_clean_index > m_index) m_clean_index = -1; // we've deleted the clean state m_command_list.append(cmd); } else { m_macro_stack.last()->d->child_list.append(cmd); } m_macro_stack.append(cmd); if (m_macro_stack.count() == 1) { emit canUndoChanged(false); emit undoTextChanged(QString()); emit canRedoChanged(false); emit redoTextChanged(QString()); } } /*! Ends composition of a macro command. If this is the outermost macro in a set nested macros, this function emits indexChanged() once for the entire macro command. \sa beginMacro() */ void KUndo2QStack::endMacro() { if (m_macro_stack.isEmpty()) { qWarning("KUndo2QStack::endMacro(): no matching beginMacro()"); return; } m_macro_stack.removeLast(); if (m_macro_stack.isEmpty()) { checkUndoLimit(); setIndex(m_index + 1, false); } } /*! \since 4.4 Returns a const pointer to the command at \a index. This function returns a const pointer, because modifying a command, once it has been pushed onto the stack and executed, almost always causes corruption of the state of the document, if the command is later undone or redone. \sa KUndo2Command::child() */ const KUndo2Command *KUndo2QStack::command(int index) const { if (index < 0 || index >= m_command_list.count()) return 0; return m_command_list.at(index); } /*! Returns the text of the command at index \a idx. \sa beginMacro() */ QString KUndo2QStack::text(int idx) const { if (idx < 0 || idx >= m_command_list.size()) return QString(); return m_command_list.at(idx)->text().toString(); } /*! \property KUndo2QStack::undoLimit \brief the maximum number of commands on this stack. \since 4.3 When the number of commands on a stack exceedes the stack's undoLimit, commands are deleted from the bottom of the stack. Macro commands (commands with child commands) are treated as one command. The default value is 0, which means that there is no limit. This property may only be set when the undo stack is empty, since setting it on a non-empty stack might delete the command at the current index. Calling setUndoLimit() on a non-empty stack prints a warning and does nothing. */ void KUndo2QStack::setUndoLimit(int limit) { if (!m_command_list.isEmpty()) { qWarning("KUndo2QStack::setUndoLimit(): an undo limit can only be set when the stack is empty"); return; } if (limit == m_undo_limit) return; m_undo_limit = limit; checkUndoLimit(); } int KUndo2QStack::undoLimit() const { return m_undo_limit; } /*! \property KUndo2QStack::active \brief the active status of this stack. An application often has multiple undo stacks, one for each opened document. The active stack is the one associated with the currently active document. If the stack belongs to a KUndo2Group, calls to KUndo2Group::undo() or KUndo2Group::redo() will be forwarded to this stack when it is active. If the KUndo2Group is watched by a KUndo2View, the view will display the contents of this stack when it is active. If the stack does not belong to a KUndo2Group, making it active has no effect. It is the programmer's responsibility to specify which stack is active by calling setActive(), usually when the associated document window receives focus. \sa KUndo2Group */ void KUndo2QStack::setActive(bool active) { #ifdef QT_NO_UNDOGROUP Q_UNUSED(active); #else if (m_group != 0) { if (active) m_group->setActiveStack(this); else if (m_group->activeStack() == this) m_group->setActiveStack(0); } #endif } bool KUndo2QStack::isActive() const { #ifdef QT_NO_UNDOGROUP return true; #else return m_group == 0 || m_group->activeStack() == this; #endif } void KUndo2QStack::setUseCumulativeUndoRedo(bool value) { m_useCumulativeUndoRedo = value; } bool KUndo2QStack::useCumulativeUndoRedo() { return m_useCumulativeUndoRedo; } void KUndo2QStack::setTimeT1(double value) { m_timeT1 = value; } double KUndo2QStack::timeT1() { return m_timeT1; } void KUndo2QStack::setTimeT2(double value) { m_timeT2 = value; } double KUndo2QStack::timeT2() { return m_timeT2; } int KUndo2QStack::strokesN() { return m_strokesN; } void KUndo2QStack::setStrokesN(int value) { m_strokesN = value; } QAction* KUndo2Stack::createRedoAction(KActionCollection* actionCollection, const QString& actionName) { QAction* action = KUndo2QStack::createRedoAction(actionCollection); if (actionName.isEmpty()) { action->setObjectName(KStandardAction::name(KStandardAction::Redo)); } else { action->setObjectName(actionName); } action->setIcon(koIcon("edit-redo")); action->setIconText(i18n("Redo")); action->setShortcuts(KStandardShortcut::redo()); actionCollection->addAction(action->objectName(), action); return action; } QAction* KUndo2Stack::createUndoAction(KActionCollection* actionCollection, const QString& actionName) { QAction* action = KUndo2QStack::createUndoAction(actionCollection); if (actionName.isEmpty()) { action->setObjectName(KStandardAction::name(KStandardAction::Undo)); } else { action->setObjectName(actionName); } action->setIcon(koIcon("edit-undo")); action->setIconText(i18n("Undo")); action->setShortcuts(KStandardShortcut::undo()); actionCollection->addAction(action->objectName(), action); return action; } /*! \fn void KUndo2QStack::indexChanged(int idx) This signal is emitted whenever a command modifies the state of the document. This happens when a command is undone or redone. When a macro command is undone or redone, or setIndex() is called, this signal is emitted only once. \a idx specifies the index of the current command, ie. the command which will be executed on the next call to redo(). \sa index() setIndex() */ /*! \fn void KUndo2QStack::cleanChanged(bool clean) This signal is emitted whenever the stack enters or leaves the clean state. If \a clean is true, the stack is in a clean state; otherwise this signal indicates that it has left the clean state. \sa isClean() setClean() */ /*! \fn void KUndo2QStack::undoTextChanged(const QString &undoText) This signal is emitted whenever the value of undoText() changes. It is used to update the text property of the undo action returned by createUndoAction(). \a undoText specifies the new text. */ /*! \fn void KUndo2QStack::canUndoChanged(bool canUndo) This signal is emitted whenever the value of canUndo() changes. It is used to enable or disable the undo action returned by createUndoAction(). \a canUndo specifies the new value. */ /*! \fn void KUndo2QStack::redoTextChanged(const QString &redoText) This signal is emitted whenever the value of redoText() changes. It is used to update the text property of the redo action returned by createRedoAction(). \a redoText specifies the new text. */ /*! \fn void KUndo2QStack::canRedoChanged(bool canRedo) This signal is emitted whenever the value of canRedo() changes. It is used to enable or disable the redo action returned by createRedoAction(). \a canRedo specifies the new value. */ KUndo2Stack::KUndo2Stack(QObject *parent): KUndo2QStack(parent) { } #endif // QT_NO_UNDOSTACK diff --git a/libs/flake/KoCanvasController.h b/libs/flake/KoCanvasController.h index ddb6644d82..81e105fcd8 100644 --- a/libs/flake/KoCanvasController.h +++ b/libs/flake/KoCanvasController.h @@ -1,482 +1,482 @@ /* This file is part of the KDE project * Copyright (C) 2006, 2008 Thomas Zander * Copyright (C) 2007-2010 Boudewijn Rempt * Copyright (C) 2007-2008 C. Boemann * Copyright (C) 2006-2007 Jan Hambrecht * Copyright (C) 2009 Thorsten Zachmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOCANVASCONTROLLER_H #define KOCANVASCONTROLLER_H #include "kritaflake_export.h" #include #include #include #include #include class KActionCollection; class QRect; class QRectF; class KoShape; class KoCanvasBase; class KoCanvasControllerProxyObject; /** * KoCanvasController is the base class for wrappers around your canvas * that provides scrolling and zooming for your canvas. * * Flake does not provide a canvas, the application will have to * implement a canvas themselves. You canvas can be QWidget-based * or something we haven't invented yet -- as long the class that holds the canvas - * imlements KoCanvasController, tools, scrolling and zooming will work. + * implements KoCanvasController, tools, scrolling and zooming will work. * * A KoCanvasController implementation acts as a decorator around the canvas widget * and provides a way to scroll the canvas, allows the canvas to be centered * in the viewArea and manages tool activation. * *

The using application can instantiate this class and add its * canvas using the setCanvas() call. Which is designed so it can be * called multiple times if you need to exchange one canvas * widget for another, for instance, switching between a plain QWidget or a QOpenGLWidget. * *

There is _one_ KoCanvasController per canvas in your * application. * *

The canvas widget is at most as big as the viewport of the scroll * area, and when the view on the document is near its edges, smaller. * In your canvas widget code, you can find the right place in your * document in view coordinates (pixels) by adding the documentOffset */ class KRITAFLAKE_EXPORT KoCanvasController { public: // proxy QObject: use this to connect to slots and signals. QPointer proxyObject; /** * Constructor. * @param actionCollection the action collection for this canvas */ explicit KoCanvasController(KActionCollection* actionCollection); virtual ~KoCanvasController(); public: /** * Returns the current margin that is used to pad the canvas with. * This value is read from the KConfig property "canvasmargin" */ virtual int margin() const; /** * Set the new margin to pad the canvas with. */ virtual void setMargin(int margin); /** * compatibility with QAbstractScrollArea */ virtual void scrollContentsBy(int dx, int dy) = 0; /** * @return the size of the viewport */ virtual QSize viewportSize() const = 0; /** * Set the new canvas to be shown as a child * Calling this will emit canvasRemoved() if there was a canvas before, and will emit * canvasSet() with the new canvas. * @param canvas the new canvas. The KoCanvasBase::canvas() will be called to retrieve the * actual widget which will then be added as child of this one. */ virtual void setCanvas(KoCanvasBase *canvas) = 0; /** * Return the currently set canvas. The default implementation will return Null * @return the currently set canvas */ virtual KoCanvasBase *canvas() const; /** * return the amount of pixels vertically visible of the child canvas. * @return the amount of pixels vertically visible of the child canvas. */ virtual int visibleHeight() const = 0; /** * return the amount of pixels horizontally visible of the child canvas. * @return the amount of pixels horizontally visible of the child canvas. */ virtual int visibleWidth() const = 0; /** * return the amount of pixels that are not visible on the left side of the canvas. * The leftmost pixel that is shown is returned. */ virtual int canvasOffsetX() const = 0; /** * return the amount of pixels that are not visible on the top side of the canvas. * The topmost pixel that is shown is returned. */ virtual int canvasOffsetY() const = 0; /** * @brief Scrolls the content of the canvas so that the given rect is visible. * * The rect is to be specified in view coordinates (pixels). The scrollbar positions * are changed so that the centerpoint of the rectangle is centered if possible. * * @param rect the rectangle to make visible * @param smooth if true the viewport translation will make be just enough to ensure visibility, no more. * @see KoViewConverter::documentToView() */ virtual void ensureVisible(const QRectF &rect, bool smooth = false) = 0; /** * @brief Scrolls the content of the canvas so that the given shape is visible. * * This is just a wrapper function of the above function. * * @param shape the shape to make visible */ virtual void ensureVisible(KoShape *shape) = 0; /** * @brief zooms in around the center. * * The center must be specified in view coordinates (pixels). The scrollbar positions * are changed so that the center becomes center if possible. * * @param center the position to zoom in on */ virtual void zoomIn(const QPoint ¢er) = 0; /** * @brief zooms out around the center. * * The center must be specified in view coordinates (pixels). The scrollbar positions * are changed so that the center becomes center if possible. * * @param center the position to zoom out around */ virtual void zoomOut(const QPoint ¢er) = 0; /** * @brief zooms around the center. * * The center must be specified in view coordinates (pixels). The scrollbar positions * are changed so that the center becomes center if possible. * * @param center the position to zoom around * @param zoom the zoom to apply */ virtual void zoomBy(const QPoint ¢er, qreal zoom) = 0; /** * @brief zoom so that rect is exactly visible (as close as possible) * * The rect must be specified in view coordinates (pixels). The scrollbar positions * are changed so that the center of the rect becomes center if possible. * * @param rect the rect in view coordinates (pixels) that should fit the view afterwards */ virtual void zoomTo(const QRect &rect) = 0; /** * @brief repositions the scrollbars so previous center is once again center * * The previous center is cached from when the user uses the scrollbars or zoomTo * are called. zoomTo is mostly used when a zoom tool of sorts have marked an area * to zoom in on * * The success of this method is limited by the size of thing. But we try our best. */ virtual void recenterPreferred() = 0; /** * Sets the preferred center point in view coordinates (pixels). * @param viewPoint the new preferred center */ virtual void setPreferredCenter(const QPointF &viewPoint) = 0; /// Returns the currently set preferred center point in view coordinates (pixels) virtual QPointF preferredCenter() const = 0; /** * Move the canvas over the x and y distance of the parameter distance * @param distance the distance in view coordinates (pixels). A positive distance means moving the canvas up/left. */ virtual void pan(const QPoint &distance) = 0; /** * Move the canvas up. This behaves the same as \sa pan() with a positive y coordinate. */ virtual void panUp() = 0; /** * Move the canvas down. This behaves the same as \sa pan() with a negative y coordinate. */ virtual void panDown() = 0; /** * Move the canvas to the left. This behaves the same as \sa pan() with a positive x coordinate. */ virtual void panLeft() = 0; /** * Move the canvas to the right. This behaves the same as \sa pan() with a negative x coordinate. */ virtual void panRight() = 0; /** * Get the position of the scrollbar */ virtual QPoint scrollBarValue() const = 0; /** * Set the position of the scrollbar * @param value the new values of the scroll bars */ virtual void setScrollBarValue(const QPoint &value) = 0; /** * Update the range of scroll bars */ virtual void resetScrollBars() = 0; /** * Called when the size of your document in view coordinates (pixels) changes, for instance when zooming. * * @param newSize the new size, in view coordinates (pixels), of the document. * @param recalculateCenter if true the offset in the document we center on after calling * recenterPreferred() will be recalculated for the new document size so the visual offset stays the same. */ virtual void updateDocumentSize(const QSize &sz, bool recalculateCenter) = 0; /** * Set mouse wheel to zoom behaviour * @param zoom if true wheel will zoom instead of scroll, control modifier will scroll */ virtual void setZoomWithWheel(bool zoom) = 0; /** * Set scroll area to be bigger than actual document. * It allows the user to move the corner of the document * to e.g. the center of the screen * * @param factor the coefficient, defining how much we can scroll out, * measured in parts of the widget size. Null value means vast * scrolling is disabled. */ virtual void setVastScrolling(qreal factor) = 0; /** * Returns the action collection for the canvas * @returns action collection for this canvas, can be 0 */ virtual KActionCollection* actionCollection() const; QPoint documentOffset() const; /** * @return the current position of the cursor fetched from QCursor::pos() and * converted into document coordinates */ virtual QPointF currentCursorPosition() const = 0; protected: void setDocumentSize(const QSize &sz); QSize documentSize() const; void setPreferredCenterFractionX(qreal); qreal preferredCenterFractionX() const; void setPreferredCenterFractionY(qreal); qreal preferredCenterFractionY() const; void setDocumentOffset( QPoint &offset); private: class Private; Private * const d; }; /** * Workaround class for the problem that Qt does not allow two QObject base classes. * KoCanvasController can be implemented by for instance QWidgets, so it cannot be * a QObject directly. The interface of this class should be considered public interface * for KoCanvasController. */ class KRITAFLAKE_EXPORT KoCanvasControllerProxyObject : public QObject { Q_OBJECT Q_DISABLE_COPY(KoCanvasControllerProxyObject) public: explicit KoCanvasControllerProxyObject(KoCanvasController *canvasController, QObject *parent = 0); public: // Convenience methods to invoke the signals from subclasses void emitCanvasRemoved(KoCanvasController *canvasController) { emit canvasRemoved(canvasController); } void emitCanvasSet(KoCanvasController *canvasController) { emit canvasSet(canvasController); } void emitCanvasOffsetXChanged(int offset) { emit canvasOffsetXChanged(offset); } void emitCanvasOffsetYChanged(int offset) { emit canvasOffsetYChanged(offset); } void emitCanvasMousePositionChanged(const QPoint &position) { emit canvasMousePositionChanged(position); } void emitDocumentMousePositionChanged(const QPointF &position) { emit documentMousePositionChanged(position); } void emitSizeChanged(const QSize &size) { emit sizeChanged(size); } void emitMoveDocumentOffset(const QPoint &point) { emit moveDocumentOffset(point); } void emitZoomRelative(const qreal factor, const QPointF &stillPoint) { emit zoomRelative(factor, stillPoint); } // Convenience method to retrieve the canvas controller for who needs to use QPointer KoCanvasController *canvasController() const { return m_canvasController; } Q_SIGNALS: /** * Emitted when a previously added canvas is about to be removed. * @param canvasController this object */ void canvasRemoved(KoCanvasController *canvasController); /** * Emitted when a canvas is set on this widget * @param canvasController this object */ void canvasSet(KoCanvasController *canvasController); /** * Emitted when canvasOffsetX() changes * @param offset the new canvas offset */ void canvasOffsetXChanged(int offset); /** * Emitted when canvasOffsetY() changes * @param offset the new canvas offset */ void canvasOffsetYChanged(int offset); /** * Emitted when the cursor is moved over the canvas widget. * @param position the position in view coordinates (pixels). */ void canvasMousePositionChanged(const QPoint &position); /** * Emitted when the cursor is moved over the canvas widget. * @param position the position in document coordinates. * * Use \ref canvasMousePositionChanged to get the position * in view coordinates. */ void documentMousePositionChanged(const QPointF &position); /** * Emitted when the entire controller size changes * @param size the size in widget pixels. */ void sizeChanged(const QSize &size); /** * Emitted whenever the document is scrolled. * * @param point the new top-left point from which the document should * be drawn. */ void moveDocumentOffset(const QPoint &point); /** * Emitted when zoomRelativeToPoint have calculated a factor by which * the zoom should change and the point which should stand still * on screen. * Someone needs to connect to this and take action * * @param factor by how much the zoom needs to change. * @param stillPoint the point which will not change its position * in widget during the zooming. It is measured in * view coordinate system *before* zoom. */ void zoomRelative(const qreal factor, const QPointF &stillPoint); public Q_SLOTS: /** * Call this slot whenever the size of your document in view coordinates (pixels) * changes, for instance when zooming. * @param newSize the new size, in view coordinates (pixels), of the document. * @param recalculateCenter if true the offset in the document we center on after calling * recenterPreferred() will be recalculated for the new document size so the visual offset stays the same. */ void updateDocumentSize(const QSize &newSize, bool recalculateCenter = true); private: KoCanvasController *m_canvasController; }; class KRITAFLAKE_EXPORT KoDummyCanvasController : public KoCanvasController { public: explicit KoDummyCanvasController(KActionCollection* actionCollection) : KoCanvasController(actionCollection) {} ~KoDummyCanvasController() override {} void scrollContentsBy(int /*dx*/, int /*dy*/) override {} QSize viewportSize() const override { return QSize(); } void setCanvas(KoCanvasBase *canvas) override {Q_UNUSED(canvas)} KoCanvasBase *canvas() const override {return 0;} int visibleHeight() const override {return 0;} int visibleWidth() const override {return 0;} int canvasOffsetX() const override {return 0;} int canvasOffsetY() const override {return 0;} void ensureVisible(const QRectF &/*rect*/, bool /*smooth */ = false) override {} void ensureVisible(KoShape *shape) override {Q_UNUSED(shape)} void zoomIn(const QPoint &/*center*/) override {} void zoomOut(const QPoint &/*center*/) override {} void zoomBy(const QPoint &/*center*/, qreal /*zoom*/) override {} void zoomTo(const QRect &/*rect*/) override {} void recenterPreferred() override {} void setPreferredCenter(const QPointF &/*viewPoint*/) override {} QPointF preferredCenter() const override {return QPointF();} void pan(const QPoint &/*distance*/) override {} void panUp() override {} void panDown() override {} void panLeft() override {} void panRight() override {} QPoint scrollBarValue() const override {return QPoint();} void setScrollBarValue(const QPoint &/*value*/) override {} void resetScrollBars() override {} void updateDocumentSize(const QSize &/*sz*/, bool /*recalculateCenter*/) override {} void setZoomWithWheel(bool /*zoom*/) override {} void setVastScrolling(qreal /*factor*/) override {} QPointF currentCursorPosition() const override { return QPointF(); } }; #endif diff --git a/libs/flake/KoCanvasResourceProvider.h b/libs/flake/KoCanvasResourceProvider.h index 98b968d68c..d09ea01ac3 100644 --- a/libs/flake/KoCanvasResourceProvider.h +++ b/libs/flake/KoCanvasResourceProvider.h @@ -1,281 +1,281 @@ /* Copyright (c) 2006, 2011 Boudewijn Rempt (boud@valdyas.org) Copyright (C) 2007, 2009, 2010 Thomas Zander Copyright (c) 2008 Carlos Licea This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KO_CANVASRESOURCEMANAGER_H #define KO_CANVASRESOURCEMANAGER_H #include #include "kritaflake_export.h" #include "KoDerivedResourceConverter.h" #include "KoResourceUpdateMediator.h" class KoShape; class KoShapeStroke; class KoColor; class KoUnit; class QVariant; class QSizeF; /** * The KoCanvasResourceProvider contains a set of per-canvas * properties, like current foreground color, current background * color and more. All tools belonging to the current canvas are * notified when a Resource changes (is set). * * The manager can contain all sorts of variable types and there are accessors * for the most common ones. All variables are always stored inside a QVariant * instance internally and you can always just use the resource() method to get * that directly. - * The way to store arbitairy data objects that are stored as pointers you can use + * The way to store arbitrary data objects that are stored as pointers you can use * the following code snippets; * @code * QVariant variant; * variant.setValue(textShapeData->document()); * resourceManager->setResource(KoText::CurrentTextDocument, variant); * // and get it out again. * QVariant var = resourceManager->resource(KoText::CurrentTextDocument); * document = static_cast(var.value()); * @endcode */ class KRITAFLAKE_EXPORT KoCanvasResourceProvider : public QObject { Q_OBJECT public: /** * This enum holds identifiers to the resources that can be stored in here. */ enum CanvasResource { ForegroundColor, ///< The active foreground color selected for this canvas. BackgroundColor, ///< The active background color selected for this canvas. PageSize, ///< The size of the (current) page in postscript points. Unit, ///< The unit of this canvas CurrentPage, ///< The current page number ActiveStyleType, ///< the actual active style type see KoFlake::StyleType for valid values ActiveRange, ///< The area where the rulers should show white ShowTextShapeOutlines, ///< Paint of text shape outlines ? ShowFormattingCharacters, ///< Paint of formatting characters ? ShowTableBorders, ///< Paint of table borders (when not really there) ? ShowSectionBounds, ///< Paint of sections bounds ? ShowInlineObjectVisualization, ///< paint a different background for inline objects ApplicationSpeciality, ///< Special features and limitations of the application KarbonStart = 1000, ///< Base number for Karbon specific values. KexiStart = 2000, ///< Base number for Kexi specific values. FlowStart = 3000, ///< Base number for Flow specific values. PlanStart = 4000, ///< Base number for Plan specific values. StageStart = 5000, ///< Base number for Stage specific values. KritaStart = 6000, ///< Base number for Krita specific values. SheetsStart = 7000, ///< Base number for Sheets specific values. WordsStart = 8000, ///< Base number for Words specific values. KoPageAppStart = 9000 ///< Base number for KoPageApp specific values. }; enum ApplicationSpecial { NoSpecial = 0, NoAdvancedText = 1 }; /** * Constructor. * @param parent the parent QObject, used for memory management. */ explicit KoCanvasResourceProvider(QObject *parent = 0); ~KoCanvasResourceProvider() override; public Q_SLOTS: /** * Set a resource of any type. * @param key the integer key * @param value the new value for the key. * @see KoCanvasResourceProvider::CanvasResource */ void setResource(int key, const QVariant &value); /** * Set a resource of type KoColor. * @param key the integer key * @param color the new value for the key. * @see KoCanvasResourceProvider::CanvasResource */ void setResource(int key, const KoColor &color); /** * Set a resource of type KoShape*. * @param key the integer key * @param id the new value for the key. * @see KoCanvasResourceProvider::CanvasResource */ void setResource(int key, KoShape *shape); /** * Set a resource of type KoUnit * @param key the integer key * @param id the new value for the key. * @see KoCanvasResourceProvider::CanvasResource */ void setResource(int key, const KoUnit &unit); public: /** * Returns a qvariant containing the specified resource or a standard one if the * specified resource does not exist. * @param key the key * @see KoCanvasResourceProvider::CanvasResource */ QVariant resource(int key) const; /** * Set the foregroundColor resource. * @param color the new foreground color */ void setForegroundColor(const KoColor &color); /** * Return the foregroundColor */ KoColor foregroundColor() const; /** * Set the backgroundColor resource. * @param color the new background color */ void setBackgroundColor(const KoColor &color); /** * Return the backgroundColor */ KoColor backgroundColor() const; /** * Return the resource determined by param key as a boolean. * @param key the identifying key for the resource * @see KoCanvasResourceProvider::CanvasResource */ bool boolResource(int key) const; /** * Return the resource determined by param key as an integer. * @param key the identifying key for the resource * @see KoCanvasResourceProvider::CanvasResource */ int intResource(int key) const; /** * Return the resource determined by param key as a KoColor. * @param key the identifying key for the resource * @see KoCanvasResourceProvider::CanvasResource */ KoColor koColorResource(int key) const; /** * Return the resource determined by param key as a pointer to a KoShape. * @param key the identifying key for the resource * @see KoCanvasResourceProvider::CanvasResource */ KoShape *koShapeResource(int key) const; /** * Return the resource determined by param key as a QString . * @param key the identifying key for the resource * @see KoCanvasResourceProvider::CanvasResource */ QString stringResource(int key) const; /** * Return the resource determined by param key as a QSizeF. * @param key the identifying key for the resource * @see KoCanvasResourceProvider::CanvasResource */ QSizeF sizeResource(int key) const; /** * Return the resource determined by param key as a KoUnit. * @param key the identifying key for the resource * @see KoCanvasResourceProvider::CanvasResource */ KoUnit unitResource(int key) const; /** * Returns true if there is a resource set with the requested key. * @param key the identifying key for the resource * @see KoCanvasResourceProvider::CanvasResource */ bool hasResource(int key) const; /** * Remove the resource with @p key from the provider. * @param key the key that will be used to remove the resource * There will be a signal emitted with a variable that will return true on QVariable::isNull(); * @see KoCanvasResourceProvider::CanvasResource */ void clearResource(int key); /** * @see KoReosurceManager::addDerivedResourceConverter() */ void addDerivedResourceConverter(KoDerivedResourceConverterSP converter); /** * @see KoReosurceManager::hasDerivedResourceConverter() */ bool hasDerivedResourceConverter(int key); /** * @see KoReosurceManager::removeDerivedResourceConverter() */ void removeDerivedResourceConverter(int key); /** * @see KoReosurceManager::addResourceUpdateMediator */ void addResourceUpdateMediator(KoResourceUpdateMediatorSP mediator); /** * @see KoReosurceManager::hasResourceUpdateMediator */ bool hasResourceUpdateMediator(int key); /** * @see KoReosurceManager::removeResourceUpdateMediator */ void removeResourceUpdateMediator(int key); Q_SIGNALS: /** * This signal is emitted every time a resource is set that is either * new or different from the previous set value. * @param key the identifying key for the resource * @param value the variants new value. * @see KoCanvasResourceProvider::CanvasResource */ void canvasResourceChanged(int key, const QVariant &value); private: KoCanvasResourceProvider(const KoCanvasResourceProvider&); KoCanvasResourceProvider& operator=(const KoCanvasResourceProvider&); class Private; Private *const d; }; #endif diff --git a/libs/flake/KoConnectionShape.cpp b/libs/flake/KoConnectionShape.cpp index fa37c5e84c..e8b5c8003a 100644 --- a/libs/flake/KoConnectionShape.cpp +++ b/libs/flake/KoConnectionShape.cpp @@ -1,780 +1,780 @@ /* This file is part of the KDE project * Copyright (C) 2007 Boudewijn Rempt * Copyright (C) 2007,2009 Thorsten Zachmann * Copyright (C) 2007,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 "KoConnectionShape.h" #include "KoConnectionShape_p.h" #include "KoViewConverter.h" #include "KoShapeLoadingContext.h" #include "KoShapeSavingContext.h" #include "KoConnectionShapeLoadingUpdater.h" #include "KoPathShapeLoader.h" #include "KoPathPoint.h" #include "KoShapeBackground.h" #include #include #include #include #include #include KoConnectionShapePrivate::KoConnectionShapePrivate(KoConnectionShape *q) : KoParameterShapePrivate(q), shape1(0), shape2(0), connectionPointId1(-1), connectionPointId2(-1), connectionType(KoConnectionShape::Standard), forceUpdate(false), hasCustomPath(false) { } KoConnectionShapePrivate::KoConnectionShapePrivate(const KoConnectionShapePrivate &rhs, KoConnectionShape *q) : KoParameterShapePrivate(rhs, q), path(rhs.path), shape1(0), // FIXME: it should point to the new shapes!!! shape2(0), // FIXME: it should point to the new shapes!!! connectionPointId1(rhs.connectionPointId1), connectionPointId2(rhs.connectionPointId2), connectionType(rhs.connectionType), forceUpdate(rhs.forceUpdate), hasCustomPath(rhs.hasCustomPath) { } QPointF KoConnectionShapePrivate::escapeDirection(int handleId) const { Q_Q(const KoConnectionShape); QPointF direction; if (handleConnected(handleId)) { KoShape *attachedShape = handleId == KoConnectionShape::StartHandle ? shape1 : shape2; int connectionPointId = handleId == KoConnectionShape::StartHandle ? connectionPointId1 : connectionPointId2; KoConnectionPoint::EscapeDirection ed = attachedShape->connectionPoint(connectionPointId).escapeDirection; if (ed == KoConnectionPoint::AllDirections) { QPointF handlePoint = q->shapeToDocument(handles[handleId]); QPointF centerPoint = attachedShape->absolutePosition(KoFlake::Center); /* * Determine the best escape direction from the position of the handle point * and the position and orientation of the attached shape. * The idea is to define 4 sectors, one for each edge of the attached shape. * Each sector starts at the center point of the attached shape and has it * left and right edge going through the two points which define the edge. * Then we check which sector contains our handle point, for which we can * simply calculate the corresponding direction which is orthogonal to the * corresponding bounding box edge. * From that we derive the escape direction from looking at the main coordinate * of the orthogonal direction. */ // define our edge points in the right order const KoFlake::AnchorPosition corners[4] = { KoFlake::BottomRight, KoFlake::BottomLeft, KoFlake::TopLeft, KoFlake::TopRight }; QPointF vHandle = handlePoint-centerPoint; for (int i = 0; i < 4; ++i) { // first point of bounding box edge QPointF p1 = attachedShape->absolutePosition(corners[i]); // second point of bounding box edge QPointF p2 = attachedShape->absolutePosition(corners[(i+1)%4]); // check on which side of the first sector edge our second sector edge is const qreal c0 = crossProd(p1-centerPoint, p2-centerPoint); // check on which side of the first sector edge our handle point is const qreal c1 = crossProd(p1-centerPoint, vHandle); // second edge and handle point must be on the same side of first edge if ((c0 < 0 && c1 > 0) || (c0 > 0 && c1 < 0)) continue; // check on which side of the handle point our second sector edge is const qreal c2 = crossProd(vHandle, p2-centerPoint); // second edge must be on the same side of the handle point as on first edge if ((c0 < 0 && c2 > 0) || (c0 > 0 && c2 < 0)) continue; // now we found the correct edge QPointF vDir = 0.5 *(p1+p2) - centerPoint; // look at coordinate with the greatest absolute value // and construct our escape direction accordingly const qreal xabs = qAbs(vDir.x()); const qreal yabs = qAbs(vDir.y()); if (xabs > yabs) { direction.rx() = vDir.x() > 0 ? 1.0 : -1.0; direction.ry() = 0.0; } else { direction.rx() = 0.0; direction.ry() = vDir.y() > 0 ? 1.0 : -1.0; } break; } } else if (ed == KoConnectionPoint::HorizontalDirections) { QPointF handlePoint = q->shapeToDocument(handles[handleId]); QPointF centerPoint = attachedShape->absolutePosition(KoFlake::Center); // use horizontal direction pointing away from center point if (handlePoint.x() < centerPoint.x()) direction = QPointF(-1.0, 0.0); else direction = QPointF(1.0, 0.0); } else if (ed == KoConnectionPoint::VerticalDirections) { QPointF handlePoint = q->shapeToDocument(handles[handleId]); QPointF centerPoint = attachedShape->absolutePosition(KoFlake::Center); // use vertical direction pointing away from center point if (handlePoint.y() < centerPoint.y()) direction = QPointF(0.0, -1.0); else direction = QPointF(0.0, 1.0); } else if (ed == KoConnectionPoint::LeftDirection) { direction = QPointF(-1.0, 0.0); } else if (ed == KoConnectionPoint::RightDirection) { direction = QPointF(1.0, 0.0); } else if (ed == KoConnectionPoint::UpDirection) { direction = QPointF(0.0, -1.0); } else if (ed == KoConnectionPoint::DownDirection) { direction = QPointF(0.0, 1.0); } // transform escape direction by using our own transformation matrix QTransform invMatrix = q->absoluteTransformation(0).inverted(); direction = invMatrix.map(direction) - invMatrix.map(QPointF()); direction /= sqrt(direction.x() * direction.x() + direction.y() * direction.y()); } return direction; } bool KoConnectionShapePrivate::intersects(const QPointF &p1, const QPointF &d1, const QPointF &p2, const QPointF &d2, QPointF &isect) { qreal sp1 = scalarProd(d1, p2 - p1); if (sp1 < 0.0) return false; qreal sp2 = scalarProd(d2, p1 - p2); if (sp2 < 0.0) return false; // use cross product to check if rays intersects at all qreal cp = crossProd(d1, d2); if (cp == 0.0) { - // rays are parallel or coincidient + // rays are parallel or coincident if (p1.x() == p2.x() && d1.x() == 0.0 && d1.y() != d2.y()) { // vertical, coincident isect = 0.5 * (p1 + p2); } else if (p1.y() == p2.y() && d1.y() == 0.0 && d1.x() != d2.x()) { // horizontal, coincident isect = 0.5 * (p1 + p2); } else { return false; } } else { // they are intersecting normally isect = p1 + sp1 * d1; } return true; } QPointF KoConnectionShapePrivate::perpendicularDirection(const QPointF &p1, const QPointF &d1, const QPointF &p2) { QPointF perpendicular(d1.y(), -d1.x()); qreal sp = scalarProd(perpendicular, p2 - p1); if (sp < 0.0) perpendicular *= -1.0; return perpendicular; } void KoConnectionShapePrivate::normalPath(const qreal MinimumEscapeLength) { // Clear the path to build it again. path.clear(); path.append(handles[KoConnectionShape::StartHandle]); QList edges1; QList edges2; QPointF direction1 = escapeDirection(KoConnectionShape::StartHandle); QPointF direction2 = escapeDirection(KoConnectionShape::EndHandle); QPointF edgePoint1 = handles[KoConnectionShape::StartHandle] + MinimumEscapeLength * direction1; QPointF edgePoint2 = handles[KoConnectionShape::EndHandle] + MinimumEscapeLength * direction2; edges1.append(edgePoint1); edges2.prepend(edgePoint2); if (handleConnected(KoConnectionShape::StartHandle) && handleConnected(KoConnectionShape::EndHandle)) { QPointF intersection; // TODO: check if this loop actually ever exits? (DK) while (true) { // first check if directions from current edge points intersect if (intersects(edgePoint1, direction1, edgePoint2, direction2, intersection)) { // directions intersect, we have another edge point and be done edges1.append(intersection); break; } // check if we are going toward the other handle qreal sp = scalarProd(direction1, edgePoint2 - edgePoint1); if (sp >= 0.0) { // if we are having the same direction, go all the way toward // the other handle, else only go half the way if (direction1 == direction2) edgePoint1 += sp * direction1; else edgePoint1 += 0.5 * sp * direction1; edges1.append(edgePoint1); // switch direction direction1 = perpendicularDirection(edgePoint1, direction1, edgePoint2); } else { // we are not going into the same direction, so switch direction direction1 = perpendicularDirection(edgePoint1, direction1, edgePoint2); } } } path.append(edges1); path.append(edges2); path.append(handles[KoConnectionShape::EndHandle]); } qreal KoConnectionShapePrivate::scalarProd(const QPointF &v1, const QPointF &v2) const { return v1.x() * v2.x() + v1.y() * v2.y(); } qreal KoConnectionShapePrivate::crossProd(const QPointF &v1, const QPointF &v2) const { return v1.x() * v2.y() - v1.y() * v2.x(); } bool KoConnectionShapePrivate::handleConnected(int handleId) const { if (handleId == KoConnectionShape::StartHandle && shape1 && connectionPointId1 >= 0) return true; if (handleId == KoConnectionShape::EndHandle && shape2 && connectionPointId2 >= 0) return true; return false; } void KoConnectionShape::updateConnections() { Q_D(KoConnectionShape); bool updateHandles = false; if (d->handleConnected(StartHandle)) { if (d->shape1->hasConnectionPoint(d->connectionPointId1)) { // map connection point into our shape coordinates QPointF p = documentToShape(d->shape1->absoluteTransformation(0).map(d->shape1->connectionPoint(d->connectionPointId1).position)); if (d->handles[StartHandle] != p) { d->handles[StartHandle] = p; updateHandles = true; } } } if (d->handleConnected(EndHandle)) { if (d->shape2->hasConnectionPoint(d->connectionPointId2)) { // map connection point into our shape coordinates QPointF p = documentToShape(d->shape2->absoluteTransformation(0).map(d->shape2->connectionPoint(d->connectionPointId2).position)); if (d->handles[EndHandle] != p) { d->handles[EndHandle] = p; updateHandles = true; } } } if (updateHandles || d->forceUpdate) { update(); // ugly, for repainting the connection we just changed updatePath(QSizeF()); update(); // ugly, for repainting the connection we just changed d->forceUpdate = false; } } KoConnectionShape::KoConnectionShape() : KoParameterShape(new KoConnectionShapePrivate(this)) { Q_D(KoConnectionShape); d->handles.push_back(QPointF(0, 0)); d->handles.push_back(QPointF(140, 140)); moveTo(d->handles[StartHandle]); lineTo(d->handles[EndHandle]); updatePath(QSizeF(140, 140)); clearConnectionPoints(); } KoConnectionShape::KoConnectionShape(const KoConnectionShape &rhs) : KoParameterShape(new KoConnectionShapePrivate(*rhs.d_func(), this)) { } KoConnectionShape::~KoConnectionShape() { Q_D(KoConnectionShape); if (d->shape1) d->shape1->removeDependee(this); if (d->shape2) d->shape2->removeDependee(this); } KoShape *KoConnectionShape::cloneShape() const { return new KoConnectionShape(*this); } void KoConnectionShape::saveOdf(KoShapeSavingContext & context) const { Q_D(const KoConnectionShape); context.xmlWriter().startElement("draw:connector"); saveOdfAttributes(context, OdfMandatories | OdfAdditionalAttributes); switch (d->connectionType) { case Lines: context.xmlWriter().addAttribute("draw:type", "lines"); break; case Straight: context.xmlWriter().addAttribute("draw:type", "line"); break; case Curve: context.xmlWriter().addAttribute("draw:type", "curve"); break; default: context.xmlWriter().addAttribute("draw:type", "standard"); break; } if (d->shape1) { context.xmlWriter().addAttribute("draw:start-shape", context.xmlid(d->shape1, "shape", KoElementReference::Counter).toString()); context.xmlWriter().addAttribute("draw:start-glue-point", d->connectionPointId1); } else { QPointF p(shapeToDocument(d->handles[StartHandle]) * context.shapeOffset(this)); context.xmlWriter().addAttribute("svg:x1", p.x()); context.xmlWriter().addAttribute("svg:y1", p.y()); } if (d->shape2) { context.xmlWriter().addAttribute("draw:end-shape", context.xmlid(d->shape2, "shape", KoElementReference::Counter).toString()); context.xmlWriter().addAttribute("draw:end-glue-point", d->connectionPointId2); } else { QPointF p(shapeToDocument(d->handles[EndHandle]) * context.shapeOffset(this)); context.xmlWriter().addAttribute("svg:x2", p.x()); context.xmlWriter().addAttribute("svg:y2", p.y()); } // write the path data context.xmlWriter().addAttribute("svg:d", toString()); saveOdfAttributes(context, OdfViewbox); saveOdfCommonChildElements(context); saveText(context); context.xmlWriter().endElement(); } bool KoConnectionShape::loadOdf(const KoXmlElement & element, KoShapeLoadingContext &context) { Q_D(KoConnectionShape); loadOdfAttributes(element, context, OdfMandatories | OdfCommonChildElements | OdfAdditionalAttributes); QString type = element.attributeNS(KoXmlNS::draw, "type", "standard"); if (type == "lines") d->connectionType = Lines; else if (type == "line") d->connectionType = Straight; else if (type == "curve") d->connectionType = Curve; else d->connectionType = Standard; // reset connection point indices d->connectionPointId1 = -1; d->connectionPointId2 = -1; // reset connected shapes d->shape1 = 0; d->shape2 = 0; if (element.hasAttributeNS(KoXmlNS::draw, "start-shape")) { d->connectionPointId1 = element.attributeNS(KoXmlNS::draw, "start-glue-point", QString()).toInt(); QString shapeId1 = element.attributeNS(KoXmlNS::draw, "start-shape", QString()); debugFlake << "references start-shape" << shapeId1 << "at glue-point" << d->connectionPointId1; d->shape1 = context.shapeById(shapeId1); if (d->shape1) { debugFlake << "start-shape was already loaded"; d->shape1->addDependee(this); if (d->shape1->hasConnectionPoint(d->connectionPointId1)) { debugFlake << "connecting to start-shape"; d->handles[StartHandle] = d->shape1->absoluteTransformation(0).map(d->shape1->connectionPoint(d->connectionPointId1).position); debugFlake << "start handle position =" << d->handles[StartHandle]; } } else { debugFlake << "start-shape not loaded yet, deferring connection"; context.updateShape(shapeId1, new KoConnectionShapeLoadingUpdater(this, KoConnectionShapeLoadingUpdater::First)); } } else { d->handles[StartHandle].setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x1", QString()))); d->handles[StartHandle].setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y1", QString()))); } if (element.hasAttributeNS(KoXmlNS::draw, "end-shape")) { d->connectionPointId2 = element.attributeNS(KoXmlNS::draw, "end-glue-point", "").toInt(); QString shapeId2 = element.attributeNS(KoXmlNS::draw, "end-shape", ""); debugFlake << "references end-shape " << shapeId2 << "at glue-point" << d->connectionPointId2; d->shape2 = context.shapeById(shapeId2); if (d->shape2) { debugFlake << "end-shape was already loaded"; d->shape2->addDependee(this); if (d->shape2->hasConnectionPoint(d->connectionPointId2)) { debugFlake << "connecting to end-shape"; d->handles[EndHandle] = d->shape2->absoluteTransformation(0).map(d->shape2->connectionPoint(d->connectionPointId2).position); debugFlake << "end handle position =" << d->handles[EndHandle]; } } else { debugFlake << "end-shape not loaded yet, deferring connection"; context.updateShape(shapeId2, new KoConnectionShapeLoadingUpdater(this, KoConnectionShapeLoadingUpdater::Second)); } } else { d->handles[EndHandle].setX(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "x2", QString()))); d->handles[EndHandle].setY(KoUnit::parseValue(element.attributeNS(KoXmlNS::svg, "y2", QString()))); } QString skew = element.attributeNS(KoXmlNS::draw, "line-skew", QString()); QStringList skewValues = skew.simplified().split(' ', QString::SkipEmptyParts); // TODO apply skew values once we support them // load the path data if there is any d->hasCustomPath = element.hasAttributeNS(KoXmlNS::svg, "d"); if (d->hasCustomPath) { KoPathShapeLoader loader(this); loader.parseSvg(element.attributeNS(KoXmlNS::svg, "d"), true); if (d->subpaths.size() > 0) { QRectF viewBox = loadOdfViewbox(element); if (viewBox.isEmpty()) { // there should be a viewBox to transform the path data // if there is none, use the bounding rectangle of the parsed path viewBox = outline().boundingRect(); } // convert path to viewbox coordinates to have a bounding rect of (0,0 1x1) // which can later be fitted back into the target rect once we have all // the required information QTransform viewMatrix; viewMatrix.scale(viewBox.width() ? static_cast(1.0) / viewBox.width() : 1.0, viewBox.height() ? static_cast(1.0) / viewBox.height() : 1.0); viewMatrix.translate(-viewBox.left(), -viewBox.top()); d->map(viewMatrix); // trigger finishing the connections in case we have all data // otherwise it gets called again once the shapes we are // connected to are loaded } else { d->hasCustomPath = false; } finishLoadingConnection(); } else { d->forceUpdate = true; updateConnections(); } loadText(element, context); return true; } void KoConnectionShape::finishLoadingConnection() { Q_D(KoConnectionShape); if (d->hasCustomPath) { const bool loadingFinished1 = d->connectionPointId1 >= 0 ? d->shape1 != 0 : true; const bool loadingFinished2 = d->connectionPointId2 >= 0 ? d->shape2 != 0 : true; if (loadingFinished1 && loadingFinished2) { QPointF p1, p2; if (d->handleConnected(StartHandle)) { if (d->shape1->hasConnectionPoint(d->connectionPointId1)) { p1 = d->shape1->absoluteTransformation(0).map(d->shape1->connectionPoint(d->connectionPointId1).position); } } else { p1 = d->handles[StartHandle]; } if (d->handleConnected(EndHandle)) { if (d->shape2->hasConnectionPoint(d->connectionPointId2)) { p2 = d->shape2->absoluteTransformation(0).map(d->shape2->connectionPoint(d->connectionPointId2).position); } } else { p2 = d->handles[EndHandle]; } QPointF relativeBegin = d->subpaths.first()->first()->point(); QPointF relativeEnd = d->subpaths.last()->last()->point(); QPointF diffRelative(relativeBegin - relativeEnd); QPointF diffAbsolute(p1 - p2); qreal factorX = diffRelative.x() ? diffAbsolute.x() / diffRelative.x(): 1.0; qreal factorY = diffRelative.y() ? diffAbsolute.y() / diffRelative.y(): 1.0; p1.setX(p1.x() - relativeBegin.x() * factorX); p1.setY(p1.y() - relativeBegin.y() * factorY); p2.setX(p2.x() + (1 - relativeEnd.x()) * factorX); p2.setY(p2.y() + (1 - relativeEnd.y()) * factorY); QRectF targetRect = QRectF(p1, p2).normalized(); // transform the normalized coordinates back to our target rectangle QTransform viewMatrix; viewMatrix.translate(targetRect.x(), targetRect.y()); viewMatrix.scale(targetRect.width(), targetRect.height()); d->map(viewMatrix); // pretend we are during a forced update, so normalize() // will not trigger an updateConnections() call d->forceUpdate = true; normalize(); d->forceUpdate = false; } } else { updateConnections(); } } void KoConnectionShape::moveHandleAction(int handleId, const QPointF &point, Qt::KeyboardModifiers modifiers) { Q_UNUSED(modifiers); Q_D(KoConnectionShape); if (handleId >= d->handles.size()) return; d->handles[handleId] = point; } void KoConnectionShape::updatePath(const QSizeF &size) { Q_UNUSED(size); Q_D(KoConnectionShape); const qreal MinimumEscapeLength = (qreal)20.; clear(); switch (d->connectionType) { case Standard: { d->normalPath(MinimumEscapeLength); if (d->path.count() != 0){ moveTo(d->path[0]); for (int index = 1; index < d->path.count(); ++index) lineTo(d->path[index]); } break; } case Lines: { QPointF direction1 = d->escapeDirection(0); QPointF direction2 = d->escapeDirection(d->handles.count() - 1); moveTo(d->handles[StartHandle]); if (! direction1.isNull()) lineTo(d->handles[StartHandle] + MinimumEscapeLength * direction1); if (! direction2.isNull()) lineTo(d->handles[EndHandle] + MinimumEscapeLength * direction2); lineTo(d->handles[EndHandle]); break; } case Straight: moveTo(d->handles[StartHandle]); lineTo(d->handles[EndHandle]); break; case Curve: // TODO QPointF direction1 = d->escapeDirection(0); QPointF direction2 = d->escapeDirection(d->handles.count() - 1); moveTo(d->handles[StartHandle]); if (! direction1.isNull() && ! direction2.isNull()) { QPointF curvePoint1 = d->handles[StartHandle] + 5.0 * MinimumEscapeLength * direction1; QPointF curvePoint2 = d->handles[EndHandle] + 5.0 * MinimumEscapeLength * direction2; curveTo(curvePoint1, curvePoint2, d->handles[EndHandle]); } else { lineTo(d->handles[EndHandle]); } break; } normalize(); } bool KoConnectionShape::connectFirst(KoShape * shape1, int connectionPointId) { Q_D(KoConnectionShape); // refuse to connect to a shape that depends on us (e.g. a artistic text shape) if (hasDependee(shape1)) return false; if (shape1) { // check if the connection point does exist if (!shape1->hasConnectionPoint(connectionPointId)) return false; // do not connect to the same connection point twice if (d->shape2 == shape1 && d->connectionPointId2 == connectionPointId) return false; } if (d->shape1) d->shape1->removeDependee(this); d->shape1 = shape1; if (d->shape1) d->shape1->addDependee(this); d->connectionPointId1 = connectionPointId; return true; } bool KoConnectionShape::connectSecond(KoShape * shape2, int connectionPointId) { Q_D(KoConnectionShape); // refuse to connect to a shape that depends on us (e.g. a artistic text shape) if (hasDependee(shape2)) return false; if (shape2) { // check if the connection point does exist if (!shape2->hasConnectionPoint(connectionPointId)) return false; // do not connect to the same connection point twice if (d->shape1 == shape2 && d->connectionPointId1 == connectionPointId) return false; } if (d->shape2) d->shape2->removeDependee(this); d->shape2 = shape2; if (d->shape2) d->shape2->addDependee(this); d->connectionPointId2 = connectionPointId; return true; } KoShape *KoConnectionShape::firstShape() const { Q_D(const KoConnectionShape); return d->shape1; } int KoConnectionShape::firstConnectionId() const { Q_D(const KoConnectionShape); return d->connectionPointId1; } KoShape *KoConnectionShape::secondShape() const { Q_D(const KoConnectionShape); return d->shape2; } int KoConnectionShape::secondConnectionId() const { Q_D(const KoConnectionShape); return d->connectionPointId2; } KoConnectionShape::Type KoConnectionShape::type() const { Q_D(const KoConnectionShape); return d->connectionType; } void KoConnectionShape::setType(Type connectionType) { Q_D(KoConnectionShape); d->connectionType = connectionType; updatePath(size()); } void KoConnectionShape::shapeChanged(ChangeType type, KoShape *shape) { Q_D(KoConnectionShape); KoTosContainer::shapeChanged(type, shape); // check if we are during a forced update const bool updateIsActive = d->forceUpdate; switch (type) { case PositionChanged: case RotationChanged: case ShearChanged: case ScaleChanged: case GenericMatrixChange: case ParameterChanged: if (isParametricShape() && shape == 0) d->forceUpdate = true; break; case Deleted: if (shape != d->shape1 && shape != d->shape2) return; if (shape == d->shape1) connectFirst(0, -1); if (shape == d->shape2) connectSecond(0, -1); break; case ConnectionPointChanged: if (shape == d->shape1 && !shape->hasConnectionPoint(d->connectionPointId1)) { connectFirst(0, -1); } else if ( shape == d->shape2 && !shape->hasConnectionPoint(d->connectionPointId2)){ connectSecond(0, -1); } else { d->forceUpdate = true; } break; case BackgroundChanged: { // connection shape should not have a background QSharedPointer fill = background(); if (fill) { setBackground(QSharedPointer(0)); } return; } default: return; } // the connection was moved while it is connected to some other shapes const bool connectionChanged = !shape && (d->shape1 || d->shape2); // one of the connected shape has moved const bool connectedShapeChanged = shape && (shape == d->shape1 || shape == d->shape2); if (!updateIsActive && (connectionChanged || connectedShapeChanged) && isParametricShape()) updateConnections(); // reset the forced update flag d->forceUpdate = false; } QString KoConnectionShape::pathShapeId() const { return KOCONNECTIONSHAPEID; } diff --git a/libs/flake/KoDocumentResourceManager.h b/libs/flake/KoDocumentResourceManager.h index e4c70bbd1f..a68befc906 100644 --- a/libs/flake/KoDocumentResourceManager.h +++ b/libs/flake/KoDocumentResourceManager.h @@ -1,253 +1,253 @@ /* Copyright (c) 2006 Boudewijn Rempt (boud@valdyas.org) Copyright (C) 2007, 2009, 2010 Thomas Zander Copyright (c) 2008 Carlos Licea This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KO_DOCUMENTRESOURCEMANAGER_H #define KO_DOCUMENTRESOURCEMANAGER_H #include #include "kritaflake_export.h" class KoShape; class KUndo2Stack; class KoImageCollection; class KoDocumentBase; class KoShapeController; class KoColor; class KoUnit; class QVariant; class QSizeF; /** * The KoResourceManager contains a set of per-canvas or per-document * properties, like current foreground color, current background * color and more. All tools belonging to the current canvas are * notified when a Resource changes (is set). * * The properties come from the KoDocumentResourceManager::DocumentResource * See KoShapeController::resourceManager * * The manager can contain all sorts of variable types and there are accessors * for the most common ones. All variables are always stored inside a QVariant * instance internally and you can always just use the resource() method to get * that directly. - * The way to store arbitairy data objects that are stored as pointers you can use + * The way to store arbitrary data objects that are stored as pointers you can use * the following code snippets; * @code * QVariant variant; * variant.setValue(textShapeData->document()); * resourceManager->setResource(KoText::CurrentTextDocument, variant); * // and get it out again. * QVariant var = resourceManager->resource(KoText::CurrentTextDocument); * document = static_cast(var.value()); * @endcode */ class KRITAFLAKE_EXPORT KoDocumentResourceManager : public QObject { Q_OBJECT public: /** * This enum holds identifiers to the resources that can be stored in here. */ enum DocumentResource { UndoStack, ///< The document-wide undo stack (KUndo2Stack) ImageCollection, ///< The KoImageCollection for the document OdfDocument, ///< The document this canvas shows (KoDocumentBase) HandleRadius, ///< The handle radius used for drawing handles of any kind GrabSensitivity, ///< The grab sensitivity used for grabbing handles of any kind MarkerCollection, ///< The collection holding all markers ShapeController, ///< The KoShapeController for the document KarbonStart = 1000, ///< Base number for Karbon specific values. KexiStart = 2000, ///< Base number for Kexi specific values. FlowStart = 3000, ///< Base number for Flow specific values. PlanStart = 4000, ///< Base number for Plan specific values. StageStart = 5000, ///< Base number for Stage specific values. KritaStart = 6000, ///< Base number for Krita specific values. SheetsStart = 7000, ///< Base number for Sheets specific values. WordsStart = 8000, ///< Base number for Words specific values. KoPageAppStart = 9000, ///< Base number for KoPageApp specific values. KoTextStart = 10000 ///< Base number for KoText specific values. }; /** * Constructor. * @param parent the parent QObject, used for memory management. */ explicit KoDocumentResourceManager(QObject *parent = 0); ~KoDocumentResourceManager() override; /** * Set a resource of any type. * @param key the integer key * @param value the new value for the key. * @see KoDocumentResourceManager::DocumentResource */ void setResource(int key, const QVariant &value); /** * Set a resource of type KoColor. * @param key the integer key * @param color the new value for the key. * @see KoDocumentResourceManager::DocumentResource */ void setResource(int key, const KoColor &color); /** * Set a resource of type KoShape*. * @param key the integer key * @param id the new value for the key. * @see KoDocumentResourceManager::DocumentResource */ void setResource(int key, KoShape *shape); /** * Set a resource of type KoUnit * @param key the integer key * @param id the new value for the key. * @see KoDocumentResourceManager::DocumentResource */ void setResource(int key, const KoUnit &unit); /** * Returns a qvariant containing the specified resource or a standard one if the * specified resource does not exist. * @param key the key * @see KoDocumentResourceManager::DocumentResource */ QVariant resource(int key) const; /** * Return the resource determined by param key as a boolean. * @param key the identifying key for the resource * @see KoDocumentResourceManager::DocumentResource */ bool boolResource(int key) const; /** * Return the resource determined by param key as an integer. * @param key the identifying key for the resource * @see KoDocumentResourceManager::DocumentResource */ int intResource(int key) const; /** * Return the resource determined by param key as a KoColor. * @param key the identifying key for the resource * @see KoDocumentResourceManager::DocumentResource */ KoColor koColorResource(int key) const; /** * Return the resource determined by param key as a pointer to a KoShape. * @param key the identifying key for the resource * @see KoDocumentResourceManager::DocumentResource */ KoShape *koShapeResource(int key) const; /** * Return the resource determined by param key as a QString . * @param key the identifying key for the resource * @see KoDocumentResourceManager::DocumentResource */ QString stringResource(int key) const; /** * Return the resource determined by param key as a QSizeF. * @param key the identifying key for the resource * @see KoDocumentResourceManager::DocumentResource */ QSizeF sizeResource(int key) const; /** * Return the resource determined by param key as a KoUnit. * @param key the identifying key for the resource * @see KoDocumentResourceManager::DocumentResource */ KoUnit unitResource(int key) const; /** * Returns true if there is a resource set with the requested key. * @param key the identifying key for the resource * @see KoDocumentResourceManager::DocumentResource */ bool hasResource(int key) const; /** * Remove the resource with @p key from the provider. * @param key the key that will be used to remove the resource * There will be a signal emitted with a variable that will return true on QVariable::isNull(); * @see KoDocumentResourceManager::DocumentResource */ void clearResource(int key); /** * Tools that provide a handle for controlling the content that the tool can edit can * use this property to alter the radius that a circular handle should have on screen. * @param handleSize the radius in pixels. */ void setHandleRadius(int handleSize); /// Returns the actual handle radius int handleRadius() const; /** * Tools that are used to grab handles or similar with the mouse * should use this value to determine if the mouse is near enough * @param grabSensitivity the grab sensitivity in pixels */ void setGrabSensitivity(int grabSensitivity); /// Returns the actual grab sensitivity int grabSensitivity() const; KUndo2Stack *undoStack() const; void setUndoStack(KUndo2Stack *undoStack); KoImageCollection *imageCollection() const; void setImageCollection(KoImageCollection *ic); KoDocumentBase *odfDocument() const; void setOdfDocument(KoDocumentBase *currentDocument); KoShapeController *shapeController() const; void setShapeController(KoShapeController *shapeController); Q_SIGNALS: /** * This signal is emitted every time a resource is set that is either * new or different from the previous set value. * @param key the identifying key for the resource * @param value the variants new value. * @see KoDocumentResourceManager::DocumentResource */ void resourceChanged(int key, const QVariant &value); private: KoDocumentResourceManager(const KoDocumentResourceManager&); KoDocumentResourceManager& operator=(const KoDocumentResourceManager&); class Private; Private *const d; }; #endif diff --git a/libs/flake/KoDragOdfSaveHelper.h b/libs/flake/KoDragOdfSaveHelper.h index 2eb13c1a17..7d126f18d0 100644 --- a/libs/flake/KoDragOdfSaveHelper.h +++ b/libs/flake/KoDragOdfSaveHelper.h @@ -1,66 +1,66 @@ /* This file is part of the KDE project * Copyright ( C ) 2007 Thorsten Zachmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KODRAGODFSAVEHELPER_H #define KODRAGODFSAVEHELPER_H #include "kritaflake_export.h" #include class KoShapeSavingContext; class KoXmlWriter; class KoGenStyles; class KoEmbeddedDocumentSaver; class KoDragOdfSaveHelperPrivate; class KRITAFLAKE_EXPORT KoDragOdfSaveHelper { public: KoDragOdfSaveHelper(); virtual ~KoDragOdfSaveHelper(); /** * Create and return the context used for saving * - * If you need a special context for saving you can reimplent this function. + * If you need a special context for saving you can reimplement this function. * The default implementation return a KoShapeSavingContext. * * The returned context is valid as long as the KoDragOdfSaveHelper is existing */ virtual KoShapeSavingContext *context(KoXmlWriter *bodyWriter, KoGenStyles &mainStyles, KoEmbeddedDocumentSaver &embeddedSaver); /** * This method is called for writing the body of odf document. * * You need to have created a context before calling this function */ virtual bool writeBody() = 0; protected: /// constructor KoDragOdfSaveHelper(KoDragOdfSaveHelperPrivate &); KoDragOdfSaveHelperPrivate *d_ptr; private: Q_DECLARE_PRIVATE(KoDragOdfSaveHelper) }; #endif /* KODRAGODFSAVEHELPER_H */ diff --git a/libs/flake/KoFilterEffect.h b/libs/flake/KoFilterEffect.h index 2817f2861c..b4c4fd0cb0 100644 --- a/libs/flake/KoFilterEffect.h +++ b/libs/flake/KoFilterEffect.h @@ -1,170 +1,170 @@ /* This file is part of the KDE project * Copyright (c) 2009 Cyrille Berger * Copyright (c) 2009 Jan Hambrecht * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef _KO_FILTER_EFFECT_H_ #define _KO_FILTER_EFFECT_H_ class QImage; class QString; class QRectF; class KoXmlWriter; class KoFilterEffectRenderContext; class KoFilterEffectLoadingContext; #include #include "kritaflake_export.h" #include /** * This is the base for filter effect (blur, invert...) that can be applied on a shape. * All sizes and coordinates of the filter effect are stored in object bounding box * coordinates, where (0,0) refers to the top-left corner of a shapes bounding rect * and (1,1) refers to the bottom-right corner. * When loading, a transformation matrix is given to convert from user space coordinates. * Another transformation matrix is given via the render context to convert back to * user space coordinates when applying the effect. * Using object bounding box coordinates internally makes it easy to share effects * between shapes or even between users via the filter effect resources. */ class KRITAFLAKE_EXPORT KoFilterEffect { public: KoFilterEffect(const QString &id, const QString &name); virtual ~KoFilterEffect(); /// Returns the user visible name of the filter QString name() const; /// Returns the unique id of the filter QString id() const; /// Sets the region the filter is applied to in bounding box units void setFilterRect(const QRectF &filterRect); /// Returns the region this filter is applied to in bounding box units QRectF filterRect() const; /// Returns the region this filter is applied to for the given bounding rect QRectF filterRectForBoundingRect(const QRectF &boundingRect) const; /** * Sets the name of the output image * * The name is used so that other effects can reference * the output of this effect as one of their input images. * * @param output the output image name */ void setOutput(const QString &output); /// Returns the name of the output image QString output() const; /** * Returns list of named input images of this filter effect. * * These names identify the input images for this filter effect. * These can be one of the keywords SourceGraphic, SourceAlpha, * BackgroundImage, BackgroundAlpha, FillPaint or StrokePaint, * as well as a named output of another filter effect in the stack. * An empty input list of the first effect in the stack default to * SourceGraphic, whereas on subsequent effects it defaults to the * result of the previous filter effect. */ QList inputs() const; /// Adds a new input at the end of the input list void addInput(const QString &input); /// Inserts an input at the giben position in the input list void insertInput(int index, const QString &input); /// Sets an existing input to a new value void setInput(int index, const QString &input); /// Removes an input from the given position in the input list void removeInput(int index); /** * Return the required number of input images. * The default required number of input images is 1. * Derived classes should call setRequiredInputCount to set * a different number. */ int requiredInputCount() const; /** * Returns the maximal number of input images. * The default maximal number of input images is 1. * Derived classes should call setMaximalInputCount to set * a different number. */ int maximalInputCount() const; /** * Apply the effect on an image. * @param image the image the filter should be applied to * @param context the render context providing additional data */ virtual QImage processImage(const QImage &image, const KoFilterEffectRenderContext &context) const = 0; /** * Apply the effect on a list of images. * @param images the images the filter should be applied to * @param context the render context providing additional data */ virtual QImage processImages(const QList &images, const KoFilterEffectRenderContext &context) const; /** * Loads data from given xml element. * @param element the xml element to load data from * @param context the loading context providing additional data * @return true if loading was successful, else false */ virtual bool load(const KoXmlElement &element, const KoFilterEffectLoadingContext &context) = 0; /** * Writes custom data to given xml element. * @param writer the xml writer to write data to */ virtual void save(KoXmlWriter &writer) = 0; protected: /// Sets the required number of input images void setRequiredInputCount(int count); /// Sets the maximal number of input images void setMaximalInputCount(int count); /** * Saves common filter attributes * - * Saves result, subregion and input attributes. The input attrinbute + * Saves result, subregion and input attributes. The input attribute * is only saved if required, maximal and actual input count equals 1. * All other filters have to write inputs on their own. */ void saveCommonAttributes(KoXmlWriter &writer); private: class Private; Private* const d; }; #endif // _KO_FILTER_EFFECT_H_ diff --git a/libs/flake/KoGradientBackground.cpp b/libs/flake/KoGradientBackground.cpp index e6aeb1df62..3f69791c82 100644 --- a/libs/flake/KoGradientBackground.cpp +++ b/libs/flake/KoGradientBackground.cpp @@ -1,191 +1,191 @@ /* This file is part of the KDE project * Copyright (C) 2008 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 "KoGradientBackground.h" #include "KoShapeBackground_p.h" #include "KoFlake.h" #include #include #include #include #include #include #include #include #include class KoGradientBackgroundPrivate : public KoShapeBackgroundPrivate { public: KoGradientBackgroundPrivate() : gradient(0) {} QGradient *gradient; QTransform matrix; }; KoGradientBackground::KoGradientBackground(QGradient * gradient, const QTransform &matrix) : KoShapeBackground(*(new KoGradientBackgroundPrivate())) { Q_D(KoGradientBackground); d->gradient = gradient; d->matrix = matrix; Q_ASSERT(d->gradient); } KoGradientBackground::KoGradientBackground(const QGradient & gradient, const QTransform &matrix) : KoShapeBackground(*(new KoGradientBackgroundPrivate())) { Q_D(KoGradientBackground); d->gradient = KoFlake::cloneGradient(&gradient); d->matrix = matrix; Q_ASSERT(d->gradient); } KoGradientBackground::~KoGradientBackground() { Q_D(KoGradientBackground); delete d->gradient; } bool KoGradientBackground::compareTo(const KoShapeBackground *other) const { Q_D(const KoGradientBackground); const KoGradientBackground *otherGradient = dynamic_cast(other); return otherGradient && d->matrix == otherGradient->d_func()->matrix && *d->gradient == *otherGradient->d_func()->gradient; } void KoGradientBackground::setTransform(const QTransform &matrix) { Q_D(KoGradientBackground); d->matrix = matrix; } QTransform KoGradientBackground::transform() const { Q_D(const KoGradientBackground); return d->matrix; } void KoGradientBackground::setGradient(const QGradient &gradient) { Q_D(KoGradientBackground); delete d->gradient; d->gradient = KoFlake::cloneGradient(&gradient); Q_ASSERT(d->gradient); } const QGradient * KoGradientBackground::gradient() const { Q_D(const KoGradientBackground); return d->gradient; } void KoGradientBackground::paint(QPainter &painter, const KoViewConverter &/*converter*/, KoShapePaintingContext &/*context*/, const QPainterPath &fillPath) const { Q_D(const KoGradientBackground); if (!d->gradient) return; if (d->gradient->coordinateMode() == QGradient::ObjectBoundingMode) { /** * NOTE: important hack! * * Qt has different notation of QBrush::setTransform() in comparison * to what SVG defines. SVG defines gradientToUser matrix to be postmultiplied - * by QBrush::transform(), but Qt does exectly reverse! + * by QBrush::transform(), but Qt does exactly reverse! * * That most probably has beed caused by the fact that Qt uses transposed * matrices and someone just mistyped the stuff long ago :( * * So here we basically emulate this feature by converting the gradient into * QGradient::LogicalMode and doing transformations manually. */ const QRectF boundingRect = fillPath.boundingRect(); QTransform gradientToUser(boundingRect.width(), 0, 0, boundingRect.height(), boundingRect.x(), boundingRect.y()); // TODO: how about slicing the object? QGradient g = *d->gradient; g.setCoordinateMode(QGradient::LogicalMode); QBrush b(g); b.setTransform(d->matrix * gradientToUser); painter.setBrush(b); } else { QBrush b(*d->gradient); b.setTransform(d->matrix); painter.setBrush(b); } painter.drawPath(fillPath); } void KoGradientBackground::fillStyle(KoGenStyle &style, KoShapeSavingContext &context) { Q_D(KoGradientBackground); if (!d->gradient) return; QBrush brush(*d->gradient); brush.setTransform(d->matrix); KoOdfGraphicStyles::saveOdfFillStyle(style, context.mainStyles(), brush); } bool KoGradientBackground::loadStyle(KoOdfLoadingContext &context, const QSizeF &shapeSize) { Q_D(KoGradientBackground); KoStyleStack &styleStack = context.styleStack(); if (! styleStack.hasProperty(KoXmlNS::draw, "fill")) return false; QString fillStyle = styleStack.property(KoXmlNS::draw, "fill"); if (fillStyle == "gradient") { QBrush brush = KoOdfGraphicStyles::loadOdfGradientStyle(styleStack, context.stylesReader(), shapeSize); const QGradient * gradient = brush.gradient(); if (gradient) { d->gradient = KoFlake::cloneGradient(gradient); d->matrix = brush.transform(); //Gopalakrishna Bhat: If the brush has transparency then we ignore the draw:opacity property and use the brush transparency. // Brush will have transparency if the svg:linearGradient stop point has stop-opacity property otherwise it is opaque if (brush.isOpaque() && styleStack.hasProperty(KoXmlNS::draw, "opacity")) { QString opacityPercent = styleStack.property(KoXmlNS::draw, "opacity"); if (! opacityPercent.isEmpty() && opacityPercent.right(1) == "%") { float opacity = qMin(opacityPercent.left(opacityPercent.length() - 1).toDouble(), 100.0) / 100; QGradientStops stops; Q_FOREACH (QGradientStop stop, d->gradient->stops()) { stop.second.setAlphaF(opacity); stops << stop; } d->gradient->setStops(stops); } } return true; } } return false; } diff --git a/libs/flake/KoPathPoint.h b/libs/flake/KoPathPoint.h index 0f853588bd..c51af494ac 100644 --- a/libs/flake/KoPathPoint.h +++ b/libs/flake/KoPathPoint.h @@ -1,285 +1,285 @@ /* This file is part of the KDE project Copyright (C) 2006 Thorsten Zachmann Copyright (C) 2007 Thomas Zander Copyright (C) 2006-2008 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. */ #ifndef KOPATHPOINT_H #define KOPATHPOINT_H #include "kritaflake_export.h" #include class KoPathShape; class QPointF; class QTransform; class QRectF; class QPainter; class KisHandlePainterHelper; /** * @brief A KoPathPoint represents a point in a path. * * A KoPathPoint stores a point in a path. Additional to this point * 2 control points are stored. * controlPoint1 is used to describe the second point of a cubic * bezier ending at the point. controlPoint2 is used to describe the * first point of a cubic bezier curve starting at the point. */ class KRITAFLAKE_EXPORT KoPathPoint { public: /// property enum enum PointProperty { Normal = 0, ///< it has no control points StartSubpath = 1, ///< it starts a new subpath by a moveTo command StopSubpath = 2, ///< it stops a subpath (last point of subpath) CloseSubpath = 8, ///< it closes a subpath (only applicable on StartSubpath and StopSubpath) IsSmooth = 16, ///< it is smooth, both control points on a line through the point IsSymmetric = 32 ///< it is symmetric, like smooth but control points have same distance to point }; Q_DECLARE_FLAGS(PointProperties, PointProperty) /// the type for identifying part of a KoPathPoint enum PointType { None = 0, Node = 1, ///< the node point ControlPoint1 = 2, ///< the first control point ControlPoint2 = 4, ///< the second control point All = 7 }; Q_DECLARE_FLAGS(PointTypes, PointType) /// Default constructor KoPathPoint(); /** * @brief Constructor * * @param path is a pointer to the path shape this point is used in * @param point the position relative to the shape origin * @param properties describing the point */ KoPathPoint(KoPathShape *path, const QPointF &point, PointProperties properties = Normal); /** * @brief Copy Constructor */ KoPathPoint(const KoPathPoint &pathPoint); KoPathPoint(const KoPathPoint &pathPoint, KoPathShape *newParent); /** * @brief Assignment operator. */ KoPathPoint& operator=(const KoPathPoint &other); /// Compare operator bool operator == (const KoPathPoint &other) const; /** * @brief Destructor */ ~KoPathPoint(); /** * @brief return the position relative to the shape origin * * @return point */ QPointF point() const; /** * @brief get the control point 1 * * This points is used for controlling a curve ending at this point * * @return control point 1 of this point */ QPointF controlPoint1() const; /** * @brief get the second control point * * This points is used for controlling a curve starting at this point * * @return control point 2 of this point */ QPointF controlPoint2() const; /** * @brief alter the point * * @param point to set */ void setPoint(const QPointF &point); /** * @brief Set the control point 1 * * @param point to set */ void setControlPoint1(const QPointF &point); /** * @brief Set the control point 2 * * @param point to set */ void setControlPoint2(const QPointF &point); /// Removes the first control point void removeControlPoint1(); /// Removes the second control point void removeControlPoint2(); /** * @brief Get the properties of a point * * @return properties of the point */ PointProperties properties() const; /** * @brief Set the properties of a point * @param properties the new properties */ void setProperties(PointProperties properties); /** * @brief Sets a single property of a point. * @param property the property to set */ void setProperty(PointProperty property); /** * @brief Removes a property from the point. * @param property the property to remove */ void unsetProperty(PointProperty property); /** * @brief Checks if there is a controlPoint1 * * The control point is active if a control point was set by * calling setControlPoint1. However a start point of a subpath * (StartSubpath) can only have an active control 1 if the * subpath is closed (CloseSubpath on first and last point). * * @return true if control point is active, false otherwise */ bool activeControlPoint1() const; /** * @brief Checks if there is a controlPoint2 * * The control point is active if a control point was set by * calling setControlPoint2. However a end point of a subpath * (StopSubpath) can only have an active control point 2 if there * subpath is closed (CloseSubpath on first and last point). * * @return true if control point is active, false otherwise */ bool activeControlPoint2() const; /** * @brief apply matrix on the point * * This does a matrix multiplication on all points of the point * * @param matrix which will be applied to all points */ void map(const QTransform &matrix); /** * Paints the path point with the actual brush and pen * @param painter used for painting the shape point * @param handleRadius size of point handles in pixel * @param types the points which should be painted * @param active If true only the given active points are painted * If false all given points are used. */ void paint(KisHandlePainterHelper &handlesHelper, PointTypes types, bool active = true); /** * @brief Sets the parent path shape. * @param parent the new parent path shape */ void setParent(KoPathShape* parent); /** * @brief Get the path shape the point belongs to * @return the path shape the point belongs to */ KoPathShape *parent() const; /** * @brief Get the bounding rect of the point. * * This takes into account if there are controlpoints * * @param active If true only the active points are used in calculation * of the bounding rectangle. If false all points are used. * * @return bounding rect in document coordinates */ QRectF boundingRect(bool active = true) const; /** * @brief Reverses the path point. * * The control points are swapped and the point properties are adjusted. * The position dependent properties like StartSubpath and CloseSubpath * are not changed. */ void reverse(); /** * Returns if this point is a smooth join of adjacent path segments. * - * The smoothess is defined by the parallelness of the tangents emanating + * The smoothness is defined by the parallelness of the tangents emanating * from the knot point, i.e. the normalized vectors from the knot to the * first and second control point. * The previous and next path points are used to determine the smoothness * in case this path point has not two control points. * * @param previous the previous path point * @param next the next path point */ bool isSmooth(KoPathPoint *previous, KoPathPoint *next) const; protected: friend class KoPathShapePrivate; private: class Private; Private * const d; }; // /// a KoSubpath contains a path from a moveTo until a close or a new moveTo // typedef QList KoSubpath; // typedef QList KoSubpathList; // /// A KoPathSegment is a pair two neighboring KoPathPoints // typedef QPair KoPathSegment; // /// The position of a path point within a path shape // typedef QPair KoPointPosition; Q_DECLARE_OPERATORS_FOR_FLAGS(KoPathPoint::PointProperties) Q_DECLARE_OPERATORS_FOR_FLAGS(KoPathPoint::PointTypes) #endif diff --git a/libs/flake/KoPathSegment.cpp b/libs/flake/KoPathSegment.cpp index 4a2f112cc4..88979faebe 100644 --- a/libs/flake/KoPathSegment.cpp +++ b/libs/flake/KoPathSegment.cpp @@ -1,1479 +1,1479 @@ /* This file is part of the KDE project * Copyright (C) 2008-2009 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 "KoPathSegment.h" #include "KoPathPoint.h" #include #include #include #include #include "kis_global.h" /// Maximal recursion depth for finding root params const int MaxRecursionDepth = 64; /// Flatness tolerance for finding root params const qreal FlatnessTolerance = ldexp(1.0,-MaxRecursionDepth-1); class BezierSegment { public: BezierSegment(int degree = 0, QPointF *p = 0) { if (degree) { for (int i = 0; i <= degree; ++i) points.append(p[i]); } } void setDegree(int degree) { points.clear(); if (degree) { for (int i = 0; i <= degree; ++i) points.append(QPointF()); } } int degree() const { return points.count() - 1; } QPointF point(int index) const { if (static_cast(index) > degree()) return QPointF(); return points[index]; } void setPoint(int index, const QPointF &p) { if (static_cast(index) > degree()) return; points[index] = p; } QPointF evaluate(qreal t, BezierSegment *left, BezierSegment *right) const { int deg = degree(); if (! deg) return QPointF(); QVector > Vtemp(deg + 1); for (int i = 0; i <= deg; ++i) Vtemp[i].resize(deg + 1); /* Copy control points */ for (int j = 0; j <= deg; j++) { Vtemp[0][j] = points[j]; } /* Triangle computation */ for (int i = 1; i <= deg; i++) { for (int j =0 ; j <= deg - i; j++) { Vtemp[i][j].rx() = (1.0 - t) * Vtemp[i-1][j].x() + t * Vtemp[i-1][j+1].x(); Vtemp[i][j].ry() = (1.0 - t) * Vtemp[i-1][j].y() + t * Vtemp[i-1][j+1].y(); } } if (left) { left->setDegree(deg); for (int j = 0; j <= deg; j++) { left->setPoint(j, Vtemp[j][0]); } } if (right) { right->setDegree(deg); for (int j = 0; j <= deg; j++) { right->setPoint(j, Vtemp[deg-j][j]); } } return (Vtemp[deg][0]); } QList roots(int depth = 0) const { QList rootParams; if (! degree()) return rootParams; // Calculate how often the control polygon crosses the x-axis // This is the upper limit for the number of roots. int xAxisCrossings = controlPolygonZeros(points); if (! xAxisCrossings) { // No solutions. return rootParams; } else if (xAxisCrossings == 1) { // Exactly one solution. // Stop recursion when the tree is deep enough if (depth >= MaxRecursionDepth) { // if deep enough, return 1 solution at midpoint rootParams.append((points.first().x() + points.last().x()) / 2.0); return rootParams; } else if (isFlat(FlatnessTolerance)) { // Calculate intersection of chord with x-axis. QPointF chord = points.last() - points.first(); QPointF segStart = points.first(); rootParams.append((chord.x() * segStart.y() - chord.y() * segStart.x()) / - chord.y()); return rootParams; } } // Many solutions. Do recursive midpoint subdivision. BezierSegment left, right; evaluate(0.5, &left, &right); rootParams += left.roots(depth+1); rootParams += right.roots(depth+1); return rootParams; } static uint controlPolygonZeros(const QList &controlPoints) { int controlPointCount = controlPoints.count(); if (controlPointCount < 2) return 0; int signChanges = 0; int currSign = controlPoints[0].y() < 0.0 ? -1 : 1; int oldSign; for (short i = 1; i < controlPointCount; ++i) { oldSign = currSign; currSign = controlPoints[i].y() < 0.0 ? -1 : 1; if (currSign != oldSign) { ++signChanges; } } return signChanges; } int isFlat (qreal tolerance) const { int deg = degree(); // Find the perpendicular distance from each interior control point to // the line connecting points[0] and points[degree] qreal *distance = new qreal[deg + 1]; // Derive the implicit equation for line connecting first and last control points qreal a = points[0].y() - points[deg].y(); qreal b = points[deg].x() - points[0].x(); qreal c = points[0].x() * points[deg].y() - points[deg].x() * points[0].y(); qreal abSquared = (a * a) + (b * b); for (int i = 1; i < deg; i++) { // Compute distance from each of the points to that line distance[i] = a * points[i].x() + b * points[i].y() + c; if (distance[i] > 0.0) { distance[i] = (distance[i] * distance[i]) / abSquared; } if (distance[i] < 0.0) { distance[i] = -((distance[i] * distance[i]) / abSquared); } } // Find the largest distance qreal max_distance_above = 0.0; qreal max_distance_below = 0.0; for (int i = 1; i < deg; i++) { if (distance[i] < 0.0) { max_distance_below = qMin(max_distance_below, distance[i]); } if (distance[i] > 0.0) { max_distance_above = qMax(max_distance_above, distance[i]); } } delete [] distance; // Implicit equation for zero line qreal a1 = 0.0; qreal b1 = 1.0; qreal c1 = 0.0; // Implicit equation for "above" line qreal a2 = a; qreal b2 = b; qreal c2 = c + max_distance_above; qreal det = a1 * b2 - a2 * b1; qreal dInv = 1.0/det; qreal intercept_1 = (b1 * c2 - b2 * c1) * dInv; // Implicit equation for "below" line a2 = a; b2 = b; c2 = c + max_distance_below; det = a1 * b2 - a2 * b1; dInv = 1.0/det; qreal intercept_2 = (b1 * c2 - b2 * c1) * dInv; // Compute intercepts of bounding box qreal left_intercept = qMin(intercept_1, intercept_2); qreal right_intercept = qMax(intercept_1, intercept_2); qreal error = 0.5 * (right_intercept-left_intercept); return (error < tolerance); } #ifndef NDEBUG void printDebug() const { int index = 0; foreach (const QPointF &p, points) { debugFlake << QString("P%1 ").arg(index++) << p; } } #endif private: QList points; }; class Q_DECL_HIDDEN KoPathSegment::Private { public: Private(KoPathSegment *qq, KoPathPoint *p1, KoPathPoint *p2) : first(p1), second(p2), q(qq) { } /// calculates signed distance of given point from segment chord qreal distanceFromChord(const QPointF &point) const; /// Returns the chord length, i.e. the distance between first and last control point qreal chordLength() const; /// Returns intersection of lines if one exists QList linesIntersection(const KoPathSegment &segment) const; /// Returns parameters for curve extrema QList extrema() const; /// Returns parameters for curve roots QList roots() const; /** * The DeCasteljau algorithm for parameter t. * @param t the parameter to evaluate at * @param p1 the new control point of the segment start (for cubic curves only) * @param p2 the first control point at t * @param p3 the new point at t * @param p4 the second control point at t * @param p3 the new control point of the segment end (for cubic curbes only) */ void deCasteljau(qreal t, QPointF *p1, QPointF *p2, QPointF *p3, QPointF *p4, QPointF *p5) const; KoPathPoint *first; KoPathPoint *second; KoPathSegment *q; }; void KoPathSegment::Private::deCasteljau(qreal t, QPointF *p1, QPointF *p2, QPointF *p3, QPointF *p4, QPointF *p5) const { if (!q->isValid()) return; int deg = q->degree(); QPointF q[4]; q[0] = first->point(); if (deg == 2) { q[1] = first->activeControlPoint2() ? first->controlPoint2() : second->controlPoint1(); } else if (deg == 3) { q[1] = first->controlPoint2(); q[2] = second->controlPoint1(); } q[deg] = second->point(); // points of the new segment after the split point QPointF p[3]; // the De Casteljau algorithm for (unsigned short j = 1; j <= deg; ++j) { for (unsigned short i = 0; i <= deg - j; ++i) { q[i] = (1.0 - t) * q[i] + t * q[i + 1]; } p[j - 1] = q[0]; } if (deg == 2) { if (p2) *p2 = p[0]; if (p3) *p3 = p[1]; if (p4) *p4 = q[1]; } else if (deg == 3) { if (p1) *p1 = p[0]; if (p2) *p2 = p[1]; if (p3) *p3 = p[2]; if (p4) *p4 = q[1]; if (p5) *p5 = q[2]; } } QList KoPathSegment::Private::roots() const { QList rootParams; if (!q->isValid()) return rootParams; // Calculate how often the control polygon crosses the x-axis // This is the upper limit for the number of roots. int xAxisCrossings = BezierSegment::controlPolygonZeros(q->controlPoints()); if (!xAxisCrossings) { // No solutions. } else if (xAxisCrossings == 1 && q->isFlat(0.01 / chordLength())) { // Exactly one solution. // Calculate intersection of chord with x-axis. QPointF chord = second->point() - first->point(); QPointF segStart = first->point(); rootParams.append((chord.x() * segStart.y() - chord.y() * segStart.x()) / - chord.y()); } else { // Many solutions. Do recursive midpoint subdivision. QPair splitSegments = q->splitAt(0.5); rootParams += splitSegments.first.d->roots(); rootParams += splitSegments.second.d->roots(); } return rootParams; } QList KoPathSegment::Private::extrema() const { int deg = q->degree(); if (deg <= 1) return QList(); QList params; /* * The basic idea for calculating the extrama for bezier segments * was found in comp.graphics.algorithms: * * Both the x coordinate and the y coordinate are polynomial. Newton told * us that at a maximum or minimum the derivative will be zero. * * We have a helpful trick for the derivatives: use the curve r(t) defined by * differences of successive control points. * Setting r(t) to zero and using the x and y coordinates of differences of * successive control points lets us find the parameters t, where the original * bezier curve has a minimum or a maximum. */ if (deg == 2) { /* * For quadratic bezier curves r(t) is a linear Bezier curve: * * 1 * r(t) = Sum Bi,1(t) *Pi = B0,1(t) * P0 + B1,1(t) * P1 * i=0 * * r(t) = (1-t) * P0 + t * P1 * * r(t) = (P1 - P0) * t + P0 */ // calculating the differences between successive control points QPointF cp = first->activeControlPoint2() ? first->controlPoint2() : second->controlPoint1(); QPointF x0 = cp - first->point(); QPointF x1 = second->point() - cp; // calculating the coefficients QPointF a = x1 - x0; QPointF c = x0; if (a.x() != 0.0) params.append(-c.x() / a.x()); if (a.y() != 0.0) params.append(-c.y() / a.y()); } else if (deg == 3) { /* * For cubic bezier curves r(t) is a quadratic Bezier curve: * * 2 * r(t) = Sum Bi,2(t) *Pi = B0,2(t) * P0 + B1,2(t) * P1 + B2,2(t) * P2 * i=0 * * r(t) = (1-t)^2 * P0 + 2t(1-t) * P1 + t^2 * P2 * * r(t) = (P2 - 2*P1 + P0) * t^2 + (2*P1 - 2*P0) * t + P0 * */ // calculating the differences between successive control points QPointF x0 = first->controlPoint2() - first->point(); QPointF x1 = second->controlPoint1() - first->controlPoint2(); QPointF x2 = second->point() - second->controlPoint1(); // calculating the coefficients QPointF a = x2 - 2.0 * x1 + x0; QPointF b = 2.0 * x1 - 2.0 * x0; QPointF c = x0; // calculating parameter t at minimum/maximum in x-direction if (a.x() == 0.0) { params.append(- c.x() / b.x()); } else { qreal rx = b.x() * b.x() - 4.0 * a.x() * c.x(); if (rx < 0.0) rx = 0.0; params.append((-b.x() + sqrt(rx)) / (2.0*a.x())); params.append((-b.x() - sqrt(rx)) / (2.0*a.x())); } // calculating parameter t at minimum/maximum in y-direction if (a.y() == 0.0) { params.append(- c.y() / b.y()); } else { qreal ry = b.y() * b.y() - 4.0 * a.y() * c.y(); if (ry < 0.0) ry = 0.0; params.append((-b.y() + sqrt(ry)) / (2.0*a.y())); params.append((-b.y() - sqrt(ry)) / (2.0*a.y())); } } return params; } qreal KoPathSegment::Private::distanceFromChord(const QPointF &point) const { // the segments chord QPointF chord = second->point() - first->point(); // the point relative to the segment QPointF relPoint = point - first->point(); // project point to chord qreal scale = chord.x() * relPoint.x() + chord.y() * relPoint.y(); scale /= chord.x() * chord.x() + chord.y() * chord.y(); // the vector form the point to the projected point on the chord QPointF diffVec = scale * chord - relPoint; // the unsigned distance of the point to the chord qreal distance = sqrt(diffVec.x() * diffVec.x() + diffVec.y() * diffVec.y()); // determine sign of the distance using the cross product if (chord.x()*relPoint.y() - chord.y()*relPoint.x() > 0) { return distance; } else { return -distance; } } qreal KoPathSegment::Private::chordLength() const { QPointF chord = second->point() - first->point(); return sqrt(chord.x() * chord.x() + chord.y() * chord.y()); } QList KoPathSegment::Private::linesIntersection(const KoPathSegment &segment) const { //debugFlake << "intersecting two lines"; /* we have to line segments: s1 = A + r * (B-A), s2 = C + s * (D-C) for r,s in [0,1] if s1 and s2 intersect we set s1 = s2 so we get these two equations: Ax + r * (Bx-Ax) = Cx + s * (Dx-Cx) Ay + r * (By-Ay) = Cy + s * (Dy-Cy) which we can solve to get r and s */ QList isects; QPointF A = first->point(); QPointF B = second->point(); QPointF C = segment.first()->point(); QPointF D = segment.second()->point(); qreal denom = (B.x() - A.x()) * (D.y() - C.y()) - (B.y() - A.y()) * (D.x() - C.x()); qreal num_r = (A.y() - C.y()) * (D.x() - C.x()) - (A.x() - C.x()) * (D.y() - C.y()); // check if lines are collinear if (denom == 0.0 && num_r == 0.0) return isects; qreal num_s = (A.y() - C.y()) * (B.x() - A.x()) - (A.x() - C.x()) * (B.y() - A.y()); qreal r = num_r / denom; qreal s = num_s / denom; // check if intersection is inside our line segments if (r < 0.0 || r > 1.0) return isects; if (s < 0.0 || s > 1.0) return isects; // calculate the actual intersection point isects.append(A + r * (B - A)); return isects; } /////////////////// KoPathSegment::KoPathSegment(KoPathPoint * first, KoPathPoint * second) : d(new Private(this, first, second)) { } KoPathSegment::KoPathSegment(const KoPathSegment & segment) : d(new Private(this, 0, 0)) { if (! segment.first() || segment.first()->parent()) setFirst(segment.first()); else setFirst(new KoPathPoint(*segment.first())); if (! segment.second() || segment.second()->parent()) setSecond(segment.second()); else setSecond(new KoPathPoint(*segment.second())); } KoPathSegment::KoPathSegment(const QPointF &p0, const QPointF &p1) : d(new Private(this, new KoPathPoint(), new KoPathPoint())) { d->first->setPoint(p0); d->second->setPoint(p1); } KoPathSegment::KoPathSegment(const QPointF &p0, const QPointF &p1, const QPointF &p2) : d(new Private(this, new KoPathPoint(), new KoPathPoint())) { d->first->setPoint(p0); d->first->setControlPoint2(p1); d->second->setPoint(p2); } KoPathSegment::KoPathSegment(const QPointF &p0, const QPointF &p1, const QPointF &p2, const QPointF &p3) : d(new Private(this, new KoPathPoint(), new KoPathPoint())) { d->first->setPoint(p0); d->first->setControlPoint2(p1); d->second->setControlPoint1(p2); d->second->setPoint(p3); } KoPathSegment &KoPathSegment::operator=(const KoPathSegment &rhs) { if (this == &rhs) return (*this); if (! rhs.first() || rhs.first()->parent()) setFirst(rhs.first()); else setFirst(new KoPathPoint(*rhs.first())); if (! rhs.second() || rhs.second()->parent()) setSecond(rhs.second()); else setSecond(new KoPathPoint(*rhs.second())); return (*this); } KoPathSegment::~KoPathSegment() { if (d->first && ! d->first->parent()) delete d->first; if (d->second && ! d->second->parent()) delete d->second; delete d; } KoPathPoint *KoPathSegment::first() const { return d->first; } void KoPathSegment::setFirst(KoPathPoint *first) { if (d->first && !d->first->parent()) delete d->first; d->first = first; } KoPathPoint *KoPathSegment::second() const { return d->second; } void KoPathSegment::setSecond(KoPathPoint *second) { if (d->second && !d->second->parent()) delete d->second; d->second = second; } bool KoPathSegment::isValid() const { return (d->first && d->second); } bool KoPathSegment::operator==(const KoPathSegment &rhs) const { if (!isValid() && !rhs.isValid()) return true; if (isValid() && !rhs.isValid()) return false; if (!isValid() && rhs.isValid()) return false; return (*first() == *rhs.first() && *second() == *rhs.second()); } int KoPathSegment::degree() const { if (!d->first || !d->second) return -1; bool c1 = d->first->activeControlPoint2(); bool c2 = d->second->activeControlPoint1(); if (!c1 && !c2) return 1; if (c1 && c2) return 3; return 2; } QPointF KoPathSegment::pointAt(qreal t) const { if (!isValid()) return QPointF(); if (degree() == 1) { return d->first->point() + t * (d->second->point() - d->first->point()); } else { QPointF splitP; d->deCasteljau(t, 0, 0, &splitP, 0, 0); return splitP; } } QRectF KoPathSegment::controlPointRect() const { if (!isValid()) return QRectF(); QList points = controlPoints(); QRectF bbox(points.first(), points.first()); Q_FOREACH (const QPointF &p, points) { bbox.setLeft(qMin(bbox.left(), p.x())); bbox.setRight(qMax(bbox.right(), p.x())); bbox.setTop(qMin(bbox.top(), p.y())); bbox.setBottom(qMax(bbox.bottom(), p.y())); } if (degree() == 1) { // adjust bounding rect of horizontal and vertical lines if (bbox.height() == 0.0) bbox.setHeight(0.1); if (bbox.width() == 0.0) bbox.setWidth(0.1); } return bbox; } QRectF KoPathSegment::boundingRect() const { if (!isValid()) return QRectF(); QRectF rect = QRectF(d->first->point(), d->second->point()).normalized(); if (degree() == 1) { // adjust bounding rect of horizontal and vertical lines if (rect.height() == 0.0) rect.setHeight(0.1); if (rect.width() == 0.0) rect.setWidth(0.1); } else { /* * The basic idea for calculating the axis aligned bounding box (AABB) for bezier segments * was found in comp.graphics.algorithms: * Use the points at the extrema of the curve to calculate the AABB. */ foreach (qreal t, d->extrema()) { if (t >= 0.0 && t <= 1.0) { QPointF p = pointAt(t); rect.setLeft(qMin(rect.left(), p.x())); rect.setRight(qMax(rect.right(), p.x())); rect.setTop(qMin(rect.top(), p.y())); rect.setBottom(qMax(rect.bottom(), p.y())); } } } return rect; } QList KoPathSegment::intersections(const KoPathSegment &segment) const { // this function uses a technique known as bezier clipping to find the // intersections of the two bezier curves QList isects; if (!isValid() || !segment.isValid()) return isects; int degree1 = degree(); int degree2 = segment.degree(); QRectF myBound = boundingRect(); QRectF otherBound = segment.boundingRect(); //debugFlake << "my boundingRect =" << myBound; //debugFlake << "other boundingRect =" << otherBound; if (!myBound.intersects(otherBound)) { //debugFlake << "segments do not intersect"; return isects; } // short circuit lines intersection if (degree1 == 1 && degree2 == 1) { //debugFlake << "intersecting two lines"; isects += d->linesIntersection(segment); return isects; } // first calculate the fat line L by using the signed distances // of the control points from the chord qreal dmin, dmax; if (degree1 == 1) { dmin = 0.0; dmax = 0.0; } else if (degree1 == 2) { qreal d1; if (d->first->activeControlPoint2()) d1 = d->distanceFromChord(d->first->controlPoint2()); else d1 = d->distanceFromChord(d->second->controlPoint1()); dmin = qMin(qreal(0.0), qreal(0.5 * d1)); dmax = qMax(qreal(0.0), qreal(0.5 * d1)); } else { qreal d1 = d->distanceFromChord(d->first->controlPoint2()); qreal d2 = d->distanceFromChord(d->second->controlPoint1()); if (d1*d2 > 0.0) { dmin = 0.75 * qMin(qreal(0.0), qMin(d1, d2)); dmax = 0.75 * qMax(qreal(0.0), qMax(d1, d2)); } else { dmin = 4.0 / 9.0 * qMin(qreal(0.0), qMin(d1, d2)); dmax = 4.0 / 9.0 * qMax(qreal(0.0), qMax(d1, d2)); } } //debugFlake << "using fat line: dmax =" << dmax << " dmin =" << dmin; /* the other segment is given as a bezier curve of the form: (1) P(t) = sum_i P_i * B_{n,i}(t) our chord line is of the form: (2) ax + by + c = 0 we can determine the distance d(t) from any point P(t) to our chord by substituting formula (1) into formula (2): d(t) = sum_i d_i B_{n,i}(t), where d_i = a*x_i + b*y_i + c which forms another explicit bezier curve D(t) = (t,d(t)) = sum_i D_i B_{n,i}(t) now values of t for which P(t) lies outside of our fat line L - corrsponds to values of t for which D(t) lies above d = dmax or + corresponds to values of t for which D(t) lies above d = dmax or below d = dmin we can determine parameter ranges of t for which P(t) is guaranteed to lie outside of L by identifying ranges of t which the convex hull of D(t) lies above dmax or below dmin */ // now calculate the control points of D(t) by using the signed // distances of P_i to our chord KoPathSegment dt; if (degree2 == 1) { QPointF p0(0.0, d->distanceFromChord(segment.first()->point())); QPointF p1(1.0, d->distanceFromChord(segment.second()->point())); dt = KoPathSegment(p0, p1); } else if (degree2 == 2) { QPointF p0(0.0, d->distanceFromChord(segment.first()->point())); QPointF p1 = segment.first()->activeControlPoint2() ? QPointF(0.5, d->distanceFromChord(segment.first()->controlPoint2())) : QPointF(0.5, d->distanceFromChord(segment.second()->controlPoint1())); QPointF p2(1.0, d->distanceFromChord(segment.second()->point())); dt = KoPathSegment(p0, p1, p2); } else if (degree2 == 3) { QPointF p0(0.0, d->distanceFromChord(segment.first()->point())); QPointF p1(1. / 3., d->distanceFromChord(segment.first()->controlPoint2())); QPointF p2(2. / 3., d->distanceFromChord(segment.second()->controlPoint1())); QPointF p3(1.0, d->distanceFromChord(segment.second()->point())); dt = KoPathSegment(p0, p1, p2, p3); } else { //debugFlake << "invalid degree of segment -> exiting"; return isects; } // get convex hull of the segment D(t) QList hull = dt.convexHull(); // now calculate intersections with the line y1 = dmin, y2 = dmax // with the convex hull edges int hullCount = hull.count(); qreal tmin = 1.0, tmax = 0.0; bool intersectionsFoundMax = false; bool intersectionsFoundMin = false; for (int i = 0; i < hullCount; ++i) { QPointF p1 = hull[i]; QPointF p2 = hull[(i+1) % hullCount]; //debugFlake << "intersecting hull edge (" << p1 << p2 << ")"; // hull edge is completely above dmax if (p1.y() > dmax && p2.y() > dmax) continue; // hull edge is completely below dmin if (p1.y() < dmin && p2.y() < dmin) continue; if (p1.x() == p2.x()) { // vertical edge bool dmaxIntersection = (dmax < qMax(p1.y(), p2.y()) && dmax > qMin(p1.y(), p2.y())); bool dminIntersection = (dmin < qMax(p1.y(), p2.y()) && dmin > qMin(p1.y(), p2.y())); if (dmaxIntersection || dminIntersection) { tmin = qMin(tmin, p1.x()); tmax = qMax(tmax, p1.x()); if (dmaxIntersection) { intersectionsFoundMax = true; //debugFlake << "found intersection with dmax at " << p1.x() << "," << dmax; } else { intersectionsFoundMin = true; //debugFlake << "found intersection with dmin at " << p1.x() << "," << dmin; } } } else if (p1.y() == p2.y()) { // horizontal line if (p1.y() == dmin || p1.y() == dmax) { if (p1.y() == dmin) { intersectionsFoundMin = true; //debugFlake << "found intersection with dmin at " << p1.x() << "," << dmin; //debugFlake << "found intersection with dmin at " << p2.x() << "," << dmin; } else { intersectionsFoundMax = true; //debugFlake << "found intersection with dmax at " << p1.x() << "," << dmax; //debugFlake << "found intersection with dmax at " << p2.x() << "," << dmax; } tmin = qMin(tmin, p1.x()); tmin = qMin(tmin, p2.x()); tmax = qMax(tmax, p1.x()); tmax = qMax(tmax, p2.x()); } } else { qreal dx = p2.x() - p1.x(); qreal dy = p2.y() - p1.y(); qreal m = dy / dx; qreal n = p1.y() - m * p1.x(); qreal t1 = (dmax - n) / m; if (t1 >= 0.0 && t1 <= 1.0) { tmin = qMin(tmin, t1); tmax = qMax(tmax, t1); intersectionsFoundMax = true; //debugFlake << "found intersection with dmax at " << t1 << "," << dmax; } qreal t2 = (dmin - n) / m; if (t2 >= 0.0 && t2 < 1.0) { tmin = qMin(tmin, t2); tmax = qMax(tmax, t2); intersectionsFoundMin = true; //debugFlake << "found intersection with dmin at " << t2 << "," << dmin; } } } bool intersectionsFound = intersectionsFoundMin && intersectionsFoundMax; //if (intersectionsFound) // debugFlake << "clipping segment to interval [" << tmin << "," << tmax << "]"; if (!intersectionsFound || (1.0 - (tmax - tmin)) <= 0.2) { //debugFlake << "could not clip enough -> split segment"; // we could not reduce the interval significantly // so split the curve and calculate intersections // with the remaining parts QPair parts = splitAt(0.5); if (d->chordLength() < 1e-5) isects += parts.first.second()->point(); else { isects += segment.intersections(parts.first); isects += segment.intersections(parts.second); } } else if (qAbs(tmin - tmax) < 1e-5) { //debugFlake << "Yay, we found an intersection"; // the interval is pretty small now, just calculate the intersection at this point isects.append(segment.pointAt(tmin)); } else { QPair clip1 = segment.splitAt(tmin); //debugFlake << "splitting segment at" << tmin; qreal t = (tmax - tmin) / (1.0 - tmin); QPair clip2 = clip1.second.splitAt(t); //debugFlake << "splitting second part at" << t << "("<first); KoPathPoint * p2 = new KoPathPoint(*d->second); p1->map(matrix); p2->map(matrix); return KoPathSegment(p1, p2); } KoPathSegment KoPathSegment::toCubic() const { if (! isValid()) return KoPathSegment(); KoPathPoint * p1 = new KoPathPoint(*d->first); KoPathPoint * p2 = new KoPathPoint(*d->second); if (degree() == 1) { p1->setControlPoint2(p1->point() + 0.3 * (p2->point() - p1->point())); p2->setControlPoint1(p2->point() + 0.3 * (p1->point() - p2->point())); } else if (degree() == 2) { /* quadric bezier (a0,a1,a2) to cubic bezier (b0,b1,b2,b3): * * b0 = a0 * b1 = a0 + 2/3 * (a1-a0) * b2 = a1 + 1/3 * (a2-a1) * b3 = a2 */ QPointF a1 = p1->activeControlPoint2() ? p1->controlPoint2() : p2->controlPoint1(); QPointF b1 = p1->point() + 2.0 / 3.0 * (a1 - p1->point()); QPointF b2 = a1 + 1.0 / 3.0 * (p2->point() - a1); p1->setControlPoint2(b1); p2->setControlPoint1(b2); } return KoPathSegment(p1, p2); } qreal KoPathSegment::length(qreal error) const { /* * This algorithm is implemented based on an idea by Jens Gravesen: * "Adaptive subdivision and the length of Bezier curves" mat-report no. 1992-10, Mathematical Institute, * The Technical University of Denmark. * * By subdividing the curve at parameter value t you only have to find the length of a full Bezier curve. * If you denote the length of the control polygon by L1 i.e.: * L1 = |P0 P1| +|P1 P2| +|P2 P3| * * and the length of the cord by L0 i.e.: * L0 = |P0 P3| * * then * L = 1/2*L0 + 1/2*L1 * * is a good approximation to the length of the curve, and the difference * ERR = L1-L0 * * is a measure of the error. If the error is to large, then you just subdivide curve at parameter value * 1/2, and find the length of each half. * If m is the number of subdivisions then the error goes to zero as 2^-4m. * If you don't have a cubic curve but a curve of degree n then you put * L = (2*L0 + (n-1)*L1)/(n+1) */ int deg = degree(); if (deg == -1) return 0.0; QList ctrlPoints = controlPoints(); // calculate chord length qreal chordLen = d->chordLength(); if (deg == 1) { return chordLen; } // calculate length of control polygon qreal polyLength = 0.0; for (int i = 0; i < deg; ++i) { QPointF ctrlSegment = ctrlPoints[i+1] - ctrlPoints[i]; polyLength += sqrt(ctrlSegment.x() * ctrlSegment.x() + ctrlSegment.y() * ctrlSegment.y()); } if ((polyLength - chordLen) > error) { // the error is still bigger than our tolerance -> split segment QPair parts = splitAt(0.5); return parts.first.length(error) + parts.second.length(error); } else { // the error is smaller than our tolerance if (deg == 3) return 0.5 * chordLen + 0.5 * polyLength; else return (2.0 * chordLen + polyLength) / 3.0; } } qreal KoPathSegment::lengthAt(qreal t, qreal error) const { if (t == 0.0) return 0.0; if (t == 1.0) return length(error); QPair parts = splitAt(t); return parts.first.length(error); } qreal KoPathSegment::paramAtLength(qreal length, qreal tolerance) const { const int deg = degree(); // invalid degree or invalid specified length if (deg < 1 || length <= 0.0) { return 0.0; } if (deg == 1) { // make sure we return a maximum value of 1.0 return qMin(qreal(1.0), length / d->chordLength()); } // for curves we need to make sure, that the specified length // value does not exceed the actual length of the segment // if that happens, we return 1.0 to avoid infinite looping if (length >= d->chordLength() && length >= this->length(tolerance)) { return 1.0; } qreal startT = 0.0; // interval start qreal midT = 0.5; // interval center qreal endT = 1.0; // interval end // divide and conquer, split a midpoint and check // on which side of the midpoint to continue qreal midLength = lengthAt(0.5); while (qAbs(midLength - length) / length > tolerance) { if (midLength < length) startT = midT; else endT = midT; // new interval center midT = 0.5 * (startT + endT); // length at new interval center midLength = lengthAt(midT); } return midT; } bool KoPathSegment::isFlat(qreal tolerance) const { /* * Calculate the height of the bezier curve. * This is done by rotating the curve so that then chord * is parallel to the x-axis and the calculating the * parameters t for the extrema of the curve. * The curve points at the extrema are then used to * calculate the height. */ if (degree() <= 1) return true; QPointF chord = d->second->point() - d->first->point(); // calculate angle of chord to the x-axis qreal chordAngle = atan2(chord.y(), chord.x()); QTransform m; m.translate(d->first->point().x(), d->first->point().y()); m.rotate(chordAngle * M_PI / 180.0); m.translate(-d->first->point().x(), -d->first->point().y()); KoPathSegment s = mapped(m); qreal minDist = 0.0; qreal maxDist = 0.0; foreach (qreal t, s.d->extrema()) { if (t >= 0.0 && t <= 1.0) { QPointF p = pointAt(t); qreal dist = s.d->distanceFromChord(p); minDist = qMin(dist, minDist); maxDist = qMax(dist, maxDist); } } return (maxDist - minDist <= tolerance); } QList KoPathSegment::convexHull() const { QList hull; int deg = degree(); if (deg == 1) { // easy just the two end points hull.append(d->first->point()); hull.append(d->second->point()); } else if (deg == 2) { // we want to have a counter-clockwise oriented triangle // of the three control points QPointF chord = d->second->point() - d->first->point(); QPointF cp = d->first->activeControlPoint2() ? d->first->controlPoint2() : d->second->controlPoint1(); QPointF relP = cp - d->first->point(); // check on which side of the chord the control point is bool pIsRight = (chord.x() * relP.y() - chord.y() * relP.x() > 0); hull.append(d->first->point()); if (pIsRight) hull.append(cp); hull.append(d->second->point()); if (! pIsRight) hull.append(cp); } else if (deg == 3) { // we want a counter-clockwise oriented polygon QPointF chord = d->second->point() - d->first->point(); QPointF relP1 = d->first->controlPoint2() - d->first->point(); // check on which side of the chord the control points are bool p1IsRight = (chord.x() * relP1.y() - chord.y() * relP1.x() > 0); hull.append(d->first->point()); if (p1IsRight) hull.append(d->first->controlPoint2()); hull.append(d->second->point()); if (! p1IsRight) hull.append(d->first->controlPoint2()); // now we have a counter-clockwise triangle with the points i,j,k // we have to check where the last control points lies bool rightOfEdge[3]; QPointF lastPoint = d->second->controlPoint1(); for (int i = 0; i < 3; ++i) { QPointF relP = lastPoint - hull[i]; QPointF edge = hull[(i+1)%3] - hull[i]; rightOfEdge[i] = (edge.x() * relP.y() - edge.y() * relP.x() > 0); } for (int i = 0; i < 3; ++i) { int prev = (3 + i - 1) % 3; int next = (i + 1) % 3; // check if point is only right of the n-th edge if (! rightOfEdge[prev] && rightOfEdge[i] && ! rightOfEdge[next]) { // insert by breaking the n-th edge hull.insert(i + 1, lastPoint); break; } // check if it is right of the n-th and right of the (n+1)-th edge if (rightOfEdge[i] && rightOfEdge[next]) { // remove both edge, insert two new edges hull[i+1] = lastPoint; break; } // check if it is right of n-th and right of (n-1)-th edge if (rightOfEdge[i] && rightOfEdge[prev]) { hull[i] = lastPoint; break; } } } return hull; } QPair KoPathSegment::splitAt(qreal t) const { QPair results; if (!isValid()) return results; if (degree() == 1) { QPointF p = d->first->point() + t * (d->second->point() - d->first->point()); results.first = KoPathSegment(d->first->point(), p); results.second = KoPathSegment(p, d->second->point()); } else { QPointF newCP2, newCP1, splitP, splitCP1, splitCP2; d->deCasteljau(t, &newCP2, &splitCP1, &splitP, &splitCP2, &newCP1); if (degree() == 2) { if (second()->activeControlPoint1()) { KoPathPoint *s1p1 = new KoPathPoint(0, d->first->point()); KoPathPoint *s1p2 = new KoPathPoint(0, splitP); s1p2->setControlPoint1(splitCP1); KoPathPoint *s2p1 = new KoPathPoint(0, splitP); KoPathPoint *s2p2 = new KoPathPoint(0, d->second->point()); s2p2->setControlPoint1(splitCP2); results.first = KoPathSegment(s1p1, s1p2); results.second = KoPathSegment(s2p1, s2p2); } else { results.first = KoPathSegment(d->first->point(), splitCP1, splitP); results.second = KoPathSegment(splitP, splitCP2, d->second->point()); } } else { results.first = KoPathSegment(d->first->point(), newCP2, splitCP1, splitP); results.second = KoPathSegment(splitP, splitCP2, newCP1, d->second->point()); } } return results; } QList KoPathSegment::controlPoints() const { QList controlPoints; controlPoints.append(d->first->point()); if (d->first->activeControlPoint2()) controlPoints.append(d->first->controlPoint2()); if (d->second->activeControlPoint1()) controlPoints.append(d->second->controlPoint1()); controlPoints.append(d->second->point()); return controlPoints; } qreal KoPathSegment::nearestPoint(const QPointF &point) const { if (!isValid()) return -1.0; const int deg = degree(); // use shortcut for line segments if (deg == 1) { // the segments chord QPointF chord = d->second->point() - d->first->point(); // the point relative to the segment QPointF relPoint = point - d->first->point(); // project point to chord (dot product) qreal scale = chord.x() * relPoint.x() + chord.y() * relPoint.y(); // normalize using the chord length scale /= chord.x() * chord.x() + chord.y() * chord.y(); if (scale < 0.0) { return 0.0; } else if (scale > 1.0) { return 1.0; } else { return scale; } } /* This function solves the "nearest point on curve" problem. That means, it * calculates the point q (to be precise: it's parameter t) on this segment, which * is located nearest to the input point P. * The basic idea is best described (because it is freely available) in "Phoenix: * An Interactive Curve Design System Based on the Automatic Fitting of * Hand-Sketched Curves", Philip J. Schneider (Master thesis, University of * Washington). * * For the nearest point q = C(t) on this segment, the first derivative is * orthogonal to the distance vector "C(t) - P". In other words we are looking for * solutions of f(t) = (C(t) - P) * C'(t) = 0. * (C(t) - P) is a nth degree curve, C'(t) a n-1th degree curve => f(t) is a * (2n - 1)th degree curve and thus has up to 2n - 1 distinct solutions. * We solve the problem f(t) = 0 by using something called "Approximate Inversion Method". * Let's write f(t) explicitly (with c_i = p_i - P and d_j = p_{j+1} - p_j): * * n n-1 * f(t) = SUM c_i * B^n_i(t) * SUM d_j * B^{n-1}_j(t) * i=0 j=0 * * n n-1 * = SUM SUM w_{ij} * B^{2n-1}_{i+j}(t) * i=0 j=0 * * with w_{ij} = c_i * d_j * z_{ij} and * * BinomialCoeff(n, i) * BinomialCoeff(n - i ,j) * z_{ij} = ----------------------------------------------- * BinomialCoeff(2n - 1, i + j) * * This Bernstein-Bezier polynom representation can now be solved for its roots. */ QList ctlPoints = controlPoints(); // Calculate the c_i = point(i) - P. QPointF * c_i = new QPointF[ deg + 1 ]; for (int i = 0; i <= deg; ++i) { c_i[ i ] = ctlPoints[ i ] - point; } // Calculate the d_j = point(j + 1) - point(j). QPointF *d_j = new QPointF[deg]; for (int j = 0; j <= deg - 1; ++j) { d_j[j] = 3.0 * (ctlPoints[j+1] - ctlPoints[j]); } // Calculate the dot products of c_i and d_i. qreal *products = new qreal[deg * (deg + 1)]; for (int j = 0; j <= deg - 1; ++j) { for (int i = 0; i <= deg; ++i) { products[j * (deg + 1) + i] = d_j[j].x() * c_i[i].x() + d_j[j].y() * c_i[i].y(); } } // We don't need the c_i and d_i anymore. delete[] d_j ; delete[] c_i ; // Calculate the control points of the new 2n-1th degree curve. BezierSegment newCurve; newCurve.setDegree(2 * deg - 1); // Set up control points in the (u, f(u))-plane. for (unsigned short u = 0; u <= 2 * deg - 1; ++u) { newCurve.setPoint(u, QPointF(static_cast(u) / static_cast(2 * deg - 1), 0.0)); } // Precomputed "z" for cubics static const qreal z3[3*4] = {1.0, 0.6, 0.3, 0.1, 0.4, 0.6, 0.6, 0.4, 0.1, 0.3, 0.6, 1.0}; // Precomputed "z" for quadrics static const qreal z2[2*3] = {1.0, 2./3., 1./3., 1./3., 2./3., 1.0}; const qreal *z = degree() == 3 ? z3 : z2; // Set f(u)-values. for (int k = 0; k <= 2 * deg - 1; ++k) { int min = qMin(k, deg); for (unsigned short i = qMax(0, k - (deg - 1)); i <= min; ++i) { unsigned short j = k - i; // p_k += products[j][i] * z[j][i]. QPointF currentPoint = newCurve.point(k); currentPoint.ry() += products[j * (deg + 1) + i] * z[j * (deg + 1) + i]; newCurve.setPoint(k, currentPoint); } } // We don't need the c_i/d_i dot products and the z_{ij} anymore. delete[] products; // Find roots. QList rootParams = newCurve.roots(); // Now compare the distances of the candidate points. // First candidate is the previous knot. qreal distanceSquared = kisSquareDistance(point, d->first->point()); qreal minDistanceSquared = distanceSquared; qreal resultParam = 0.0; // Iterate over the found candidate params. foreach (qreal root, rootParams) { distanceSquared = kisSquareDistance(point, pointAt(root)); if (distanceSquared < minDistanceSquared) { minDistanceSquared = distanceSquared; resultParam = root; } } // Last candidate is the knot. distanceSquared = kisSquareDistance(point, d->second->point()); if (distanceSquared < minDistanceSquared) { minDistanceSquared = distanceSquared; resultParam = 1.0; } return resultParam; } KoPathSegment KoPathSegment::interpolate(const QPointF &p0, const QPointF &p1, const QPointF &p2, qreal t) { if (t <= 0.0 || t >= 1.0) return KoPathSegment(); /* B(t) = [x2 y2] = (1-t)^2*P0 + 2t*(1-t)*P1 + t^2*P2 B(t) - (1-t)^2*P0 - t^2*P2 P1 = -------------------------- 2t*(1-t) */ QPointF c1 = p1 - (1.0-t) * (1.0-t)*p0 - t * t * p2; qreal denom = 2.0 * t * (1.0-t); c1.rx() /= denom; c1.ry() /= denom; return KoPathSegment(p0, c1, p2); } #if 0 void KoPathSegment::printDebug() const { int deg = degree(); debugFlake << "degree:" << deg; if (deg < 1) return; debugFlake << "P0:" << d->first->point(); if (deg == 1) { debugFlake << "P2:" << d->second->point(); } else if (deg == 2) { if (d->first->activeControlPoint2()) debugFlake << "P1:" << d->first->controlPoint2(); else debugFlake << "P1:" << d->second->controlPoint1(); debugFlake << "P2:" << d->second->point(); } else if (deg == 3) { debugFlake << "P1:" << d->first->controlPoint2(); debugFlake << "P2:" << d->second->controlPoint1(); debugFlake << "P3:" << d->second->point(); } } #endif diff --git a/libs/flake/KoPathShape.cpp b/libs/flake/KoPathShape.cpp index faa9152a35..956fb76188 100644 --- a/libs/flake/KoPathShape.cpp +++ b/libs/flake/KoPathShape.cpp @@ -1,1757 +1,1757 @@ /* 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()); } 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().addAttribute("svg:width", size().width()); context.xmlWriter().addAttribute("svg:height", size().height()); const QSizeF s(size()); QString viewBox = QString("0 0 %1 %2").arg(qRound(1000*s.width())).arg(qRound(1000*s.height())); context.xmlWriter().addAttribute("svg:viewBox", viewBox); context.xmlWriter().addAttribute("draw:points", points); context.xmlWriter().addAttribute("draw:recreate-on-edit", "true"); context.xmlWriter().endElement(); return; } } // if we get here we couldn't save as polygon - let-s try contour-path context.xmlWriter().startElement("draw:contour-path"); saveOdfAttributes(context, OdfViewbox); context.xmlWriter().addAttribute("svg:d", toString()); context.xmlWriter().addAttribute("calligra:nodeTypes", d->nodeTypes()); context.xmlWriter().addAttribute("draw:recreate-on-edit", "true"); context.xmlWriter().endElement(); } void KoPathShape::saveOdf(KoShapeSavingContext & context) const { 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(); notifyPointsChanged(); } 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 + // which itself uses size() -> leads to infinite recursion 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); notifyPointsChanged(); 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); notifyPointsChanged(); 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); notifyPointsChanged(); 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); notifyPointsChanged(); 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 approximation to the circle qreal parts = ceil(qAbs(sweepAngle / 90.0)); qreal sa_rad = startAngle * M_PI / 180.0; qreal partangle = sweepAngle / parts; qreal endangle = startAngle + partangle; qreal se_rad = endangle * M_PI / 180.0; qreal sinsa = sin(sa_rad); qreal cossa = cos(sa_rad); qreal kappa = 4.0 / 3.0 * tan((se_rad - sa_rad) / 4); // startpoint is at the last point is the path but when it is closed // it is at the first point QPointF startpoint(offset); //center berechnen QPointF center(startpoint - QPointF(cossa * rx, -sinsa * ry)); //debugFlake <<"kappa" << kappa <<"parts" << parts; for (int part = 0; part < parts; ++part) { // start tangent curvePoints[pointCnt++] = QPointF(startpoint - QPointF(sinsa * rx * kappa, cossa * ry * kappa)); qreal sinse = sin(se_rad); qreal cosse = cos(se_rad); // end point QPointF endpoint(center + QPointF(cosse * rx, -sinse * ry)); // end tangent curvePoints[pointCnt++] = QPointF(endpoint - QPointF(-sinse * rx * kappa, -cosse * ry * kappa)); curvePoints[pointCnt++] = endpoint; // set the endpoint as next start point startpoint = endpoint; sinsa = sinse; cossa = cosse; endangle += partangle; se_rad = endangle * M_PI / 180.0; } return pointCnt; } void KoPathShape::close() { 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) { 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); notifyPointsChanged(); 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); } } notifyPointsChanged(); 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); notifyPointsChanged(); 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; notifyPointsChanged(); 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); notifyPointsChanged(); 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); notifyPointsChanged(); 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); notifyPointsChanged(); 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); notifyPointsChanged(); 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); } notifyPointsChanged(); 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); notifyPointsChanged(); 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(); notifyPointsChanged(); 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(); shape->setStroke(stroke()); shape->setBackground(background()); shape->setShapeId(shapeId()); shape->setZIndex(zIndex()); KoSubpath *newSubpath = new KoSubpath(); Q_FOREACH (KoPathPoint* point, *subpath) { KoPathPoint *newPoint = new KoPathPoint(*point, shape); newPoint->map(myMatrix); newSubpath->append(newPoint); } shape->d_func()->subpaths.append(newSubpath); shape->normalize(); // NOTE: shape cannot have any listeners yet, so no notification about // points modification is needed separatedPaths.append(shape); } return true; } void KoPathShapePrivate::closeSubpath(KoSubpath *subpath) { Q_Q(KoPathShape); if (! subpath) return; subpath->last()->setProperty(KoPathPoint::CloseSubpath); subpath->first()->setProperty(KoPathPoint::CloseSubpath); q->notifyPointsChanged(); } void KoPathShapePrivate::closeMergeSubpath(KoSubpath *subpath) { Q_Q(KoPathShape); 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); q->notifyPointsChanged(); } else { closeSubpath(subpath); } } KoSubpath *KoPathShapePrivate::subPath(int subpathIndex) const { if (subpathIndex < 0 || subpathIndex >= subpaths.size()) return 0; return subpaths.at(subpathIndex); } QString KoPathShape::pathShapeId() const { return KoPathShapeId; } QString KoPathShape::toString(const QTransform &matrix) const { 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); if (!currPoint) { qWarning() << "Found a zero point in the shape's path!"; continue; } // first point of subpath ? if (currPoint == firstPoint) { // are we starting a subpath ? if (currPoint->properties() & KoPathPoint::StartSubpath) { const QPointF p = matrix.map(currPoint->point()); pathString += QString("M%1 %2").arg(p.x()).arg(p.y()); } } // end point of curve segment ? else if (activeControlPoint2 || currPoint->activeControlPoint1()) { // check if we have a cubic or quadratic curve const bool isCubic = activeControlPoint2 && currPoint->activeControlPoint1(); KoPathSegment cubicSeg = isCubic ? KoPathSegment(lastPoint, currPoint) : KoPathSegment(lastPoint, currPoint).toCubic(); const QPointF cp1 = matrix.map(cubicSeg.first()->controlPoint2()); const QPointF cp2 = matrix.map(cubicSeg.second()->controlPoint1()); const QPointF p = matrix.map(cubicSeg.second()->point()); pathString += QString("C%1 %2 %3 %4 %5 %6") .arg(cp1.x()).arg(cp1.y()) .arg(cp2.x()).arg(cp2.y()) .arg(p.x()).arg(p.y()); } // end point of line segment! else { const QPointF p = matrix.map(currPoint->point()); pathString += QString("L%1 %2").arg(p.x()).arg(p.y()); } // last point closes subpath ? if (currPoint->properties() & KoPathPoint::StopSubpath && currPoint->properties() & KoPathPoint::CloseSubpath) { // add curve when there is a curve on the way to the first point if (currPoint->activeControlPoint2() || firstPoint->activeControlPoint1()) { // check if we have a cubic or quadratic curve const bool isCubic = currPoint->activeControlPoint2() && firstPoint->activeControlPoint1(); KoPathSegment cubicSeg = isCubic ? KoPathSegment(currPoint, firstPoint) : KoPathSegment(currPoint, firstPoint).toCubic(); const QPointF cp1 = matrix.map(cubicSeg.first()->controlPoint2()); const QPointF cp2 = matrix.map(cubicSeg.second()->controlPoint1()); const QPointF p = matrix.map(cubicSeg.second()->point()); pathString += QString("C%1 %2 %3 %4 %5 %6") .arg(cp1.x()).arg(cp1.y()) .arg(cp2.x()).arg(cp2.y()) .arg(p.x()).arg(p.y()); } pathString += QString("Z"); } activeControlPoint2 = currPoint->activeControlPoint2(); lastPoint = currPoint; } } return pathString; } char nodeType(const KoPathPoint * point) { if (point->properties() & KoPathPoint::IsSmooth) { return 's'; } else if (point->properties() & KoPathPoint::IsSymmetric) { return 'z'; } else { return 'c'; } } QString KoPathShapePrivate::nodeTypes() const { QString types; KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { if (it == (*pathIt)->constBegin()) { types.append('c'); } else { types.append(nodeType(*it)); } if ((*it)->properties() & KoPathPoint::StopSubpath && (*it)->properties() & KoPathPoint::CloseSubpath) { KoPathPoint * firstPoint = (*pathIt)->first(); types.append(nodeType(firstPoint)); } } } return types; } void updateNodeType(KoPathPoint * point, const QChar & nodeType) { if (nodeType == 's') { point->setProperty(KoPathPoint::IsSmooth); } else if (nodeType == 'z') { point->setProperty(KoPathPoint::IsSymmetric); } } void KoPathShapePrivate::loadNodeTypes(const KoXmlElement &element) { if (element.hasAttributeNS(KoXmlNS::calligra, "nodeTypes")) { QString nodeTypes = element.attributeNS(KoXmlNS::calligra, "nodeTypes"); QString::const_iterator nIt(nodeTypes.constBegin()); KoSubpathList::const_iterator pathIt(subpaths.constBegin()); for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it, nIt++) { // be sure not to crash if there are not enough nodes in nodeTypes if (nIt == nodeTypes.constEnd()) { warnFlake << "not enough nodes in calligra:nodeTypes"; return; } // the first node is always of type 'c' if (it != (*pathIt)->constBegin()) { updateNodeType(*it, *nIt); } if ((*it)->properties() & KoPathPoint::StopSubpath && (*it)->properties() & KoPathPoint::CloseSubpath) { ++nIt; updateNodeType((*pathIt)->first(), *nIt); } } } } } Qt::FillRule KoPathShape::fillRule() const { 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->setShapeId(KoPathShapeId); //shape->normalize(); return shape; } bool KoPathShape::hitTest(const QPointF &position) const { if (parent() && parent()->isClipped(this) && ! parent()->hitTest(position)) return false; QPointF point = absoluteTransformation(0).inverted().map(position); const QPainterPath outlinePath = outline(); if (stroke()) { KoInsets insets; stroke()->strokeInsets(this, insets); QRectF roi(QPointF(-insets.left, -insets.top), QPointF(insets.right, insets.bottom)); roi.moveCenter(point); if (outlinePath.intersects(roi) || outlinePath.contains(roi)) return true; } else { if (outlinePath.contains(point)) return true; } // if there is no shadow we can as well just leave if (! shadow()) return false; // the shadow has an offset to the shape, so we simply // check if the position minus the shadow offset hits the shape point = absoluteTransformation(0).inverted().map(position - shadow()->offset()); return outlinePath.contains(point); } void KoPathShape::setMarker(KoMarker *marker, KoFlake::MarkerPosition pos) { 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; } void KoPathShape::recommendPointSelectionChange(const QList &newSelection) { Q_D(KoShape); Q_FOREACH (KoShape::ShapeChangeListener *listener, d->listeners) { PointSelectionChangeListener *pointListener = dynamic_cast(listener); if (pointListener) { pointListener->recommendPointSelectionChange(this, newSelection); } } } void KoPathShape::notifyPointsChanged() { Q_D(KoShape); Q_FOREACH (KoShape::ShapeChangeListener *listener, d->listeners) { PointSelectionChangeListener *pointListener = dynamic_cast(listener); if (pointListener) { pointListener->notifyPathPointsChanged(this); } } } 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; // TODO: these variables are never(!) initialized! KoPathPoint *firstPoint = 0; KoPathPoint *lastPoint = 0; KoPathPoint *secondPoint = 0; KoPathPoint *preLastPoint = 0; KoSubpath *firstSubpath = d->subpaths.first(); stroker.setWidth(pen.widthF()); stroker.setJoinStyle(pen.joinStyle()); stroker.setMiterLimit(pen.miterLimit()); stroker.setCapStyle(pen.capStyle()); stroker.setDashOffset(pen.dashOffset()); stroker.setDashPattern(pen.dashPattern()); // shortent the path to make it look nice // replace the point temporarily in case there is an arrow // BE AWARE: this changes the content of the path so that outline give the correct values. if (firstPoint) { firstSubpath->first() = firstSegments.second.first(); if (secondPoint) { (*firstSubpath)[1] = firstSegments.second.second(); } } if (lastPoint) { if (preLastPoint) { (*firstSubpath)[firstSubpath->count() - 2] = lastSegments.first.first(); } firstSubpath->last() = lastSegments.first.second(); } QPainterPath path = stroker.createStroke(outline()); if (firstPoint) { firstSubpath->first() = firstPoint; if (secondPoint) { (*firstSubpath)[1] = secondPoint; } } if (lastPoint) { if (preLastPoint) { (*firstSubpath)[firstSubpath->count() - 2] = preLastPoint; } firstSubpath->last() = lastPoint; } pathOutline.addPath(path); pathOutline.setFillRule(Qt::WindingFill); return pathOutline; } void KoPathShape::PointSelectionChangeListener::notifyShapeChanged(KoShape::ChangeType type, KoShape *shape) { Q_UNUSED(type); Q_UNUSED(shape); } diff --git a/libs/flake/KoPathShape.h b/libs/flake/KoPathShape.h index 044882ad59..6a788ae89b 100644 --- a/libs/flake/KoPathShape.h +++ b/libs/flake/KoPathShape.h @@ -1,522 +1,522 @@ /* This file is part of the KDE project Copyright (C) 2006, 2011 Thorsten Zachmann Copyright (C) 2007,2009 Thomas Zander Copyright (C) 2006-2008 Jan Hambrecht 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. */ #ifndef KOPATHSHAPE_H #define KOPATHSHAPE_H #include "kritaflake_export.h" #include #include #include "KoTosContainer.h" #define KoPathShapeId "KoPathShape" class KoPathSegment; class KoPathPoint; class KoPathShapePrivate; class KoMarker; class KisHandlePainterHelper; typedef QPair KoPathPointIndex; /// a KoSubpath contains a path from a moveTo until a close or a new moveTo typedef QList KoSubpath; typedef QList KoSubpathList; /// The position of a path point within a path shape /** * @brief This is the base for all graphical objects. * * All graphical objects are based on this object e.g. lines, rectangulars, pies * and so on. * * The KoPathShape uses KoPathPoint's to describe the path of the shape. * * Here a short example: * 3 points connected by a curveTo's described by the following svg: * M 100,200 C 100,100 250,100 250,200 C 250,200 400,300 400,200. * * This will be stored in 3 KoPathPoint's as * The first point contains in * point 100,200 * controlPoint2 100,100 * The second point contains in * point 250,200 * controlPoint1 250,100 * controlPoint2 250,300 * The third point contains in * point 400,300 * controlPoint1 400,200 * * Not the segments are stored but the points. Out of the points the segments are * generated. See the outline method. The reason for storing it like that is that * it is the points that are modified by the user and not the segments. */ class KRITAFLAKE_EXPORT KoPathShape : public KoTosContainer { public: /** * @brief constructor */ KoPathShape(); /** * @brief */ ~KoPathShape() override; KoShape *cloneShape() const override; /// reimplemented void paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) override; virtual void paintPoints(KisHandlePainterHelper &handlesHelper); /// reimplemented QRectF outlineRect() const override; /// reimplemented QPainterPath outline() const override; /// reimplemented QRectF boundingRect() const override; /// reimplemented QSizeF size() const override; QPainterPath pathStroke(const QPen &pen) const; /** * Resize the shape * * This makes sure that the pathshape will not be resized to 0 if the new size * is null as that makes it impossible to undo the change. * * All functions that overwrite this function should also use the resizeMatrix * function to get and use the same data in resizing. * * @see resizeMatrix() */ void setSize(const QSizeF &size) override; /// reimplemented bool hitTest(const QPointF &position) const override; // reimplemented void saveOdf(KoShapeSavingContext &context) const override; // reimplemented bool loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context) override; // basically the same as loadOdf but adapted to the contour cases // tag needs to be either contour-polygon or contour-path from another shape bool loadContourOdf(const KoXmlElement & element, KoShapeLoadingContext &context, const QSizeF &scaleFactor); /** basically the equivalent saveOdf but adapted to the contour cases * @param originalSize the original size of the unscaled image. */ void saveContourOdf(KoShapeSavingContext &context, const QSizeF &originalSize) const; /// Removes all subpaths and their points from the path void clear(); /** * @brief Starts a new Subpath * * Moves the pen to p and starts a new subpath. * * @return the newly created point */ KoPathPoint *moveTo(const QPointF &p); /** * @brief Adds a new line segment * * Adds a straight line between the last point and the given point p. * * @return the newly created point */ KoPathPoint *lineTo(const QPointF &p); /** * @brief Adds a new cubic Bezier curve segment. * * Adds a cubic Bezier curve between the last point and the given point p, * using the control points specified by c1 and c2. * * @param c1 control point1 * @param c2 control point2 * @param p the endpoint of this curve segment * * @return The newly created point */ KoPathPoint *curveTo(const QPointF &c1, const QPointF &c2, const QPointF &p); /** * @brief Adds a new quadratic Bezier curve segment. * * Adds a quadratic Bezier curve between the last point and the given point p, * using the control point specified by c. * * @param c control point * @param p the endpoint of this curve segment * * @return The newly created point */ KoPathPoint *curveTo(const QPointF &c, const QPointF &p); /** * @brief Add an arc. * * Adds an arc starting at the current point. The arc will be converted to bezier curves. * * @param rx x radius of the ellipse * @param ry y radius of the ellipse * @param startAngle the angle where the arc will be started * @param sweepAngle the length of the angle * TODO add param to have angle of the ellipse * * @return The newly created point */ KoPathPoint *arcTo(qreal rx, qreal ry, qreal startAngle, qreal sweepAngle); /** * @brief Closes the current subpath */ void close(); /** * @brief Closes the current subpath * * It tries to merge the last and first point of the subpath * to one point and then closes the subpath. If merging is not * possible as the two point are to far from each other a close * will be done. * TODO define a maximum distance between the two points until this is working */ void closeMerge(); /** * @brief Normalizes the path data. * * The path points are transformed so that the top-left corner * of the bounding rect is at (0,0). * This should be called after adding points to the path or changing * positions of path points. * @return the offset by which the points are moved in shape coordinates. */ virtual QPointF normalize(); /** * @brief Returns the path points within the given rectangle. * @param rect the rectangle the requested points are in * @return list of points within the rectangle */ QList pointsAt(const QRectF &rect) const; /** * @brief Returns the list of path segments within the given rectangle. * @param rect the rectangle the requested segments are in * @return list of segments within the rectangle */ QList segmentsAt(const QRectF &rect) const; /** * @brief Returns the path point index of a given path point * * @param point the point for which you want to get the index * @return path point index of the point if it exists * otherwise KoPathPointIndex( -1, -1 ) */ KoPathPointIndex pathPointIndex(const KoPathPoint *point) const; /** * @brief Returns the path point specified by a path point index * * @param pointIndex index of the point to get * * @return KoPathPoint on success, 0 otherwise e.g. out of bounds */ KoPathPoint *pointByIndex(const KoPathPointIndex &pointIndex) const; /** * @brief Returns the segment specified by a path point index * - * A semgent is defined by the point index of the first point in the segment. + * A segment is defined by the point index of the first point in the segment. * A segment contains the defined point and its following point. If the subpath is * closed and the and the pointIndex point to the last point in the subpath, the * following point is the first point in the subpath. * * @param pointIndex index of the first point of the segment * * @return Segment containing both points of the segment or KoPathSegment( 0, 0 ) on error e.g. out of bounds */ KoPathSegment segmentByIndex(const KoPathPointIndex &pointIndex) const; /** * @brief Returns the number of points in the path * * @return The number of points in the path */ int pointCount() const; /** * @brief Returns the number of subpaths in the path * * @return The number of subpaths in the path */ int subpathCount() const; /** * @brief Returns the number of points in a subpath * * @return The number of points in the subpath or -1 if subpath out of bounds */ int subpathPointCount(int subpathIndex) const; /** * @brief Checks if a subpath is closed * * @param subpathIndex index of the subpath to check * * @return true when the subpath is closed, false otherwise */ bool isClosedSubpath(int subpathIndex) const; /** * @brief Inserts a new point into the given subpath at the specified position * * This method keeps the subpath closed if it is closed, and open when it was * open. So it can change the properties of the point inserted. * You might need to update the point before/after to get the desired result * e.g. when you insert the point into a curve. * * @param point to insert * @param pointIndex index at which the point should be inserted * * @return true on success, * false when pointIndex is out of bounds */ bool insertPoint(KoPathPoint *point, const KoPathPointIndex &pointIndex); /** * @brief Removes a point from the path. * * Note that the ownership of the point will pass to the caller. * * @param pointIndex index of the point which should be removed * * @return The removed point on success, * otherwise 0 */ KoPathPoint *removePoint(const KoPathPointIndex &pointIndex); /** * @brief Breaks the path after the point index * * The new subpath will be behind the one that was broken. The segment between * the given point and the one behind will be removed. If you want to split at * one point insert first a copy of the point behind it. * This does not work when the subpath is closed. Use openSubpath for this. * It does not break at the last position of a subpath or if there is only one * point in the subpath. * * @param pointIndex index of the point after which the path should be broken * * @return true if the subpath was broken, otherwise false */ bool breakAfter(const KoPathPointIndex &pointIndex); /** * @brief Joins the given subpath with the following one * * Joins the given subpath with the following one by inserting a segment between * the two subpaths. * This does nothing if the specified subpath is the last subpath * or one of both subpaths is closed. * * @param subpathIndex index of the subpath being joined with the following subpath * * @return true if the subpath was joined, otherwise false */ bool join(int subpathIndex); /** * @brief Moves the position of a subpath within a path * * @param oldSubpathIndex old index of the subpath * @param newSubpathIndex new index of the subpath * * @return true if the subpath was moved, otherwise false e.g. if an index is out of bounds */ bool moveSubpath(int oldSubpathIndex, int newSubpathIndex); /** * @brief Opens a closed subpath * * The subpath is opened by removing the segment before the given point, making * the given point the new start point of the subpath. * * @param pointIndex the index of the point at which to open the closed subpath * @return the new position of the old first point in the subpath * otherwise KoPathPointIndex( -1, -1 ) */ KoPathPointIndex openSubpath(const KoPathPointIndex &pointIndex); /** * @brief Close a open subpath * * The subpath is closed be inserting a segment between the start and end point, making * the given point the new start point of the subpath. * * @return the new position of the old first point in the subpath * otherwise KoPathPointIndex( -1, -1 ) */ KoPathPointIndex closeSubpath(const KoPathPointIndex &pointIndex); /** * @brief Reverse subpath * * The last point becomes the first point and the first one becomes the last one. * * @param subpathIndex the index of the subpath to reverse */ bool reverseSubpath(int subpathIndex); /** * @brief Removes subpath from the path * @param subpathIndex the index of the subpath to remove * @return the removed subpath on success, 0 otherwise. */ KoSubpath *removeSubpath(int subpathIndex); /** * @brief Adds a subpath at the given index to the path * @param subpath the subpath to add * @param subpathIndex the index at which the new subpath should be inserted * @return true on success, false otherwise e.g. subpathIndex out of bounds */ bool addSubpath(KoSubpath *subpath, int subpathIndex); /** * @brief Combines two path shapes by appending the data of the specified path. * @param path the path to combine with * @return index of the first segment inserted or -1 on failure */ int combine(KoPathShape *path); /** * @brief Creates separate path shapes, one for each existing subpath. * @param separatedPaths the list which contains the separated path shapes * @return true if separating the path was successful, false otherwise */ bool separate(QList &separatedPaths); /** * Returns the specific path shape id. * * Path shape derived shapes have a different shape id which link them * to their respective shape factories. In most cases they do not have * a special tool for editing them. * This function returns the specific shape id for finding the shape * factory from KoShapeRegistry. The default KoPathShapeId is returned * from KoShape::shapeId() so that the generic path editing tool gets * activated when the shape is selected. * * @return the specific shape id */ virtual QString pathShapeId() const; - /// Returns a odf/svg string represenatation of the path data with the given matrix applied. + /// Returns a odf/svg string representation of the path data with the given matrix applied. QString toString(const QTransform &matrix = QTransform()) const; /// Returns the fill rule for the path object Qt::FillRule fillRule() const; /// Sets the fill rule to be used for painting the background void setFillRule(Qt::FillRule fillRule); /// Creates path shape from given QPainterPath static KoPathShape *createShapeFromPainterPath(const QPainterPath &path); /// Returns the viewbox from the given xml element. static QRect loadOdfViewbox(const KoXmlElement &element); void setMarker(KoMarker *marker, KoFlake::MarkerPosition pos); KoMarker* marker(KoFlake::MarkerPosition pos) const; bool hasMarkers() const; bool autoFillMarkers() const; void setAutoFillMarkers(bool value); public: struct KRITAFLAKE_EXPORT PointSelectionChangeListener : public ShapeChangeListener { void notifyShapeChanged(ChangeType type, KoShape *shape) override; virtual void recommendPointSelectionChange(KoPathShape *shape, const QList &newSelection) = 0; virtual void notifyPathPointsChanged(KoPathShape *shape) = 0; }; void recommendPointSelectionChange(const QList &newSelection); protected: void notifyPointsChanged(); private: /// constructor: to be used in cloneShape(), not in descendants! /// \internal KoPathShape(const KoPathShape &rhs); protected: /// constructor:to be used in descendant shapes /// \internal KoPathShape(KoPathShapePrivate *); /// reimplemented QString saveStyle(KoGenStyle &style, KoShapeSavingContext &context) const override; /// reimplemented void loadStyle(const KoXmlElement &element, KoShapeLoadingContext &context) override; /** * @brief Add an arc. * * Adds an arc starting at the current point. The arc will be converted to bezier curves. * @param rx x radius of the ellipse * @param ry y radius of the ellipse * @param startAngle the angle where the arc will be started * @param sweepAngle the length of the angle * TODO add param to have angle of the ellipse * @param offset to the first point in the arc - * @param curvePoints a array which take the cuve points, pass a 'QPointF curvePoins[12]'; + * @param curvePoints a array which take the curve points, pass a 'QPointF curvePoins[12]'; * * @return number of points created by the curve */ int arcToCurve(qreal rx, qreal ry, qreal startAngle, qreal sweepAngle, const QPointF &offset, QPointF *curvePoints) const; /** * Get the resize matrix * * This makes sure that also if the newSize isNull that there will be a * very small size of 0.000001 pixels */ QTransform resizeMatrix( const QSizeF &newSize ) const; private: Q_DECLARE_PRIVATE(KoPathShape) }; Q_DECLARE_METATYPE(KoPathShape*) #endif /* KOPATHSHAPE_H */ diff --git a/libs/flake/KoPathShape_p.h b/libs/flake/KoPathShape_p.h index 0da839f097..5e5c80c25c 100644 --- a/libs/flake/KoPathShape_p.h +++ b/libs/flake/KoPathShape_p.h @@ -1,102 +1,102 @@ /* This file is part of the KDE project * Copyright (C) 2009 Thomas Zander * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOPATHSHAPEPRIVATE_H #define KOPATHSHAPEPRIVATE_H #include "KoPathShape.h" #include "KoTosContainer_p.h" #include "KoMarker.h" class KoPathShapePrivate : public KoTosContainerPrivate { public: explicit KoPathShapePrivate(KoPathShape *q); explicit KoPathShapePrivate(const KoPathShapePrivate &rhs, KoPathShape *q); QRectF handleRect(const QPointF &p, qreal radius) const; /// Applies the viewbox transformation defined in the given element void applyViewboxTransformation(const KoXmlElement &element); void map(const QTransform &matrix); void updateLast(KoPathPoint **lastPoint); /// closes specified subpath void closeSubpath(KoSubpath *subpath); /// close-merges specified subpath void closeMergeSubpath(KoSubpath *subpath); /** * @brief Saves the node types * * This is inspired by inkscape and uses the same mechanism as they do. * The only difference is that they use sodipodi:nodeTypes as element and * we use calligra:nodeTyes as attribute. * This attribute contains of a string which has the node type of each point * in it. The following node types exist: * * c corner * s smooth * z symmetric * * The first point of a path is always of the type c. - * If the path is closed the type of the first point is saved in the last elemeent + * If the path is closed the type of the first point is saved in the last element * E.g. you have a closed path with 2 points in it. The first one (start/end of path) * is symmetric and the second one is smooth that will result in the nodeType="czs" * So if there is a closed sub path the nodeTypes contain one more entry then there * are points. That is due to the first and the last point of a closed sub path get * merged into one when they are on the same position. * * @return The node types as string */ QString nodeTypes() const; /** * @brief Loads node types */ void loadNodeTypes(const KoXmlElement &element); /** * @brief Returns subpath at given index * @param subpathIndex the index of the subpath to return * @return subPath on success, or 0 when subpathIndex is out of bounds */ KoSubpath *subPath(int subpathIndex) const; #ifndef NDEBUG /// \internal void paintDebug(QPainter &painter); /** * @brief print debug information about a the points of the path */ void debugPath() const; #endif Qt::FillRule fillRule; Q_DECLARE_PUBLIC(KoPathShape) KoSubpathList subpaths; QMap> markersNew; bool autoFillMarkers; QList pointChangeListeners; }; #endif diff --git a/libs/flake/KoRTree.h b/libs/flake/KoRTree.h index 06bf55e636..42e97d7785 100644 --- a/libs/flake/KoRTree.h +++ b/libs/flake/KoRTree.h @@ -1,1165 +1,1165 @@ /* This file is part of the KDE project Copyright (c) 2006 Thorsten Zachmann 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. Based on code from Wolfgang Baer - WBaer@gmx.de */ #ifndef KORTREE_H #define KORTREE_H #include #include #include #include #include #include #include #include #include "kis_assert.h" // #define CALLIGRA_RTREE_DEBUG #ifdef CALLIGRA_RTREE_DEBUG #include #endif /** * @brief The KoRTree class is a template class that provides a R-tree. * * This class implements a R-tree as described in - * "R-TREES. A DYNAMIC INDEX STRUCTURE FOR SPATIAL SEARCHING" by Antomn Guttman + * "R-TREES. A DYNAMIC INDEX STRUCTURE FOR SPATIAL SEARCHING" by Antonin Guttman * * It only supports 2 dimensional bounding boxes which are represented by a QRectF. * For node splitting the Quadratic-Cost Algorithm is used as described by Guttman. */ template class KoRTree { public: /** * @brief Constructor * * @param capacity the capacity a node can take * @param minimum the minimum filling of a node max 0.5 * capacity */ KoRTree(int capacity, int minimum); /** * @brief Destructor */ virtual ~KoRTree(); /** * @brief Insert data item into the tree * * This will insert a data item into the tree. If necessary the tree will * adjust itself. * * @param data * @param bb */ virtual void insert(const QRectF& bb, const T& data); /** * @brief Show if a shape is a part of the tree * @param data */ bool contains(const T &data); /** * @brief Remove a data item from the tree * * This removed a data item from the tree. If necessary the tree will * adjust itself. * * @param data */ void remove(const T& data); /** * @brief Find all data items which intersects rect * The items are sorted by insertion time in ascending order. * * @param rect where the objects have to be in * * @return objects intersecting the rect */ virtual QList intersects(const QRectF& rect) const; /** * @brief Find all data item which contain the point * The items are sorted by insertion time in ascending order. * * @param point which should be contained in the objects * * @return objects which contain the point */ QList contains(const QPointF &point) const; /** * @brief Find all data item which contain the point * The items are sorted by insertion time in ascending order. * * @param point which should be contained in the objects * * @return objects which contain the point */ QList contained(const QRectF &point) const; /** * @brief Find all data rectangles * The order is NOT guaranteed to be the same as that used by values(). * * @return a list containing all the data rectangles used in the tree */ QList keys() const; /** * @brief Find all data items * The order is NOT guaranteed to be the same as that used by keys(). * * @return a list containing all the data used in the tree */ QList values() const; virtual void clear() { delete m_root; m_root = createLeafNode(m_capacity + 1, 0, 0); m_leafMap.clear(); } #ifdef CALLIGRA_RTREE_DEBUG /** * @brief Paint the tree * * @param p painter which should be used for painting */ void paint(QPainter & p) const; /** * @brief Print the tree using qdebug */ void debug() const; #endif protected: class NonLeafNode; class LeafNode; class Node { public: #ifdef CALLIGRA_RTREE_DEBUG static int nodeIdCnt; #endif Node(int capacity, int level, Node * parent); virtual ~Node() {} virtual void remove(int index); // move node between nodes of the same type from node virtual void move(Node * node, int index) = 0; virtual LeafNode * chooseLeaf(const QRectF& bb) = 0; virtual NonLeafNode * chooseNode(const QRectF& bb, int level) = 0; virtual void intersects(const QRectF& rect, QMap & result) const = 0; virtual void contains(const QPointF & point, QMap & result) const = 0; virtual void contained(const QRectF & point, QMap & result) const = 0; virtual void keys(QList & result) const = 0; virtual void values(QMap & result) const = 0; virtual Node * parent() const { return m_parent; } virtual void setParent(Node * parent) { m_parent = parent; } virtual int childCount() const { return m_counter; } virtual const QRectF& boundingBox() const { return m_boundingBox; } virtual void updateBoundingBox(); virtual const QRectF& childBoundingBox(int index) const { return m_childBoundingBox[index]; } virtual void setChildBoundingBox(int index, const QRectF& rect) { m_childBoundingBox[index] = rect; } virtual void clear(); virtual bool isRoot() const { return m_parent == 0; } virtual bool isLeaf() const { return false; } virtual int place() const { return m_place; } virtual void setPlace(int place) { m_place = place; } virtual int level() const { return m_level; } virtual void setLevel(int level) { m_level = level; } #ifdef CALLIGRA_RTREE_DEBUG virtual int nodeId() const { return m_nodeId; } virtual void paint(QPainter & p, int level) const = 0; virtual void debug(QString line) const = 0; protected: #define levelColorSize 5 static QColor levelColor[levelColorSize]; virtual void paintRect(QPainter & p, int level) const; #endif protected: Node * m_parent; QRectF m_boundingBox; QVector m_childBoundingBox; int m_counter; // the position in the parent int m_place; #ifdef CALLIGRA_RTREE_DEBUG int m_nodeId; #endif int m_level; }; class NonLeafNode : virtual public Node { public: NonLeafNode(int capacity, int level, Node * parent); ~NonLeafNode() override; virtual void insert(const QRectF& bb, Node * data); void remove(int index) override; void move(Node * node, int index) override; LeafNode * chooseLeaf(const QRectF& bb) override; NonLeafNode * chooseNode(const QRectF& bb, int level) override; void intersects(const QRectF& rect, QMap & result) const override; void contains(const QPointF & point, QMap & result) const override; void contained(const QRectF & point, QMap & result) const override; void keys(QList & result) const override; void values(QMap & result) const override; virtual Node * getNode(int index) const; #ifdef CALLIGRA_RTREE_DEBUG virtual void paint(QPainter & p, int level) const; virtual void debug(QString line) const; #endif protected: virtual Node * getLeastEnlargement(const QRectF& bb) const; QVector m_childs; }; class LeafNode : virtual public Node { public: static int dataIdCounter; LeafNode(int capacity, int level, Node * parent); ~LeafNode() override; virtual void insert(const QRectF& bb, const T& data, int id); void remove(int index) override; virtual void remove(const T& data); void move(Node * node, int index) override; LeafNode * chooseLeaf(const QRectF& bb) override; NonLeafNode * chooseNode(const QRectF& bb, int level) override; void intersects(const QRectF& rect, QMap & result) const override; void contains(const QPointF & point, QMap & result) const override; void contained(const QRectF & point, QMap & result) const override; void keys(QList & result) const override; void values(QMap & result) const override; virtual const T& getData(int index) const; virtual int getDataId(int index) const; bool isLeaf() const override { return true; } #ifdef CALLIGRA_RTREE_DEBUG virtual void debug(QString line) const; virtual void paint(QPainter & p, int level) const; #endif protected: QVector m_data; QVector m_dataIds; }; // factory methods virtual LeafNode* createLeafNode(int capacity, int level, Node * parent) { return new LeafNode(capacity, level, parent); } virtual NonLeafNode* createNonLeafNode(int capacity, int level, Node * parent) { return new NonLeafNode(capacity, level, parent); } // methods for insert QPair splitNode(Node * node); QPair pickSeeds(Node * node); QPair pickNext(Node * node, QVector & marker, Node * group1, Node * group2); virtual void adjustTree(Node * node1, Node * node2); void insertHelper(const QRectF& bb, const T& data, int id); // methods for delete void insert(Node * node); virtual void condenseTree(Node * node, QVector & reinsert); int m_capacity; int m_minimum; Node * m_root; QMap m_leafMap; }; template KoRTree::KoRTree(int capacity, int minimum) : m_capacity(capacity) , m_minimum(minimum) , m_root(createLeafNode(m_capacity + 1, 0, 0)) { if (minimum > capacity / 2) qFatal("KoRTree::KoRTree minimum can be maximal capacity/2"); //debugFlake << "root node " << m_root->nodeId(); } template KoRTree::~KoRTree() { delete m_root; } template void KoRTree::insert(const QRectF& bb, const T& data) { // check if the shape is not already registered KIS_SAFE_ASSERT_RECOVER_NOOP(!m_leafMap[data]); insertHelper(bb, data, LeafNode::dataIdCounter++); } template void KoRTree::insertHelper(const QRectF& bb, const T& data, int id) { QRectF nbb(bb.normalized()); // This has to be done as it is not possible to use QRectF::united() with a isNull() if (nbb.isNull()) { nbb.setWidth(0.0001); nbb.setHeight(0.0001); qWarning() << "KoRTree::insert boundingBox isNull setting size to" << nbb.size(); } else { // This has to be done as QRectF::intersects() return false if the rect does not have any area overlapping. // If there is no width or height there is no area and therefore no overlapping. if ( nbb.width() == 0 ) { nbb.setWidth(0.0001); } if ( nbb.height() == 0 ) { nbb.setHeight(0.0001); } } LeafNode * leaf = m_root->chooseLeaf(nbb); //debugFlake << " leaf" << leaf->nodeId() << nbb; if (leaf->childCount() < m_capacity) { leaf->insert(nbb, data, id); m_leafMap[data] = leaf; adjustTree(leaf, 0); } else { leaf->insert(nbb, data, id); m_leafMap[data] = leaf; QPair newNodes = splitNode(leaf); LeafNode * l = dynamic_cast(newNodes.first); if (l) for (int i = 0; i < l->childCount(); ++i) m_leafMap[l->getData(i)] = l; l = dynamic_cast(newNodes.second); if (l) for (int i = 0; i < l->childCount(); ++i) m_leafMap[l->getData(i)] = l; adjustTree(newNodes.first, newNodes.second); } } template void KoRTree::insert(Node * node) { if (node->level() == m_root->level()) { adjustTree(m_root, node); } else { QRectF bb(node->boundingBox()); NonLeafNode * newParent = m_root->chooseNode(bb, node->level() + 1); newParent->insert(bb, node); QPair newNodes(node, 0); if (newParent->childCount() > m_capacity) { newNodes = splitNode(newParent); } adjustTree(newNodes.first, newNodes.second); } } template bool KoRTree::contains(const T &data) { return m_leafMap[data]; } template void KoRTree::remove(const T&data) { //debugFlake << "KoRTree remove"; LeafNode * leaf = m_leafMap[data]; - // Trying to remove unexistent leaf. Most probably, this leaf hasn't been added + // Trying to remove inexistent leaf. Most probably, this leaf hasn't been added // to the shape manager correctly KIS_SAFE_ASSERT_RECOVER_RETURN(leaf); m_leafMap.remove(data); leaf->remove(data); /** * WARNING: after calling condenseTree() the temporary enters an inconsistent state! * m_leafMap still points to the nodes now stored in 'reinsert' list, although * they are not a part of the hierarchy. This state does not cause any use * visible changes, but should be considered while implementing sanity checks. */ QVector reinsert; condenseTree(leaf, reinsert); for (int i = 0; i < reinsert.size(); ++i) { if (reinsert[i]->isLeaf()) { LeafNode * leaf = dynamic_cast(reinsert[i]); for (int j = 0; j < leaf->childCount(); ++j) { insertHelper(leaf->childBoundingBox(j), leaf->getData(j), leaf->getDataId(j)); } // clear is needed as the data items are not removed when insert into a new node leaf->clear(); delete leaf; } else { NonLeafNode * node = dynamic_cast(reinsert[i]); for (int j = 0; j < node->childCount(); ++j) { insert(node->getNode(j)); } // clear is needed as the data items are not removed when insert into a new node node->clear(); delete node; } } } template QList KoRTree::intersects(const QRectF& rect) const { QMap found; m_root->intersects(rect, found); return found.values(); } template QList KoRTree::contains(const QPointF &point) const { QMap found; m_root->contains(point, found); return found.values(); } template QList KoRTree::contained(const QRectF& rect) const { QMap found; m_root->contained(rect, found); return found.values(); } template QList KoRTree::keys() const { QList found; m_root->keys(found); return found; } template QList KoRTree::values() const { QMap found; m_root->values(found); return found.values(); } #ifdef CALLIGRA_RTREE_DEBUG template void KoRTree::paint(QPainter & p) const { if (m_root) { m_root->paint(p, 0); } } template void KoRTree::debug() const { QString prefix(""); m_root->debug(prefix); } #endif template QPair< typename KoRTree::Node*, typename KoRTree::Node* > KoRTree::splitNode(typename KoRTree::Node* node) { //debugFlake << "KoRTree::splitNode" << node; Node * n1; Node * n2; if (node->isLeaf()) { n1 = createLeafNode(m_capacity + 1, node->level(), node->parent()); n2 = createLeafNode(m_capacity + 1, node->level(), node->parent()); } else { n1 = createNonLeafNode(m_capacity + 1, node->level(), node->parent()); n2 = createNonLeafNode(m_capacity + 1, node->level(), node->parent()); } //debugFlake << " n1" << n1 << n1->nodeId(); //debugFlake << " n2" << n2 << n2->nodeId(); QVector marker(m_capacity + 1); QPair seeds(pickSeeds(node)); n1->move(node, seeds.first); n2->move(node, seeds.second); marker[seeds.first] = true; marker[seeds.second] = true; // There is one more in a node to split than the capacity and as we // already put the seeds in the new nodes subtract them. int remaining = m_capacity + 1 - 2; while (remaining > 0) { if (m_minimum - n1->childCount() == remaining) { for (int i = 0; i < m_capacity + 1; ++i) { if (!marker[i]) { n1->move(node, i); --remaining; } } } else if (m_minimum - n2->childCount() == remaining) { for (int i = 0; i < m_capacity + 1; ++i) { if (!marker[i]) { n2->move(node, i); --remaining; } } } else { QPair next(pickNext(node, marker, n1, n2)); if (next.first == 0) { n1->move(node, next.second); } else { n2->move(node, next.second); } --remaining; } } Q_ASSERT(n1->childCount() + n2->childCount() == node->childCount()); // move the data back to the old node // this has to be done as the current node is already in the tree. node->clear(); for (int i = 0; i < n1->childCount(); ++i) { node->move(n1, i); } //debugFlake << " delete n1" << n1 << n1->nodeId(); // clear is needed as the data items are not removed n1->clear(); delete n1; return qMakePair(node, n2); } template QPair KoRTree::pickSeeds(Node *node) { int s1 = 0; int s2 = 1; qreal max = 0; for (int i = 0; i < m_capacity + 1; ++i) { for (int j = i+1; j < m_capacity + 1; ++j) { if (i != j) { QRectF bb1(node->childBoundingBox(i)); QRectF bb2(node->childBoundingBox(j)); QRectF comp(node->childBoundingBox(i).united(node->childBoundingBox(j))); qreal area = comp.width() * comp.height() - bb1.width() * bb1.height() - bb2.width() * bb2.height(); //debugFlake << " ps" << i << j << area; if (area > max) { max = area; s1 = i; s2 = j; } } } } return qMakePair(s1, s2); } template QPair KoRTree::pickNext(Node * node, QVector & marker, Node * group1, Node * group2) { //debugFlake << "KoRTree::pickNext" << marker; qreal max = -1.0; int select = 0; int group = 0; for (int i = 0; i < m_capacity + 1; ++i) { if (marker[i] == false) { QRectF bb1 = group1->boundingBox().united(node->childBoundingBox(i)); QRectF bb2 = group2->boundingBox().united(node->childBoundingBox(i)); qreal d1 = bb1.width() * bb1.height() - group1->boundingBox().width() * group1->boundingBox().height(); qreal d2 = bb2.width() * bb2.height() - group2->boundingBox().width() * group2->boundingBox().height(); qreal diff = qAbs(d1 - d2); //debugFlake << " diff" << diff << i << d1 << d2; if (diff > max) { max = diff; select = i; //debugFlake << " i =" << i; if (qAbs(d1) > qAbs(d2)) { group = 1; } else { group = 0; } //debugFlake << " group =" << group; } } } marker[select] = true; return qMakePair(group, select); } template void KoRTree::adjustTree(Node *node1, Node *node2) { //debugFlake << "KoRTree::adjustTree"; if (node1->isRoot()) { //debugFlake << " root"; if (node2) { NonLeafNode * newRoot = createNonLeafNode(m_capacity + 1, node1->level() + 1, 0); newRoot->insert(node1->boundingBox(), node1); newRoot->insert(node2->boundingBox(), node2); m_root = newRoot; //debugFlake << "new root" << m_root->nodeId(); } } else { NonLeafNode * parent = dynamic_cast(node1->parent()); if (!parent) { qFatal("KoRTree::adjustTree: no parent node found!"); return; } //QRectF pbbold( parent->boundingBox() ); parent->setChildBoundingBox(node1->place(), node1->boundingBox()); parent->updateBoundingBox(); //debugFlake << " bb1 =" << node1->boundingBox() << node1->place() << pbbold << "->" << parent->boundingBox() << parent->nodeId(); if (!node2) { //debugFlake << " update"; adjustTree(parent, 0); } else { if (parent->childCount() < m_capacity) { //debugFlake << " no split needed"; parent->insert(node2->boundingBox(), node2); adjustTree(parent, 0); } else { //debugFlake << " split again"; parent->insert(node2->boundingBox(), node2); QPair newNodes = splitNode(parent); adjustTree(newNodes.first, newNodes.second); } } } } template void KoRTree::condenseTree(Node *node, QVector & reinsert) { //debugFlake << "KoRTree::condenseTree begin reinsert.size()" << reinsert.size(); if (!node->isRoot()) { Node * parent = node->parent(); //debugFlake << " !node->isRoot us" << node->childCount(); if (node->childCount() < m_minimum) { //debugFlake << " remove node"; parent->remove(node->place()); reinsert.push_back(node); /** * WARNING: here we leave the tree in an inconsistent state! 'reinsert' * nodes may still be kept in m_leafMap structure, but we will * *not* remove them for the efficiency reasons. They are guaranteed * to be readded in remove(). */ } else { //debugFlake << " update BB parent is root" << parent->isRoot(); parent->setChildBoundingBox(node->place(), node->boundingBox()); parent->updateBoundingBox(); } condenseTree(parent, reinsert); } else { //debugFlake << " node->isRoot us" << node->childCount(); if (node->childCount() == 1 && !node->isLeaf()) { //debugFlake << " usedSpace = 1"; NonLeafNode * n = dynamic_cast(node); if (n) { Node * kid = n->getNode(0); // clear is needed as the data items are not removed m_root->clear(); delete m_root; m_root = kid; m_root->setParent(0); //debugFlake << " new root" << m_root; } else { qFatal("KoRTree::condenseTree cast to NonLeafNode failed"); } } } //debugFlake << "KoRTree::condenseTree end reinsert.size()" << reinsert.size(); } #ifdef CALLIGRA_RTREE_DEBUG template QColor KoRTree::Node::levelColor[] = { QColor(Qt::green), QColor(Qt::red), QColor(Qt::cyan), QColor(Qt::magenta), QColor(Qt::yellow), }; template int KoRTree::Node::nodeIdCnt = 0; #endif template KoRTree::Node::Node(int capacity, int level, Node * parent) : m_parent(parent) , m_childBoundingBox(capacity) , m_counter(0) #ifdef CALLIGRA_RTREE_DEBUG , m_nodeId(nodeIdCnt++) #endif , m_level(level) { } template void KoRTree::Node::remove(int index) { for (int i = index + 1; i < m_counter; ++i) { m_childBoundingBox[i-1] = m_childBoundingBox[i]; } --m_counter; updateBoundingBox(); } template void KoRTree::Node::updateBoundingBox() { m_boundingBox = QRectF(); for (int i = 0; i < m_counter; ++i) { m_boundingBox = m_boundingBox.united(m_childBoundingBox[i]); } } template void KoRTree::Node::clear() { m_counter = 0; m_boundingBox = QRectF(); } #ifdef CALLIGRA_RTREE_DEBUG template void KoRTree::Node::paintRect(QPainter & p, int level) const { QColor c(Qt::black); if (level < levelColorSize) { c = levelColor[level]; } QPen pen(c, 0); p.setPen(pen); QRectF bbdraw(this->m_boundingBox); bbdraw.adjust(level * 2, level * 2, -level * 2, -level * 2); p.drawRect(bbdraw); } #endif template KoRTree::NonLeafNode::NonLeafNode(int capacity, int level, Node * parent) : Node(capacity, level, parent) , m_childs(capacity) { //debugFlake << "NonLeafNode::NonLeafNode()" << this; } template KoRTree::NonLeafNode::~NonLeafNode() { //debugFlake << "NonLeafNode::~NonLeafNode()" << this; for (int i = 0; i < this->m_counter; ++i) { delete m_childs[i]; } } template void KoRTree::NonLeafNode::insert(const QRectF& bb, Node * data) { m_childs[this->m_counter] = data; data->setPlace(this->m_counter); data->setParent(this); this->m_childBoundingBox[this->m_counter] = bb; this->m_boundingBox = this->m_boundingBox.united(bb); //debugFlake << "NonLeafNode::insert" << this->nodeId() << data->nodeId(); ++this->m_counter; } template void KoRTree::NonLeafNode::remove(int index) { for (int i = index + 1; i < this->m_counter; ++i) { m_childs[i-1] = m_childs[i]; m_childs[i-1]->setPlace(i - 1); } Node::remove(index); } template void KoRTree::NonLeafNode::move(Node * node, int index) { //debugFlake << "NonLeafNode::move" << this << node << index << node->nodeId() << "->" << this->nodeId(); NonLeafNode * n = dynamic_cast(node); if (n) { QRectF bb = n->childBoundingBox(index); insert(bb, n->getNode(index)); } } template typename KoRTree::LeafNode * KoRTree::NonLeafNode::chooseLeaf(const QRectF& bb) { return getLeastEnlargement(bb)->chooseLeaf(bb); } template typename KoRTree::NonLeafNode * KoRTree::NonLeafNode::chooseNode(const QRectF& bb, int level) { if (this->m_level > level) { return getLeastEnlargement(bb)->chooseNode(bb, level); } else { return this; } } template void KoRTree::NonLeafNode::intersects(const QRectF& rect, QMap & result) const { for (int i = 0; i < this->m_counter; ++i) { if (this->m_childBoundingBox[i].intersects(rect)) { m_childs[i]->intersects(rect, result); } } } template void KoRTree::NonLeafNode::contains(const QPointF & point, QMap & result) const { for (int i = 0; i < this->m_counter; ++i) { if (this->m_childBoundingBox[i].contains(point)) { m_childs[i]->contains(point, result); } } } template void KoRTree::NonLeafNode::contained(const QRectF& rect, QMap & result) const { for (int i = 0; i < this->m_counter; ++i) { if (this->m_childBoundingBox[i].intersects(rect)) { m_childs[i]->contained(rect, result); } } } template void KoRTree::NonLeafNode::keys(QList & result) const { for (int i = 0; i < this->m_counter; ++i) { m_childs[i]->keys(result); } } template void KoRTree::NonLeafNode::values(QMap & result) const { for (int i = 0; i < this->m_counter; ++i) { m_childs[i]->values(result); } } template typename KoRTree::Node * KoRTree::NonLeafNode::getNode(int index) const { return m_childs[index]; } template typename KoRTree::Node * KoRTree::NonLeafNode::getLeastEnlargement(const QRectF& bb) const { //debugFlake << "NonLeafNode::getLeastEnlargement"; QVarLengthArray area(this->m_counter); for (int i = 0; i < this->m_counter; ++i) { QSizeF big(this->m_childBoundingBox[i].united(bb).size()); area[i] = big.width() * big.height() - this->m_childBoundingBox[i].width() * this->m_childBoundingBox[i].height(); } int minIndex = 0; qreal minArea = area[minIndex]; //debugFlake << " min" << minIndex << minArea; for (int i = 1; i < this->m_counter; ++i) { if (area[i] < minArea) { minIndex = i; minArea = area[i]; //debugFlake << " min" << minIndex << minArea; } } return m_childs[minIndex]; } #ifdef CALLIGRA_RTREE_DEBUG template void KoRTree::NonLeafNode::debug(QString line) const { for (int i = 0; i < this->m_counter; ++i) { qDebug("%s %d %d", qPrintable(line), this->nodeId(), i); m_childs[i]->debug(line + " "); } } template void KoRTree::NonLeafNode::paint(QPainter & p, int level) const { this->paintRect(p, level); for (int i = 0; i < this->m_counter; ++i) { m_childs[i]->paint(p, level + 1); } } #endif template int KoRTree::LeafNode::dataIdCounter = 0; template KoRTree::LeafNode::LeafNode(int capacity, int level, Node * parent) : Node(capacity, level, parent) , m_data(capacity) , m_dataIds(capacity) { //debugFlake << "LeafNode::LeafNode" << this; } template KoRTree::LeafNode::~LeafNode() { //debugFlake << "LeafNode::~LeafNode" << this; } template void KoRTree::LeafNode::insert(const QRectF& bb, const T& data, int id) { m_data[this->m_counter] = data; m_dataIds[this->m_counter] = id; this->m_childBoundingBox[this->m_counter] = bb; this->m_boundingBox = this->m_boundingBox.united(bb); ++this->m_counter; } template void KoRTree::LeafNode::remove(int index) { for (int i = index + 1; i < this->m_counter; ++i) { m_data[i-1] = m_data[i]; m_dataIds[i-1] = m_dataIds[i]; } Node::remove(index); } template void KoRTree::LeafNode::remove(const T& data) { int old_counter = this->m_counter; for (int i = 0; i < this->m_counter; ++i) { if (m_data[i] == data) { //debugFlake << "LeafNode::remove id" << i; remove(i); break; } } if (old_counter == this->m_counter) { qWarning() << "LeafNode::remove( const T&data) data not found"; } } template void KoRTree::LeafNode::move(Node * node, int index) { LeafNode * n = dynamic_cast(node); if (n) { //debugFlake << "LeafNode::move" << this << node << index // << node->nodeId() << "->" << this->nodeId() << n->childBoundingBox( index ); QRectF bb = n->childBoundingBox(index); insert(bb, n->getData(index), n->getDataId(index)); } } template typename KoRTree::LeafNode * KoRTree::LeafNode::chooseLeaf(const QRectF& bb) { Q_UNUSED(bb); return this; } template typename KoRTree::NonLeafNode * KoRTree::LeafNode::chooseNode(const QRectF& bb, int level) { Q_UNUSED(bb); Q_UNUSED(level); qFatal("LeafNode::chooseNode called. This should not happen!"); return 0; } template void KoRTree::LeafNode::intersects(const QRectF& rect, QMap & result) const { for (int i = 0; i < this->m_counter; ++i) { if (this->m_childBoundingBox[i].intersects(rect)) { result.insert(m_dataIds[i], m_data[i]); } } } template void KoRTree::LeafNode::contains(const QPointF & point, QMap & result) const { for (int i = 0; i < this->m_counter; ++i) { if (this->m_childBoundingBox[i].contains(point)) { result.insert(m_dataIds[i], m_data[i]); } } } template void KoRTree::LeafNode::contained(const QRectF& rect, QMap & result) const { for (int i = 0; i < this->m_counter; ++i) { if (rect.contains(this->m_childBoundingBox[i])) { result.insert(m_dataIds[i], m_data[i]); } } } template void KoRTree::LeafNode::keys(QList & result) const { for (int i = 0; i < this->m_counter; ++i) { result.push_back(this->m_childBoundingBox[i]); } } template void KoRTree::LeafNode::values(QMap & result) const { for (int i = 0; i < this->m_counter; ++i) { result.insert(m_dataIds[i], m_data[i]); } } template const T& KoRTree::LeafNode::getData(int index) const { return m_data[ index ]; } template int KoRTree::LeafNode::getDataId(int index) const { return m_dataIds[ index ]; } #ifdef CALLIGRA_RTREE_DEBUG template void KoRTree::LeafNode::debug(QString line) const { for (int i = 0; i < this->m_counter; ++i) { qDebug("%s %d %d %p", qPrintable(line), this->nodeId(), i, &(m_data[i])); debugFlake << this->m_childBoundingBox[i].toRect(); } } template void KoRTree::LeafNode::paint(QPainter & p, int level) const { if (this->m_counter) { this->paintRect(p, level); } } #endif #endif /* KORTREE_H */ diff --git a/libs/flake/KoShape.h b/libs/flake/KoShape.h index b06340545e..ecbfa117de 100644 --- a/libs/flake/KoShape.h +++ b/libs/flake/KoShape.h @@ -1,1305 +1,1305 @@ /* This file is part of the KDE project Copyright (C) 2006-2008 Thorsten Zachmann Copyright (C) 2006, 2008 C. Boemann Copyright (C) 2006-2010 Thomas Zander Copyright (C) 2007-2009,2011 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. */ #ifndef KOSHAPE_H #define KOSHAPE_H #include "KoFlake.h" #include "KoFlakeTypes.h" #include "KoConnectionPoint.h" #include #include #include #include #include "kritaflake_export.h" class QPainter; class QRectF; class QPainterPath; class QTransform; class KoShapeContainer; class KoShapeStrokeModel; class KoShapeUserData; class KoViewConverter; class KoShapeApplicationData; class KoShapeSavingContext; class KoShapeLoadingContext; class KoGenStyle; class KoShapeShadow; class KoShapePrivate; class KoFilterEffectStack; class KoSnapData; class KoClipPath; class KoClipMask; class KoShapePaintingContext; class KoShapeAnchor; class KoBorder; struct KoInsets; class KoShapeBackground; class KisHandlePainterHelper; /** * * Base class for all flake shapes. Shapes extend this class * to allow themselves to be manipulated. This class just represents * a graphical shape in the document and can be manipulated by some default * tools in this library. * * Due to the limited responsibility of this class, the extending object * can have any data backend and is responsible for painting itself. * * We strongly suggest that any extending class will use a Model View * Controller (MVC) design where the View part is all in this class, as well * as the one that inherits from this one. This allows the data that rests * in the model to be reused in different parts of the document. For example * by having two flake objects that show that same data. Or each showing a section of it. * * The KoShape data is completely in postscript-points (pt) (see KoUnit * for conversion methods to and from points). * This image will explain the real-world use of the shape and its options. *
* The Rotation center can be returned with absolutePosition() * *

Flake objects can be created in three ways: *

    *
  • a simple new KoDerivedFlake(), *
  • through an associated tool, *
  • through a factory *
* *

Shape interaction notifications

* We had several notification methods that allow your shape to be notified of changes in other * shapes positions or rotation etc. *
  1. The most general is KoShape::shapeChanged().
    * a virtual method that you can use to check various changed to your shape made by tools or otherwise.
  2. *
  3. for shape hierarchies the parent may receive a notification when a child was modified. * This is done though KoShapeContainerModel::childChanged()
  4. *
  5. any shape that is at a similar position as another shape there is collision detection. * You can register your shape to be sensitive to any changes like moving or whatever to * other shapes that intersect yours. * Such changes will then be notified to your shape using the method from (1) You should call * KoShape::setCollisionDetection(bool) to enable this. *
*/ class KRITAFLAKE_EXPORT KoShape { public: /// Used by shapeChanged() to select which change was made enum ChangeType { PositionChanged, ///< used after a setPosition() RotationChanged, ///< used after a setRotation() ScaleChanged, ///< used after a scale() ShearChanged, ///< used after a shear() SizeChanged, ///< used after a setSize() GenericMatrixChange, ///< used after the matrix was changed without knowing which property explicitly changed KeepAspectRatioChange, ///< used after setKeepAspectRatio() ParentChanged, ///< used after a setParent() CollisionDetected, ///< used when another shape moved in our boundingrect Deleted, ///< the shape was deleted StrokeChanged, ///< the shapes stroke has changed BackgroundChanged, ///< the shapes background has changed ShadowChanged, ///< the shapes shadow has changed BorderChanged, ///< the shapes border has changed ParameterChanged, ///< the shapes parameter has changed (KoParameterShape only) ContentChanged, ///< the content of the shape changed e.g. a new image inside a pixmap/text change inside a textshape TextRunAroundChanged, ///< used after a setTextRunAroundSide() ChildChanged, ///< a child of a container was changed/removed. This is propagated to all parents ConnectionPointChanged, ///< a connection point has changed ClipPathChanged, ///< the shapes clip path has changed TransparencyChanged ///< the shapetransparency value has changed }; /// The behavior text should do when intersecting this shape. enum TextRunAroundSide { BiggestRunAroundSide, ///< Run other text around the side that has the most space LeftRunAroundSide, ///< Run other text around the left side of the frame RightRunAroundSide, ///< Run other text around the right side of the frame EnoughRunAroundSide, ///< Run other text dynamically around both sides of the shape, provided there is sufficient space left BothRunAroundSide, ///< Run other text around both sides of the shape NoRunAround, ///< The text will be completely avoiding the frame by keeping the horizontal space that this frame occupies blank. RunThrough ///< The text will completely ignore the frame and layout as if it was not there }; /// The behavior text should do when intersecting this shape. enum TextRunAroundContour { ContourBox, /// Run other text around a bounding rect of the outline ContourFull, ///< Run other text around also on the inside ContourOutside ///< Run other text around only on the outside }; /** * TODO */ enum RunThroughLevel { Background, Foreground }; /** * @brief Constructor */ KoShape(); /** * @brief Destructor */ virtual ~KoShape(); /** * @brief creates a deep copy of the shape or shape's subtree * @return a cloned shape */ virtual KoShape* cloneShape() const; /** * @brief Paint the shape fill * The class extending this one is responsible for painting itself. Since we do not * assume the shape is square the paint must also clear its background if it will draw * something transparent on top. * This can be done with a method like: * painter.fillRect(converter.normalToView(QRectF(QPointF(0.0,0.0), size())), background()); - * Or equavalent for non-square objects. + * Or equivalent for non-square objects. * Do note that a shape's top-left is always at coordinate 0,0. Even if the shape itself is rotated * or translated. * @param painter used for painting the shape * @param converter to convert between internal and view coordinates. * @see applyConversion() * @param paintcontext the painting context. */ virtual void paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintcontext) = 0; /** * @brief paintStroke paints the shape's stroked outline * @param painter used for painting the shape * @param converter to convert between internal and view coordinates. * @see applyConversion() * @param paintcontext the painting context. */ virtual void paintStroke(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintcontext); /** * @brief Paint the shape's border * This is a helper function that could be called from the paint() method of all shapes. * @param painter used for painting the shape * @param converter to convert between internal and view coordinates. * @see applyConversion() */ virtual void paintBorder(QPainter &painter, const KoViewConverter &converter); /** * Load a shape from odf * * @param context the KoShapeLoadingContext used for loading * @param element element which represents the shape in odf * * @return false if loading failed */ virtual bool loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context) = 0; /** * @brief store the shape data as ODF XML. * This is the method that will be called when saving a shape as a described in * OpenDocument 9.2 Drawing Shapes. * @see saveOdfAttributes */ virtual void saveOdf(KoShapeSavingContext &context) const = 0; /** * This method can be used while saving the shape as ODF to add the data * stored on this shape to the current element. * * @param context the context for the current save. * @param attributes a number of OdfAttribute items to state which attributes to save. * @see saveOdf */ void saveOdfAttributes(KoShapeSavingContext &context, int attributes) const; /** * This method can be used while saving the shape as Odf to add common child elements * * The office:event-listeners and draw:glue-point are saved. * @param context the context for the current save. */ void saveOdfCommonChildElements(KoShapeSavingContext &context) const; /** * This method can be used to save contour data from the clipPath() * * The draw:contour-polygon or draw:contour-path elements are saved. * @param context the context for the current save. * @param originalSize the original size of the unscaled image. */ void saveOdfClipContour(KoShapeSavingContext &context, const QSizeF &originalSize) const; /** * @brief Scale the shape using the zero-point which is the top-left corner. * @see position() * * @param sx scale in x direction * @param sy scale in y direction */ void scale(qreal sx, qreal sy); /** * @brief Rotate the shape (relative) * * The shape will be rotated from the current rotation using the center of the shape using the size() * * @param angle change the angle of rotation increasing it with 'angle' degrees */ void rotate(qreal angle); /** * Return the current rotation in degrees. * It returns NaN if the shape has a shearing or scaling transformation applied. */ qreal rotation() const; /** * @brief Shear the shape * The shape will be sheared using the zero-point which is the top-left corner. * @see position() * * @param sx shear in x direction * @param sy shear in y direction */ void shear(qreal sx, qreal sy); /** * @brief Resize the shape * * @param size the new size of the shape. This is different from scaling as * scaling is a so called secondary operation which is comparable to zooming in * instead of changing the size of the basic shape. * Easiest example of this difference is that using this method will not distort the * size of pattern-fills and strokes. */ virtual void setSize(const QSizeF &size); /** * @brief Get the size of the shape in pt. * * The size is in shape coordinates. * * @return the size of the shape as set by setSize() */ virtual QSizeF size() const; /** * @brief Set the position of the shape in pt * * @param position the new position of the shape */ virtual void setPosition(const QPointF &position); /** * @brief Get the position of the shape in pt * * @return the position of the shape */ QPointF position() const; /** * @brief Check if the shape is hit on position * @param position the position where the user clicked. * @return true when it hits. */ virtual bool hitTest(const QPointF &position) const; /** * @brief Get the bounding box of the shape * * This includes the line width and the shadow of the shape * * @return the bounding box of the shape */ virtual QRectF boundingRect() const; /** * Get the united bounding box of a group of shapes. This is a utility * function used in many places in Krita. */ static QRectF boundingRect(const QList &shapes); /** * @return the bounding rect of the outline of the shape measured * in absolute coordinate system. Please note that in contrast to * boundingRect() this rect doesn't include the stroke and other * insets. */ QRectF absoluteOutlineRect(KoViewConverter *converter = 0) const; /** * Same as a member function, but applies to a list of shapes and returns a * united rect. */ static QRectF absoluteOutlineRect(const QList &shapes, KoViewConverter *converter = 0); /** * @brief Add a connector point to the shape * * A connector is a place on the shape that allows a graphical connection to be made * using a line, for example. * * @param point the connection point to add * @return the id of the new connection point */ int addConnectionPoint(const KoConnectionPoint &point); /** * Sets data of connection point with specified id. * * The position of the connector is restricted to the bounding rectangle of the shape. * When setting a default connection point, the new position is ignored, as these * are fixed at their default position. * The function will insert a new connection point if the specified id was not used * before. * * @param connectionPointId the id of the connection point to set * @param point the connection point data * @return false if specified connection point id is invalid, else true */ bool setConnectionPoint(int connectionPointId, const KoConnectionPoint &point); /// Checks if a connection point with the specified id exists bool hasConnectionPoint(int connectionPointId) const; /// Returns connection point with specified connection point id KoConnectionPoint connectionPoint(int connectionPointId) const; /** * Return a list of the connection points that have been added to this shape. * All the points are relative to the shape position, see absolutePosition(). * @return a list of the connectors that have been added to this shape. */ KoConnectionPoints connectionPoints() const; /// Removes connection point with specified id void removeConnectionPoint(int connectionPointId); /// Removes all connection points void clearConnectionPoints(); /** * Return the side text should flow around this shape. This implements the ODF style:wrap * attribute that specifies how text is displayed around a frame or graphic object. */ TextRunAroundSide textRunAroundSide() const; /** * Set the side text should flow around this shape. * @param side the requested side * @param runThrought run through the foreground or background or... */ void setTextRunAroundSide(TextRunAroundSide side, RunThroughLevel runThrough = Background); /** * The space between this shape's left edge and text that runs around this shape. * @return the space around this shape to keep free from text */ qreal textRunAroundDistanceLeft() const; /** * Set the space between this shape's left edge and the text that run around this shape. * @param distance the space around this shape to keep free from text */ void setTextRunAroundDistanceLeft(qreal distance); /** * The space between this shape's top edge and text that runs around this shape. * @return the space around this shape to keep free from text */ qreal textRunAroundDistanceTop() const; /** * Set the space between this shape's top edge and the text that run around this shape. * @param distance the space around this shape to keep free from text */ void setTextRunAroundDistanceTop(qreal distance); /** * The space between this shape's right edge and text that runs around this shape. * @return the space around this shape to keep free from text */ qreal textRunAroundDistanceRight() const; /** * Set the space between this shape's right edge and the text that run around this shape. * @param distance the space around this shape to keep free from text */ void setTextRunAroundDistanceRight(qreal distance); /** * The space between this shape's bottom edge and text that runs around this shape. * @return the space around this shape to keep free from text */ qreal textRunAroundDistanceBottom() const; /** * Set the space between this shape's bottom edge and the text that run around this shape. * @param distance the space around this shape to keep free from text */ void setTextRunAroundDistanceBottom(qreal distance); /** * Return the threshold above which text should flow around this shape. * The text will not flow around the shape on a side unless the space available on that side * is above this threshold. Only used when the text run around side is EnoughRunAroundSide. * @return threshold the threshold */ qreal textRunAroundThreshold() const; /** * Set the threshold above which text should flow around this shape. * The text will not flow around the shape on a side unless the space available on that side * is above this threshold. Only used when the text run around side is EnoughRunAroundSide. * @param threshold the new threshold */ void setTextRunAroundThreshold(qreal threshold); /** * Return the how tight text run around is done around this shape. * @return the contour */ TextRunAroundContour textRunAroundContour() const; /** * Set how tight text run around is done around this shape. * @param contour the new contour */ void setTextRunAroundContour(TextRunAroundContour contour); /** * Set the KoShapeAnchor */ void setAnchor(KoShapeAnchor *anchor); /** * Return the KoShapeAnchor, or 0 */ KoShapeAnchor *anchor() const; /** * Set the minimum height of the shape. * Currently it's not respected but only for informational purpose * @param minimumShapeHeight the minimum height of the frame. */ void setMinimumHeight(qreal height); /** * Return the minimum height of the shape. * @return the minimum height of the shape. Default is 0.0. */ qreal minimumHeight() const; /** * Set the background of the shape. * A shape background can be a plain color, a gradient, a pattern, be fully transparent * or have a complex fill. * Setting such a background will allow the shape to be filled and will be able to tell * if it is transparent or not. * * If the shape inherited the background from its parent, its stops inheriting it, that * is inheritBackground property resets to false. * * @param background the new shape background. */ void setBackground(QSharedPointer background); /** * return the brush used to paint te background of this shape with. * A QBrush can have a plain color, be fully transparent or have a complex fill. * setting such a brush will allow the shape to fill itself using that brush and * will be able to tell if its transparent or not. * @return the background-brush */ QSharedPointer background() const; /** * @brief setInheritBackground marks a shape as inhiriting the background * from the parent shape. NOTE: The currently selected background is destroyed. * @param value true if the shape should inherit the filling background */ void setInheritBackground(bool value); /** * @brief inheritBackground shows if the shape inherits background from its parent * @return true if the shape inherits the fill */ bool inheritBackground() const; /** * Returns true if there is some transparency, false if the shape is fully opaque. * The default implementation will just return if the background has some transparency, * you should override it and always return true if your shape is not square. * @return if the shape is (partly) transparent. */ virtual bool hasTransparency() const; /** * Sets shape level transparency. * @param transparency the new shape level transparency */ void setTransparency(qreal transparency); /** * Returns the shape level transparency. * @param recursive when true takes the parents transparency into account */ qreal transparency(bool recursive=false) const; /** * Retrieve the z-coordinate of this shape. * The zIndex property is used to determine which shape lies on top of other objects. * An shape with a higher z-order is on top, and can obscure another shape. * @return the z-index of this shape. * @see setZIndex() */ qint16 zIndex() const; /** * Set the z-coordinate of this shape. * The zIndex property is used to determine which shape lies on top of other objects. * An shape with a higher z-order is on top, and can obscure, another shape. *

Just like two objects having the same x or y coordinate will make them 'touch', * so will two objects with the same z-index touch on the z plane. In layering the * shape this, however, can cause a little confusion as one always has to be on top. * The layering if two overlapping objects have the same index is implementation dependent * and probably depends on the order in which they are added to the shape manager. * @param zIndex the new z-index; */ void setZIndex(qint16 zIndex); /** * Maximum value of z-index */ static const qint16 maxZIndex; /** * Minimum value of z-index */ static const qint16 minZIndex; /** * Retrieve the run through property of this shape. * The run through property is used to determine if the shape is behind, inside or before text. * @return the run through of this shape. */ int runThrough(); /** * Set the run through property of this shape. * The run through property is used to determine if the shape is behind, inside or before text. * @param runThrough the new run through; */ virtual void setRunThrough(short int runThrough); /** * Changes the Shape to be visible or invisible. * Being visible means being painted, as well as being used for * things like guidelines or searches. * @param on when true; set the shape to be visible. * @see setGeometryProtected(), setContentProtected(), setSelectable() */ void setVisible(bool on); /** * Returns current visibility state of this shape. * Being visible means being painted, as well as being used for * things like guidelines or searches. * @param recursive when true, checks visibility recursively * @return current visibility state of this shape. * @see isGeometryProtected(), isContentProtected(), isSelectable() */ bool isVisible(bool recursive = true) const; /** * Changes the shape to be printable or not. The default is true. * * If a Shape's print flag is true, the shape will be printed. If * false, the shape will not be printed. If a shape is not visible (@see isVisible), * it isPrinted will return false, too. */ void setPrintable(bool on); /** * Returns the current printable state of this shape. * * A shape can be visible but not printable, not printable and not visible * or visible and printable, but not invisible and still printable. * * @return current printable state of this shape. */ bool isPrintable() const; /** * Makes it possible for the user to select this shape. * This parameter defaults to true. * @param selectable when true; set the shape to be selectable by the user. * @see setGeometryProtected(), setContentProtected(), setVisible() */ void setSelectable(bool selectable); /** * Returns if this shape can be selected by the user. * @return true only when the object is selectable. * @see isGeometryProtected(), isContentProtected(), isVisible() */ bool isSelectable() const; /** * Tells the shape to have its position/rotation and size protected from user-changes. * The geometry being protected means the user can not change shape or position of the * shape. This includes any matrix operation such as rotation. * @param on when true; set the shape to have its geometry protected. * @see setContentProtected(), setSelectable(), setVisible() */ void setGeometryProtected(bool on); /** * Returns current geometry protection state of this shape. * The geometry being protected means the user can not change shape or position of the * shape. This includes any matrix operation such as rotation. * @return current geometry protection state of this shape. * @see isContentProtected(), isSelectable(), isVisible() */ bool isGeometryProtected() const; /** * Marks the shape to have its content protected against editing. * Content protection is a hint for tools to disallow the user editing the content. * @param protect when true set the shapes content to be protected from user modification. * @see setGeometryProtected(), setSelectable(), setVisible() */ void setContentProtected(bool protect); /** * Returns current content protection state of this shape. * Content protection is a hint for tools to disallow the user editing the content. * @return current content protection state of this shape. * @see isGeometryProtected(), isSelectable(), isVisible() */ bool isContentProtected() const; /** * Returns the parent, or 0 if there is no parent. * @return the parent, or 0 if there is no parent. */ KoShapeContainer *parent() const; /** * Set the parent of this shape. * @param parent the new parent of this shape. Can be 0 if the shape has no parent anymore. */ void setParent(KoShapeContainer *parent); /** * @brief inheritsTransformFromAny checks if the shape inherits transformation from * any of the shapes listed in \p ancestorsInQuestion. The inheritance is checked * in recursive way. * @return true if there is a (transitive) transformation-wise parent found in \p ancestorsInQuestion */ bool inheritsTransformFromAny(const QList ancestorsInQuestion) const; /** * @return true if this shape has a common parent with \p shape */ bool hasCommonParent(const KoShape *shape) const; /** * Request a repaint to be queued. * The repaint will be of the entire Shape, including its selection handles should this * shape be selected. *

This method will return immediately and only request a repaint. Successive calls * will be merged into an appropriate repaint action. */ virtual void update() const; /** * Request a repaint to be queued. * The repaint will be restricted to the parameters rectangle, which is expected to be * in absolute coordinates of the canvas and it is expected to be * normalized. *

This method will return immediately and only request a repaint. Successive calls * will be merged into an appropriate repaint action. * @param rect the rectangle (in pt) to queue for repaint. */ virtual void updateAbsolute(const QRectF &rect) const; /// Used by compareShapeZIndex() to order shapes enum ChildZOrderPolicy { ChildZDefault, ChildZParentChild = ChildZDefault, ///< normal parent/child ordering ChildZPassThrough ///< children are considered equal to this shape }; /** * Returns if during compareShapeZIndex() how this shape portrays the values * of its children. The default behaviour is to let this shape's z values take * the place of its childrens values, so you get a parent/child relationship. * The children are naturally still ordered relatively to their z values * * But for special cases (like Calligra's TextShape) it can be overloaded to return * ChildZPassThrough which means the children keep their own z values * @returns the z order policy of this shape */ virtual ChildZOrderPolicy childZOrderPolicy(); /** * This is a method used to sort a list using the STL sorting methods. * @param s1 the first shape * @param s2 the second shape */ static bool compareShapeZIndex(KoShape *s1, KoShape *s2); /** * returns the outline of the shape in the form of a path. * The outline returned will always be relative to the position() of the shape, so * moving the shape will not alter the result. The outline is used to draw the stroke * on, for example. * @returns the outline of the shape in the form of a path. */ virtual QPainterPath outline() const; /** * returns the outline of the shape in the form of a rect. * The outlineRect returned will always be relative to the position() of the shape, so * moving the shape will not alter the result. The outline is used to calculate * the boundingRect. * @returns the outline of the shape in the form of a rect. */ virtual QRectF outlineRect() const; /** * returns the outline of the shape in the form of a path for the use of painting a shadow. * * Normally this would be the same as outline() if there is a fill (background) set on the * shape and empty if not. However, a shape could reimplement this to return an outline * even if no fill is defined. A typical example of this would be the picture shape * which has a picture but almost never a background. * * @returns the outline of the shape in the form of a path. */ virtual QPainterPath shadowOutline() const; /** * Returns the currently set stroke, or 0 if there is no stroke. * @return the currently set stroke, or 0 if there is no stroke. */ KoShapeStrokeModelSP stroke() const; /** * Set a new stroke, removing the old one. The stroke inheritance becomes disabled. * @param stroke the new stroke, or 0 if there should be no stroke. */ void setStroke(KoShapeStrokeModelSP stroke); /** * @brief setInheritStroke marks a shape as inhiriting the stroke * from the parent shape. NOTE: The currently selected stroke is destroyed. * @param value true if the shape should inherit the stroke style */ void setInheritStroke(bool value); /** * @brief inheritStroke shows if the shape inherits the stroke from its parent * @return true if the shape inherits the stroke style */ bool inheritStroke() const; /** * Return the insets of the stroke. * Convenience method for KoShapeStrokeModel::strokeInsets() */ KoInsets strokeInsets() const; /// Sets the new shadow, removing the old one void setShadow(KoShapeShadow *shadow); /// Returns the currently set shadow or 0 if there is no shadow set KoShapeShadow *shadow() const; /// Sets the new border, removing the old one. void setBorder(KoBorder *border); /// Returns the currently set border or 0 if there is no border set KoBorder *border() const; /// Sets a new clip path, removing the old one void setClipPath(KoClipPath *clipPath); /// Returns the currently set clip path or 0 if there is no clip path set KoClipPath * clipPath() const; /// Sets a new clip mask, removing the old one. The mask is owned by the shape. void setClipMask(KoClipMask *clipMask); /// Returns the currently set clip mask or 0 if there is no clip mask set KoClipMask* clipMask() const; /** * Setting the shape to keep its aspect-ratio has the effect that user-scaling will * keep the width/height ratio intact so as not to distort shapes that rely on that * ratio. * @param keepAspect the new value */ void setKeepAspectRatio(bool keepAspect); /** * Setting the shape to keep its aspect-ratio has the effect that user-scaling will * keep the width/height ratio intact so as not to distort shapes that rely on that * ratio. * @return whether to keep aspect ratio of this shape */ bool keepAspectRatio() const; /** * Return the position of this shape regardless of rotation/skew/scaling and regardless of * this shape having a parent (being in a group) or not.
* @param anchor The place on the (unaltered) shape that you want the position of. * @return the point that is the absolute, centered position of this shape. */ QPointF absolutePosition(KoFlake::AnchorPosition anchor = KoFlake::Center) const; /** * Move this shape to an absolute position where the end location will be the same * regardless of the shape's rotation/skew/scaling and regardless of this shape having * a parent (being in a group) or not.
* The newPosition is going to be the center of the shape. * This has the convenient effect that:

     shape->setAbsolutePosition(QPointF(0,0));
     shape->rotate(45);
Will result in the same visual position of the shape as the opposite:
     shape->rotate(45);
     shape->setAbsolutePosition(QPointF(0,0));
* @param newPosition the new absolute center of the shape. * @param anchor The place on the (unaltered) shape that you set the position of. */ void setAbsolutePosition(const QPointF &newPosition, KoFlake::AnchorPosition anchor = KoFlake::Center); /** * Set a data object on the shape to be used by an application. * This is specifically useful when a shape is created in a plugin and that data from that * shape should be accessible outside the plugin. * @param userData the new user data, or 0 to delete the current one. */ void setUserData(KoShapeUserData *userData); /** * Return the current userData. */ KoShapeUserData *userData() const; /** * Return the Id of this shape, identifying the type of shape by the id of the factory. * @see KoShapeFactoryBase::shapeId() * @return the id of the shape-type */ QString shapeId() const; /** * Set the Id of this shape. A shapeFactory is expected to set the Id at creation * so applications can find out what kind of shape this is. * @see KoShapeFactoryBase::shapeId() * @param id the ID from the factory that created this shape */ void setShapeId(const QString &id); /** * Create a matrix that describes all the transformations done on this shape. * * The absolute transformation is the combined transformation of this shape * and all its parents and grandparents. * * @param converter if not null, this method uses the converter to mark the right * offsets in the current view. */ QTransform absoluteTransformation(const KoViewConverter *converter) const; /** * Applies a transformation to this shape. * * The transformation given is relative to the global coordinate system, i.e. the document. * This is a convenience function to apply a global transformation to this shape. * @see applyTransformation * * @param matrix the transformation matrix to apply */ void applyAbsoluteTransformation(const QTransform &matrix); /** * Sets a new transformation matrix describing the local transformations on this shape. * @param matrix the new transformation matrix */ void setTransformation(const QTransform &matrix); /// Returns the shapes local transformation matrix QTransform transformation() const; /** * Applies a transformation to this shape. * * The transformation given is relative to the shape coordinate system. * * @param matrix the transformation matrix to apply */ void applyTransformation(const QTransform &matrix); /** * Copy all the settings from the parameter shape and apply them to this shape. * Settings like the position and rotation to visible and locked. The parent * is a notable exclusion. * @param shape the shape to use as original */ void copySettings(const KoShape *shape); /** * Convenience method that allows people implementing paint() to use the shape * internal coordinate system directly to paint itself instead of considering the * views zoom. * @param painter the painter to alter the zoom level of. * @param converter the converter for the current views zoom. */ static void applyConversion(QPainter &painter, const KoViewConverter &converter); /** * A convenience method that creates a handles helper with applying transformations at * the same time. Please note that you shouldn't save/restore additionally. All the work * on restoring original painter's transformations is done by the helper. */ static KisHandlePainterHelper createHandlePainterHelper(QPainter *painter, KoShape *shape, const KoViewConverter &converter, qreal handleRadius = 0.0); /** * @brief Transforms point from shape coordinates to document coordinates * @param point in shape coordinates * @return point in document coordinates */ QPointF shapeToDocument(const QPointF &point) const; /** * @brief Transforms rect from shape coordinates to document coordinates * @param rect in shape coordinates * @return rect in document coordinates */ QRectF shapeToDocument(const QRectF &rect) const; /** * @brief Transforms point from document coordinates to shape coordinates * @param point in document coordinates * @return point in shape coordinates */ QPointF documentToShape(const QPointF &point) const; /** * @brief Transform rect from document coordinates to shape coordinates * @param rect in document coordinates * @return rect in shape coordinates */ QRectF documentToShape(const QRectF &rect) const; /** * Returns the name of the shape. * @return the shapes name */ QString name() const; /** * Sets the name of the shape. * @param name the new shape name */ void setName(const QString &name); /** * Update the position of the shape in the tree of the KoShapeManager. */ void notifyChanged(); /** * A shape can be in a state that it is doing processing data like loading or text layout. * In this case it can be shown on screen probably partially but it should really not be printed * until it is fully done processing. * Warning! This method can be blocking for a long time * @param asynchronous If set to true the processing will can take place in a different thread and the - * function will not block until the shape is finised. + * function will not block until the shape is finished. * In case of printing Flake will call this method from a non-main thread and only * start printing it when the in case of printing method returned. * If set to false the processing needs to be done synchronously and will * block until the result is finished. */ virtual void waitUntilReady(const KoViewConverter &converter, bool asynchronous = true) const; /// checks recursively if the shape or one of its parents is not visible or locked virtual bool isShapeEditable(bool recursive = true) const; /** * Adds a shape which depends on this shape. * Making a shape dependent on this one means it will get shapeChanged() called * on each update of this shape. * * If this shape already depends on the given shape, establishing the * dependency is refused to prevent circular dependencies. * * @param shape the shape which depends on this shape * @return true if dependency could be established, otherwise false * @see removeDependee(), hasDependee() */ bool addDependee(KoShape *shape); /** * Removes as shape depending on this shape. * @see addDependee(), hasDependee() */ void removeDependee(KoShape *shape); /// Returns if the given shape is dependent on this shape bool hasDependee(KoShape *shape) const; /// Returns list of shapes depending on this shape QList dependees() const; /// Returns additional snap data the shape wants to have snapping to virtual KoSnapData snapData() const; /** * Set additional attribute * * This can be used to attach additional attributes to a shape for attributes * that are application specific like presentation:placeholder * * @param name The name of the attribute in the following form prefix:tag e.g. presentation:placeholder * @param value The value of the attribute */ void setAdditionalAttribute(const QString &name, const QString &value); /** * Remove additional attribute * * @param name The name of the attribute in the following form prefix:tag e.g. presentation:placeholder */ void removeAdditionalAttribute(const QString &name); /** * Check if additional attribute is set * * @param name The name of the attribute in the following form prefix:tag e.g. presentation:placeholder * * @return true if there is a attribute with prefix:tag set, false otherwise */ bool hasAdditionalAttribute(const QString &name) const; /** * Get additional attribute * * @param name The name of the attribute in the following form prefix:tag e.g. presentation:placeholder * * @return The value of the attribute if it exists or a null string if not found. */ QString additionalAttribute(const QString &name) const; void setAdditionalStyleAttribute(const char *name, const QString &value); void removeAdditionalStyleAttribute(const char *name); /** * Returns the filter effect stack of the shape * * @return the list of filter effects applied on the shape when rendering. */ KoFilterEffectStack *filterEffectStack() const; /// Sets the new filter effect stack, removing the old one void setFilterEffectStack(KoFilterEffectStack *filterEffectStack); /** * Set the property collision detection. * Setting this to true will result in calls to shapeChanged() with the CollisionDetected * parameter whenever either this or another shape is moved/rotated etc and intersects this shape. * @param detect if true detect collisions. */ void setCollisionDetection(bool detect); /** * get the property collision detection. * @returns true if collision detection is on. */ bool collisionDetection(); /** * Return the tool delegates for this shape. * In Flake a shape being selected will cause the tool manager to make available all tools that * can edit the selected shapes. In some cases selecting one shape should allow the tool to * edit a related shape be available too. The tool delegates allows this to happen by taking * all the shapes in the set into account on tool selection. * Notice that if the set is non-empty 'this' shape is no longer looked at. You can choose * to add itself to the set too. */ QSet toolDelegates() const; /** * Set the tool delegates. * @param delegates the new delegates. * @see toolDelegates() */ void setToolDelegates(const QSet &delegates); /** * Return the hyperlink for this shape. */ QString hyperLink () const; /** * Set hyperlink for this shape. * @param hyperLink name. */ void setHyperLink(const QString &hyperLink); /** * \internal * Returns the private object for use within the flake lib */ KoShapePrivate *priv(); public: struct KRITAFLAKE_EXPORT ShapeChangeListener { virtual ~ShapeChangeListener(); virtual void notifyShapeChanged(ChangeType type, KoShape *shape) = 0; private: friend class KoShape; friend class KoShapePrivate; void registerShape(KoShape *shape); void unregisterShape(KoShape *shape); void notifyShapeChangedImpl(ChangeType type, KoShape *shape); QList m_registeredShapes; }; void addShapeChangeListener(ShapeChangeListener *listener); void removeShapeChangeListener(ShapeChangeListener *listener); public: static QList linearizeSubtree(const QList &shapes); protected: /// constructor KoShape(KoShapePrivate *); /* ** loading saving helper methods */ /// attributes from ODF 1.1 chapter 9.2.15 Common Drawing Shape Attributes enum OdfAttribute { OdfTransformation = 1, ///< Store transformation information OdfSize = 2, ///< Store size information OdfPosition = 8, ///< Store position OdfAdditionalAttributes = 4, ///< Store additional attributes of the shape OdfCommonChildElements = 16, ///< Event actions and connection points OdfLayer = 64, ///< Store layer name OdfStyle = 128, ///< Store the style OdfId = 256, ///< Store the unique ID OdfName = 512, ///< Store the name of the shape OdfZIndex = 1024, ///< Store the z-index OdfViewbox = 2048, ///< Store the viewbox /// A mask for all mandatory attributes OdfMandatories = OdfLayer | OdfStyle | OdfId | OdfName | OdfZIndex, /// A mask for geometry attributes OdfGeometry = OdfPosition | OdfSize, /// A mask for all the attributes OdfAllAttributes = OdfTransformation | OdfGeometry | OdfAdditionalAttributes | OdfMandatories | OdfCommonChildElements }; /** * This method is used during loading of the shape to load common attributes * * @param context the KoShapeLoadingContext used for loading * @param element element which represents the shape in odf * @param attributes a number of OdfAttribute items to state which attributes to load. */ bool loadOdfAttributes(const KoXmlElement &element, KoShapeLoadingContext &context, int attributes); /** * Parses the transformation attribute from the given string * @param transform the transform attribute string * @return the resulting transformation matrix */ QTransform parseOdfTransform(const QString &transform); /** * @brief Saves the style used for the shape * * This method fills the given style object with the stroke and * background properties and then adds the style to the context. * * @param style the style object to fill * @param context used for saving * @return the name of the style * @see saveOdf */ virtual QString saveStyle(KoGenStyle &style, KoShapeSavingContext &context) const; /** * Loads the stroke and fill style from the given element. * * @param element the xml element to load the style from * @param context the loading context used for loading */ virtual void loadStyle(const KoXmlElement &element, KoShapeLoadingContext &context); /// Loads the stroke style KoShapeStrokeModelSP loadOdfStroke(const KoXmlElement &element, KoShapeLoadingContext &context) const; /// Loads the fill style QSharedPointer loadOdfFill(KoShapeLoadingContext &context) const; /// Loads the connection points void loadOdfGluePoints(const KoXmlElement &element, KoShapeLoadingContext &context); /// Loads the clip contour void loadOdfClipContour(const KoXmlElement &element, KoShapeLoadingContext &context, const QSizeF &scaleFactor); /* ** end loading saving */ /** * A hook that allows inheriting classes to do something after a KoShape property changed * This is called whenever the shape, position rotation or scale properties were altered. * @param type an indicator which type was changed. */ virtual void shapeChanged(ChangeType type, KoShape *shape = 0); /// return the current matrix that contains the rotation/scale/position of this shape QTransform transform() const; KoShapePrivate *d_ptr; private: Q_DECLARE_PRIVATE(KoShape) }; Q_DECLARE_METATYPE(KoShape*) #endif diff --git a/libs/flake/KoShapeContainerModel.h b/libs/flake/KoShapeContainerModel.h index f1513952d3..f3a4677e4f 100644 --- a/libs/flake/KoShapeContainerModel.h +++ b/libs/flake/KoShapeContainerModel.h @@ -1,173 +1,173 @@ /* This file is part of the KDE project * Copyright (C) 2006-2007, 2010 Thomas Zander * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOSHAPECONTAINERMODEL_H #define KOSHAPECONTAINERMODEL_H #include "kritaflake_export.h" #include #include #include class KoShapeContainer; /** * The interface for the container model. * This class has no implementation, but only pure virtual methods. You can find a * fully implemented model using KoShapeContainerDefaultModel. Extending this * class and implementing all methods allows you to implement a custom data-backend * for the KoShapeContainer. * @see KoShapeContainer, KoShapeContainerDefaultModel */ class KRITAFLAKE_EXPORT KoShapeContainerModel { public: /// default constructor KoShapeContainerModel(); /// destructor virtual ~KoShapeContainerModel(); void deleteOwnedShapes(); /** * Add a shape to this models store. * @param shape the shape to be managed in the container. */ virtual void add(KoShape *shape) = 0; /** * Remove a shape to be completely separated from the model. * @param shape the shape to be removed. */ virtual void remove(KoShape *shape) = 0; /** * Set the argument shape to have its 'clipping' property set. * * A shape that is clipped by the container will have its visible portion * limited to the area where it intersects with the container. * If a shape is positioned or sized such that it would be painted outside * of the KoShape::outline() of its parent container, setting this property * to true will clip the shape painting to the container outline. * * @param shape the shape for which the property will be changed. * @param clipping the new value */ virtual void setClipped(const KoShape *shape, bool clipping) = 0; /** * Returns if the argument shape has its 'clipping' property set. * * A shape that is clipped by the container will have its visible portion * limited to the area where it intersects with the container. * If a shape is positioned or sized such that it would be painted outside * of the KoShape::outline() of its parent container, setting this property * to true will clip the shape painting to the container outline. * * @return if the argument shape has its 'clipping' property set. * @param shape the shape for which the property will be returned. */ virtual bool isClipped(const KoShape *shape) const = 0; /** * Set the shape to inherit the container transform. * * A shape that inherits the transform of the parent container will have its * share / rotation / skew etc be calculated as being the product of both its * own local transformation and also that of its parent container. * If you set this to true and rotate the container, the shape will get that * rotation as well automatically. * * @param shape the shape for which the property will be changed. * @param inherit the new value */ virtual void setInheritsTransform(const KoShape *shape, bool inherit) = 0; /** * Returns if the shape inherits the container transform. * * A shape that inherits the transform of the parent container will have its * share / rotation / skew etc be calculated as being the product of both its * own local transformation and also that of its parent container. * If you set this to true and rotate the container, the shape will get that * rotation as well automatically. * * @return if the argument shape has its 'inherits transform' property set. * @param shape the shape for which the property will be returned. */ virtual bool inheritsTransform(const KoShape *shape) const = 0; /** * Return the current number of children registered. * @return the current number of children registered. */ virtual int count() const = 0; /** * Return the list of all shapes of this model * @return the list of all shapes */ virtual QList shapes() const = 0; /** * This method is called as a notification that one of the properties of the * container changed. This can be one of size, position, rotation and skew. * Note that clipped children will automatically get all these changes, the model * does not have to do anything for that. * @param container the actual container that changed. * @param type this enum shows which change the container has had. */ virtual void containerChanged(KoShapeContainer *container, KoShape::ChangeType type) = 0; /** * This method is called when the user tries to move a shape that is a shape of the * container this model represents. * The shape itself is not yet moved; it is proposed to be moved over the param move distance. * You can alter the value of the move to affect the actual distance moved. * The default implementation does nothing. * @param shape the shape of this container that the user is trying to move. * @param move the distance that the user proposes to move shape from the current position. */ virtual void proposeMove(KoShape *shape, QPointF &move); /** * This method is called when one of the shape shapes has been modified. * When a shape shape is rotated, moved or scaled/skewed this method will be called * to inform the container of such a change. The change has already happened at the * time this method is called. * The base implementation notifies the grand parent of the shape that there was a - * change in a shape. A reimplentation if this function should call this method when - * overwriding the function. + * change in a shape. A reimplementation if this function should call this method when + * overriding the function. * * @param shape the shape that has been changed * @param type this enum shows which change the shape has had. */ virtual void childChanged(KoShape *shape, KoShape::ChangeType type); virtual void shapeHasBeenAddedToHierarchy(KoShape *shape, KoShapeContainer *addedToSubtree); virtual void shapeToBeRemovedFromHierarchy(KoShape *shape, KoShapeContainer *removedFromSubtree); protected: KoShapeContainerModel(const KoShapeContainerModel &rhs); }; #endif diff --git a/libs/flake/KoShapeController.h b/libs/flake/KoShapeController.h index 28ce73a63e..cde79865e5 100644 --- a/libs/flake/KoShapeController.h +++ b/libs/flake/KoShapeController.h @@ -1,170 +1,170 @@ /* This file is part of the KDE project * * Copyright (C) 2006-2007, 2010 Thomas Zander * Copyright (C) 2006-2008 Thorsten Zachmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOSHAPECONTROLLER_H #define KOSHAPECONTROLLER_H #include "kritaflake_export.h" #include #include #include class KoCanvasBase; class KoShape; class KoShapeContainer; class KoShapeControllerBase; class KUndo2Command; class KoDocumentResourceManager; /** * Class used by tools to maintain the list of shapes. * All applications have some sort of list of all shapes that belong to the document. * The applications implement the KoShapeControllerBase interface (all pure virtuals) * to add and remove shapes from the document. To ensure that an application can expect * a certain protocol to be adhered to when adding/removing shapes, all tools use the API * from this class for maintaining the list of shapes in the document. So no tool gets * to access the application directly. */ class KRITAFLAKE_EXPORT KoShapeController : public QObject { Q_OBJECT public: /** * Create a new Controller; typically not called by applications, only * by the KonCanvasBase constructor. * @param canvas the canvas this controller works for. The canvas can be 0 * @param shapeController the application provided shapeController that we can call. */ KoShapeController(KoCanvasBase *canvas, KoShapeControllerBase *shapeController); /// destructor ~KoShapeController() override; /** * @brief reset sets the canvas and shapebased document to 0. */ void reset(); /** * @brief Add a shape to the document. * If the shape has no parent, the active layer will become its parent. * * @param shape to add to the document * @param parent the parent command if the resulting command is a compound undo command. * * @return command which will insert the shape into the document or 0 if the * insertion was cancelled. The command is not yet executed. */ KUndo2Command* addShape(KoShape *shape, KoShapeContainer *parentShape, KUndo2Command *parent = 0); /** * @brief Add a shape to the document, skipping any dialogs or other user interaction. * * @param shape to add to the document * @param parent the parent command if the resulting command is a compound undo command. * * @return command which will insert the shape into the document. The command is not yet executed. */ KUndo2Command* addShapeDirect(KoShape *shape, KoShapeContainer *parentShape, KUndo2Command *parent = 0); /** * @brief Add shapes to the document, skipping any dialogs or other user interaction. * * @param shapes to add to the document * @param parent the parent command if the resulting command is a compound undo command. * * @return command which will insert the shapes into the document. The command is not yet executed. */ KUndo2Command* addShapesDirect(const QList shape, KoShapeContainer *parentShape, KUndo2Command *parent = 0); /** * @brief Remove a shape from the document. * * @param shape to remove from the document * @param parent the parent command if the resulting command is a compound undo command. * * @return command which will remove the shape from the document. * The command is not yet executed. */ KUndo2Command* removeShape(KoShape *shape, KUndo2Command *parent = 0); /** * Remove a shape from the document. * * @param shapes the set of shapes to remove from the document * @param parent the parent command if the resulting command is a compound undo command. * * @return command which will remove the shape from the document. * The command is not yet executed. */ KUndo2Command* removeShapes(const QList &shapes, KUndo2Command *parent = 0); /** * @brief Set the KoShapeControllerBase used to add/remove shapes. * * NOTE: only Sheets uses this method. Do not use it in your application. Sheets * has to also call: * KoToolManager::instance()->updateShapeControllerBase(shapeController, canvas->canvasController()); * * @param shapeController the new shapeController. */ void setShapeControllerBase(KoShapeControllerBase *shapeController); /** * The size of the document measured in rasterized pixels. This information is needed for loading * SVG documents that use 'px' as the default unit. */ QRectF documentRectInPixels() const; /** - * Resolution of the rasterized representaiton of the document. Used to load SVG documents correctly. + * Resolution of the rasterized representation of the document. Used to load SVG documents correctly. */ qreal pixelsPerInch() const; /** * Document rect measured in 'pt' */ QRectF documentRect() const; /** * Return a pointer to the resource manager associated with the * shape-set (typically a document). The resource manager contains * document wide resources * such as variable managers, the image * collection and others. */ KoDocumentResourceManager *resourceManager() const; /** * @brief Returns the KoShapeControllerBase used to add/remove shapes. * * @return the KoShapeControllerBase */ KoShapeControllerBase *documentBase() const; private: class Private; Private * const d; }; Q_DECLARE_METATYPE(KoShapeController *) #endif diff --git a/libs/flake/KoShapeControllerBase.h b/libs/flake/KoShapeControllerBase.h index 95f5e56f79..c11dc0b4be 100644 --- a/libs/flake/KoShapeControllerBase.h +++ b/libs/flake/KoShapeControllerBase.h @@ -1,110 +1,110 @@ /* This file is part of the KDE project Copyright (C) 2006 Jan Hambrecht Copyright (C) 2006, 2010 Thomas Zander Copyright (C) 2008 C. Boemann This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOshapeControllerBASE_H #define KOshapeControllerBASE_H #include "kritaflake_export.h" #include class QRectF; class KoShape; class KoshapeControllerBasePrivate; class KoDocumentResourceManager; class KUndo2Command; /** * The KoshapeControllerBase is an abstract interface that the application's class * that owns the shapes should implement. This tends to be the document. * @see KoShapeDeleteCommand, KoShapeCreateCommand */ class KRITAFLAKE_EXPORT KoShapeControllerBase { public: KoShapeControllerBase(); virtual ~KoShapeControllerBase(); /** * Add a shape to the shape controller, allowing it to be seen and saved. * The controller should add the shape to the ShapeManager instance(s) manually * if the shape is one that should be currently shown on screen. * @param shape the new shape */ void addShape(KoShape *shape); /** * Add shapes to the shape controller, allowing it to be seen and saved. * The controller should add the shape to the ShapeManager instance(s) manually * if the shape is one that should be currently shown on screen. * @param shape the new shape */ virtual void addShapes(const QList shapes) = 0; /** * Remove a shape from the shape controllers control, allowing it to be deleted shortly after * The controller should remove the shape from all the ShapeManager instance(s) manually * @param shape the shape to remove */ virtual void removeShape(KoShape *shape) = 0; /** * This method gets called after the KoShapeDeleteCommand is executed * * This passes the KoShapeDeleteCommand as the command parameter. This makes it possible * for applications that need to do something after the KoShapeDeleteCommand is done, e.g. * adding one commands that need to be executed when a shape was deleted. * The default implementation is empty. * @param shapes The list of shapes that got removed. * @param command The command that was used to remove the shapes from the document. */ virtual void shapesRemoved(const QList &shapes, KUndo2Command *command); /** * Return a pointer to the resource manager associated with the * shape-set (typically a document). The resource manager contains * document wide resources * such as variable managers, the image * collection and others. */ virtual KoDocumentResourceManager *resourceManager() const; /** * The size of the document measured in rasterized pixels. This information is needed for loading * SVG documents that use 'px' as the default unit. */ virtual QRectF documentRectInPixels() const = 0; /** * The size of the document measured in 'pt' */ QRectF documentRect() const; /** - * Resolution of the rasterized representaiton of the document. Used to load SVG documents correctly. + * Resolution of the rasterized representation of the document. Used to load SVG documents correctly. */ virtual qreal pixelsPerInch() const = 0; private: KoshapeControllerBasePrivate * const d; }; #endif diff --git a/libs/flake/KoToolBase.cpp b/libs/flake/KoToolBase.cpp index dc3ef76143..431bf8d89e 100644 --- a/libs/flake/KoToolBase.cpp +++ b/libs/flake/KoToolBase.cpp @@ -1,429 +1,429 @@ /* This file is part of the KDE project * Copyright (C) 2006, 2010 Thomas Zander * Copyright (C) 2011 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 #include #include "KoToolBase.h" #include "KoToolBase_p.h" #include "KoCanvasBase.h" #include "KoPointerEvent.h" #include "KoDocumentResourceManager.h" #include "KoCanvasResourceProvider.h" #include "KoViewConverter.h" #include "KoShapeController.h" #include "KoShapeControllerBase.h" #include "KoToolSelection.h" #include #include #include #include #include #include KoToolBase::KoToolBase(KoCanvasBase *canvas) : d_ptr(new KoToolBasePrivate(this, canvas)) { Q_D(KoToolBase); d->connectSignals(); } KoToolBase::KoToolBase(KoToolBasePrivate &dd) : d_ptr(&dd) { Q_D(KoToolBase); d->connectSignals(); } KoToolBase::~KoToolBase() { // Enable this to easily generate action files for tools // if (actions().size() > 0) { // QDomDocument doc; // QDomElement e = doc.createElement("Actions"); // e.setAttribute("name", toolId()); // e.setAttribute("version", "2"); // doc.appendChild(e); // Q_FOREACH (QAction *action, actions().values()) { // QDomElement a = doc.createElement("Action"); // a.setAttribute("name", action->objectName()); // // But seriously, XML is the worst format ever designed // auto addElement = [&](QString title, QString content) { // QDomElement newNode = doc.createElement(title); // QDomText newText = doc.createTextNode(content); // newNode.appendChild(newText); // a.appendChild(newNode); // }; // addElement("icon", action->icon().name()); // addElement("text", action->text()); // addElement("whatsThis" , action->whatsThis()); // addElement("toolTip" , action->toolTip()); // addElement("iconText" , action->iconText()); // addElement("shortcut" , action->shortcut().toString()); // addElement("isCheckable" , QString((action->isChecked() ? "true" : "false"))); // addElement("statusTip", action->statusTip()); // e.appendChild(a); // } // QFile f(toolId() + ".action"); // f.open(QFile::WriteOnly); // f.write(doc.toString().toUtf8()); // f.close(); // } // else { // debugFlake << "Tool" << toolId() << "has no actions"; // } qDeleteAll(d_ptr->optionWidgets); delete d_ptr; } bool KoToolBase::isActivated() const { Q_D(const KoToolBase); return d->isActivated; } void KoToolBase::activate(KoToolBase::ToolActivation toolActivation, const QSet &shapes) { Q_UNUSED(toolActivation); Q_UNUSED(shapes); Q_D(KoToolBase); d->isActivated = true; } void KoToolBase::deactivate() { Q_D(KoToolBase); d->isActivated = false; } void KoToolBase::canvasResourceChanged(int key, const QVariant & res) { Q_UNUSED(key); Q_UNUSED(res); } void KoToolBase::documentResourceChanged(int key, const QVariant &res) { Q_UNUSED(key); Q_UNUSED(res); } bool KoToolBase::wantsAutoScroll() const { return true; } void KoToolBase::mouseDoubleClickEvent(KoPointerEvent *event) { event->ignore(); } void KoToolBase::mouseTripleClickEvent(KoPointerEvent *event) { event->ignore(); } void KoToolBase::keyPressEvent(QKeyEvent *e) { e->ignore(); } void KoToolBase::keyReleaseEvent(QKeyEvent *e) { e->ignore(); } void KoToolBase::explicitUserStrokeEndRequest() { } QVariant KoToolBase::inputMethodQuery(Qt::InputMethodQuery query, const KoViewConverter &) const { Q_D(const KoToolBase); if (d->canvas->canvasWidget() == 0) return QVariant(); switch (query) { case Qt::ImMicroFocus: return QRect(d->canvas->canvasWidget()->width() / 2, 0, 1, d->canvas->canvasWidget()->height()); case Qt::ImFont: return d->canvas->canvasWidget()->font(); default: return QVariant(); } } void KoToolBase::inputMethodEvent(QInputMethodEvent * event) { if (! event->commitString().isEmpty()) { QKeyEvent ke(QEvent::KeyPress, -1, 0, event->commitString()); keyPressEvent(&ke); } event->accept(); } void KoToolBase::customPressEvent(KoPointerEvent * event) { event->ignore(); } void KoToolBase::customReleaseEvent(KoPointerEvent * event) { event->ignore(); } void KoToolBase::customMoveEvent(KoPointerEvent * event) { event->ignore(); } void KoToolBase::useCursor(const QCursor &cursor) { Q_D(KoToolBase); d->currentCursor = cursor; emit cursorChanged(d->currentCursor); } QList > KoToolBase::optionWidgets() { Q_D(KoToolBase); if (d->optionWidgets.empty()) { d->optionWidgets = createOptionWidgets(); } return d->optionWidgets; } void KoToolBase::addAction(const QString &name, QAction *action) { Q_D(KoToolBase); if (action->objectName().isEmpty()) { action->setObjectName(name); } d->actions.insert(name, action); } QHash KoToolBase::actions() const { Q_D(const KoToolBase); return d->actions; } QAction *KoToolBase::action(const QString &name) const { Q_D(const KoToolBase); return d->actions.value(name); } QWidget * KoToolBase::createOptionWidget() { return 0; } QList > KoToolBase::createOptionWidgets() { QList > ow; if (QWidget *widget = createOptionWidget()) { if (widget->objectName().isEmpty()) { widget->setObjectName(toolId()); } ow.append(widget); } return ow; } void KoToolBase::setToolId(const QString &id) { Q_D(KoToolBase); d->toolId = id; } QString KoToolBase::toolId() const { Q_D(const KoToolBase); return d->toolId; } QCursor KoToolBase::cursor() const { Q_D(const KoToolBase); return d->currentCursor; } void KoToolBase::deleteSelection() { } void KoToolBase::cut() { copy(); deleteSelection(); } QMenu *KoToolBase::popupActionsMenu() { return 0; } KoCanvasBase * KoToolBase::canvas() const { Q_D(const KoToolBase); return d->canvas; } void KoToolBase::setStatusText(const QString &statusText) { emit statusTextChanged(statusText); } uint KoToolBase::handleRadius() const { Q_D(const KoToolBase); if(d->canvas->shapeController()->resourceManager()) { return d->canvas->shapeController()->resourceManager()->handleRadius(); } else { return 3; } } uint KoToolBase::grabSensitivity() const { Q_D(const KoToolBase); if(d->canvas->shapeController()->resourceManager()) { return d->canvas->shapeController()->resourceManager()->grabSensitivity(); } else { return 3; } } QRectF KoToolBase::handleGrabRect(const QPointF &position) const { Q_D(const KoToolBase); const KoViewConverter * converter = d->canvas->viewConverter(); uint handleSize = 2*grabSensitivity(); QRectF r = converter->viewToDocument(QRectF(0, 0, handleSize, handleSize)); r.moveCenter(position); return r; } QRectF KoToolBase::handlePaintRect(const QPointF &position) const { Q_D(const KoToolBase); const KoViewConverter * converter = d->canvas->viewConverter(); uint handleSize = 2*handleRadius(); QRectF r = converter->viewToDocument(QRectF(0, 0, handleSize, handleSize)); r.moveCenter(position); return r; } void KoToolBase::setTextMode(bool value) { Q_D(KoToolBase); d->isInTextMode=value; } bool KoToolBase::paste() { return false; } void KoToolBase::copy() const { } void KoToolBase::dragMoveEvent(QDragMoveEvent *event, const QPointF &point) { Q_UNUSED(event); Q_UNUSED(point); } void KoToolBase::dragLeaveEvent(QDragLeaveEvent *event) { Q_UNUSED(event); } void KoToolBase::dropEvent(QDropEvent *event, const QPointF &point) { Q_UNUSED(event); Q_UNUSED(point); } bool KoToolBase::hasSelection() { KoToolSelection *sel = selection(); return (sel && sel->hasSelection()); } KoToolSelection *KoToolBase::selection() { return 0; } void KoToolBase::repaintDecorations() { } bool KoToolBase::isInTextMode() const { Q_D(const KoToolBase); return d->isInTextMode; } void KoToolBase::requestUndoDuringStroke() { /** - * Default implementation just cancells the stroke + * Default implementation just cancels the stroke */ requestStrokeCancellation(); } void KoToolBase::requestStrokeCancellation() { } void KoToolBase::requestStrokeEnd() { } bool KoToolBase::maskSyntheticEvents() const { Q_D(const KoToolBase); return d->maskSyntheticEvents; } void KoToolBase::setMaskSyntheticEvents(bool value) { Q_D(KoToolBase); d->maskSyntheticEvents = value; } diff --git a/libs/flake/KoToolBase.h b/libs/flake/KoToolBase.h index 5214ea8249..6eebb6bddf 100644 --- a/libs/flake/KoToolBase.h +++ b/libs/flake/KoToolBase.h @@ -1,544 +1,544 @@ /* This file is part of the KDE project * Copyright (C) 2006 Thomas Zander * Copyright (C) 2011 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. */ #ifndef KOTOOLBASE_H #define KOTOOLBASE_H #include #include #include #include #include #include "kritaflake_export.h" class KoShape; class KoCanvasBase; class KoPointerEvent; class KoViewConverter; class KoToolSelection; class KoToolBasePrivate; class KoShapeControllerBase; class QAction; class QKeyEvent; class QWidget; class QCursor; class QPainter; class QString; class QStringList; class QRectF; class QPointF; class QInputMethodEvent; class QDragMoveEvent; class QDragLeaveEvent; class QDropEvent; class QTouchEvent; class QMenu; /** * Abstract base class for all tools. Tools can create or manipulate * flake shapes, canvas state or any other thing that a user may wish * to do to his document or his view on a document with a pointing * device. * * There exists an instance of every tool for every pointer device. * These instances are managed by the toolmanager.. */ class KRITAFLAKE_EXPORT KoToolBase : public QObject { Q_OBJECT public: /// Option for activate() enum ToolActivation { TemporaryActivation, ///< The tool is activated temporarily and works 'in-place' of another one. DefaultActivation ///< The tool is activated normally and emitting 'done' goes to the defaultTool }; /** * Constructor, normally only called by the factory (see KoToolFactoryBase) * @param canvas the canvas interface this tool will work for. */ explicit KoToolBase(KoCanvasBase *canvas); ~KoToolBase() override; /** * request a repaint of the decorations to be made. This triggers * an update call on the canvas, but does not paint directly. */ virtual void repaintDecorations(); /** * Return if dragging (moving with the mouse down) to the edge of a canvas should scroll the * canvas (default is true). * @return if this tool wants mouse events to cause scrolling of canvas. */ virtual bool wantsAutoScroll() const; /** * Called by the canvas to paint any decorations that the tool deems needed. * The painter has the top left of the canvas as its origin. * @param painter used for painting the shape * @param converter to convert between internal and view coordinates. */ virtual void paint(QPainter &painter, const KoViewConverter &converter) = 0; /** * Return the option widgets for this tool. Create them if they * do not exist yet. If the tool does not have an option widget, * this method return an empty list. (After discussion with Thomas, who prefers * the toolmanager to handle that case.) * * @see m_optionWidgets */ QList > optionWidgets(); /** * Retrieves the entire collection of actions for the tool. */ QHash actions() const; /** * Retrieve an action by name. */ QAction *action(const QString &name) const; /** * Called when (one of) the mouse or stylus buttons is pressed. * Implementors should call event->ignore() if they do not actually use the event. * @param event state and reason of this mouse or stylus press */ virtual void mousePressEvent(KoPointerEvent *event) = 0; /** * Called when (one of) the mouse or stylus buttons is double clicked. * Implementors should call event->ignore() if they do not actually use the event. * Default implementation ignores this event. * @param event state and reason of this mouse or stylus press */ virtual void mouseDoubleClickEvent(KoPointerEvent *event); /** * Called when (one of) the mouse or stylus buttons is triple clicked. * Implementors should call event->ignore() if they do not actually use the event. * Default implementation ignores this event. * @param event state and reason of this mouse or stylus press */ virtual void mouseTripleClickEvent(KoPointerEvent *event); /** * Called when the mouse or stylus moved over the canvas. * Implementors should call event->ignore() if they do not actually use the event. * @param event state and reason of this mouse or stylus move */ virtual void mouseMoveEvent(KoPointerEvent *event) = 0; /** * Called when (one of) the mouse or stylus buttons is released. * Implementors should call event->ignore() if they do not actually use the event. * @param event state and reason of this mouse or stylus release */ virtual void mouseReleaseEvent(KoPointerEvent *event) = 0; /** * Called when a key is pressed. * Implementors should call event->ignore() if they do not actually use the event. * Default implementation ignores this event. * @param event state and reason of this key press */ virtual void keyPressEvent(QKeyEvent *event); /** * Called when a key is released * Implementors should call event->ignore() if they do not actually use the event. * Default implementation ignores this event. * @param event state and reason of this key release */ virtual void keyReleaseEvent(QKeyEvent *event); /** * @brief explicitUserStrokeEndRequest is called by the input manager * when the user presses Enter key or any equivalent. This callback * comes before requestStrokeEnd(), which comes from a different source. */ virtual void explicitUserStrokeEndRequest(); /** * This method is used to query a set of properties of the tool to be * able to support complex input method operations as support for surrounding * text and reconversions. * Default implementation returns simple defaults, for tools that want to provide - * a more responsive text entry experience for CJK languages it would be good to reimplemnt. + * a more responsive text entry experience for CJK languages it would be good to reimplement. * @param query specifies which property is queried. * @param converter the view converter for the current canvas. */ virtual QVariant inputMethodQuery(Qt::InputMethodQuery query, const KoViewConverter &converter) const; /** * Text entry of complex text, like CJK, can be made more interactive if a tool * implements this and the InputMethodQuery() methods. * Reimplementing this only provides the user with a more responsive text experience, since the * default implementation forwards the typed text as key pressed events. * @param event the input method event. */ virtual void inputMethodEvent(QInputMethodEvent *event); /** * Called when (one of) a custom device buttons is pressed. * Implementors should call event->ignore() if they do not actually use the event. * @param event state and reason of this custom device press */ virtual void customPressEvent(KoPointerEvent *event); /** * Called when (one of) a custom device buttons is released. * Implementors should call event->ignore() if they do not actually use the event. * @param event state and reason of this custom device release */ virtual void customReleaseEvent(KoPointerEvent *event); /** * Called when a custom device moved over the canvas. * Implementors should call event->ignore() if they do not actually use the event. * @param event state and reason of this custom device move */ virtual void customMoveEvent(KoPointerEvent *event); /** * @return true if synthetic mouse events on the canvas should be eaten. * * For example, the guides tool should allow click and drag with touch, * while the same touch events should be rejected by the freehand tool. * * These events are sent by the OS in Windows */ bool maskSyntheticEvents() const; /** * get the identifier code from the KoToolFactoryBase that created this tool. * @return the toolId. * @see KoToolFactoryBase::id() */ Q_INVOKABLE QString toolId() const; /// return the last emitted cursor QCursor cursor() const; /** * Returns the internal selection object of this tool. * Each tool can have a selection which is private to that tool and the specified shape that it comes with. * The default returns 0. */ virtual KoToolSelection *selection(); /** * @returns true if the tool has selected data. */ virtual bool hasSelection(); /** * copies the tools selection to the clipboard. * The default implementation is empty to aid tools that don't have any selection. * @see selection() */ virtual void copy() const; /** * Delete the tools selection. * The default implementation is empty to aid tools that don't have any selection. * @see selection() */ virtual void deleteSelection(); /** * Cut the tools selection and copy it to the clipboard. * The default implementation calls copy() and then deleteSelection() * @see copy() * @see deleteSelection() */ virtual void cut(); /** * Paste the clipboard selection. * A tool typically has one or more shapes selected and pasting should do something meaningful * for this specific shape and tool combination. Inserting text in a text tool, for example. * @return will return true if pasting succeeded. False if nothing happened. */ virtual bool paste(); /** * Handle the dragMoveEvent * A tool typically has one or more shapes selected and dropping into should do * something meaningful for this specific shape and tool combination. For example * dropping text in a text tool. * The tool should Accept the event if it is meaningful; Default implementation does not. */ virtual void dragMoveEvent(QDragMoveEvent *event, const QPointF &point); /** * Handle the dragLeaveEvent * Basically just a noticification that the drag is no long relevant * The tool should Accept the event if it is meaningful; Default implementation does not. */ virtual void dragLeaveEvent(QDragLeaveEvent *event); /** * Handle the dropEvent * A tool typically has one or more shapes selected and dropping into should do * something meaningful for this specific shape and tool combination. For example * dropping text in a text tool. * The tool should Accept the event if it is meaningful; Default implementation does not. */ virtual void dropEvent(QDropEvent *event, const QPointF &point); /** * @return a menu with context-aware actions for the current selection. If * the returned value is null, no context menu is shown. */ virtual QMenu* popupActionsMenu(); /// Returns the canvas the tool is working on KoCanvasBase *canvas() const; /** * This method can be reimplemented in a subclass. * @return returns true, if the tool is in text mode. that means, that there is * any kind of textual input and all single key shortcuts should be eaten. */ bool isInTextMode() const; public Q_SLOTS: /** * Called when the user requested undo while the stroke is * active. If you tool supports undo of the part of its actions, * override this method and do the needed work there. * * NOTE: Default implementation forwards this request to * requestStrokeCancellation() method, so that the stroke * would be cancelled. */ virtual void requestUndoDuringStroke(); /** * Called when the user requested the cancellation of the current * stroke. If you tool supports cancelling, override this method * and do the needed work there */ virtual void requestStrokeCancellation(); /** * Called when the image decided that the stroke should better be * ended. If you tool supports long strokes, override this method * and do the needed work there */ virtual void requestStrokeEnd(); /** * This method is called when this tool instance is activated. * For any main window there is only one tool active at a time, which then gets all * user input. Switching between tools will call deactivate on one and activate on the * new tool allowing the tool to flush items (like a selection) * when it is not in use. * *

There is one case where two tools are activated at the same. This is the case * where one tool delegates work to another temporarily. For example, while shift is * being held down. The second tool will get activated with temporary=true and * it should emit done() when the state that activated it is ended. *

One of the important tasks of activate is to call useCursor() * * @param shapes the set of shapes that are selected or suggested for editing by a * selected shape for the tool to work on. Not all shapes will be meant for this * tool. * @param toolActivation if TemporaryActivation, this tool is only temporarily activated * and should emit done when it is done. * @see deactivate() */ virtual void activate(ToolActivation toolActivation, const QSet &shapes); /** * This method is called whenever this tool is no longer the * active tool * @see activate() */ virtual void deactivate(); /** * This method is called whenever a property in the resource * provider associated with the canvas this tool belongs to * changes. An example is currently selected foreground color. */ virtual void canvasResourceChanged(int key, const QVariant &res); /** * This method is called whenever a property in the resource * provider associated with the document this tool belongs to * changes. An example is the handle radius */ virtual void documentResourceChanged(int key, const QVariant &res); /** * This method just relays the given text via the tools statusTextChanged signal. * @param statusText the new status text */ void setStatusText(const QString &statusText); Q_SIGNALS: /** * Emitted when this tool wants itself to be replaced by another tool. * * @param id the identification of the desired tool * @see toolId(), KoToolFactoryBase::id() */ void activateTool(const QString &id); /** * Emitted when this tool wants itself to temporarily be replaced by another tool. * For instance, a paint tool could desire to be * temporarily replaced by a pan tool which could be temporarily * replaced by a colorpicker. * @param id the identification of the desired tool */ void activateTemporary(const QString &id); /** * Emitted when the tool has been temporarily activated and wants * to notify the world that it's done. */ void done(); /** * Emitted by useCursor() when the cursor to display on the canvas is changed. * The KoToolManager should connect to this signal to handle cursors further. */ void cursorChanged(const QCursor &cursor); /** * A tool can have a selection that is copy-able, this signal is emitted when that status changes. * @param hasSelection is true when the tool holds selected data. */ void selectionChanged(bool hasSelection); /** * Emitted when the tool wants to display a different status text * @param statusText the new status text */ void statusTextChanged(const QString &statusText); protected: /** * Classes inheriting from this one can call this method to signify which cursor * the tool wants to display at this time. Logical place to call it is after an * incoming event has been handled. * @param cursor the new cursor. */ void useCursor(const QCursor &cursor); /** * Reimplement this if your tool actually has an option widget. * Sets the option widget to 0 by default. */ virtual QWidget *createOptionWidget(); virtual QList > createOptionWidgets(); /** * Add an action under the given name to the collection. * * Inserting an action under a name that is already used for another action will replace * the other action in the collection. * * @param name The name by which the action be retrieved again from the collection. * @param action The action to add. * @param readWrite set this to ReadOnlyAction to keep the action available on * read-only documents */ void addAction(const QString &name, QAction *action); /// Convenience function to get the current handle radius uint handleRadius() const; /// Convencience function to get the current grab sensitivity uint grabSensitivity() const; /** * Returns a handle grab rect at the given position. * * The position is expected to be in document coordinates. The grab sensitivity * canvas resource is used for the dimension of the rectangle. * * @return the handle rectangle in document coordinates */ QRectF handleGrabRect(const QPointF &position) const; /** * Returns a handle paint rect at the given position. * * The position is expected to be in document coordinates. The handle radius * canvas resource is used for the dimension of the rectangle. * * @return the handle rectangle in document coordinates */ QRectF handlePaintRect(const QPointF &position) const; /** * You should set the text mode to true in subclasses, if this tool is in text input mode, eg if the users * are able to type. If you don't set it, then single key shortcuts will get the key event and not this tool. */ void setTextMode(bool value); /** * Allows subclasses to specify whether synthetic mouse events should be accepted. */ void setMaskSyntheticEvents(bool value); /** * Returns true if activate() has been called (more times than deactivate :) ) */ bool isActivated() const; protected: KoToolBase(KoToolBasePrivate &dd); KoToolBasePrivate *d_ptr; private: friend class ToolHelper; /** * Set the identifier code from the KoToolFactoryBase that created this tool. * @param id the identifier code * @see KoToolFactoryBase::id() */ void setToolId(const QString &id); KoToolBase(); KoToolBase(const KoToolBase&); KoToolBase& operator=(const KoToolBase&); Q_DECLARE_PRIVATE(KoToolBase) }; #endif /* KOTOOL_H */ diff --git a/libs/flake/KoToolManager.h b/libs/flake/KoToolManager.h index c71f30f38b..417e11fdbd 100644 --- a/libs/flake/KoToolManager.h +++ b/libs/flake/KoToolManager.h @@ -1,338 +1,338 @@ /* This file is part of the KDE project * Copyright (c) 2005-2006 Boudewijn Rempt * Copyright (C) 2006, 2008 Thomas Zander * Copyright (C) 2006 Thorsten Zachmann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KO_TOOL_MANAGER #define KO_TOOL_MANAGER #include "KoInputDevice.h" #include "kritaflake_export.h" #include #include class KoCanvasController; class KoShapeControllerBase; class KoToolFactoryBase; class KoCanvasBase; class KoToolBase; class KoCreateShapesTool; class KActionCollection; class KoShape; class KoInputDeviceHandlerEvent; class KoShapeLayer; class ToolHelper; class QKeySequence; class QCursor; /** * This class serves as a QAction-like control object for activation of a tool. * * It allows to implement a custom UI to control the activation of tools. * See KoToolBox & KoModeBox in the kowidgets library. * * KoToolAction objects are indirectly owned by the KoToolManager singleton * and live until the end of its lifetime. */ class KRITAFLAKE_EXPORT KoToolAction : public QObject { Q_OBJECT public: // toolHelper takes over ownership, and those live till the end of KoToolManager. explicit KoToolAction(ToolHelper *toolHelper); ~KoToolAction() override; public: QString id() const; ///< The id of the tool QString iconText() const; ///< The icontext of the tool QString toolTip() const; ///< The tooltip of the tool QString iconName() const; ///< The icon name of the tool QKeySequence shortcut() const; ///< The shortcut to activate the tool QString section() const; ///< The section the tool wants to be in. int priority() const; ///< Lower number (higher priority) means coming first in the section. int buttonGroupId() const; ///< A unique ID for this tool as passed by changedTool(), >= 0 QString visibilityCode() const; ///< This tool should become visible when we emit this string in toolCodesSelected() public Q_SLOTS: void trigger(); ///< Request the activation of the tool Q_SIGNALS: void changed(); ///< Emitted when a property changes (shortcut ATM) private: friend class ToolHelper; class Private; Private *const d; }; /** * This class manages the activation and deactivation of tools for * each input device. * * Managing the active tool and switching tool based on various variables. * * The state of the toolbox will be the same for all views in the process so practically * you can say we have one toolbox per application instance (process). Implementation * does not allow one widget to be in more then one view, so we just make sure the toolbox * is hidden in not-in-focus views. * * The ToolManager is a singleton and will manage all views in all applications that * are loaded in this process. This means you will have to register and unregister your view. * When creating your new view you should use a KoCanvasController() and register that * with the ToolManager like this: @code MyGuiWidget::MyGuiWidget() { m_canvasController = new KoCanvasController(this); m_canvasController->setCanvas(m_canvas); KoToolManager::instance()->addControllers(m_canvasController)); } MyGuiWidget::~MyGuiWidget() { KoToolManager::instance()->removeCanvasController(m_canvasController); } @endcode * * For a new view that extends KoView all you need to do is implement KoView::createToolBox() * * KoToolManager also keeps track of the current tool based on a complex set of conditions and heuristics: - there is one active tool per KoCanvasController (and there is one KoCanvasController per view, because this is a class with scrollbars and a zoomlevel and so on) - for every pointing device (determined by the unique id of tablet, or 0 for mice -- you may have more than one mouse attached, but - Qt cannot distinquish between them, there is an associated tool. + Qt cannot distinguish between them, there is an associated tool. - depending on things like tablet leave/enter proximity, incoming mouse or tablet events and a little timer (that gets stopped when we know what is what), the active pointing device is determined, and the active tool is set accordingly. Nota bene: if you use KoToolManager and register your canvases with it you no longer have to manually implement methods to route mouse, tablet, key or wheel events to the active tool. In fact, it's no longer interesting to you which tool is active; you can safely route the paint event through KoToolProxy::paint(). (The reason the input events are handled completely by the toolmanager and the paint events not is that, generally speaking, it's okay if the tools get the input events first, but you want to paint your shapes or other canvas stuff first and only then paint the tool stuff.) */ class KRITAFLAKE_EXPORT KoToolManager : public QObject { Q_OBJECT public: KoToolManager(); /// Return the toolmanager singleton static KoToolManager* instance(); ~KoToolManager() override; /** * Register actions for switching to tools at the actionCollection parameter. * The actions will have the text / shortcut as stated by the toolFactory. * If the application calls this in their KoView extending class they will have all the benefits * from allowing this in the menus and to allow the use to configure the shortcuts used. * @param ac the actionCollection that will be the parent of the actions. * @param controller tools registered with this controller will have all their actions added as well. */ void registerToolActions(KActionCollection *ac, KoCanvasController *controller); /** * Register a new canvas controller * @param controller the view controller that this toolmanager will manage the tools for */ void addController(KoCanvasController *controller); /** * Remove a set of controllers * When the controller is no longer used it should be removed so all tools can be * deleted and stop eating memory. * @param controller the controller that is removed */ void removeCanvasController(KoCanvasController *controller); /** * Attempt to remove a controller. * This is automatically called when a controller's proxy object is deleted, and * it ensures that the controller is, in fact, removed, even if the creator forgot * to do so. * @param controller the proxy object of the controller to be removed */ Q_SLOT void attemptCanvasControllerRemoval(QObject *controller); /// @return the active canvas controller KoCanvasController *activeCanvasController() const; /** * Return the tool that is able to create shapes for this param canvas. * This is typically used by the KoShapeSelector to set which shape to create next. * @param canvas the canvas that is a child of a previously registered controller * who's tool you want. * @see addController() */ KoCreateShapesTool *shapeCreatorTool(KoCanvasBase *canvas) const; /** * Returns the tool for the given tool id. The tool may be 0 * @param canvas the canvas that is a child of a previously registered controller * who's tool you want. * @see addController() */ KoToolBase *toolById(KoCanvasBase *canvas, const QString &id) const; /// @return the currently active pointing device KoInputDevice currentInputDevice() const; /** * For the list of shapes find out which tool is the highest priority tool that can handle it. * @returns the toolId for the shapes. * @param shapes a list of shapes, a selection for example, that is used to look for the tool. */ QString preferredToolForSelection(const QList &shapes); /** * Returns the list of toolActions for the current tools. * @returns lists of toolActions for the current tools. */ QList toolActionList() const; /// Update the internal shortcuts of each tool. (Activation shortcuts are exposed already.) void updateToolShortcuts(); /// Request tool activation for the given canvas controller void requestToolActivation(KoCanvasController *controller); /// Returns the toolId of the currently active tool QString activeToolId() const; void initializeCurrentToolForCanvas(); class Private; /** * \internal return the private object for the toolmanager. */ KoToolManager::Private *priv(); public Q_SLOTS: /** * Request switching tool * @param id the id of the tool */ void switchToolRequested(const QString &id); /** * Request change input device * @param id the id of the input device */ void switchInputDeviceRequested(const KoInputDevice &id); /** * Request for temporary switching the tools. * This switch can be later reverted with switchBackRequested(). * @param id the id of the tool * * @see switchBackRequested() */ void switchToolTemporaryRequested(const QString &id); /** * Switches back to the original tool after the temporary switch * has been done. It the user changed the tool manually on the way, * then it switches to the interaction tool */ void switchBackRequested(); Q_SIGNALS: /** * Emitted when a new tool is going to override the current tool * @param canvas the currently active canvas. */ void aboutToChangeTool(KoCanvasController *canvas); /** * Emitted when a new tool was selected or became active. * @param canvas the currently active canvas. * @param uniqueToolId a random but unique code for the new tool. */ void changedTool(KoCanvasController *canvas, int uniqueToolId); /** * Emitted after the selection changed to state which unique shape-types are now * in the selection. * @param canvas the currently active canvas. * @param types a list of string that are the shape types of the selected objects. */ void toolCodesSelected(const QList &types); /** * Emitted after the current layer changed either its properties or to a new layer. * @param canvas the currently active canvas. * @param layer the layer that is selected. */ void currentLayerChanged(const KoCanvasController *canvas, const KoShapeLayer *layer); /** * Every time a new input device gets used by a tool, this event is emitted. * @param device the new input device that the user picked up. */ void inputDeviceChanged(const KoInputDevice &device); /** * Emitted whenever the active canvas changed. * @param canvas the new activated canvas (might be 0) */ void changedCanvas(const KoCanvasBase *canvas); /** * Emitted whenever the active tool changes the status text. * @param statusText the new status text */ void changedStatusText(const QString &statusText); /** * emitted whenever a new tool is dynamically added for the given canvas */ void addedTool(KoToolAction *toolAction, KoCanvasController *canvas); /** * Emit the new tool option widgets to be used with this canvas. */ void toolOptionWidgetsChanged(KoCanvasController *controller, const QList > &widgets); private: KoToolManager(const KoToolManager&); KoToolManager operator=(const KoToolManager&); Q_PRIVATE_SLOT(d, void toolActivated(ToolHelper *tool)) Q_PRIVATE_SLOT(d, void detachCanvas(KoCanvasController *controller)) Q_PRIVATE_SLOT(d, void attachCanvas(KoCanvasController *controller)) Q_PRIVATE_SLOT(d, void movedFocus(QWidget *from, QWidget *to)) Q_PRIVATE_SLOT(d, void updateCursor(const QCursor &cursor)) Q_PRIVATE_SLOT(d, void selectionChanged(const QList &shapes)) Q_PRIVATE_SLOT(d, void currentLayerChanged(const KoShapeLayer *layer)) QPair createTools(KoCanvasController *controller, ToolHelper *tool); Private *const d; }; #endif diff --git a/libs/flake/commands/KoShapeDistributeCommand.h b/libs/flake/commands/KoShapeDistributeCommand.h index d81b1c7397..d9ed0a1b14 100644 --- a/libs/flake/commands/KoShapeDistributeCommand.h +++ b/libs/flake/commands/KoShapeDistributeCommand.h @@ -1,67 +1,67 @@ /* This file is part of the KDE project * Copyright (C) 2006 Thomas Zander * Copyright (C) 2006 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. */ #ifndef KOSHAPEDISTRIBUTECOMMAND_H #define KOSHAPEDISTRIBUTECOMMAND_H #include "kritaflake_export.h" #include #include class KoShape; /// The undo / redo command for distributing shapes class KRITAFLAKE_EXPORT KoShapeDistributeCommand : public KUndo2Command { public: - /// The different options to ditribute with this command + /// The different options to distribute with this command enum Distribute { HorizontalCenterDistribution, ///< Horizontal centered HorizontalGapsDistribution, ///< Horizontal Gaps HorizontalLeftDistribution, ///< Horizontal Left HorizontalRightDistribution, ///< Horizontal Right VerticalCenterDistribution, ///< Vertical centered VerticalGapsDistribution, ///< Vertical Gaps VerticalBottomDistribution, ///< Vertical bottom VerticalTopDistribution ///< Vertical top }; /** * Command to align a set of shapes in a rect * @param shapes a set of all the shapes that should be distributed * @param distribute the distribution type * @param boundingRect the rect the shapes will be distributed in * @param parent the parent command used for macro commands */ KoShapeDistributeCommand(const QList &shapes, Distribute distribute, const QRectF &boundingRect, KUndo2Command *parent = 0); ~KoShapeDistributeCommand() override; /// redo the command void redo() override; /// revert the actions done in redo void undo() override; private: class Private; Private * const d; }; #endif diff --git a/libs/flake/commands/KoShapeReorderCommand.h b/libs/flake/commands/KoShapeReorderCommand.h index 2c371b5ca3..ac640a31f2 100644 --- a/libs/flake/commands/KoShapeReorderCommand.h +++ b/libs/flake/commands/KoShapeReorderCommand.h @@ -1,130 +1,130 @@ /* This file is part of the KDE project * Copyright (C) 2006 Thomas Zander * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOSHAPEREORDERCOMMAND_H #define KOSHAPEREORDERCOMMAND_H #include "kritaflake_export.h" #include #include #include class KoShape; class KoShapeManager; class KoShapeReorderCommandPrivate; /// This command allows you to change the zIndex of a number of shapes. class KRITAFLAKE_EXPORT KoShapeReorderCommand : public KUndo2Command { public: struct KRITAFLAKE_EXPORT IndexedShape : boost::less_than_comparable { IndexedShape(); IndexedShape(KoShape *_shape); bool operator<(const IndexedShape &rhs) const; int zIndex = 0; KoShape *shape = 0; }; public: /** * Constructor. * @param shapes the set of objects that are moved. * @param newIndexes the new indexes for the shapes. * this list naturally must have the same amount of items as the shapes set. * @param parent the parent command used for macro commands */ KoShapeReorderCommand(const QList &shapes, QList &newIndexes, KUndo2Command *parent = 0); KoShapeReorderCommand(const QList &shapes, KUndo2Command *parent = 0); ~KoShapeReorderCommand() override; /// An enum for defining what kind of reordering to use. enum MoveShapeType { RaiseShape, ///< raise the selected shape to the level that it is above the shape that is on top of it. LowerShape, ///< Lower the selected shape to the level that it is below the shape that is below it. BringToFront, ///< Raise the selected shape to be on top of all shapes. SendToBack ///< Lower the selected shape to be below all other shapes. }; /** * Create a new KoShapeReorderCommand by calculating the new indexes required to move the shapes * according to the move parameter. * @param shapes all the shapes that should be moved. * @param manager the shapeManager that contains all the shapes that could have their indexes changed. * @param move the moving type. * @param parent the parent command for grouping purposes. - * @return command for reording the shapes or 0 if no reordering happened + * @return command for reordering the shapes or 0 if no reordering happened */ static KoShapeReorderCommand *createCommand(const QList &shapes, KoShapeManager *manager, MoveShapeType move, KUndo2Command *parent = 0); /** * @brief mergeInShape adjust zIndex of all the \p shapes and \p newShape to * avoid collisions between \p shapes and \p newShape. * * Note1: \p newShape may or may not be contained in \p shapes, there * is no difference. * Note2: the collisions inside \p shapes are ignored. They are just * adjusted to avoid collisions with \p newShape only * @param parent the parent command for grouping purposes. - * @return command for reording the shapes or 0 if no reordering happened + * @return command for reordering the shapes or 0 if no reordering happened */ static KoShapeReorderCommand *mergeInShape(QList shapes, KoShape *newShape, KUndo2Command *parent = 0); /** * Recalculates the attached z-indexes of \p shapes so that all indexes go * strictly in ascending order and no shapes have repetitive indexes. The * physical order of the shapes in the array is not changed, on the indexes * in IndexedShape are corrected. */ static QList homogenizeZIndexes(QList shapes); /** * Convenience version of homogenizeZIndexes() that removes all the IndexedShape * objects, which z-index didn't change during homogenization. In a result * you get a list that can be passed to KoShapeReorderCommand directly. */ static QList homogenizeZIndexesLazy(QList shapes); /** * Put all the shapes in \p shapesAbove above the shapes in \p shapesBelow, adjusting their * z-index values. */ static QList mergeDownShapes(QList shapesBelow, QList shapesAbove); /// redo the command void redo() override; /// revert the actions done in redo void undo() override; private: KoShapeReorderCommandPrivate * const d; }; KRITAFLAKE_EXPORT QDebug operator<<(QDebug dbg, const KoShapeReorderCommand::IndexedShape &indexedShape); #endif diff --git a/libs/flake/tests/TestSvgParser.cpp b/libs/flake/tests/TestSvgParser.cpp index 05b9f2f332..f7c3399e55 100644 --- a/libs/flake/tests/TestSvgParser.cpp +++ b/libs/flake/tests/TestSvgParser.cpp @@ -1,3219 +1,3219 @@ /* * 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 "TestSvgParser.h" #include #include #include #include "SvgParserTestingUtils.h" #include "../../sdk/tests/qimage_test_util.h" #ifdef USE_ROUND_TRIP #include "SvgWriter.h" #include #include #endif void TestSvgParser::testUnitPx() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(0,0,10,20), 0.5)); QCOMPARE(shape->absoluteTransformation(0), QTransform()); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(10,20)); } void TestSvgParser::testUnitPxResolution() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(0,0,5,10), 0.25)); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(5,10)); } void TestSvgParser::testUnitPt() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 666 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(0,0,10,20), 0.5)); QCOMPARE(shape->absoluteTransformation(0), QTransform()); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(10,20)); } void TestSvgParser::testUnitIn() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 666 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(0,0,720,1440), 36)); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromScale(72, 72)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(720,1440)); } void TestSvgParser::testUnitPercentInitial() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 80, 80) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(0,0,5,10), 0.25)); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(5,10)); } void TestSvgParser::testScalingViewport() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testScalingViewportKeepMeet1() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testScalingViewportKeepMeet2() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testScalingViewportKeepMeetAlign() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 24) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,12)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,12)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,28)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,28)); } void TestSvgParser::testScalingViewportKeepSlice1() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testScalingViewportKeepSlice2() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testScalingViewportResolution() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.25, 0.25)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(1,1)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(4,1)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(1,9)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(4,9)); } void TestSvgParser::testScalingViewportPercentInternal() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(4, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(2,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(8,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(2,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(8,18)); } void TestSvgParser::testParsePreserveAspectRatio() { { SvgUtil::PreserveAspectRatioParser p(" defer xMinYMax meet"); QCOMPARE(p.defer, true); QCOMPARE(p.mode, Qt::KeepAspectRatio); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Min); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Max); } { SvgUtil::PreserveAspectRatioParser p(" xMinYMid slice"); QCOMPARE(p.defer, false); QCOMPARE(p.mode, Qt::KeepAspectRatioByExpanding); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Min); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Middle); } { SvgUtil::PreserveAspectRatioParser p(" xmidYMid "); QCOMPARE(p.defer, false); QCOMPARE(p.mode, Qt::KeepAspectRatio); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Middle); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Middle); } { SvgUtil::PreserveAspectRatioParser p(" NoNe "); QCOMPARE(p.defer, false); QCOMPARE(p.mode, Qt::IgnoreAspectRatio); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Min); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Min); } { SvgUtil::PreserveAspectRatioParser p("defer NoNe "); QCOMPARE(p.defer, true); QCOMPARE(p.mode, Qt::IgnoreAspectRatio); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Min); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Min); } { SvgUtil::PreserveAspectRatioParser p("sweet brown fox jumps over a nice svg file"); QCOMPARE(p.defer, false); QCOMPARE(p.mode, Qt::IgnoreAspectRatio); QCOMPARE(p.xAlignment, SvgUtil::PreserveAspectRatioParser::Min); QCOMPARE(p.yAlignment, SvgUtil::PreserveAspectRatioParser::Min); } } #include "parsers/SvgTransformParser.h" void TestSvgParser::testParseTransform() { { QString str("translate(-111.0, 33) translate(-111.0, 33) matrix (1 1 0 0 1, 3), translate(1)" "scale(0.5) rotate(10) rotate(10, 3 3) skewX(1) skewY(2)"); SvgTransformParser p(str); QCOMPARE(p.isValid(), true); } { // forget about one brace QString str("translate(-111.0, 33) translate(-111.0, 33 matrix (1 1 0 0 1, 3), translate(1)" "scale(0.5) rotate(10) rotate(10, 3 3) skewX(1) skewY(2)"); SvgTransformParser p(str); QCOMPARE(p.isValid(), false); } { SvgTransformParser p("translate(100, 50)"); QCOMPARE(p.isValid(), true); QCOMPARE(p.transform(), QTransform::fromTranslate(100, 50)); } { SvgTransformParser p("translate(100 50)"); QCOMPARE(p.isValid(), true); QCOMPARE(p.transform(), QTransform::fromTranslate(100, 50)); } { SvgTransformParser p("translate(100)"); QCOMPARE(p.isValid(), true); QCOMPARE(p.transform(), QTransform::fromTranslate(100, 0)); } { SvgTransformParser p("scale(100, 50)"); QCOMPARE(p.isValid(), true); QCOMPARE(p.transform(), QTransform::fromScale(100, 50)); } { SvgTransformParser p("scale(100)"); QCOMPARE(p.isValid(), true); QCOMPARE(p.transform(), QTransform::fromScale(100, 100)); } { SvgTransformParser p("rotate(90 70 74.0)"); QCOMPARE(p.isValid(), true); QTransform t; t.rotate(90); t = QTransform::fromTranslate(-70, -74) * t * QTransform::fromTranslate(70, 74); qDebug() << ppVar(p.transform()); QCOMPARE(p.transform(), t); } } void TestSvgParser::testScalingViewportTransform() { /** * Note: 'transform' affects all the attributes of the *current* * element, while 'viewBox' affects only the descendants! */ const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->absoluteTransformation(0), QTransform::fromTranslate(10, 4) * QTransform::fromScale(0.5, 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,12,32)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(5,2)); QCOMPARE(shape->absolutePosition(KoFlake::TopRight), QPointF(11,2)); QCOMPARE(shape->absolutePosition(KoFlake::BottomLeft), QPointF(5,18)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(11,18)); } void TestSvgParser::testTransformNesting() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), QRectF(10 - 1,10 - 0.5, 20 + 2, 20 + 1)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(10,10)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(30,30)); } void TestSvgParser::testTransformNestingGroups() { const QString data = "" "" " " "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), QRectF(10 - 1,10 - 0.5, 20 + 2, 20 + 1)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(10,10)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(30,30)); } void TestSvgParser::testTransformRotation1() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(-20,0,20,10), 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(0,0)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(-20,10)); } void TestSvgParser::testTransformRotation2() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 72 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->boundingRect(), kisGrowRect(QRectF(5,5,20,10), 0.5)); QCOMPARE(shape->outlineRect(), QRectF(0,0,10,20)); QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(5,15)); QCOMPARE(shape->absolutePosition(KoFlake::BottomRight), QPointF(25,5)); } void TestSvgParser::testRenderStrokeNone() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_none"); } void TestSvgParser::testRenderStrokeColorName() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorHex3() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorHex6() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorRgbValues() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorRgbPercent() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorCurrent() { const QString data = "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeColorNonexistentIri() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue"); } void TestSvgParser::testRenderStrokeWidth() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_width_2"); } void TestSvgParser::testRenderStrokeZeroWidth() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_none"); } void TestSvgParser::testRenderStrokeOpacity() { const QString data = "" "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(1); t.test_standard_30px_72ppi("stroke_blue_0_3_opacity"); } void TestSvgParser::testRenderStrokeJointRound() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_join_round"); } void TestSvgParser::testRenderStrokeLinecap() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_linecap_round"); } void TestSvgParser::testRenderStrokeMiterLimit() { // TODO:seems like doesn't work!! qWarning() << "WARNING: Miter limit test is skipped!!!"; return; const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_miter_limit"); } void TestSvgParser::testRenderStrokeDashArrayEven() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_dasharray_even"); } void TestSvgParser::testRenderStrokeDashArrayEvenOffset() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_dasharray_even_offset"); } void TestSvgParser::testRenderStrokeDashArrayOdd() { // SVG 1.1: if the dasharray is odd, repeat it const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_dasharray_odd"); } void TestSvgParser::testRenderStrokeDashArrayRelative() { // SVG 1.1: relative to view box // (40 x 50) * sqrt(2) => dash length = 5 px const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_dasharray_relative"); } void TestSvgParser::testRenderFillDefault() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_black"); } void TestSvgParser::testRenderFillRuleNonZero() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_non_zero"); } void TestSvgParser::testRenderFillRuleEvenOdd() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_even_odd"); } void TestSvgParser::testRenderFillOpacity() { const QString data = "" "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(1); t.test_standard_30px_72ppi("fill_opacity_0_3"); } void TestSvgParser::testRenderDisplayAttribute() { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(), false); } void TestSvgParser::testRenderVisibilityAttribute() { { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(), true); } { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(), false); } { const QString data = "" "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(), false); } } void TestSvgParser::testRenderVisibilityInheritance() { const QString data = "" "" " " "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(false), true); QCOMPARE(shape->isVisible(true), false); } void TestSvgParser::testRenderDisplayInheritance() { const QString data = "" "" " " "" ""; SvgTester t (data); t.parser.setResolution(QRectF(0, 0, 600, 400) /* px */, 144 /* ppi */); t.run(); KoShape *shape = t.findShape("testRect"); QVERIFY(shape); QCOMPARE(shape->isVisible(false), true); QEXPECT_FAIL("", "TODO: Fix 'display' attribute not to be inherited in shapes hierarchy!", Continue); QCOMPARE(shape->isVisible(true), true); } void TestSvgParser::testRenderStrokeWithInlineStyle() { const QString data = "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_blue_width_2"); } void TestSvgParser::testIccColor() { const QString data = "" "" " " " " " " "" ""; SvgRenderTester t (data); int numFetches = 0; t.parser.setFileFetcher( [&numFetches](const QString &name) { numFetches++; const QString fileName = TestUtil::fetchDataFileLazy(name); QFile file(fileName); KIS_ASSERT(file.exists()); file.open(QIODevice::ReadOnly); return file.readAll(); }); t.test_standard_30px_72ppi("stroke_blue_width_2"); QCOMPARE(numFetches, 1); } void TestSvgParser::testRenderFillLinearGradientRelativePercent() { const QString data = "" "" " " " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient"); } void TestSvgParser::testRenderFillLinearGradientRelativePortion() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient"); } void TestSvgParser::testRenderFillLinearGradientUserCoord() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient"); } void TestSvgParser::testRenderFillLinearGradientStopPortion() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient"); } void TestSvgParser::testRenderFillLinearGradientTransform() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient_vertical"); } void TestSvgParser::testRenderFillLinearGradientTransformUserCoord() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient_vertical_in_user"); } void TestSvgParser::testRenderFillLinearGradientRotatedShape() { // DK: I'm not sure I fully understand if it is a correct transformation, // but inkscape opens the file in the same way... const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient_shape_rotated", false); } void TestSvgParser::testRenderFillLinearGradientRotatedShapeUserCoord() { // DK: I'm not sure I fully understand if it is a correct transformation, // but inkscape opens the file in the same way... const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient_shape_rotated_in_user", false); } void TestSvgParser::testRenderFillRadialGradient() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(1); t.test_standard_30px_72ppi("fill_gradient_radial"); } void TestSvgParser::testRenderFillRadialGradientUserCoord() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(1); t.test_standard_30px_72ppi("fill_gradient_radial_in_user"); } void TestSvgParser::testRenderFillLinearGradientUserCoordPercent() { const QString data = "" "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_gradient"); } void TestSvgParser::testRenderStrokeLinearGradient() { const QString data = "" "" " " " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("stroke_gradient_dashed"); } QTransform rotateTransform(qreal degree, const QPointF ¢er) { QTransform rotate; rotate.rotate(degree); return QTransform::fromTranslate(-center.x(), -center.y()) * rotate * QTransform::fromTranslate(center.x(), center.y()); } QTransform viewTransform(const QRectF &src, const QRectF &dst) { return QTransform::fromTranslate(-src.x(), -src.y()) * QTransform::fromScale(dst.width() / src.width(), dst.height() / src.height()) * QTransform::fromTranslate(dst.x(), dst.y()); } QPainterPath bakeShape(const QPainterPath &path, const QTransform &bakeTransform, bool contentIsObb = false, const QRectF &shapeBoundingRect = QRectF(), bool contentIsViewBox = false, const QRectF &viewBoxRect= QRectF(), const QRectF &refRect = QRectF()) { const QTransform relativeToShape(shapeBoundingRect.width(), 0, 0, shapeBoundingRect.height(), shapeBoundingRect.x(), shapeBoundingRect.y()); QTransform newTransform = bakeTransform; if (contentIsObb) { newTransform = relativeToShape * newTransform; } if (contentIsViewBox) { newTransform = viewTransform(viewBoxRect, refRect) * newTransform; } return newTransform.map(path); } #include void renderBakedPath(QPainter &painter, const QPainterPath &bakedFillPath, const QTransform &bakedTransform, const QRect &shapeOutline, const QTransform &shapeTransform, const QRectF &referenceRect, bool contentIsObb, const QRectF &bakedShapeBoundingRect, bool referenceIsObb, const QTransform &patternTransform, QImage *stampResult) { painter.setTransform(QTransform()); painter.setPen(Qt::NoPen); QPainterPath shapeOutlinePath; shapeOutlinePath.addRect(shapeOutline); KoBakedShapeRenderer renderer( shapeOutlinePath, shapeTransform, bakedTransform, referenceRect, contentIsObb, bakedShapeBoundingRect, referenceIsObb, patternTransform); QPainter *patchPainter = renderer.bakeShapePainter(); patchPainter->fillPath(bakedFillPath, Qt::blue); patchPainter->end(); renderer.renderShape(painter); if (stampResult) { *stampResult = renderer.patchImage(); } } void TestSvgParser::testManualRenderPattern_ContentUser_RefObb() { const QRectF referenceRect(0, 0, 1.0, 0.5); QPainterPath fillPath; fillPath.addRect(QRect(2, 2, 6, 6)); fillPath.addRect(QRect(8, 4, 3, 2)); QTransform bakedTransform = QTransform::fromTranslate(10, 10) * QTransform::fromScale(2, 2); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform); QRect shape1OutlineRect(0,0,10,20); QImage stampResult; QImage fillResult(QSize(60,60), QImage::Format_ARGB32); QPainter gc(&fillResult); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape1OutlineRect, bakedTransform, referenceRect, false, QRectF(), true, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_user_r_obb_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_user_r_obb_fill1")); QRect shape2OutlineRect(5,5,20,10); QTransform shape2Transform = QTransform::fromScale(2, 2) * QTransform::fromTranslate(5, 5); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, false, QRectF(), true, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_user_r_obb_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_user_r_obb_fill2")); } void TestSvgParser::testManualRenderPattern_ContentObb_RefObb() { const QRectF referenceRect(0.3, 0.3, 0.4, 0.4); QPainterPath fillPath; fillPath.addRect(QRectF(0.4, 0.4, 0.2, 0.2)); fillPath.addRect(QRectF(0.6, 0.5, 0.1, 0.1)); fillPath.addRect(QRectF(0.3, 0.4, 0.1, 0.1)); const QRect bakedShapeRect(2,2,10,10); QTransform bakedTransform = QTransform::fromTranslate(10, 10) * QTransform::fromScale(2, 2); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform, true, bakedShapeRect); QImage stampResult; QImage fillResult(QSize(60,60), QImage::Format_ARGB32); QPainter gc(&fillResult); // Round trip to the same shape fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, bakedShapeRect, bakedTransform, referenceRect, true, bakedShapeRect, true, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_obb_r_obb_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_obb_r_obb_fill1")); // Move to a different shape QRect shape2OutlineRect(5,5,20,10); QTransform shape2Transform = QTransform::fromScale(2, 2) * QTransform::fromTranslate(5, 5); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, true, bakedShapeRect, true, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_obb_r_obb_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_obb_r_obb_fill2")); } void TestSvgParser::testManualRenderPattern_ContentUser_RefUser() { const QRectF referenceRect(5, 2, 8, 8); QPainterPath fillPath; fillPath.addRect(QRect(2, 2, 6, 6)); fillPath.addRect(QRect(8, 4, 3, 2)); QTransform bakedTransform = QTransform::fromTranslate(10, 10) * QTransform::fromScale(2, 2); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform); QRect shape1OutlineRect(0,0,10,20); QImage stampResult; QImage fillResult(QSize(60,60), QImage::Format_ARGB32); QPainter gc(&fillResult); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape1OutlineRect, bakedTransform, referenceRect, false, QRectF(), false, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_user_r_user_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_user_r_user_fill1")); QRect shape2OutlineRect(5,5,20,10); QTransform shape2Transform = QTransform::fromScale(2, 2) * QTransform::fromTranslate(5, 5); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, false, QRectF(), false, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_user_r_user_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_user_r_user_fill2")); } void TestSvgParser::testManualRenderPattern_ContentObb_RefObb_Transform_Rotate() { const QRectF referenceRect(0.0, 0.0, 0.4, 0.2); QPainterPath fillPath; fillPath.addRect(QRectF(0.0, 0.0, 0.5, 0.1)); fillPath.addRect(QRectF(0.0, 0.1, 0.1, 0.1)); const QRect bakedShapeRect(2,1,10,10); QTransform bakedTransform = QTransform::fromScale(2, 2) * QTransform::fromTranslate(10,10); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform, true, bakedShapeRect); QImage stampResult; QImage fillResult(QSize(60,60), QImage::Format_ARGB32); QPainter gc(&fillResult); QTransform patternTransform; patternTransform.rotate(90); patternTransform = patternTransform * QTransform::fromTranslate(0.5, 0.0); // Round trip to the same shape fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, bakedShapeRect, bakedTransform, referenceRect, true, bakedShapeRect, true, patternTransform, &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_obb_r_obb_rotate_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_obb_r_obb_rotate_fill1")); QRect shape2OutlineRect(5,5,20,10); QTransform shape2Transform = QTransform::fromScale(2, 2) * QTransform::fromTranslate(5, 5); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, true, bakedShapeRect, true, patternTransform, &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_obb_r_obb_rotate_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_obb_r_obb_rotate_fill2")); } void TestSvgParser::testManualRenderPattern_ContentView_RefObb() { const QRectF referenceRect(0, 0, 0.5, 1.0/3.0); const QRectF viewRect(10,10,60,90); QPainterPath fillPath; fillPath.addRect(QRect(30, 10, 20, 60)); fillPath.addRect(QRect(50, 40, 20, 30)); QRect shape1OutlineRect(10,20,40,120); QTransform bakedTransform = QTransform::fromScale(2, 0.5) * QTransform::fromTranslate(40,30); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform, true, shape1OutlineRect, true, viewRect, referenceRect); QImage stampResult; QImage fillResult(QSize(220,160), QImage::Format_ARGB32); QPainter gc(&fillResult); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape1OutlineRect, bakedTransform, referenceRect, true, shape1OutlineRect, true, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_view_r_obb_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_view_r_obb_fill1")); QRect shape2OutlineRect(20,10,60,90); QTransform shape2Transform = QTransform::fromScale(2, 1) * QTransform::fromTranslate(50, 50); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, true, shape1OutlineRect, true, rotateTransform(90, QPointF(0, 1.0 / 3.0)), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_view_r_obb_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_view_r_obb_fill2")); } void TestSvgParser::testManualRenderPattern_ContentView_RefUser() { const QRectF referenceRect(60, 0, 30, 20); const QRectF viewRect(10,10,60,90); QPainterPath fillPath; fillPath.addRect(QRect(30, 10, 20, 60)); fillPath.addRect(QRect(50, 40, 20, 30)); QRect shape1OutlineRect(10,20,40,120); QTransform bakedTransform = QTransform::fromScale(2, 0.5) * QTransform::fromTranslate(40,30); QPainterPath bakedFillPath = bakeShape(fillPath, bakedTransform, false, shape1OutlineRect, true, viewRect, referenceRect); QImage stampResult; QImage fillResult(QSize(220,160), QImage::Format_ARGB32); QPainter gc(&fillResult); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape1OutlineRect, bakedTransform, referenceRect, false, shape1OutlineRect, false, QTransform(), &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_view_r_user_patch1")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_view_r_user_fill1")); QRect shape2OutlineRect(20,10,60,90); QTransform shape2Transform = QTransform::fromScale(2, 1) * QTransform::fromTranslate(50, 50); QTransform patternTransform2 = rotateTransform(90, QPointF()) * QTransform::fromTranslate(40, 10); fillResult.fill(0); renderBakedPath(gc, bakedFillPath, bakedTransform, shape2OutlineRect, shape2Transform, referenceRect, false, shape1OutlineRect, false, patternTransform2, &stampResult); QVERIFY(TestUtil::checkQImage(stampResult, "svg_render", "render", "pattern_c_view_r_user_patch2")); QVERIFY(TestUtil::checkQImage(fillResult, "svg_render", "render", "pattern_c_view_r_user_fill2")); } void TestSvgParser::testRenderPattern_r_User_c_User() { const QString data = "" "" " " " " " " " " " " " " "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_pattern_base", false, QSize(160, 160)); } void TestSvgParser::testRenderPattern_InfiniteRecursionWhenInherited() { const QString data = "" "" " " " " " " " " " " " " "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_pattern_base_black", false, QSize(160, 160)); } void TestSvgParser::testRenderPattern_r_User_c_View() { const QString data = "" "" " " " " " " // y is changed to 39 from 40 to fix a rounding issue! " " " " " " "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_pattern_base", false, QSize(160, 160)); } void TestSvgParser::testRenderPattern_r_User_c_Obb() { const QString data = "" "" " " " " " " " " " " " " "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_pattern_base", false, QSize(160, 160)); } void TestSvgParser::testRenderPattern_r_User_c_View_Rotated() { const QString data = "" "" " " " " " " " " " " " " "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_pattern_rotated", false, QSize(220, 160)); } void TestSvgParser::testRenderPattern_r_Obb_c_View_Rotated() { /** * This test case differs from any application existent in the world :( * * Chrome and Firefox premultiply the patternTransform instead of doing post- * multiplication. Photoshop forgets to multiply the reference rect on it. * * So... */ const QString data = "" "" " " " " " " " " " " " " "" "" " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("fill_pattern_rotated_odd", false, QSize(220, 160)); } #include #include #include #include void TestSvgParser::testKoClipPathRendering() { QPainterPath path1; path1.addRect(QRect(5,5,15,15)); QPainterPath path2; path2.addRect(QRect(10,10,15,15)); QPainterPath clipPath1; clipPath1.addRect(QRect(10, 0, 10, 30)); QPainterPath clipPath2; clipPath2.moveTo(0,7); clipPath2.lineTo(30,7); clipPath2.lineTo(15,30); clipPath2.lineTo(0,7); QScopedPointer shape1(KoPathShape::createShapeFromPainterPath(path1)); shape1->setBackground(QSharedPointer(new KoColorBackground(Qt::blue))); QScopedPointer shape2(KoPathShape::createShapeFromPainterPath(path2)); shape2->setBackground(QSharedPointer(new KoColorBackground(Qt::red))); QScopedPointer clipShape1(KoPathShape::createShapeFromPainterPath(clipPath1)); KoClipPath *koClipPath1 = new KoClipPath({clipShape1.take()}, KoFlake::UserSpaceOnUse); koClipPath1->setClipRule(Qt::WindingFill); shape1->setClipPath(koClipPath1); QScopedPointer group(new KoShapeGroup()); { QList shapes({shape1.take(), shape2.take()}); KoShapeGroupCommand cmd(group.data(), shapes, false); cmd.redo(); } QScopedPointer clipShape2(KoPathShape::createShapeFromPainterPath(clipPath2)); KoClipPath *koClipPath2 = new KoClipPath({clipShape2.take()}, KoFlake::UserSpaceOnUse); koClipPath2->setClipRule(Qt::WindingFill); group->setClipPath(koClipPath2); SvgRenderTester::testRender(group.take(), "load", "clip_render_test", QSize(30,30)); } void TestSvgParser::testKoClipPathRelativeRendering() { QPainterPath path1; path1.addRect(QRect(5,5,15,15)); QPainterPath path2; path2.addRect(QRect(10,10,15,15)); QPainterPath clipPath1; clipPath1.addRect(QRect(10, 0, 10, 30)); QPainterPath clipPath2; clipPath2.moveTo(0,0); clipPath2.lineTo(1,0); clipPath2.lineTo(0.5,1); clipPath2.lineTo(0,0); QScopedPointer shape1(KoPathShape::createShapeFromPainterPath(path1)); shape1->setBackground(QSharedPointer(new KoColorBackground(Qt::blue))); QScopedPointer shape2(KoPathShape::createShapeFromPainterPath(path2)); shape2->setBackground(QSharedPointer(new KoColorBackground(Qt::red))); QScopedPointer clipShape1(KoPathShape::createShapeFromPainterPath(clipPath1)); KoClipPath *koClipPath1 = new KoClipPath({clipShape1.take()}, KoFlake::UserSpaceOnUse); koClipPath1->setClipRule(Qt::WindingFill); shape1->setClipPath(koClipPath1); QScopedPointer group(new KoShapeGroup()); { QList shapes({shape1.take(), shape2.take()}); KoShapeGroupCommand cmd(group.data(), shapes, false); cmd.redo(); } QScopedPointer clipShape2(KoPathShape::createShapeFromPainterPath(clipPath2)); KoClipPath *koClipPath2 = new KoClipPath({clipShape2.take()}, KoFlake::ObjectBoundingBox); koClipPath2->setClipRule(Qt::WindingFill); group->setClipPath(koClipPath2); SvgRenderTester::testRender(group.take(), "load", "relative_clip_render_test", QSize(30,30)); } void TestSvgParser::testRenderClipPath_User() { const QString data = "" "" " " " " "" "" " " "" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("clip_render_test", false); } void TestSvgParser::testRenderClipPath_Obb() { const QString data = "" "" " " " " "" "" " " "" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("relative_clip_render_test", false); } void TestSvgParser::testRenderClipPath_Obb_Transform() { const QString data = "" "" " " " " "" "" " " "" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("clip_render_test_rotated", false); } void TestSvgParser::testRenderClipMask_Obb() { const QString data = "" //"" " " " " " " " " " " " " " " //"" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("clip_mask_obb", false); } void TestSvgParser::testRenderClipMask_User_Clip_Obb() { const QString data = "" //"" " " " " " " " " " " " " " " //"" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("clip_mask_obb", false); } void TestSvgParser::testRenderClipMask_User_Clip_User() { const QString data = "" //"" " " " " " " " " " " " " " " //"" "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("clip_mask_obb", false); } QByteArray fileFetcherFunc(const QString &name) { const QString fileName = TestUtil::fetchDataFileLazy(name); QFile file(fileName); KIS_ASSERT(file.exists()); file.open(QIODevice::ReadOnly); return file.readAll(); } void TestSvgParser::testRenderImage_AspectDefault() { const QString data = "" "" " " " " " " " My image" " " "" ""; SvgRenderTester t (data); t.parser.setFileFetcher(fileFetcherFunc); t.test_standard_30px_72ppi("image_aspect_default", false); } void TestSvgParser::testRenderImage_AspectNone() { const QString data = "" "" " " " " " " " My image" " " "" ""; SvgRenderTester t (data); t.setFuzzyThreshold(5); t.parser.setFileFetcher(fileFetcherFunc); t.test_standard_30px_72ppi("image_aspect_none", false); } void TestSvgParser::testRenderImage_AspectMeet() { const QString data = "" "" " " " " " " " My image" " " "" ""; SvgRenderTester t (data); t.parser.setFileFetcher(fileFetcherFunc); t.test_standard_30px_72ppi("image_aspect_meet", false); } void TestSvgParser::testRectShapeRoundUniformX() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("rect_5_5", false); } void TestSvgParser::testRectShapeRoundUniformY() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("rect_5_5", false); } void TestSvgParser::testRectShapeRoundXY() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("rect_5_10", false); } void TestSvgParser::testRectShapeRoundXYOverflow() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("rect_5_10", false); } void TestSvgParser::testCircleShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("circle", false); } void TestSvgParser::testEllipseShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("ellipse", false); } void TestSvgParser::testLineShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("line", false); } void TestSvgParser::testPolylineShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("polyline", false); } void TestSvgParser::testPolygonShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("polygon", false); } void TestSvgParser::testPathShape() { const QString data = "" " " ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("polygon", false); } void TestSvgParser::testDefsHidden() { const QString data = "" "" " " " " " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("test_defs_hidden", false); } void TestSvgParser::testDefsUseInheritance() { const QString data = "" "" " " " " " " "" /** * NOTES: * 1) width/height attributes for are not implemented yet * 2) x and y are summed up * 3) stroke="white" is overridden by the original templated object * 4) fill="green" attribute from is not inherited */ "" " " " " "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("defs_use_inheritance", false); } void TestSvgParser::testUseWithoutDefs() { const QString data = "" // technical rect for rendering "" "" " " "" /** * NOTES: * 1) width/height attributes for are not implemented yet * 2) x and y are summed up * 3) stroke="white" is overridden by the original templated object * 4) fill="green" attribute from is not inherited */ "" " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("use_without_defs", false); } void TestSvgParser::testMarkersAutoOrientation() { const QString data = "" "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers", false); } void TestSvgParser::testMarkersAutoOrientationScaled() { const QString data = "" "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_scaled", false); } void TestSvgParser::testMarkersAutoOrientationScaledUserCoordinates() { const QString data = "" "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_user_coordinates", false); } void TestSvgParser::testMarkersCustomOrientation() { const QString data = "" "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_custom_orientation", false); } void TestSvgParser::testMarkersDifferent() { const QString data = "" "" " " " " " " "" "" " " " " " " "" "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_different", false); } void TestSvgParser::testMarkersFillAsShape() { const QString data = "" "" " " " " " " " " " " " " " " " " " " "" "" //" d=\"M5,15 L25,15\"/>" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_scaled_fill_as_shape", false); } void TestSvgParser::testMarkersOnClosedPath() { const QString data = "" "" " " " " " " "" "" " " " " " " "" "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_on_closed_path", false); } -void TestSvgParser::testGradientRecoveringTrasnform() +void TestSvgParser::testGradientRecoveringTransform() { // used for experimenting purposes only! QImage image(100,100,QImage::Format_ARGB32); image.fill(0); QPainter painter(&image); painter.setPen(QPen(Qt::black, 0)); QLinearGradient gradient(0, 0.5, 1, 0.5); gradient.setCoordinateMode(QGradient::ObjectBoundingMode); //QLinearGradient gradient(0, 50, 100, 50); //gradient.setCoordinateMode(QGradient::LogicalMode); gradient.setColorAt(0.0, Qt::red); gradient.setColorAt(1.0, Qt::blue); - QTransform gradientTrasnform; - gradientTrasnform.shear(0.2, 0); + QTransform gradientTransform; + gradientTransform.shear(0.2, 0); { QBrush brush(gradient); - brush.setTransform(gradientTrasnform); + brush.setTransform(gradientTransform); painter.setBrush(brush); } QRect mainShape(3,3,94,94); painter.drawRect(mainShape); QTransform gradientToUser(mainShape.width(), 0, 0, mainShape.height(), mainShape.x(), mainShape.y()); QRect smallShape(0,0,20,20); QTransform smallShapeTransform; { smallShapeTransform = QTransform::fromTranslate(-smallShape.center().x(), -smallShape.center().y()); QTransform r; r.rotate(90); smallShapeTransform *= r; smallShapeTransform *= QTransform::fromTranslate(mainShape.center().x(), mainShape.center().y()); } { gradient.setCoordinateMode(QGradient::LogicalMode); QBrush brush(gradient); - brush.setTransform(gradientTrasnform * gradientToUser * smallShapeTransform.inverted()); + brush.setTransform(gradientTransform * gradientToUser * smallShapeTransform.inverted()); painter.setBrush(brush); painter.setPen(Qt::NoPen); } painter.setTransform(smallShapeTransform); painter.drawRect(smallShape); //image.save("gradient_recovering_transform.png"); } void TestSvgParser::testMarkersAngularUnits() { const QString data = "" // start "" " " " " " " "" // end "" " " " " " " "" // mid "" " " " " " " "" "" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("markers_angular_units", false); } #include "KoParameterShape.h" void TestSvgParser::testSodipodiArcShape() { const QString data = "" "" " d=\" some weird unparsable text \" />" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("sodipodi_closed_arc", false); KoShape *shape = t.findShape("testRect"); QVERIFY(dynamic_cast(shape)); } void TestSvgParser::testSodipodiArcShapeOpen() { const QString data = "" "" " d=\" some weird unparsable text \" />" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("sodipodi_open_arc", false); KoShape *shape = t.findShape("testRect"); QVERIFY(dynamic_cast(shape)); } void TestSvgParser::testKritaChordShape() { const QString data = "" "" " d=\" some weird unparsable text \" />" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("sodipodi_chord_arc", false); KoShape *shape = t.findShape("testRect"); QVERIFY(dynamic_cast(shape)); } void TestSvgParser::testSodipodiChordShape() { const QString data = "" "" " d=\" some weird unparsable text \" />" ""; SvgRenderTester t (data); t.test_standard_30px_72ppi("sodipodi_chord_arc", false); KoShape *shape = t.findShape("testRect"); QVERIFY(dynamic_cast(shape)); } QTEST_MAIN(TestSvgParser) diff --git a/libs/flake/tests/TestSvgParser.h b/libs/flake/tests/TestSvgParser.h index 8fd39dcf34..9e5292c76d 100644 --- a/libs/flake/tests/TestSvgParser.h +++ b/libs/flake/tests/TestSvgParser.h @@ -1,175 +1,175 @@ /* * 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. */ #ifndef TESTSVGPARSER_H #define TESTSVGPARSER_H #include class TestSvgParser : public QObject { Q_OBJECT private Q_SLOTS: void testUnitPx(); void testUnitPxResolution(); void testUnitPt(); void testUnitIn(); void testUnitPercentInitial(); void testScalingViewport(); void testScalingViewportKeepMeet1(); void testScalingViewportKeepMeet2(); void testScalingViewportKeepMeetAlign(); void testScalingViewportKeepSlice1(); void testScalingViewportKeepSlice2(); void testScalingViewportResolution(); void testScalingViewportPercentInternal(); void testParsePreserveAspectRatio(); void testParseTransform(); void testScalingViewportTransform(); void testTransformNesting(); void testTransformNestingGroups(); void testTransformRotation1(); void testTransformRotation2(); void testRenderStrokeNone(); void testRenderStrokeColorName(); void testRenderStrokeColorHex3(); void testRenderStrokeColorHex6(); void testRenderStrokeColorRgbValues(); void testRenderStrokeColorRgbPercent(); void testRenderStrokeColorCurrent(); void testRenderStrokeColorNonexistentIri(); void testRenderStrokeWidth(); void testRenderStrokeZeroWidth(); void testRenderStrokeOpacity(); void testRenderStrokeJointRound(); void testRenderStrokeLinecap(); void testRenderStrokeMiterLimit(); void testRenderStrokeDashArrayEven(); void testRenderStrokeDashArrayEvenOffset(); void testRenderStrokeDashArrayOdd(); void testRenderStrokeDashArrayRelative(); void testRenderFillDefault(); void testRenderFillRuleNonZero(); void testRenderFillRuleEvenOdd(); void testRenderFillOpacity(); void testRenderDisplayAttribute(); void testRenderVisibilityAttribute(); void testRenderVisibilityInheritance(); void testRenderDisplayInheritance(); void testRenderStrokeWithInlineStyle(); void testIccColor(); void testRenderFillLinearGradientRelativePercent(); void testRenderFillLinearGradientRelativePortion(); void testRenderFillLinearGradientUserCoord(); void testRenderFillLinearGradientStopPortion(); void testRenderFillLinearGradientTransform(); void testRenderFillLinearGradientTransformUserCoord(); void testRenderFillLinearGradientRotatedShape(); void testRenderFillLinearGradientRotatedShapeUserCoord(); void testRenderFillRadialGradient(); void testRenderFillRadialGradientUserCoord(); void testRenderFillLinearGradientUserCoordPercent(); void testRenderStrokeLinearGradient(); void testManualRenderPattern_ContentUser_RefObb(); void testManualRenderPattern_ContentObb_RefObb(); void testManualRenderPattern_ContentUser_RefUser(); void testManualRenderPattern_ContentObb_RefObb_Transform_Rotate(); void testManualRenderPattern_ContentView_RefObb(); void testManualRenderPattern_ContentView_RefUser(); void testRenderPattern_r_User_c_User(); void testRenderPattern_InfiniteRecursionWhenInherited(); void testRenderPattern_r_User_c_View(); void testRenderPattern_r_User_c_Obb(); void testRenderPattern_r_User_c_View_Rotated(); void testRenderPattern_r_Obb_c_View_Rotated(); void testKoClipPathRendering(); void testKoClipPathRelativeRendering(); void testRenderClipPath_User(); void testRenderClipPath_Obb(); void testRenderClipPath_Obb_Transform(); void testRenderClipMask_Obb(); void testRenderClipMask_User_Clip_Obb(); void testRenderClipMask_User_Clip_User(); void testRenderImage_AspectDefault(); void testRenderImage_AspectNone(); void testRenderImage_AspectMeet(); void testRectShapeRoundUniformX(); void testRectShapeRoundUniformY(); void testRectShapeRoundXY(); void testRectShapeRoundXYOverflow(); void testCircleShape(); void testEllipseShape(); void testLineShape(); void testPolylineShape(); void testPolygonShape(); void testPathShape(); void testDefsHidden(); void testDefsUseInheritance(); void testUseWithoutDefs(); void testMarkersAutoOrientation(); void testMarkersAutoOrientationScaled(); void testMarkersAutoOrientationScaledUserCoordinates(); void testMarkersCustomOrientation(); void testMarkersDifferent(); - void testGradientRecoveringTrasnform(); + void testGradientRecoveringTransform(); void testMarkersOnClosedPath(); void testMarkersAngularUnits(); void testSodipodiArcShape(); void testSodipodiArcShapeOpen(); void testKritaChordShape(); void testSodipodiChordShape(); void testMarkersFillAsShape(); private: }; #endif // TESTSVGPARSER_H diff --git a/libs/flake/text/KoSvgTextChunkShape.h b/libs/flake/text/KoSvgTextChunkShape.h index 00f54ac54c..fd790df9a6 100644 --- a/libs/flake/text/KoSvgTextChunkShape.h +++ b/libs/flake/text/KoSvgTextChunkShape.h @@ -1,174 +1,174 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KOSVGTEXTCHUNKSHAPE_H #define KOSVGTEXTCHUNKSHAPE_H #include "kritaflake_export.h" #include #include class HtmlSavingContext; class KoSvgTextProperties; class KoSvgTextChunkShapePrivate; class KoSvgTextChunkShapeLayoutInterface; /** * KoSvgTextChunkShape is an elementary block of SVG text object. * * KoSvgTextChunkShape represents either a or element of SVG. * The chunk shape uses flake hierarchy to represent the DOM hierarchy of the * supplied text. All the attributes of text blocks can be fetched using * textProperties() method. * * KoSvgTextChunkShape uses special text properties object to overcome the * flake's "property inheritance" limitation. Basically, flake doesn't support * the inheritance of shape properties: every shape stores all the properties * that were defined at the stage of loading/creation. KoSvgTextProperties is a * wrapper that allows the user to compare the properties of the two shapes and * return only the ones that are unique for a child shape. That allows us to * generate a correct SVG/markup code that can be edited by the user easily. * * WARNING: beware the difference between "svg-text-chunk" and * KoSvgTextChunkShape! The chunk shape is **not** a "text chunk" in SVG's - * definition. Accrding to SVG, "text chunk" is a set of characters anchored to + * definition. According to SVG, "text chunk" is a set of characters anchored to * a specific absolute position on canvas. And KoSvgTextChunkShape is just one * or element. Obviously, one can contain multiple "text * chunks" and, vice versa, a "text chunk" can spread onto multiple 's. */ class KRITAFLAKE_EXPORT KoSvgTextChunkShape : public KoShapeContainer, public SvgShape { public: KoSvgTextChunkShape(); KoSvgTextChunkShape(const KoSvgTextChunkShape &rhs); ~KoSvgTextChunkShape() override; KoShape* cloneShape() const override; QSizeF size() const override; void setSize(const QSizeF &size) override; QRectF outlineRect() const override; QPainterPath outline() const override; void paintComponent(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) override; void saveOdf(KoShapeSavingContext &Context) const override; bool loadOdf(const KoXmlElement &element, KoShapeLoadingContext &Context) override; bool saveHtml(HtmlSavingContext &context); /** * Reset the text shape into initial state, removing all the child shapes. * This method is used by text-updating code to upload the updated text * into shape. The uploading code first calls resetTextShape() and then adds * new children. */ virtual void resetTextShape(); bool saveSvg(SvgSavingContext &context) override; bool loadSvg(const KoXmlElement &element, SvgLoadingContext &context) override; bool loadSvgTextNode(const KoXmlText &text, SvgLoadingContext &context); /** * Normalize the SVG character transformations * * In SVG x,y,dx,dy,rotate attributes are inherited from the parent shapes * in a curious ways. We do not want to unwind the links on the fly, so we * just parse and adjust them right when the loading completed. After * normalizeCharTransformations() is finished, all the child shapes will * have local transformations set according to the parent's lists. */ void normalizeCharTransformations(); /** * Compress the inheritance of 'fill' and 'stroke'. * * The loading code makes no difference if the fill or stroke was inherited * or not. That is not a problem for normal shapes, but it cannot work for * text shapes. The text has different inheritance rules: if the parent * text chunk has a gradient fill, its inheriting descendants will - * **smoothly continue** this grandient. They will not start a new gradient + * **smoothly continue** this gradient. They will not start a new gradient * in their local coordinate system. * * Therefore, after loading, the loading code calls * simplifyFillStrokeInheritance() which marks the coinciding strokes and * fills so that the rendering code will be able to distinguish them in the * future. * * Proper fill inheritance is also needed for the GUI. When the user * changes the color of the parent text chunk, all the inheriting children * should update its color automatically, without GUI recursively * traversing the shapes. * */ void simplifyFillStrokeInheritance(); /** * SVG properties of the text chunk * @return the properties object with fill and stroke included as a property */ KoSvgTextProperties textProperties() const; /** * Return the type of the chunk. * * The chunk can be either a "text chunk", that contains a text string * itself, of an "intermediate chunk" that doesn't contain any text itself, * but works as a group for a set of child chunks, which might be either * text (leaf) or intermediate chunks. Such groups are needed to define a * common text style for a group of '' objects. * * @return true if the chunk is a "text chunk" false if it is "intermediate chunk" */ bool isTextNode() const; /** * A special interface for KoSvgTextShape's layout code. Don't use it * unless you are KoSvgTextShape. */ KoSvgTextChunkShapeLayoutInterface* layoutInterface(); /** * WARNING: this propperty is available only if isRootTextNode() is true * * @return true if the shape should be edited in a rich-text editor */ bool isRichTextPreferred() const; /** * WARNING: this propperty is available only if isRootTextNode() is true * * Sets whether the shape should be edited in rich-text editor */ void setRichTextPreferred(bool value); protected: /** * Show if the shape is a root of the text hierarchy. Always true for * KoSvgTextShape and always false for KoSvgTextChunkShape */ virtual bool isRootTextNode() const; protected: KoSvgTextChunkShape(KoSvgTextChunkShapePrivate *dd); private: Q_DECLARE_PRIVATE(KoSvgTextChunkShape) }; #endif // KOSVGTEXTCHUNKSHAPE_H diff --git a/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp b/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp index 6de318be7f..c1f0db21ea 100644 --- a/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp +++ b/libs/flake/text/KoSvgTextShapeMarkupConverter.cpp @@ -1,1244 +1,1244 @@ /* * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "KoSvgTextShapeMarkupConverter.h" #include "klocalizedstring.h" #include "kis_assert.h" #include "kis_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kis_dom_utils.h" #include #include struct KoSvgTextShapeMarkupConverter::Private { Private(KoSvgTextShape *_shape) : shape(_shape) {} KoSvgTextShape *shape; QStringList errors; QStringList warnings; void clearErrors() { errors.clear(); warnings.clear(); } }; KoSvgTextShapeMarkupConverter::KoSvgTextShapeMarkupConverter(KoSvgTextShape *shape) : d(new Private(shape)) { } KoSvgTextShapeMarkupConverter::~KoSvgTextShapeMarkupConverter() { } bool KoSvgTextShapeMarkupConverter::convertToSvg(QString *svgText, QString *stylesText) { d->clearErrors(); QBuffer shapesBuffer; QBuffer stylesBuffer; shapesBuffer.open(QIODevice::WriteOnly); stylesBuffer.open(QIODevice::WriteOnly); { SvgSavingContext savingContext(shapesBuffer, stylesBuffer); savingContext.setStrippedTextMode(true); SvgWriter writer({d->shape}); writer.saveDetached(savingContext); } shapesBuffer.close(); stylesBuffer.close(); *svgText = QString::fromUtf8(shapesBuffer.data()); *stylesText = QString::fromUtf8(stylesBuffer.data()); return true; } bool KoSvgTextShapeMarkupConverter::convertFromSvg(const QString &svgText, const QString &stylesText, const QRectF &boundsInPixels, qreal pixelsPerInch) { debugFlake << "convertFromSvg. text:" << svgText << "styles:" << stylesText << "bounds:" << boundsInPixels << "ppi:" << pixelsPerInch; d->clearErrors(); QString errorMessage; int errorLine = 0; int errorColumn = 0; const QString fullText = QString("\n%1\n%2\n\n").arg(stylesText).arg(svgText); KoXmlDocument doc = SvgParser::createDocumentFromSvg(fullText, &errorMessage, &errorLine, &errorColumn); if (doc.isNull()) { d->errors << QString("line %1, col %2: %3").arg(errorLine).arg(errorColumn).arg(errorMessage); return false; } d->shape->resetTextShape(); KoDocumentResourceManager resourceManager; SvgParser parser(&resourceManager); parser.setResolution(boundsInPixels, pixelsPerInch); KoXmlElement root = doc.documentElement(); KoXmlNode node = root.firstChild(); bool textNodeFound = false; for (; !node.isNull(); node = node.nextSibling()) { KoXmlElement el = node.toElement(); if (el.isNull()) continue; if (el.tagName() == "defs") { parser.parseDefsElement(el); } else if (el.tagName() == "text") { if (textNodeFound) { d->errors << i18n("More than one 'text' node found!"); return false; } KoShape *shape = parser.parseTextElement(el, d->shape); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(shape == d->shape, false); textNodeFound = true; break; } else { d->errors << i18n("Unknown node of type \'%1\' found!", el.tagName()); return false; } } if (!textNodeFound) { d->errors << i18n("No \'text\' node found!"); return false; } return true; } bool KoSvgTextShapeMarkupConverter::convertToHtml(QString *htmlText) { d->clearErrors(); QBuffer shapesBuffer; shapesBuffer.open(QIODevice::WriteOnly); { HtmlWriter writer({d->shape}); if (!writer.save(shapesBuffer)) { d->errors = writer.errors(); d->warnings = writer.warnings(); return false; } } shapesBuffer.close(); *htmlText = QString(shapesBuffer.data()); debugFlake << "\t\t" << *htmlText; return true; } bool KoSvgTextShapeMarkupConverter::convertFromHtml(const QString &htmlText, QString *svgText, QString *styles) { debugFlake << ">>>>>>>>>>>" << htmlText; QBuffer svgBuffer; svgBuffer.open(QIODevice::WriteOnly); QXmlStreamReader htmlReader(htmlText); QXmlStreamWriter svgWriter(&svgBuffer); svgWriter.setAutoFormatting(true); QStringRef elementName; bool newLine = false; int lineCount = 0; QString bodyEm = "1em"; QString em; QString p("p"); //previous style string is for keeping formatting proper on linebreaks and appendstyle is for specific tags QString previousStyleString; QString appendStyle; while (!htmlReader.atEnd()) { QXmlStreamReader::TokenType token = htmlReader.readNext(); switch (token) { case QXmlStreamReader::StartElement: { newLine = false; if (htmlReader.name() == "br") { debugFlake << "\tdoing br"; svgWriter.writeEndElement(); elementName = QStringRef(&p); em = bodyEm; appendStyle = previousStyleString; } else { elementName = htmlReader.name(); em = ""; } if (elementName == "body") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("text"); appendStyle = QString(); } else if (elementName == "p") { // new line debugFlake << "\t\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); newLine = true; if (em.isEmpty()) { em = bodyEm; appendStyle = QString(); } lineCount++; } else if (elementName == "span") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = QString(); } else if (elementName == "b" || elementName == "strong") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "font-weight:700;"; } else if (elementName == "i" || elementName == "em") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "font-style:italic;"; } else if (elementName == "u") { debugFlake << "\tstart Element" << elementName; svgWriter.writeStartElement("tspan"); appendStyle = "text-decoration:underline"; } QXmlStreamAttributes attributes = htmlReader.attributes(); QString textAlign; if (attributes.hasAttribute("align")) { textAlign = attributes.value("align").toString(); } if (attributes.hasAttribute("style")) { QString filteredStyles; QStringList svgStyles = QString("font-family font-size font-weight font-variant word-spacing text-decoration font-style font-size-adjust font-stretch direction").split(" "); QStringList styles = attributes.value("style").toString().split(";"); for(int i=0; i 1) { debugFlake << "\t\tAdvancing to the next line"; svgWriter.writeAttribute("x", "0"); svgWriter.writeAttribute("dy", em); } break; } case QXmlStreamReader::EndElement: { if (htmlReader.name() == "br") break; if (elementName == "p" || elementName == "span" || elementName == "body") { debugFlake << "\tEndElement" << htmlReader.name() << "(" << elementName << ")"; svgWriter.writeEndElement(); } break; } case QXmlStreamReader::Characters: { if (elementName == "style") { *styles = htmlReader.text().toString(); } else { if (!htmlReader.isWhitespace()) { debugFlake << "\tCharacters:" << htmlReader.text(); svgWriter.writeCharacters(htmlReader.text().toString()); } } break; } default: ; } } if (htmlReader.hasError()) { d->errors << htmlReader.errorString(); return false; } if (svgWriter.hasError()) { d->errors << i18n("Unknown error writing SVG text element"); return false; } *svgText = QString::fromUtf8(svgBuffer.data()); return true; } void postCorrectBlockHeight(QTextDocument *doc, qreal currLineAscent, qreal prevLineAscent, qreal prevLineDescent, int prevBlockCursorPosition, qreal currentBlockAbsoluteLineOffset) { KIS_SAFE_ASSERT_RECOVER_RETURN(prevBlockCursorPosition >= 0); QTextCursor postCorrectionCursor(doc); postCorrectionCursor.setPosition(prevBlockCursorPosition); if (!postCorrectionCursor.isNull()) { const qreal relativeLineHeight = ((currentBlockAbsoluteLineOffset - currLineAscent + prevLineAscent) / (prevLineAscent + prevLineDescent)) * 100.0; QTextBlockFormat format = postCorrectionCursor.blockFormat(); format.setLineHeight(relativeLineHeight, QTextBlockFormat::ProportionalHeight); postCorrectionCursor.setBlockFormat(format); postCorrectionCursor = QTextCursor(); } } QTextFormat findMostCommonFormat(const QList &allFormats) { QTextCharFormat mostCommonFormat; QSet propertyIds; /** * Get all existing property ids */ Q_FOREACH (const QTextFormat &format, allFormats) { const QMap formatProperties = format.properties(); Q_FOREACH (int id, formatProperties.keys()) { propertyIds.insert(id); } } /** * Filter out properties that do not exist in some formats. Otherwise, the * global format may override the default value used in these formats * (and yes, we do not have access to the default values to use them * in difference calculation algorithm */ Q_FOREACH (const QTextFormat &format, allFormats) { for (auto it = propertyIds.begin(); it != propertyIds.end();) { if (!format.hasProperty(*it)) { it = propertyIds.erase(it); } else { ++it; } } if (propertyIds.isEmpty()) break; } if (!propertyIds.isEmpty()) { QMap> propertyFrequency; /** * Calculate the frequency of values used in *all* the formats */ Q_FOREACH (const QTextFormat &format, allFormats) { const QMap formatProperties = format.properties(); Q_FOREACH (int id, propertyIds) { KIS_SAFE_ASSERT_RECOVER_BREAK(formatProperties.contains(id)); propertyFrequency[id][formatProperties.value(id)]++; } } /** * Add the most popular property value to the set of most common properties */ for (auto it = propertyFrequency.constBegin(); it != propertyFrequency.constEnd(); ++it) { const int id = it.key(); const QMap allValues = it.value(); int maxCount = 0; QVariant maxValue; for (auto valIt = allValues.constBegin(); valIt != allValues.constEnd(); ++valIt) { if (valIt.value() > maxCount) { maxCount = valIt.value(); maxValue = valIt.key(); } } KIS_SAFE_ASSERT_RECOVER_BREAK(maxCount > 0); mostCommonFormat.setProperty(id, maxValue); } } return mostCommonFormat; } qreal calcLineWidth(const QTextBlock &block) { const QString blockText = block.text(); QTextLayout lineLayout; lineLayout.setText(blockText); lineLayout.setFont(block.charFormat().font()); lineLayout.setFormats(block.textFormats()); lineLayout.setTextOption(block.layout()->textOption()); lineLayout.beginLayout(); QTextLine fullLine = lineLayout.createLine(); if (!fullLine.isValid()) { fullLine.setNumColumns(blockText.size()); } lineLayout.endLayout(); return lineLayout.boundingRect().width(); } bool KoSvgTextShapeMarkupConverter::convertDocumentToSvg(const QTextDocument *doc, QString *svgText) { QBuffer svgBuffer; svgBuffer.open(QIODevice::WriteOnly); QXmlStreamWriter svgWriter(&svgBuffer); // disable auto-formatting to avoid axtra spaces appearing here and there svgWriter.setAutoFormatting(false); qreal maxParagraphWidth = 0.0; QTextCharFormat mostCommonCharFormat; QTextBlockFormat mostCommonBlockFormat; struct LineInfo { LineInfo() {} LineInfo(QTextBlock _block, int _numSkippedLines) : block(_block), numSkippedLines(_numSkippedLines) {} QTextBlock block; int numSkippedLines = 0; }; QVector lineInfoList; { QTextBlock block = doc->begin(); QList allCharFormats; QList allBlockFormats; int numSequentialEmptyLines = 0; while (block.isValid()) { if (!block.text().trimmed().isEmpty()) { lineInfoList.append(LineInfo(block, numSequentialEmptyLines)); numSequentialEmptyLines = 0; maxParagraphWidth = qMax(maxParagraphWidth, calcLineWidth(block)); allBlockFormats.append(block.blockFormat()); Q_FOREACH (const QTextLayout::FormatRange &range, block.textFormats()) { QTextFormat format = range.format; allCharFormats.append(format); } } else { numSequentialEmptyLines++; } block = block.next(); } mostCommonCharFormat = findMostCommonFormat(allCharFormats).toCharFormat(); mostCommonBlockFormat = findMostCommonFormat(allBlockFormats).toBlockFormat(); } //Okay, now the actual writing. QTextBlock block = doc->begin(); Q_UNUSED(block); svgWriter.writeStartElement("text"); { const QString commonTextStyle = style(mostCommonCharFormat, mostCommonBlockFormat); if (!commonTextStyle.isEmpty()) { svgWriter.writeAttribute("style", commonTextStyle); } } int prevBlockRelativeLineSpacing = mostCommonBlockFormat.lineHeight(); int prevBlockLineType = mostCommonBlockFormat.lineHeightType(); qreal prevBlockAscent = 0.0; qreal prevBlockDescent= 0.0; Q_FOREACH (const LineInfo &info, lineInfoList) { QTextBlock block = info.block; const QTextBlockFormat blockFormatDiff = formatDifference(block.blockFormat(), mostCommonBlockFormat).toBlockFormat(); QTextCharFormat blockCharFormatDiff = QTextCharFormat(); const QVector formats = block.textFormats(); if (formats.size()==1) { blockCharFormatDiff = formatDifference(formats.at(0).format, mostCommonCharFormat).toCharFormat(); } const QTextLayout *layout = block.layout(); const QTextLine line = layout->lineAt(0); svgWriter.writeStartElement("tspan"); const QString text = block.text(); /** * Mindblowing part: QTextEdit uses a hi-end algorithm for auto-estimation for the text * directionality, so the user expects his text being saved to SVG with the same * directionality. Just emulate behavior of direction="auto", which is not supported by * SVG 1.1 * * BUG: 392064 */ bool isRightToLeft = false; for (int i = 0; i < text.size(); i++) { const QChar ch = text[i]; if (ch.direction() == QChar::DirR || ch.direction() == QChar::DirAL) { isRightToLeft = true; break; } else if (ch.direction() == QChar::DirL) { break; } } if (isRightToLeft) { svgWriter.writeAttribute("direction", "rtl"); svgWriter.writeAttribute("unicode-bidi", "embed"); } { const QString blockStyleString = style(blockCharFormatDiff, blockFormatDiff); if (!blockStyleString.isEmpty()) { svgWriter.writeAttribute("style", blockStyleString); } } /** * The alignment rule will be inverted while rendering the text in the text shape - * (accordign to the standard the alignment is defined not by "left" or "right", + * (according to the standard the alignment is defined not by "left" or "right", * but by "start" and "end", which inverts for rtl text) */ Qt::Alignment blockAlignment = block.blockFormat().alignment(); if (isRightToLeft) { if (blockAlignment & Qt::AlignLeft) { blockAlignment &= ~Qt::AlignLeft; blockAlignment |= Qt::AlignRight; } else if (blockAlignment & Qt::AlignRight) { blockAlignment &= ~Qt::AlignRight; blockAlignment |= Qt::AlignLeft; } } if (blockAlignment & Qt::AlignHCenter) { svgWriter.writeAttribute("x", KisDomUtils::toString(0.5 * maxParagraphWidth) + "pt"); } else if (blockAlignment & Qt::AlignRight) { svgWriter.writeAttribute("x", KisDomUtils::toString(maxParagraphWidth) + "pt"); } else { svgWriter.writeAttribute("x", "0"); } if (block.blockNumber() > 0) { const qreal lineHeightPt = line.ascent() - prevBlockAscent + (prevBlockAscent + prevBlockDescent) * qreal(prevBlockRelativeLineSpacing) / 100.0; const qreal currentLineSpacing = (info.numSkippedLines + 1) * lineHeightPt; svgWriter.writeAttribute("dy", KisDomUtils::toString(currentLineSpacing) + "pt"); } prevBlockRelativeLineSpacing = blockFormatDiff.hasProperty(QTextFormat::LineHeight) ? blockFormatDiff.lineHeight() : mostCommonBlockFormat.lineHeight(); prevBlockLineType = blockFormatDiff.hasProperty(QTextFormat::LineHeightType) ? blockFormatDiff.lineHeightType() : mostCommonBlockFormat.lineHeightType(); if (prevBlockLineType == QTextBlockFormat::SingleHeight) { //single line will set lineHeight to 100% prevBlockRelativeLineSpacing = 100; } prevBlockAscent = line.ascent(); prevBlockDescent = line.descent(); if (formats.size()>1) { QStringList texts; QVector charFormats; for (int f=0; ferrors << i18n("Unknown error writing SVG text element"); return false; } *svgText = QString::fromUtf8(svgBuffer.data()).trimmed(); return true; } void parseTextAttributes(const QXmlStreamAttributes &elementAttributes, QTextCharFormat &charFormat, QTextBlockFormat &blockFormat) { QString styleString; // we convert all the presentation attributes into styles QString presentationAttributes; for (int a = 0; a < elementAttributes.size(); a++) { if (elementAttributes.at(a).name() != "style") { presentationAttributes .append(elementAttributes.at(a).name().toString()) .append(":") .append(elementAttributes.at(a).value().toString()) .append(";"); } } if (presentationAttributes.endsWith(";")) { presentationAttributes.chop(1); } if (elementAttributes.hasAttribute("style")) { styleString = elementAttributes.value("style").toString(); if (styleString.endsWith(";")) { styleString.chop(1); } } if (!styleString.isEmpty() || !presentationAttributes.isEmpty()) { //add attributes to parse them as part of the style. styleString.append(";") .append(presentationAttributes); QStringList styles = styleString.split(";"); QVector formats = KoSvgTextShapeMarkupConverter::stylesFromString(styles, charFormat, blockFormat); charFormat = formats.at(0).toCharFormat(); blockFormat = formats.at(1).toBlockFormat(); } } bool KoSvgTextShapeMarkupConverter::convertSvgToDocument(const QString &svgText, QTextDocument *doc) { QXmlStreamReader svgReader(svgText.trimmed()); doc->clear(); QTextCursor cursor(doc); struct BlockFormatRecord { BlockFormatRecord() {} BlockFormatRecord(QTextBlockFormat _blockFormat, QTextCharFormat _charFormat) : blockFormat(_blockFormat), charFormat(_charFormat) {} QTextBlockFormat blockFormat; QTextCharFormat charFormat; }; QStack formatStack; formatStack.push(BlockFormatRecord(QTextBlockFormat(), QTextCharFormat())); qreal currBlockAbsoluteLineOffset = 0.0; int prevBlockCursorPosition = -1; qreal prevLineDescent = 0.0; qreal prevLineAscent = 0.0; boost::optional previousBlockAbsoluteXOffset = boost::none; while (!svgReader.atEnd()) { QXmlStreamReader::TokenType token = svgReader.readNext(); switch (token) { case QXmlStreamReader::StartElement: { bool newBlock = false; QTextBlockFormat newBlockFormat; QTextCharFormat newCharFormat; qreal absoluteLineOffset = 1.0; // fetch format of the parent block and make it default if (formatStack.size() >= 2) { newBlockFormat = formatStack[formatStack.size() - 2].blockFormat; newCharFormat = formatStack[formatStack.size() - 2].charFormat; } { const QXmlStreamAttributes elementAttributes = svgReader.attributes(); parseTextAttributes(elementAttributes, newCharFormat, newBlockFormat); // mnemonic for a newline is (dy != 0 && x == 0) boost::optional blockAbsoluteXOffset = boost::none; if (elementAttributes.hasAttribute("x")) { QString xString = elementAttributes.value("x").toString(); if (xString.contains("pt")) { xString = xString.remove("pt").trimmed(); } blockAbsoluteXOffset = KisDomUtils::toDouble(xString); } if (previousBlockAbsoluteXOffset && blockAbsoluteXOffset && qFuzzyCompare(*previousBlockAbsoluteXOffset, *blockAbsoluteXOffset) && svgReader.name() != "text" && elementAttributes.hasAttribute("dy")) { QString dyString = elementAttributes.value("dy").toString(); if (dyString.contains("pt")) { dyString = dyString.remove("pt").trimmed(); } KIS_SAFE_ASSERT_RECOVER_NOOP(formatStack.isEmpty() == (svgReader.name() == "text")); absoluteLineOffset = KisDomUtils::toDouble(dyString); newBlock = absoluteLineOffset > 0; } if (elementAttributes.hasAttribute("x")) { previousBlockAbsoluteXOffset = blockAbsoluteXOffset; } } //hack doc->setTextWidth(100); doc->setTextWidth(-1); if (newBlock && absoluteLineOffset > 0) { KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(!formatStack.isEmpty(), false); KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(cursor.block().layout()->lineCount() == 1, false); QTextLine line = cursor.block().layout()->lineAt(0); if (prevBlockCursorPosition >= 0) { postCorrectBlockHeight(doc, line.ascent(), prevLineAscent, prevLineDescent, prevBlockCursorPosition, currBlockAbsoluteLineOffset); } prevBlockCursorPosition = cursor.position(); prevLineAscent = line.ascent(); prevLineDescent = line.descent(); currBlockAbsoluteLineOffset = absoluteLineOffset; cursor.insertBlock(); cursor.setCharFormat(formatStack.top().charFormat); cursor.setBlockFormat(formatStack.top().blockFormat); } cursor.mergeCharFormat(newCharFormat); cursor.mergeBlockFormat(newBlockFormat); formatStack.push(BlockFormatRecord(cursor.blockFormat(), cursor.charFormat())); break; } case QXmlStreamReader::EndElement: { if (svgReader.name() != "text") { formatStack.pop(); KIS_SAFE_ASSERT_RECOVER(!formatStack.isEmpty()) { break; } cursor.setCharFormat(formatStack.top().charFormat); cursor.setBlockFormat(formatStack.top().blockFormat); } break; } case QXmlStreamReader::Characters: { if (!svgReader.isWhitespace()) { cursor.insertText(svgReader.text().toString()); } break; } default: break; } } if (prevBlockCursorPosition >= 0) { QTextLine line = cursor.block().layout()->lineAt(0); postCorrectBlockHeight(doc, line.ascent(), prevLineAscent, prevLineDescent, prevBlockCursorPosition, currBlockAbsoluteLineOffset); } if (svgReader.hasError()) { d->errors << svgReader.errorString(); return false; } doc->setModified(false); return true; } QStringList KoSvgTextShapeMarkupConverter::errors() const { return d->errors; } QStringList KoSvgTextShapeMarkupConverter::warnings() const { return d->warnings; } QString KoSvgTextShapeMarkupConverter::style(QTextCharFormat format, QTextBlockFormat blockFormat, QTextCharFormat mostCommon) { QStringList style; for(int i=0; i KoSvgTextShapeMarkupConverter::stylesFromString(QStringList styles, QTextCharFormat currentCharFormat, QTextBlockFormat currentBlockFormat) { Q_UNUSED(currentBlockFormat); QVector formats; QTextCharFormat charFormat; charFormat.setTextOutline(currentCharFormat.textOutline()); QTextBlockFormat blockFormat; SvgGraphicsContext *context = new SvgGraphicsContext(); for (int i=0; i props = reference.properties(); for (QMap::ConstIterator it = props.begin(), end = props.end(); it != end; ++it) if (it.value() == test.property(it.key())) diff.clearProperty(it.key()); return diff; } diff --git a/libs/flake/tools/KoShapeRubberSelectStrategy.h b/libs/flake/tools/KoShapeRubberSelectStrategy.h index 1b5464903c..8858326c02 100644 --- a/libs/flake/tools/KoShapeRubberSelectStrategy.h +++ b/libs/flake/tools/KoShapeRubberSelectStrategy.h @@ -1,76 +1,76 @@ /* This file is part of the KDE project Copyright (C) 2006 Thorsten Zachmann Copyright (C) 2006 Thomas Zander This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef KOSHAPERUBBERSELECTSTRATEGY_H #define KOSHAPERUBBERSELECTSTRATEGY_H #include "KoInteractionStrategy.h" #include #include "kritaflake_export.h" class KoToolBase; class KoShapeRubberSelectStrategyPrivate; /** * This is a base class for interactions based on dragging a rectangular area on the canvas, * such as selection, zooming or shape creation. * * When the user selects stuff in left-to-right way, selection is in "covering" * (or "containing") mode, when in "left-to-right" in "crossing" mode */ class KRITAFLAKE_EXPORT KoShapeRubberSelectStrategy : public KoInteractionStrategy { public: /** * Constructor that initiates the rubber select. * A rubber select is basically rectangle area that the user drags out * from @p clicked to a point later provided in the handleMouseMove() continuously - * showing a semi-transarant 'rubber-mat' over the objects it is about to select. + * showing a semi-transparent 'rubber-mat' over the objects it is about to select. * @param tool the parent tool which controls this strategy * @param clicked the initial point that the user depressed (in pt). * @param useSnapToGrid use the snap-to-grid settings while doing the rubberstamp. */ KoShapeRubberSelectStrategy(KoToolBase *tool, const QPointF &clicked, bool useSnapToGrid = false); void paint(QPainter &painter, const KoViewConverter &converter) override; void handleMouseMove(const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers) override; KUndo2Command *createCommand() override; protected: /// constructor KoShapeRubberSelectStrategy(KoShapeRubberSelectStrategyPrivate &); QRectF selectedRectangle() const; enum SelectionMode { CrossingSelection, CoveringSelection }; virtual SelectionMode currentMode() const; private: Q_DECLARE_PRIVATE(KoShapeRubberSelectStrategy) }; #endif /* KOSHAPERUBBERSELECTSTRATEGY_H */ diff --git a/libs/image/brushengine/kis_paintop.h b/libs/image/brushengine/kis_paintop.h index 8291eaef50..3042da6986 100644 --- a/libs/image/brushengine/kis_paintop.h +++ b/libs/image/brushengine/kis_paintop.h @@ -1,165 +1,165 @@ /* * Copyright (c) 2002 Patrick Julien * Copyright (c) 2004 Boudewijn Rempt * Copyright (c) 2004 Clarence Dang * Copyright (c) 2004 Adrian Page * Copyright (c) 2004,2010 Cyrille Berger * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_PAINTOP_H_ #define KIS_PAINTOP_H_ #include #include "kis_shared.h" #include "kis_types.h" #include class QPointF; class KoColorSpace; class KisPainter; class KisPaintInformation; class KisRunnableStrokeJobData; /** * KisPaintOp are use by tools to draw on a paint device. A paintop takes settings * and input information, like pressure, tilt or motion and uses that to draw pixels */ class KRITAIMAGE_EXPORT KisPaintOp : public KisShared { struct Private; public: KisPaintOp(KisPainter * painter); virtual ~KisPaintOp(); /** * Paint at the subpixel point pos using the specified paint information.. * * The distance/time between two calls of the paintAt is always specified by spacing and timing, * which are automatically saved into the current distance information object. */ void paintAt(const KisPaintInformation& info, KisDistanceInformation *currentDistance); /** * Updates the spacing settings in currentDistance based on the provided information. Note that * the spacing is updated automatically in the paintAt method, so there is no need to call this * method if paintAt has just been called. */ void updateSpacing(const KisPaintInformation &info, KisDistanceInformation ¤tDistance) const; /** * Updates the timing settings in currentDistance based on the provided information. Note that * the timing is updated automatically in the paintAt method, so there is no need to call this * method if paintAt has just been called. */ void updateTiming(const KisPaintInformation &info, KisDistanceInformation ¤tDistance) const; /** * Draw a line between pos1 and pos2 using the currently set brush and color. * If savedDist is less than zero, the brush is painted at pos1 before being * painted along the line using the spacing setting. * * @return the drag distance, that is the remains of the distance - * between p1 and p2 not covered because the currenlty set brush + * between p1 and p2 not covered because the currently set brush * has a spacing greater than that distance. */ virtual void paintLine(const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance); /** * Draw a Bezier curve between pos1 and pos2 using control points 1 and 2. * If savedDist is less than zero, the brush is painted at pos1 before being * painted along the curve using the spacing setting. * @return the drag distance, that is the remains of the distance between p1 and p2 not covered - * because the currenlty set brush has a spacing greater than that distance. + * because the currently set brush has a spacing greater than that distance. */ virtual void paintBezierCurve(const KisPaintInformation &pi1, const QPointF &control1, const QPointF &control2, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance); /** * Whether this paintop can paint. Can be false in case that some setting isn't read correctly. * @return if paintop is ready for painting, default is true */ virtual bool canPaint() const { return true; } /** * Split the coordinate into whole + fraction, where fraction is always >= 0. */ static void splitCoordinate(qreal coordinate, qint32 *whole, qreal *fraction); /** * If the preset supports asynchronous updates, then the stroke execution core will - * call this method with a desured frame rate. The jobs that should be run to prepare the update + * call this method with a desired frame rate. The jobs that should be run to prepare the update * are returned via \p jobs * * @return a pair of */ virtual std::pair doAsyncronousUpdate(QVector &jobs); protected: friend class KisPaintInformation; /** * The implementation of painting of a dab and updating spacing. This does NOT need to update * the timing information. */ virtual KisSpacingInformation paintAt(const KisPaintInformation& info) = 0; /** * Implementation of a spacing update */ virtual KisSpacingInformation updateSpacingImpl(const KisPaintInformation &info) const = 0; /** * Implementation of a timing update. The default implementation always disables timing. This is * suitable for paintops that do not support airbrushing. */ virtual KisTimingInformation updateTimingImpl(const KisPaintInformation &info) const; KisFixedPaintDeviceSP cachedDab(); KisFixedPaintDeviceSP cachedDab(const KoColorSpace *cs); /** * Return the painter this paintop is owned by */ KisPainter* painter() const; /** * Return the paintdevice the painter this paintop is owned by */ KisPaintDeviceSP source() const; private: friend class KisPressureRotationOption; void setFanCornersInfo(bool fanCornersEnabled, qreal fanCornersStep); private: Private* const d; }; #endif // KIS_PAINTOP_H_ diff --git a/libs/image/brushengine/kis_paintop_utils.h b/libs/image/brushengine/kis_paintop_utils.h index bf4486b3ad..e0273925e4 100644 --- a/libs/image/brushengine/kis_paintop_utils.h +++ b/libs/image/brushengine/kis_paintop_utils.h @@ -1,203 +1,203 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_PAINTOP_UTILS_H #define __KIS_PAINTOP_UTILS_H #include "kis_global.h" #include "kis_paint_information.h" #include "kis_distance_information.h" #include "kis_spacing_information.h" #include "kis_timing_information.h" #include "kritaimage_export.h" struct KisRenderedDab; namespace KisPaintOpUtils { template bool paintFan(PaintOp &op, const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance, qreal fanCornersStep) { const qreal angleStep = fanCornersStep; const qreal initialAngle = currentDistance->lastDrawingAngle(); const qreal finalAngle = pi2.drawingAngleSafe(*currentDistance); const qreal fullDistance = shortestAngularDistance(initialAngle, finalAngle); qreal lastAngle = initialAngle; int i = 0; while (shortestAngularDistance(lastAngle, finalAngle) > angleStep) { lastAngle = incrementInDirection(lastAngle, angleStep, finalAngle); qreal t = angleStep * i++ / fullDistance; QPointF pt = pi1.pos() + t * (pi2.pos() - pi1.pos()); KisPaintInformation pi = KisPaintInformation::mix(pt, t, pi1, pi2); pi.overrideDrawingAngle(lastAngle); pi.paintAt(op, currentDistance); } return i; } template void paintLine(PaintOp &op, const KisPaintInformation &pi1, const KisPaintInformation &pi2, KisDistanceInformation *currentDistance, bool fanCornersEnabled, qreal fanCornersStep) { QPointF end = pi2.pos(); qreal endTime = pi2.currentTime(); KisPaintInformation pi = pi1; qreal t = 0.0; while ((t = currentDistance->getNextPointPosition(pi.pos(), end, pi.currentTime(), endTime)) >= 0.0) { pi = KisPaintInformation::mix(t, pi, pi2); if (fanCornersEnabled && currentDistance->hasLastPaintInformation()) { paintFan(op, currentDistance->lastPaintInformation(), pi, currentDistance, fanCornersStep); } /** * A bit complicated part to ensure the registration * of the distance information is done in right order */ pi.paintAt(op, currentDistance); } /* * Perform spacing and/or timing updates between dabs if appropriate. Typically, this will not * happen if the above loop actually painted anything. This is because the * getNextPointPosition() call before the paint operation will reset the accumulators in * currentDistance and therefore make needsSpacingUpdate() and needsTimingUpdate() false. The * temporal distance between pi1 and pi2 is typically too small for the accumulators to build * back up enough to require a spacing or timing update after that. (The accumulated time values * are updated not during the paint operation, but during the call to getNextPointPosition(), * that is, updated during every paintLine() call.) */ if (currentDistance->needsSpacingUpdate()) { op.updateSpacing(pi2, *currentDistance); } if (currentDistance->needsTimingUpdate()) { op.updateTiming(pi2, *currentDistance); } } /** * A special class containing the previous position of the cursor for * the sake of painting the outline of the paint op. The main purpose * of this class is to ensure that the saved point does not equal to - * the current one, which would cause a outline flicker. To echieve - * this the class stores two previosly requested points instead of the + * the current one, which would cause a outline flicker. To achieve + * this the class stores two previously requested points instead of the * last one. */ class KRITAIMAGE_EXPORT PositionHistory { public: /** * \return the previously used point, which is guaranteed not to * be equal to \p pt and updates the history if needed */ QPointF pushThroughHistory(const QPointF &pt) { QPointF result; const qreal pointSwapThreshold = 7.0; /** * We check x *and* y separately, because events generated by * a mouse device tend to come separately for x and y offsets. * Efficienty generating the 'stairs' pattern. */ if (qAbs(pt.x() - m_second.x()) > pointSwapThreshold && qAbs(pt.y() - m_second.y()) > pointSwapThreshold) { result = m_second; m_first = m_second; m_second = pt; } else { result = m_first; } return result; } private: QPointF m_first; QPointF m_second; }; inline bool checkSizeTooSmall(qreal scale, qreal width, qreal height) { return scale * width < 0.01 || scale * height < 0.01; } inline qreal calcAutoSpacing(qreal value, qreal coeff) { return coeff * (value < 1.0 ? value : sqrt(value)); } inline QPointF calcAutoSpacing(const QPointF &pt, qreal coeff, qreal lodScale) { const qreal invLodScale = 1.0 / lodScale; const QPointF lod0Point = invLodScale * pt; return lodScale * QPointF(calcAutoSpacing(lod0Point.x(), coeff), calcAutoSpacing(lod0Point.y(), coeff)); } KRITAIMAGE_EXPORT KisSpacingInformation effectiveSpacing(qreal dabWidth, qreal dabHeight, qreal extraScale, bool distanceSpacingEnabled, bool isotropicSpacing, qreal rotation, bool axesFlipped, qreal spacingVal, bool autoSpacingActive, qreal autoSpacingCoeff, qreal lodScale); KRITAIMAGE_EXPORT KisTimingInformation effectiveTiming(bool timingEnabled, qreal timingInterval, qreal rateExtraScale); KRITAIMAGE_EXPORT QVector splitAndFilterDabRect(const QRect &totalRect, const QVector &dabRects, int idealPatchSize); KRITAIMAGE_EXPORT QVector splitDabsIntoRects(const QVector &dabRects, int idealNumRects, int diameter, qreal spacing); } #endif /* __KIS_PAINTOP_UTILS_H */ diff --git a/libs/image/kis_antialiasing_fade_maker.h b/libs/image/kis_antialiasing_fade_maker.h index 86cf92911f..4618e53f09 100644 --- a/libs/image/kis_antialiasing_fade_maker.h +++ b/libs/image/kis_antialiasing_fade_maker.h @@ -1,269 +1,269 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_ANTIALIASING_FADE_MAKER_H #define __KIS_ANTIALIASING_FADE_MAKER_H #include "kis_global.h" template class KisAntialiasingFadeMaker1D { public: KisAntialiasingFadeMaker1D(const BaseFade &baseFade, bool enableAntialiasing) : m_radius(0.0), m_fadeStartValue(0), m_antialiasingFadeStart(0), m_antialiasingFadeCoeff(0), m_enableAntialiasing(enableAntialiasing), m_baseFade(baseFade) { } KisAntialiasingFadeMaker1D(const KisAntialiasingFadeMaker1D &rhs, const BaseFade &baseFade) : m_radius(rhs.m_radius), m_fadeStartValue(rhs.m_fadeStartValue), m_antialiasingFadeStart(rhs.m_antialiasingFadeStart), m_antialiasingFadeCoeff(rhs.m_antialiasingFadeCoeff), m_enableAntialiasing(rhs.m_enableAntialiasing), m_baseFade(baseFade) { } void setSquareNormCoeffs(qreal xcoeff, qreal ycoeff) { m_radius = 1.0; qreal xf = qMax(0.0, ((1.0 / xcoeff) - 1.0) * xcoeff); qreal yf = qMax(0.0, ((1.0 / ycoeff) - 1.0) * ycoeff); m_antialiasingFadeStart = pow2(0.5 * (xf + yf)); m_fadeStartValue = m_baseFade.value(m_antialiasingFadeStart); m_antialiasingFadeCoeff = qMax(0.0, 255.0 - m_fadeStartValue) / (m_radius - m_antialiasingFadeStart); } void setRadius(qreal radius) { m_radius = radius; m_antialiasingFadeStart = qMax(0.0, m_radius - 1.0); m_fadeStartValue = m_baseFade.value(m_antialiasingFadeStart); m_antialiasingFadeCoeff = qMax(0.0, 255.0 - m_fadeStartValue) / (m_radius - m_antialiasingFadeStart); } inline bool needFade(qreal dist, quint8 *value) { if (dist > m_radius) { *value = 255; return true; } if (!m_enableAntialiasing) { return false; } if (dist > m_antialiasingFadeStart) { *value = m_fadeStartValue + (dist - m_antialiasingFadeStart) * m_antialiasingFadeCoeff; return true; } return false; } #if defined HAVE_VC Vc::float_m needFade(Vc::float_v &dist) { const Vc::float_v vOne(Vc::One); const Vc::float_v vValMax(255.f); Vc::float_v vRadius(m_radius); Vc::float_v vFadeStartValue(m_fadeStartValue); Vc::float_v vAntialiasingFadeStart(m_antialiasingFadeStart); Vc::float_v vAntialiasingFadeCoeff(m_antialiasingFadeCoeff); Vc::float_m outsideMask = dist > vRadius; dist(outsideMask) = vOne; Vc::float_m fadeStartMask(false); if(m_enableAntialiasing){ fadeStartMask = dist > vAntialiasingFadeStart; dist((outsideMask ^ fadeStartMask) & fadeStartMask) = (vFadeStartValue + (dist - vAntialiasingFadeStart) * vAntialiasingFadeCoeff) / vValMax; } return (outsideMask | fadeStartMask); } #endif /* defined HAVE_VC */ private: qreal m_radius; quint8 m_fadeStartValue; qreal m_antialiasingFadeStart; qreal m_antialiasingFadeCoeff; bool m_enableAntialiasing; const BaseFade &m_baseFade; }; template class KisAntialiasingFadeMaker2D { public: KisAntialiasingFadeMaker2D(const BaseFade &baseFade, bool enableAntialiasing) : m_xLimit(0), m_yLimit(0), m_xFadeLimitStart(0), m_yFadeLimitStart(0), m_xFadeCoeff(0), m_yFadeCoeff(0), m_enableAntialiasing(enableAntialiasing), m_baseFade(baseFade) { } KisAntialiasingFadeMaker2D(const KisAntialiasingFadeMaker2D &rhs, const BaseFade &baseFade) : m_xLimit(rhs.m_xLimit), m_yLimit(rhs.m_yLimit), m_xFadeLimitStart(rhs.m_xFadeLimitStart), m_yFadeLimitStart(rhs.m_yFadeLimitStart), m_xFadeCoeff(rhs.m_xFadeCoeff), m_yFadeCoeff(rhs.m_yFadeCoeff), m_enableAntialiasing(rhs.m_enableAntialiasing), m_baseFade(baseFade) { } void setLimits(qreal halfWidth, qreal halfHeight) { m_xLimit = halfWidth; m_yLimit = halfHeight; m_xFadeLimitStart = m_xLimit - 1.0; m_yFadeLimitStart = m_yLimit - 1.0; m_xFadeCoeff = 1.0 / (m_xLimit - m_xFadeLimitStart); m_yFadeCoeff = 1.0 / (m_yLimit - m_yFadeLimitStart); } inline bool needFade(qreal x, qreal y, quint8 *value) { x = qAbs(x); y = qAbs(y); if (x > m_xLimit) { *value = 255; return true; } if (y > m_yLimit) { *value = 255; return true; } if (!m_enableAntialiasing) { return false; } if (x > m_xFadeLimitStart) { quint8 baseValue = m_baseFade.value(x, y); *value = baseValue + (255.0 - baseValue) * (x - m_xFadeLimitStart) * m_xFadeCoeff; if (y > m_yFadeLimitStart && *value < 255) { *value += (255.0 - *value) * (y - m_yFadeLimitStart) * m_yFadeCoeff; } return true; } if (y > m_yFadeLimitStart) { quint8 baseValue = m_baseFade.value(x, y); *value = baseValue + (255.0 - baseValue) * (y - m_yFadeLimitStart) * m_yFadeCoeff; if (x > m_xFadeLimitStart && *value < 255) { *value += (255.0 - *value) * (x - m_xFadeLimitStart) * m_xFadeCoeff; } return true; } return false; } #if defined HAVE_VC Vc::float_m needFade(Vc::float_v &xr, Vc::float_v &yr) const { Vc::float_v vXLimit(m_xLimit); Vc::float_v vYLimit(m_yLimit); Vc::float_m outXMask = Vc::abs(xr) > vXLimit; Vc::float_m outYMask = Vc::abs(yr) > vYLimit; return (outXMask | outYMask); } - // Apply fader separatedly to avoid calculating vValue twice. + // Apply fader separately to avoid calculating vValue twice. void apply2DFader(Vc::float_v &vValue, Vc::float_m &excludeMask, Vc::float_v &xr, Vc::float_v &yr) const { const Vc::float_v vValMax(255.f); if(m_enableAntialiasing){ Vc::float_v vXFadeLimitStart(m_xFadeLimitStart); Vc::float_v vYFadeLimitStart(m_yFadeLimitStart); Vc::float_v vXFadeCoeff(m_xFadeCoeff); Vc::float_v vYFadeCoeff(m_yFadeCoeff); Vc::float_v xra = abs(xr); Vc::float_m fadeXStartMask(false); Vc::float_m fadeYStartMask(false); Vc::float_v fadeValue; Vc::SimdArray vBaseValue(vValue); fadeXStartMask = xra > vXFadeLimitStart; fadeXStartMask = (fadeXStartMask ^ excludeMask) & fadeXStartMask; if (!fadeXStartMask.isFull()) { fadeValue = vBaseValue + (vValMax - vBaseValue) * (xra - vXFadeLimitStart) * vXFadeCoeff; fadeValue(fadeXStartMask & ((yr > vYFadeLimitStart) & (fadeValue < vValMax)) ) = fadeValue + (vValMax - fadeValue) * (yr - vYFadeLimitStart) * vYFadeCoeff; vValue(fadeXStartMask) = fadeValue; } fadeYStartMask = yr > vYFadeLimitStart; fadeYStartMask = (fadeYStartMask ^ fadeXStartMask) & fadeYStartMask; if (!fadeYStartMask.isFull()) { fadeValue = vBaseValue + (vValMax - vBaseValue) * (yr - vYFadeLimitStart) * vYFadeCoeff; fadeValue(fadeYStartMask & ((xra > vXFadeLimitStart) & (fadeValue < vValMax)) ) = fadeValue + (vValMax - fadeValue) * (xra - vXFadeLimitStart) * vXFadeCoeff; vValue(fadeYStartMask) = fadeValue; } } return; } #endif /* defined HAVE_VC */ private: qreal m_xLimit; qreal m_yLimit; qreal m_xFadeLimitStart; qreal m_yFadeLimitStart; qreal m_xFadeCoeff; qreal m_yFadeCoeff; bool m_enableAntialiasing; const BaseFade &m_baseFade; }; #endif /* __KIS_ANTIALIASING_FADE_MAKER_H */ diff --git a/libs/image/kis_brush_mask_applicator_factories.cpp b/libs/image/kis_brush_mask_applicator_factories.cpp index a8e78690a5..c16e186c54 100644 --- a/libs/image/kis_brush_mask_applicator_factories.cpp +++ b/libs/image/kis_brush_mask_applicator_factories.cpp @@ -1,654 +1,654 @@ /* * Copyright (c) 2012 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_brush_mask_applicator_factories.h" #include "vc_extra_math.h" #include "kis_circle_mask_generator.h" #include "kis_circle_mask_generator_p.h" #include "kis_gauss_circle_mask_generator_p.h" #include "kis_curve_circle_mask_generator_p.h" #include "kis_gauss_rect_mask_generator_p.h" #include "kis_curve_rect_mask_generator_p.h" #include "kis_rect_mask_generator_p.h" #include "kis_brush_mask_applicators.h" #include "kis_brush_mask_applicator_base.h" #define a(_s) #_s #define b(_s) a(_s) template<> template<> MaskApplicatorFactory::ReturnType MaskApplicatorFactory::create(ParamType maskGenerator) { return new KisBrushMaskScalarApplicator(maskGenerator); } template<> template<> MaskApplicatorFactory::ReturnType MaskApplicatorFactory::create(ParamType maskGenerator) { return new KisBrushMaskVectorApplicator(maskGenerator); } template<> template<> MaskApplicatorFactory::ReturnType MaskApplicatorFactory::create(ParamType maskGenerator) { return new KisBrushMaskVectorApplicator(maskGenerator); } template<> template<> MaskApplicatorFactory::ReturnType MaskApplicatorFactory::create(ParamType maskGenerator) { return new KisBrushMaskVectorApplicator(maskGenerator); } template<> template<> MaskApplicatorFactory::ReturnType MaskApplicatorFactory::create(ParamType maskGenerator) { return new KisBrushMaskVectorApplicator(maskGenerator); } template<> template<> MaskApplicatorFactory::ReturnType MaskApplicatorFactory::create(ParamType maskGenerator) { return new KisBrushMaskVectorApplicator(maskGenerator); } template<> template<> MaskApplicatorFactory::ReturnType MaskApplicatorFactory::create(ParamType maskGenerator) { return new KisBrushMaskVectorApplicator(maskGenerator); } #if defined HAVE_VC struct KisCircleMaskGenerator::FastRowProcessor { FastRowProcessor(KisCircleMaskGenerator *maskGenerator) : d(maskGenerator->d.data()) {} template void process(float* buffer, int width, float y, float cosa, float sina, float centerX, float centerY); KisCircleMaskGenerator::Private *d; }; template<> void KisCircleMaskGenerator:: FastRowProcessor::process(float* buffer, int width, float y, float cosa, float sina, float centerX, float centerY) { const bool useSmoothing = d->copyOfAntialiasEdges; const bool noFading = d->noFading; float y_ = y - centerY; float sinay_ = sina * y_; float cosay_ = cosa * y_; float* bufferPointer = buffer; Vc::float_v currentIndices = Vc::float_v::IndexesFromZero(); Vc::float_v increment((float)Vc::float_v::size()); Vc::float_v vCenterX(centerX); Vc::float_v vCosa(cosa); Vc::float_v vSina(sina); Vc::float_v vCosaY_(cosay_); Vc::float_v vSinaY_(sinay_); Vc::float_v vXCoeff(d->xcoef); Vc::float_v vYCoeff(d->ycoef); Vc::float_v vTransformedFadeX(d->transformedFadeX); Vc::float_v vTransformedFadeY(d->transformedFadeY); Vc::float_v vOne(Vc::One); for (int i=0; i < width; i+= Vc::float_v::size()){ Vc::float_v x_ = currentIndices - vCenterX; Vc::float_v xr = x_ * vCosa - vSinaY_; Vc::float_v yr = x_ * vSina + vCosaY_; Vc::float_v n = pow2(xr * vXCoeff) + pow2(yr * vYCoeff); Vc::float_m outsideMask = n > vOne; if (!outsideMask.isFull()) { if (noFading) { Vc::float_v vFade(Vc::Zero); vFade(outsideMask) = vOne; vFade.store(bufferPointer, Vc::Aligned); } else { if (useSmoothing) { xr = Vc::abs(xr) + vOne; yr = Vc::abs(yr) + vOne; } Vc::float_v vNormFade = pow2(xr * vTransformedFadeX) + pow2(yr * vTransformedFadeY); //255 * n * (normeFade - 1) / (normeFade - n) Vc::float_v vFade = n * (vNormFade - vOne) / (vNormFade - n); - // Mask in the inner circe of the mask + // Mask in the inner circle of the mask Vc::float_m mask = vNormFade < vOne; vFade.setZero(mask); - // Mask out the outer circe of the mask + // Mask out the outer circle of the mask vFade(outsideMask) = vOne; vFade.store(bufferPointer, Vc::Aligned); } } else { // Mask out everything outside the circle vOne.store(bufferPointer, Vc::Aligned); } currentIndices = currentIndices + increment; bufferPointer += Vc::float_v::size(); } } struct KisGaussCircleMaskGenerator::FastRowProcessor { FastRowProcessor(KisGaussCircleMaskGenerator *maskGenerator) : d(maskGenerator->d.data()) {} template void process(float* buffer, int width, float y, float cosa, float sina, float centerX, float centerY); KisGaussCircleMaskGenerator::Private *d; }; template<> void KisGaussCircleMaskGenerator:: FastRowProcessor::process(float* buffer, int width, float y, float cosa, float sina, float centerX, float centerY) { float y_ = y - centerY; float sinay_ = sina * y_; float cosay_ = cosa * y_; float* bufferPointer = buffer; Vc::float_v currentIndices = Vc::float_v::IndexesFromZero(); Vc::float_v increment((float)Vc::float_v::size()); Vc::float_v vCenterX(centerX); Vc::float_v vCenter(d->center); Vc::float_v vCosa(cosa); Vc::float_v vSina(sina); Vc::float_v vCosaY_(cosay_); Vc::float_v vSinaY_(sinay_); Vc::float_v vYCoeff(d->ycoef); Vc::float_v vDistfactor(d->distfactor); Vc::float_v vAlphafactor(d->alphafactor); Vc::float_v vZero(Vc::Zero); Vc::float_v vValMax(255.f); for (int i=0; i < width; i+= Vc::float_v::size()){ Vc::float_v x_ = currentIndices - vCenterX; Vc::float_v xr = x_ * vCosa - vSinaY_; Vc::float_v yr = x_ * vSina + vCosaY_; Vc::float_v dist = sqrt(pow2(xr) + pow2(yr * vYCoeff)); // Apply FadeMaker mask and operations Vc::float_m excludeMask = d->fadeMaker.needFade(dist); if (!excludeMask.isFull()) { Vc::float_v valDist = dist * vDistfactor; Vc::float_v fullFade = vAlphafactor * ( VcExtraMath::erf(valDist + vCenter) - VcExtraMath::erf(valDist - vCenter)); Vc::float_m mask; - // Mask in the inner circe of the mask + // Mask in the inner circle of the mask mask = fullFade < vZero; fullFade.setZero(mask); - // Mask the outter circle + // Mask the outer circle mask = fullFade > 254.974f; fullFade(mask) = vValMax; - // Mask (value - value), presicion errors. + // Mask (value - value), precision errors. Vc::float_v vFade = (vValMax - fullFade) / vValMax; // return original dist values before vFade transform vFade(excludeMask) = dist; vFade.store(bufferPointer, Vc::Aligned); } else { dist.store(bufferPointer, Vc::Aligned); } currentIndices = currentIndices + increment; bufferPointer += Vc::float_v::size(); } } struct KisCurveCircleMaskGenerator::FastRowProcessor { FastRowProcessor(KisCurveCircleMaskGenerator *maskGenerator) : d(maskGenerator->d.data()) {} template void process(float* buffer, int width, float y, float cosa, float sina, float centerX, float centerY); KisCurveCircleMaskGenerator::Private *d; }; template<> void KisCurveCircleMaskGenerator:: FastRowProcessor::process(float* buffer, int width, float y, float cosa, float sina, float centerX, float centerY) { float y_ = y - centerY; float sinay_ = sina * y_; float cosay_ = cosa * y_; float* bufferPointer = buffer; qreal* curveDataPointer = d->curveData.data(); Vc::float_v currentIndices = Vc::float_v::IndexesFromZero(); Vc::float_v increment((float)Vc::float_v::size()); Vc::float_v vCenterX(centerX); Vc::float_v vCosa(cosa); Vc::float_v vSina(sina); Vc::float_v vCosaY_(cosay_); Vc::float_v vSinaY_(sinay_); Vc::float_v vYCoeff(d->ycoef); Vc::float_v vXCoeff(d->xcoef); Vc::float_v vCurveResolution(d->curveResolution); Vc::float_v vCurvedData(Vc::Zero); Vc::float_v vCurvedData1(Vc::Zero); Vc::float_v vOne(Vc::One); Vc::float_v vZero(Vc::Zero); for (int i=0; i < width; i+= Vc::float_v::size()){ Vc::float_v x_ = currentIndices - vCenterX; Vc::float_v xr = x_ * vCosa - vSinaY_; Vc::float_v yr = x_ * vSina + vCosaY_; Vc::float_v dist = pow2(xr * vXCoeff) + pow2(yr * vYCoeff); // Apply FadeMaker mask and operations Vc::float_m excludeMask = d->fadeMaker.needFade(dist); if (!excludeMask.isFull()) { Vc::float_v valDist = dist * vCurveResolution; // truncate Vc::float_v::IndexType vAlphaValue(valDist); Vc::float_v vFloatAlphaValue = vAlphaValue; Vc::float_v alphaValueF = valDist - vFloatAlphaValue; vCurvedData.gather(curveDataPointer,vAlphaValue); vCurvedData1.gather(curveDataPointer,vAlphaValue + 1); // Vc::float_v vCurvedData1(curveDataPointer,vAlphaValue + 1); // vAlpha Vc::float_v fullFade = ( (vOne - alphaValueF) * vCurvedData + alphaValueF * vCurvedData1); Vc::float_m mask; - // Mask in the inner circe of the mask + // Mask in the inner circle of the mask mask = fullFade < vZero; fullFade.setZero(mask); // Mask outer circle of mask mask = fullFade >= vOne; Vc::float_v vFade = (vOne - fullFade); vFade.setZero(mask); // return original dist values before vFade transform vFade(excludeMask) = dist; vFade.store(bufferPointer, Vc::Aligned); } else { dist.store(bufferPointer, Vc::Aligned); } currentIndices = currentIndices + increment; bufferPointer += Vc::float_v::size(); } } struct KisGaussRectangleMaskGenerator::FastRowProcessor { FastRowProcessor(KisGaussRectangleMaskGenerator *maskGenerator) : d(maskGenerator->d.data()) {} template void process(float* buffer, int width, float y, float cosa, float sina, float centerX, float centerY); KisGaussRectangleMaskGenerator::Private *d; }; struct KisRectangleMaskGenerator::FastRowProcessor { FastRowProcessor(KisRectangleMaskGenerator *maskGenerator) : d(maskGenerator->d.data()) {} template void process(float* buffer, int width, float y, float cosa, float sina, float centerX, float centerY); KisRectangleMaskGenerator::Private *d; }; template<> void KisRectangleMaskGenerator:: FastRowProcessor::process(float* buffer, int width, float y, float cosa, float sina, float centerX, float centerY) { const bool useSmoothing = d->copyOfAntialiasEdges; float y_ = y - centerY; float sinay_ = sina * y_; float cosay_ = cosa * y_; float* bufferPointer = buffer; Vc::float_v currentIndices = Vc::float_v::IndexesFromZero(); Vc::float_v increment((float)Vc::float_v::size()); Vc::float_v vCenterX(centerX); Vc::float_v vCosa(cosa); Vc::float_v vSina(sina); Vc::float_v vCosaY_(cosay_); Vc::float_v vSinaY_(sinay_); Vc::float_v vXCoeff(d->xcoeff); Vc::float_v vYCoeff(d->ycoeff); Vc::float_v vTransformedFadeX(d->transformedFadeX); Vc::float_v vTransformedFadeY(d->transformedFadeY); Vc::float_v vOne(Vc::One); Vc::float_v vZero(Vc::Zero); Vc::float_v vTolerance(10000.f); for (int i=0; i < width; i+= Vc::float_v::size()){ Vc::float_v x_ = currentIndices - vCenterX; Vc::float_v xr = Vc::abs(x_ * vCosa - vSinaY_); Vc::float_v yr = Vc::abs(x_ * vSina + vCosaY_); Vc::float_v nxr = xr * vXCoeff; Vc::float_v nyr = yr * vYCoeff; Vc::float_m outsideMask = (nxr > vOne) || (nyr > vOne); if (!outsideMask.isFull()) { if (useSmoothing) { xr = Vc::abs(xr) + vOne; yr = Vc::abs(yr) + vOne; } Vc::float_v fxr = xr * vTransformedFadeX; Vc::float_v fyr = yr * vTransformedFadeY; Vc::float_v fxrNorm = nxr * (fxr - vOne) / (fxr - nxr); Vc::float_v fyrNorm = nyr * (fyr - vOne) / (fyr - nyr); Vc::float_v vFade(vZero); Vc::float_v::IndexType fxrInt(fxr * vTolerance); Vc::float_v::IndexType fyrInt(fyr * vTolerance); Vc::float_m fadeXMask = (fxr > vOne) && ((fxrInt >= fyrInt) || fyr < vOne); Vc::float_m fadeYMask = (fyr > vOne) && ((fyrInt > fxrInt) || fxr < vOne); vFade(fadeXMask) = fxrNorm; vFade(!fadeXMask && fadeYMask) = fyrNorm; - // Mask out the outer circe of the mask + // Mask out the outer circle of the mask vFade(outsideMask) = vOne; vFade.store(bufferPointer, Vc::Aligned); } else { // Mask out everything outside the circle vOne.store(bufferPointer, Vc::Aligned); } currentIndices = currentIndices + increment; bufferPointer += Vc::float_v::size(); } } template<> void KisGaussRectangleMaskGenerator:: FastRowProcessor::process(float* buffer, int width, float y, float cosa, float sina, float centerX, float centerY) { float y_ = y - centerY; float sinay_ = sina * y_; float cosay_ = cosa * y_; float* bufferPointer = buffer; Vc::float_v currentIndices = Vc::float_v::IndexesFromZero(); Vc::float_v increment((float)Vc::float_v::size()); Vc::float_v vCenterX(centerX); Vc::float_v vCosa(cosa); Vc::float_v vSina(sina); Vc::float_v vCosaY_(cosay_); Vc::float_v vSinaY_(sinay_); Vc::float_v vhalfWidth(d->halfWidth); Vc::float_v vhalfHeight(d->halfHeight); Vc::float_v vXFade(d->xfade); Vc::float_v vYFade(d->yfade); Vc::float_v vAlphafactor(d->alphafactor); Vc::float_v vOne(Vc::One); Vc::float_v vZero(Vc::Zero); Vc::float_v vValMax(255.f); for (int i=0; i < width; i+= Vc::float_v::size()){ Vc::float_v x_ = currentIndices - vCenterX; Vc::float_v xr = x_ * vCosa - vSinaY_; Vc::float_v yr = Vc::abs(x_ * vSina + vCosaY_); Vc::float_v vValue; // check if we need to apply fader on values Vc::float_m excludeMask = d->fadeMaker.needFade(xr,yr); vValue(excludeMask) = vOne; if (!excludeMask.isFull()) { Vc::float_v fullFade = vValMax - (vAlphafactor * (VcExtraMath::erf((vhalfWidth + xr) * vXFade) + VcExtraMath::erf((vhalfWidth - xr) * vXFade)) * (VcExtraMath::erf((vhalfHeight + yr) * vYFade) + VcExtraMath::erf((vhalfHeight - yr) * vYFade))); // apply antialias fader d->fadeMaker.apply2DFader(fullFade,excludeMask,xr,yr); Vc::float_m mask; - // Mask in the inner circe of the mask + // Mask in the inner circle of the mask mask = fullFade < vZero; fullFade.setZero(mask); - // Mask the outter circle + // Mask the outer circle mask = fullFade > 254.974f; fullFade(mask) = vValMax; - // Mask (value - value), presicion errors. + // Mask (value - value), precision errors. Vc::float_v vFade = fullFade / vValMax; // return original vValue values before vFade transform vFade(excludeMask) = vValue; vFade.store(bufferPointer, Vc::Aligned); } else { vValue.store(bufferPointer, Vc::Aligned); } currentIndices = currentIndices + increment; bufferPointer += Vc::float_v::size(); } } struct KisCurveRectangleMaskGenerator::FastRowProcessor { FastRowProcessor(KisCurveRectangleMaskGenerator *maskGenerator) : d(maskGenerator->d.data()) {} template void process(float* buffer, int width, float y, float cosa, float sina, float centerX, float centerY); KisCurveRectangleMaskGenerator::Private *d; }; template<> void KisCurveRectangleMaskGenerator:: FastRowProcessor::process(float* buffer, int width, float y, float cosa, float sina, float centerX, float centerY) { float y_ = y - centerY; float sinay_ = sina * y_; float cosay_ = cosa * y_; float* bufferPointer = buffer; qreal* curveDataPointer = d->curveData.data(); Vc::float_v currentIndices = Vc::float_v::IndexesFromZero(); Vc::float_v increment((float)Vc::float_v::size()); Vc::float_v vCenterX(centerX); Vc::float_v vCosa(cosa); Vc::float_v vSina(sina); Vc::float_v vCosaY_(cosay_); Vc::float_v vSinaY_(sinay_); Vc::float_v vYCoeff(d->ycoeff); Vc::float_v vXCoeff(d->xcoeff); Vc::float_v vCurveResolution(d->curveResolution); Vc::float_v vOne(Vc::One); Vc::float_v vZero(Vc::Zero); Vc::float_v vValMax(255.f); for (int i=0; i < width; i+= Vc::float_v::size()){ Vc::float_v x_ = currentIndices - vCenterX; Vc::float_v xr = x_ * vCosa - vSinaY_; Vc::float_v yr = Vc::abs(x_ * vSina + vCosaY_); Vc::float_v vValue; // check if we need to apply fader on values Vc::float_m excludeMask = d->fadeMaker.needFade(xr,yr); vValue(excludeMask) = vOne; if (!excludeMask.isFull()) { // We need to mask the extra area given for aliniation // the next operation should never give values above 1 Vc::float_v preSIndex = Vc::abs(xr) * vXCoeff; Vc::float_v preTIndex = Vc::abs(yr) * vYCoeff; preSIndex(preSIndex > vOne) = vOne; preTIndex(preTIndex > vOne) = vOne; Vc::float_v::IndexType sIndex( round(preSIndex * vCurveResolution)); Vc::float_v::IndexType tIndex( round(preTIndex * vCurveResolution)); Vc::float_v::IndexType sIndexInverted = vCurveResolution - sIndex; Vc::float_v::IndexType tIndexInverted = vCurveResolution - tIndex; Vc::float_v vCurvedDataSIndex(curveDataPointer, sIndex); Vc::float_v vCurvedDataTIndex(curveDataPointer, tIndex); Vc::float_v vCurvedDataSIndexInv(curveDataPointer, sIndexInverted); Vc::float_v vCurvedDataTIndexInv(curveDataPointer, tIndexInverted); Vc::float_v fullFade = vValMax * (vOne - (vCurvedDataSIndex * (vOne - vCurvedDataSIndexInv) * vCurvedDataTIndex * (vOne - vCurvedDataTIndexInv))); // apply antialias fader d->fadeMaker.apply2DFader(fullFade,excludeMask,xr,yr); Vc::float_m mask; - // Mask in the inner circe of the mask + // Mask in the inner circle of the mask mask = fullFade < vZero; fullFade.setZero(mask); - // Mask the outter circle + // Mask the outer circle mask = fullFade > 254.974f; fullFade(mask) = vValMax; - // Mask (value - value), presicion errors. + // Mask (value - value), precision errors. Vc::float_v vFade = fullFade / vValMax; // return original vValue values before vFade transform vFade(excludeMask) = vValue; vFade.store(bufferPointer, Vc::Aligned); } else { vValue.store(bufferPointer, Vc::Aligned); } currentIndices = currentIndices + increment; bufferPointer += Vc::float_v::size(); } } #endif /* defined HAVE_VC */ diff --git a/libs/image/kis_edge_detection_kernel.h b/libs/image/kis_edge_detection_kernel.h index f00bbf0ffd..2d8b5d67f0 100644 --- a/libs/image/kis_edge_detection_kernel.h +++ b/libs/image/kis_edge_detection_kernel.h @@ -1,129 +1,129 @@ /* * Copyright (c) 2017 Wolthera van Hövell tot Westerflier * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KIS_EDGE_DETECTION_KERNEL_H #define KIS_EDGE_DETECTION_KERNEL_H #include "kritaimage_export.h" #include "kis_types.h" #include class QRect; class KRITAIMAGE_EXPORT KisEdgeDetectionKernel { public: KisEdgeDetectionKernel(); enum FilterType { Simple, //A weird simple method used in our old sobel filter Prewit, //The simpler prewitt detection, which doesn't smooth. SobelVector //Sobel does smooth. The creation of bigger kernels is based on an approach regarding vectors. }; enum FilterOutput { pythagorean, xGrowth, xFall, yGrowth, yFall, radian }; /** * @brief createHorizontalMatrix * @param radius the radius. 1 makes a 3x3 kernel. * @param type One of the entries in the enum Filtertype * @param reverse which direction the gradient goes. * The horizontal gradient by default detects the rightmost edges. * Reversed it selects the leftmost edges. * @return */ static Eigen::Matrix createHorizontalMatrix(qreal radius, FilterType type, bool reverse = false); /** * @brief createVerticalMatrix * @param radius the radius. 1 makes a 3x3 kernel. * @param type One of the entries in the enum Filtertype * @param reverse which direction the gradient goes. * The vertical gradient by default detects the topmost edges. * Reversed it selects the bottommost edges. * @return */ static Eigen::Matrix createVerticalMatrix(qreal radius, FilterType type, bool reverse = false); static KisConvolutionKernelSP createHorizontalKernel(qreal radius, FilterType type, bool denormalize = true, bool reverse = false); static KisConvolutionKernelSP createVerticalKernel(qreal radius, FilterType type, bool denormalize = true, bool reverse = false); static int kernelSizeFromRadius(qreal radius); static qreal sigmaFromRadius(qreal radius); /** * @brief applyEdgeDetection * This applies the edge detection filter to the device. * @param device the device to apply to. * @param rect the affected rect. * @param xRadius the radius of the horizontal sampling, radius of 0 is effectively disabling it. * @param yRadius the radius of the vertical sampling, refius of 0 is effectively disabling it. * @param type the type can be prewitt, sobel or simple, each of which * have a different sampling for the eventual edge detection. * @param channelFlags the affected channels. * @param progressUpdater the progress updater if it exists. * @param writeToAlpha whether or not to have the result applied to the transparency than the color channels, * this is useful for fringe effects. */ static void applyEdgeDetection(KisPaintDeviceSP device, const QRect& rect, qreal xRadius, qreal yRadius, FilterType type, const QBitArray &channelFlags, KoUpdater *progressUpdater, FilterOutput output = pythagorean, bool writeToAlpha = false); /** * @brief converToNormalMap - * Convert a channel of the device to a normal map. The channel will be interpretted as a heightmap. + * Convert a channel of the device to a normal map. The channel will be interpreted as a heightmap. * @param device the device * @param rect the rectangle to apply this to. * @param xRadius the xradius * @param yRadius the yradius * @param type the edge detection filter. * @param channelToConvert the channel to use as a grayscale. * @param channelOrder the order in which the xyz coordinates ought to be written to the pixels. * @param channelFlags * @param progressUpdater */ static void convertToNormalMap(KisPaintDeviceSP device, const QRect & rect, qreal xRadius, qreal yRadius, FilterType type, int channelToConvert, QVector channelOrder, QVector channelFlip, const QBitArray &channelFlags, KoUpdater *progressUpdater); }; #endif // KIS_EDGE_DETECTION_KERNEL_H diff --git a/libs/image/kis_filter_weights_buffer.h b/libs/image/kis_filter_weights_buffer.h index a1d306b197..aea68b9d1d 100644 --- a/libs/image/kis_filter_weights_buffer.h +++ b/libs/image/kis_filter_weights_buffer.h @@ -1,292 +1,292 @@ /* * Copyright (c) 2012 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_FILTER_WEIGHTS_BUFFER_H #define __KIS_FILTER_WEIGHTS_BUFFER_H #include "kis_fixed_point_maths.h" #include "kis_filter_strategy.h" #include "kis_debug.h" #ifdef SANITY_CHECKS_ENABLED static bool checkForAsymmetricZeros = false; #define SANITY_CENTER_POSITION() \ do { \ Q_ASSERT(scaledIter >= beginDst); \ Q_ASSERT(scaledIter <= endDst); \ \ if (j == centerIndex) { \ Q_ASSERT(scaledIter == centerSrc); \ } \ } while(0) #define SANITY_ZEROS() \ do { \ if (checkForAsymmetricZeros) { \ for (int j = 0; j < span; j++) { \ int idx2 = span - j - 1; \ \ if ((m_filterWeights[i].weight[j] && !m_filterWeights[i].weight[idx2]) || \ (!m_filterWeights[i].weight[j] && m_filterWeights[i].weight[idx2])) { \ \ dbgKrita << "*******"; \ dbgKrita << "Non-symmetric zero found:" << centerSrc; \ dbgKrita << "Weight" << j << ":" << m_filterWeights[i].weight[j]; \ dbgKrita << "Weight" << idx2 << ":" << m_filterWeights[i].weight[idx2]; \ qFatal("Non-symmetric zero -> fail"); \ } \ } \ } \ } while (0) #define SANITY_CHECKSUM() \ do { \ Q_ASSERT(sum == 255); \ } while (0) #else #define SANITY_CENTER_POSITION() #define SANITY_ZEROS() #define SANITY_CHECKSUM() #endif #ifdef DEBUG_ENABLED #define DEBUG_ALL() \ do { \ dbgKrita << "************** i =" << i; \ dbgKrita << ppVar(centerSrc); \ dbgKrita << ppVar(centerIndex); \ dbgKrita << ppVar(beginSrc) << ppVar(endSrc); \ dbgKrita << ppVar(beginDst) << ppVar(endDst); \ dbgKrita << ppVar(scaledIter) << ppVar(scaledInc); \ dbgKrita << ppVar(span); \ dbgKrita << "==="; \ } while (0) #define DEBUG_SAMPLE() \ do { \ dbgKrita << ppVar(scaledIter) << ppVar(t); \ } while (0) #else #define DEBUG_ALL() Q_UNUSED(beginDst); Q_UNUSED(endDst) #define DEBUG_SAMPLE() #endif /** * \class KisFilterWeightsBuffer * * Stores the cached values for the weights of neighbouring pixels * that would form the pixel in a result of resampling. The object of * this class is created before a pass of the transformation basing on * the desired scale factor and the filter strategy used for resampling. * - * Here is an exmple of a calculation of the span for a pixel with + * Here is an example of a calculation of the span for a pixel with * scale equal to 1.0. The result of the blending will be written into * the dst(0) pixel, which is marked with '*' sign. Note that all the * coordinates here are related to the center of the pixel, not to its * leftmost border as it is common in other systems. The centerSrc * coordinate represents the offset between the source and the * destination buffers. * * dst-coordinates: the coordinates in the resulting image. The values * of the filter strategy are calculated in these * coordinates. * * src-coordinates: the coordinates in the source image/buffer. We * pick integer values from there and calculate their * dst-position to know their weights. * * * +----+----+----+-- scaledIter (samples, measured in dst pixels, * | | | | correspond to integers in src) * * +---------+-- supportDst == filterStrategy->intSupport() * | | * +-- beginDst +-- endDst * | | | * | +-- centerDst (always zero) * | | | * * dst: ----|----|----|----|----*----|----|----|----|----|--> * -4 -3 -2 -1 0 1 2 3 4 5 * * src: --|----|----|----|----|----|----|----|----|----|----> * -4 -3 -2 -1 0 1 2 3 4 5 * * ^ ^ ^ * | | | * | +-- centerSrc * | | | * +-- beginSrc +endSrc * | | | * | +---------+-- supportSrc ~= supportDst / realScale * | | * +-------------------+-- span (number of integers in the region) */ class KisFilterWeightsBuffer { public: struct FilterWeights { ~FilterWeights() { delete[] weight; } qint16 *weight; int span; int centerIndex; }; public: KisFilterWeightsBuffer(KisFilterStrategy *filterStrategy, qreal realScale) { Q_ASSERT(realScale > 0); m_filterWeights = new FilterWeights[256]; m_maxSpan = 0; m_weightsPositionScale = 1; KisFixedPoint supportSrc; KisFixedPoint supportDst; if (realScale < 1.0) { supportSrc.from256Frac(filterStrategy->intSupport() / realScale); supportDst.from256Frac(filterStrategy->intSupport()); m_weightsPositionScale = KisFixedPoint(realScale); } else { supportSrc.from256Frac(filterStrategy->intSupport()); supportDst.from256Frac(filterStrategy->intSupport()); } for (int i = 0; i < 256; i++) { KisFixedPoint centerSrc; centerSrc.from256Frac(i); KisFixedPoint beginDst = -supportDst; KisFixedPoint endDst = supportDst; KisFixedPoint beginSrc = -supportSrc - centerSrc / m_weightsPositionScale; KisFixedPoint endSrc = supportSrc - centerSrc / m_weightsPositionScale; int span = (2 * supportSrc).toInt() + (beginSrc.isInteger() && endSrc.isInteger()); int centerIndex = -beginSrc.toInt(); m_filterWeights[i].centerIndex = centerIndex; m_filterWeights[i].span = span; m_filterWeights[i].weight = new qint16[span]; m_maxSpan = qMax(m_maxSpan, span); // in dst coordinate system: KisFixedPoint scaledIter = centerSrc + beginSrc.toInt() * m_weightsPositionScale; KisFixedPoint scaledInc = m_weightsPositionScale; DEBUG_ALL(); int sum = 0; for (int j = 0; j < span; j++) { int t = filterStrategy->intValueAt(scaledIter.to256Frac()); m_filterWeights[i].weight[j] = t; sum += t; DEBUG_SAMPLE(); SANITY_CENTER_POSITION(); scaledIter += scaledInc; } SANITY_ZEROS(); if (sum != 255) { qreal fixFactor = 255.0 / sum; sum = 0; for (int j = 0; j < span; j++) { int t = qRound(m_filterWeights[i].weight[j] * fixFactor); m_filterWeights[i].weight[j] = t; sum += t; } } while (sum != 255) { int diff = sum < 255 ? 1 : -1; int index = findMaxIndex(m_filterWeights[i].weight, span); m_filterWeights[i].weight[index] += diff; sum += diff; } SANITY_CHECKSUM(); } } ~KisFilterWeightsBuffer() { delete[] m_filterWeights; } /** * Return a weights buffer for a particular value of offset */ FilterWeights* weights(KisFixedPoint pos) const { return m_filterWeights + pos.to256Frac(); } /** * The maximum width of the buffer that would be needed for * calculation of a pixel value. In other words, the maximum * number of support pixels that are needed for calculation of a * single result pixel */ int maxSpan() const { return m_maxSpan; } /** * The scale of the support buffer. Note that it is not always * equal to the real scale of the transformation due to * interpolation/blending difference. */ KisFixedPoint weightsPositionScale() const { return m_weightsPositionScale; } private: int findMaxIndex(qint16 *buf, int size) { int maxValue = buf[0]; int maxIndex = 0; for (int i = 1; i < size; i++) { if (buf[i] > maxValue) { maxValue = buf[i]; maxIndex = i; } } return maxIndex; } private: FilterWeights *m_filterWeights; int m_maxSpan; KisFixedPoint m_weightsPositionScale; }; #endif /* __KIS_FILTER_WEIGHTS_BUFFER_H */ diff --git a/libs/image/kis_grid_interpolation_tools.h b/libs/image/kis_grid_interpolation_tools.h index f6eaeeba45..211dc32e20 100644 --- a/libs/image/kis_grid_interpolation_tools.h +++ b/libs/image/kis_grid_interpolation_tools.h @@ -1,600 +1,600 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef __KIS_GRID_INTERPOLATION_TOOLS_H #define __KIS_GRID_INTERPOLATION_TOOLS_H #include #include #include #include "kis_algebra_2d.h" #include "kis_four_point_interpolator_forward.h" #include "kis_four_point_interpolator_backward.h" #include "kis_iterator_ng.h" #include "kis_random_sub_accessor.h" //#define DEBUG_PAINTING_POLYGONS #ifdef DEBUG_PAINTING_POLYGONS #include #endif /* DEBUG_PAINTING_POLYGONS */ namespace GridIterationTools { inline int calcGridDimension(int start, int end, const int pixelPrecision) { const int alignmentMask = ~(pixelPrecision - 1); int alignedStart = (start + pixelPrecision - 1) & alignmentMask; int alignedEnd = end & alignmentMask; int size = 0; if (alignedEnd > alignedStart) { size = (alignedEnd - alignedStart) / pixelPrecision + 1; size += alignedStart != start; size += alignedEnd != end; } else { size = 2 + (end - start >= pixelPrecision); } return size; } inline QSize calcGridSize(const QRect &srcBounds, const int pixelPrecision) { return QSize(calcGridDimension(srcBounds.x(), srcBounds.right(), pixelPrecision), calcGridDimension(srcBounds.y(), srcBounds.bottom(), pixelPrecision)); } template struct CellOp { CellOp(ProcessPolygon &_polygonOp, ForwardTransform &_transformOp) : polygonOp(_polygonOp), transformOp(_transformOp) { } inline void processPoint(int col, int row, int prevCol, int prevRow, int colIndex, int rowIndex) { QPointF dstPosF = transformOp(QPointF(col, row)); currLinePoints << dstPosF; if (rowIndex >= 1 && colIndex >= 1) { QPolygonF srcPolygon; srcPolygon << QPointF(prevCol, prevRow); srcPolygon << QPointF(col, prevRow); srcPolygon << QPointF(col, row); srcPolygon << QPointF(prevCol, row); QPolygonF dstPolygon; dstPolygon << prevLinePoints.at(colIndex - 1); dstPolygon << prevLinePoints.at(colIndex); dstPolygon << currLinePoints.at(colIndex); dstPolygon << currLinePoints.at(colIndex - 1); polygonOp(srcPolygon, dstPolygon); } } inline void nextLine() { std::swap(prevLinePoints, currLinePoints); // we are erasing elements for not free'ing the occupied // memory, which is more efficient since we are going to fill // the vector again currLinePoints.erase(currLinePoints.begin(), currLinePoints.end()); } QVector prevLinePoints; QVector currLinePoints; ProcessPolygon &polygonOp; ForwardTransform &transformOp; }; template void processGrid(ProcessCell &cellOp, const QRect &srcBounds, const int pixelPrecision) { if (srcBounds.isEmpty()) return; const int alignmentMask = ~(pixelPrecision - 1); int prevRow = std::numeric_limits::max(); int prevCol = std::numeric_limits::max(); int rowIndex = 0; int colIndex = 0; for (int row = srcBounds.top(); row <= srcBounds.bottom();) { for (int col = srcBounds.left(); col <= srcBounds.right();) { cellOp.processPoint(col, row, prevCol, prevRow, colIndex, rowIndex); prevCol = col; col += pixelPrecision; colIndex++; if (col > srcBounds.right() && col <= srcBounds.right() + pixelPrecision - 1) { col = srcBounds.right(); } else { col &= alignmentMask; } } cellOp.nextLine(); colIndex = 0; prevRow = row; row += pixelPrecision; rowIndex++; if (row > srcBounds.bottom() && row <= srcBounds.bottom() + pixelPrecision - 1) { row = srcBounds.bottom(); } else { row &= alignmentMask; } } } template void processGrid(ProcessPolygon &polygonOp, ForwardTransform &transformOp, const QRect &srcBounds, const int pixelPrecision) { CellOp cellOp(polygonOp, transformOp); processGrid(cellOp, srcBounds, pixelPrecision); } struct PaintDevicePolygonOp { PaintDevicePolygonOp(KisPaintDeviceSP srcDev, KisPaintDeviceSP dstDev) : m_srcDev(srcDev), m_dstDev(dstDev) {} void operator() (const QPolygonF &srcPolygon, const QPolygonF &dstPolygon) { this->operator() (srcPolygon, dstPolygon, dstPolygon); } void operator() (const QPolygonF &srcPolygon, const QPolygonF &dstPolygon, const QPolygonF &clipDstPolygon) { QRect boundRect = clipDstPolygon.boundingRect().toAlignedRect(); if (boundRect.isEmpty()) return; KisSequentialIterator dstIt(m_dstDev, boundRect); KisRandomSubAccessorSP srcAcc = m_srcDev->createRandomSubAccessor(); KisFourPointInterpolatorBackward interp(srcPolygon, dstPolygon); int y = boundRect.top(); interp.setY(y); while (dstIt.nextPixel()) { int newY = dstIt.y(); if (y != newY) { y = newY; interp.setY(y); } QPointF srcPoint(dstIt.x(), y); if (clipDstPolygon.containsPoint(srcPoint, Qt::OddEvenFill)) { interp.setX(srcPoint.x()); QPointF dstPoint = interp.getValue(); // brain-blowing part: // // since the interpolator does the inverted - // transfomation we read data from "dstPoint" + // transformation we read data from "dstPoint" // (which is non-transformed) and write it into // "srcPoint" (which is transformed position) srcAcc->moveTo(dstPoint); srcAcc->sampledOldRawData(dstIt.rawData()); } } } KisPaintDeviceSP m_srcDev; KisPaintDeviceSP m_dstDev; }; struct QImagePolygonOp { QImagePolygonOp(const QImage &srcImage, QImage &dstImage, const QPointF &srcImageOffset, const QPointF &dstImageOffset) : m_srcImage(srcImage), m_dstImage(dstImage), m_srcImageOffset(srcImageOffset), m_dstImageOffset(dstImageOffset), m_srcImageRect(m_srcImage.rect()), m_dstImageRect(m_dstImage.rect()) { } void operator() (const QPolygonF &srcPolygon, const QPolygonF &dstPolygon) { this->operator() (srcPolygon, dstPolygon, dstPolygon); } void operator() (const QPolygonF &srcPolygon, const QPolygonF &dstPolygon, const QPolygonF &clipDstPolygon) { QRect boundRect = clipDstPolygon.boundingRect().toAlignedRect(); KisFourPointInterpolatorBackward interp(srcPolygon, dstPolygon); for (int y = boundRect.top(); y <= boundRect.bottom(); y++) { interp.setY(y); for (int x = boundRect.left(); x <= boundRect.right(); x++) { QPointF srcPoint(x, y); if (clipDstPolygon.containsPoint(srcPoint, Qt::OddEvenFill)) { interp.setX(srcPoint.x()); QPointF dstPoint = interp.getValue(); // about srcPoint/dstPoint hell please see a // comment in PaintDevicePolygonOp::operator() () srcPoint -= m_dstImageOffset; dstPoint -= m_srcImageOffset; QPoint srcPointI = srcPoint.toPoint(); QPoint dstPointI = dstPoint.toPoint(); if (!m_dstImageRect.contains(srcPointI)) continue; if (!m_srcImageRect.contains(dstPointI)) continue; m_dstImage.setPixel(srcPointI, m_srcImage.pixel(dstPointI)); } } } #ifdef DEBUG_PAINTING_POLYGONS QPainter gc(&m_dstImage); gc.setPen(Qt::red); gc.setOpacity(0.5); gc.setBrush(Qt::green); gc.drawPolygon(clipDstPolygon.translated(-m_dstImageOffset)); gc.setBrush(Qt::blue); //gc.drawPolygon(dstPolygon.translated(-m_dstImageOffset)); #endif /* DEBUG_PAINTING_POLYGONS */ } const QImage &m_srcImage; QImage &m_dstImage; QPointF m_srcImageOffset; QPointF m_dstImageOffset; QRect m_srcImageRect; QRect m_dstImageRect; }; /*************************************************************/ /* Iteration through precalculated grid */ /*************************************************************/ /** * A-----B The polygons will be in the following order: * | | * | | polygon << A << B << D << C; * C-----D */ inline QVector calculateCellIndexes(int col, int row, const QSize &gridSize) { const int tl = col + row * gridSize.width(); const int tr = tl + 1; const int bl = tl + gridSize.width(); const int br = bl + 1; QVector cellIndexes; cellIndexes << tl; cellIndexes << tr; cellIndexes << br; cellIndexes << bl; return cellIndexes; } inline int pointToIndex(const QPoint &cellPt, const QSize &gridSize) { return cellPt.x() + cellPt.y() * gridSize.width(); } namespace Private { inline QPoint pointPolygonIndexToColRow(QPoint baseColRow, int index) { static QVector pointOffsets; if (pointOffsets.isEmpty()) { pointOffsets << QPoint(0,0); pointOffsets << QPoint(1,0); pointOffsets << QPoint(1,1); pointOffsets << QPoint(0,1); } return baseColRow + pointOffsets[index]; } struct PointExtension { int near; int far; }; } template bool getOrthogonalPointApproximation(const QPoint &cellPt, const QVector &originalPoints, const QVector &transformedPoints, IndexesOp indexesOp, QPointF *srcPoint, QPointF *dstPoint) { QVector extensionPoints; Private::PointExtension ext; // left if ((ext.near = indexesOp.tryGetValidIndex(cellPt + QPoint(-1, 0))) >= 0 && (ext.far = indexesOp.tryGetValidIndex(cellPt + QPoint(-2, 0))) >= 0) { extensionPoints << ext; } // top if ((ext.near = indexesOp.tryGetValidIndex(cellPt + QPoint(0, -1))) >= 0 && (ext.far = indexesOp.tryGetValidIndex(cellPt + QPoint(0, -2))) >= 0) { extensionPoints << ext; } // right if ((ext.near = indexesOp.tryGetValidIndex(cellPt + QPoint(1, 0))) >= 0 && (ext.far = indexesOp.tryGetValidIndex(cellPt + QPoint(2, 0))) >= 0) { extensionPoints << ext; } // bottom if ((ext.near = indexesOp.tryGetValidIndex(cellPt + QPoint(0, 1))) >= 0 && (ext.far = indexesOp.tryGetValidIndex(cellPt + QPoint(0, 2))) >= 0) { extensionPoints << ext; } if (extensionPoints.isEmpty()) { // top-left if ((ext.near = indexesOp.tryGetValidIndex(cellPt + QPoint(-1, -1))) >= 0 && (ext.far = indexesOp.tryGetValidIndex(cellPt + QPoint(-2, -2))) >= 0) { extensionPoints << ext; } // top-right if ((ext.near = indexesOp.tryGetValidIndex(cellPt + QPoint(1, -1))) >= 0 && (ext.far = indexesOp.tryGetValidIndex(cellPt + QPoint(2, -2))) >= 0) { extensionPoints << ext; } // bottom-right if ((ext.near = indexesOp.tryGetValidIndex(cellPt + QPoint(1, 1))) >= 0 && (ext.far = indexesOp.tryGetValidIndex(cellPt + QPoint(2, 2))) >= 0) { extensionPoints << ext; } // bottom-left if ((ext.near = indexesOp.tryGetValidIndex(cellPt + QPoint(-1, 1))) >= 0 && (ext.far = indexesOp.tryGetValidIndex(cellPt + QPoint(-2, 2))) >= 0) { extensionPoints << ext; } } if (extensionPoints.isEmpty()) { return false; } int numResultPoints = 0; *srcPoint = indexesOp.getSrcPointForce(cellPt); *dstPoint = QPointF(); Q_FOREACH (const Private::PointExtension &ext, extensionPoints) { QPointF near = transformedPoints[ext.near]; QPointF far = transformedPoints[ext.far]; QPointF nearSrc = originalPoints[ext.near]; QPointF farSrc = originalPoints[ext.far]; QPointF base1 = nearSrc - farSrc; QPointF base2 = near - far; QPointF pt = near + KisAlgebra2D::transformAsBase(*srcPoint - nearSrc, base1, base2); *dstPoint += pt; numResultPoints++; } *dstPoint /= numResultPoints; return true; } template struct IncompletePolygonPolicy { static inline bool tryProcessPolygon(int col, int row, int numExistingPoints, PolygonOp &polygonOp, IndexesOp &indexesOp, const QVector &polygonPoints, const QVector &originalPoints, const QVector &transformedPoints) { if (numExistingPoints >= 4) return false; if (numExistingPoints == 0) return true; QPolygonF srcPolygon; QPolygonF dstPolygon; for (int i = 0; i < 4; i++) { const int index = polygonPoints[i]; if (index >= 0) { srcPolygon << originalPoints[index]; dstPolygon << transformedPoints[index]; } else { QPoint cellPt = Private::pointPolygonIndexToColRow(QPoint(col, row), i); QPointF srcPoint; QPointF dstPoint; bool result = getOrthogonalPointApproximation(cellPt, originalPoints, transformedPoints, indexesOp, &srcPoint, &dstPoint); if (!result) { //dbgKrita << "*NOT* found any valid point" << allSrcPoints[pointToIndex(cellPt)] << "->" << ppVar(pt); break; } else { srcPolygon << srcPoint; dstPolygon << dstPoint; } } } if (dstPolygon.size() == 4) { QPolygonF srcClipPolygon(srcPolygon.intersected(indexesOp.srcCropPolygon())); KisFourPointInterpolatorForward forwardTransform(srcPolygon, dstPolygon); for (int i = 0; i < srcClipPolygon.size(); i++) { const QPointF newPt = forwardTransform.map(srcClipPolygon[i]); srcClipPolygon[i] = newPt; } polygonOp(srcPolygon, dstPolygon, srcClipPolygon); } return true; } }; template struct AlwaysCompletePolygonPolicy { static inline bool tryProcessPolygon(int col, int row, int numExistingPoints, PolygonOp &polygonOp, IndexesOp &indexesOp, const QVector &polygonPoints, const QVector &originalPoints, const QVector &transformedPoints) { Q_UNUSED(col); Q_UNUSED(row); Q_UNUSED(polygonOp); Q_UNUSED(indexesOp); Q_UNUSED(polygonPoints); Q_UNUSED(originalPoints); Q_UNUSED(transformedPoints); KIS_ASSERT_RECOVER_NOOP(numExistingPoints == 4); return false; } }; /** * There is a weird problem in fetching correct bounds of the polygon. * If the rightmost (bottommost) point of the polygon is integral, then * QRectF() will end exactly on it, but when converting into QRect the last * point will not be taken into account. It happens due to the difference * between center-point/topleft-point point representation. In many cases * the latter is expected, but we don't work with it in Qt/Krita. */ inline void adjustAlignedPolygon(QPolygonF &polygon) { static const qreal eps = 1e-5; static const QPointF p1(eps, 0.0); static const QPointF p2(eps, eps); static const QPointF p3(0.0, eps); polygon[1] += p1; polygon[2] += p2; polygon[3] += p3; } template