diff --git a/krita/data/actions/InteractionTool.action b/krita/data/actions/InteractionTool.action --- a/krita/data/actions/InteractionTool.action +++ b/krita/data/actions/InteractionTool.action @@ -122,5 +122,87 @@ false Align Bottom + + + Distribute Left + l + Distribute left edges equidistantly + + object_distribute_horizontal_left + + + false + + + Distribute Centers Horizontally + c + Distribute centers equidistantly horizontally + + object_distribute_horizontal_center + + + false + + + Distribute Right + r + Distribute right edges equidistantly + + object_distribute_horizontal_right + + + false + + + Distribute Horizontal Gap + g + Make horizontal gaps between objects equal + + object_distribute_horizontal_gaps + + + false + + + + Distribute Top + t + Distribute top edges equidistantly + + object_distribute_vertical_top + + + false + + + Distribute Centers Vertically + c + Distribute centers equidistantly vertically + + object_distribute_vertical_center + + + false + + + Distribute Bottom + b + Distribute bottom edges equidistantly + + object_distribute_vertical_bottom + + + false + + + Distribute Vertical Gap + g + Make vertical gaps between objects equal + + object_distribute_vertical_gaps + + + false + diff --git a/krita/krita.action b/krita/krita.action --- a/krita/krita.action +++ b/krita/krita.action @@ -388,18 +388,6 @@ true - - - Paste at cursor - - Paste at cursor - Paste at cursor - 0 - 0 - - false - - &Invert Selection diff --git a/krita/krita.xmlgui b/krita/krita.xmlgui --- a/krita/krita.xmlgui +++ b/krita/krita.xmlgui @@ -2,7 +2,7 @@ @@ -49,6 +49,7 @@ + diff --git a/krita/kritamenu.action b/krita/kritamenu.action --- a/krita/kritamenu.action +++ b/krita/kritamenu.action @@ -368,6 +368,18 @@ false + + + Paste at Cursor + + Paste at cursor + Paste at cursor + 0 + 0 + Ctrl+Alt+V + false + + Paste into &New Image diff --git a/krita/pics/misc-dark/dark_geometry.svg b/krita/pics/misc-dark/dark_geometry.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-dark/dark_geometry.svg @@ -0,0 +1,70 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/krita/pics/misc-dark/dark_path-break-point.svg b/krita/pics/misc-dark/dark_path-break-point.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-dark/dark_path-break-point.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-dark/dark_path-break-segment.svg b/krita/pics/misc-dark/dark_path-break-segment.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-dark/dark_path-break-segment.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-dark/dark_pathpoint-corner.svg b/krita/pics/misc-dark/dark_pathpoint-corner.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-dark/dark_pathpoint-corner.svg @@ -0,0 +1,94 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/krita/pics/misc-dark/dark_pathpoint-curve.svg b/krita/pics/misc-dark/dark_pathpoint-curve.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-dark/dark_pathpoint-curve.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-dark/dark_pathpoint-insert.svg b/krita/pics/misc-dark/dark_pathpoint-insert.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-dark/dark_pathpoint-insert.svg @@ -0,0 +1,80 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-dark/dark_pathpoint-join.svg b/krita/pics/misc-dark/dark_pathpoint-join.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-dark/dark_pathpoint-join.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-dark/dark_pathpoint-line.svg b/krita/pics/misc-dark/dark_pathpoint-line.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-dark/dark_pathpoint-line.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-dark/dark_pathpoint-merge.svg b/krita/pics/misc-dark/dark_pathpoint-merge.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-dark/dark_pathpoint-merge.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-dark/dark_pathpoint-remove.svg b/krita/pics/misc-dark/dark_pathpoint-remove.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-dark/dark_pathpoint-remove.svg @@ -0,0 +1,91 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/krita/pics/misc-dark/dark_pathpoint-smooth.svg b/krita/pics/misc-dark/dark_pathpoint-smooth.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-dark/dark_pathpoint-smooth.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-dark/dark_pathpoint-symmetric.svg b/krita/pics/misc-dark/dark_pathpoint-symmetric.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-dark/dark_pathpoint-symmetric.svg @@ -0,0 +1,94 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/krita/pics/misc-dark/dark_pathsegment-curve.svg b/krita/pics/misc-dark/dark_pathsegment-curve.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-dark/dark_pathsegment-curve.svg @@ -0,0 +1,78 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-dark/dark_pathsegment-line.svg b/krita/pics/misc-dark/dark_pathsegment-line.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-dark/dark_pathsegment-line.svg @@ -0,0 +1,78 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-dark/misc-dark-icons.qrc b/krita/pics/misc-dark/misc-dark-icons.qrc --- a/krita/pics/misc-dark/misc-dark-icons.qrc +++ b/krita/pics/misc-dark/misc-dark-icons.qrc @@ -2,6 +2,7 @@ dark_draw-eraser.svg + dark_geometry.svg dark_ox16-action-object-align-horizontal-center-calligra.svg dark_ox16-action-object-align-horizontal-left-calligra.svg dark_ox16-action-object-align-horizontal-right-calligra.svg @@ -28,5 +29,19 @@ dark_onionOff.svg dark_onionOn.svg dark_onion_skin_options.svg + dark_onion_skin_options.svg + dark_path-break-point.svg + dark_path-break-segment.svg + dark_pathpoint-corner.svg + dark_pathpoint-curve.svg + dark_pathpoint-insert.svg + dark_pathpoint-join.svg + dark_pathpoint-line.svg + dark_pathpoint-merge.svg + dark_pathpoint-remove.svg + dark_pathpoint-smooth.svg + dark_pathpoint-symmetric.svg + dark_pathsegment-curve.svg + dark_pathsegment-line.svg diff --git a/krita/pics/misc-light/light_geometry.svg b/krita/pics/misc-light/light_geometry.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-light/light_geometry.svg @@ -0,0 +1,70 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/krita/pics/misc-light/light_path-break-point.svg b/krita/pics/misc-light/light_path-break-point.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-light/light_path-break-point.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-light/light_path-break-segment.svg b/krita/pics/misc-light/light_path-break-segment.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-light/light_path-break-segment.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-light/light_pathpoint-corner.svg b/krita/pics/misc-light/light_pathpoint-corner.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-light/light_pathpoint-corner.svg @@ -0,0 +1,94 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/krita/pics/misc-light/light_pathpoint-curve.svg b/krita/pics/misc-light/light_pathpoint-curve.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-light/light_pathpoint-curve.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-light/light_pathpoint-insert.svg b/krita/pics/misc-light/light_pathpoint-insert.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-light/light_pathpoint-insert.svg @@ -0,0 +1,80 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-light/light_pathpoint-join.svg b/krita/pics/misc-light/light_pathpoint-join.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-light/light_pathpoint-join.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-light/light_pathpoint-line.svg b/krita/pics/misc-light/light_pathpoint-line.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-light/light_pathpoint-line.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-light/light_pathpoint-merge.svg b/krita/pics/misc-light/light_pathpoint-merge.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-light/light_pathpoint-merge.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-light/light_pathpoint-remove.svg b/krita/pics/misc-light/light_pathpoint-remove.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-light/light_pathpoint-remove.svg @@ -0,0 +1,91 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/krita/pics/misc-light/light_pathpoint-smooth.svg b/krita/pics/misc-light/light_pathpoint-smooth.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-light/light_pathpoint-smooth.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-light/light_pathpoint-symmetric.svg b/krita/pics/misc-light/light_pathpoint-symmetric.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-light/light_pathpoint-symmetric.svg @@ -0,0 +1,94 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/krita/pics/misc-light/light_pathsegment-curve.svg b/krita/pics/misc-light/light_pathsegment-curve.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-light/light_pathsegment-curve.svg @@ -0,0 +1,78 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-light/light_pathsegment-line.svg b/krita/pics/misc-light/light_pathsegment-line.svg new file mode 100644 --- /dev/null +++ b/krita/pics/misc-light/light_pathsegment-line.svg @@ -0,0 +1,78 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/krita/pics/misc-light/misc-light-icons.qrc b/krita/pics/misc-light/misc-light-icons.qrc --- a/krita/pics/misc-light/misc-light-icons.qrc +++ b/krita/pics/misc-light/misc-light-icons.qrc @@ -2,6 +2,7 @@ light_draw-eraser.svg + light_geometry.svg light_ox16-action-object-align-horizontal-center-calligra.svg light_ox16-action-object-align-horizontal-left-calligra.svg light_ox16-action-object-align-horizontal-right-calligra.svg @@ -28,5 +29,18 @@ light_onionOff.svg light_onionOn.svg light_onion_skin_options.svg + light_path-break-point.svg + light_path-break-segment.svg + light_pathpoint-corner.svg + light_pathpoint-curve.svg + light_pathpoint-insert.svg + light_pathpoint-join.svg + light_pathpoint-line.svg + light_pathpoint-merge.svg + light_pathpoint-remove.svg + light_pathpoint-smooth.svg + light_pathpoint-symmetric.svg + light_pathsegment-curve.svg + light_pathsegment-line.svg diff --git a/libs/CMakeLists.txt b/libs/CMakeLists.txt --- a/libs/CMakeLists.txt +++ b/libs/CMakeLists.txt @@ -8,7 +8,7 @@ add_subdirectory( flake ) add_subdirectory( basicflakes ) add_subdirectory( pigment ) -add_subdirectory( kundo2 ) +add_subdirectory( command ) add_subdirectory( brush ) add_subdirectory( psd ) add_subdirectory( color ) diff --git a/libs/basicflakes/CMakeLists.txt b/libs/basicflakes/CMakeLists.txt --- a/libs/basicflakes/CMakeLists.txt +++ b/libs/basicflakes/CMakeLists.txt @@ -25,7 +25,8 @@ target_link_libraries(kritabasicflakes PUBLIC - kritawidgets + kritaui + kritawidgets kritaflake kritapigment ) diff --git a/libs/basicflakes/tools/KoCreatePathTool.h b/libs/basicflakes/tools/KoCreatePathTool.h --- a/libs/basicflakes/tools/KoCreatePathTool.h +++ b/libs/basicflakes/tools/KoCreatePathTool.h @@ -23,6 +23,7 @@ #include "kritabasicflakes_export.h" +#include #include #include @@ -72,7 +73,7 @@ public Q_SLOTS: /// reimplemented - virtual void activate(ToolActivation toolActivation, const QSet &shapes); + virtual void activate(ToolActivation activation, const QSet &shapes); /// reimplemented virtual void deactivate(); /// reimplemented @@ -101,8 +102,6 @@ virtual QList > createOptionWidgets(); private: - KoShapeStroke *createStroke(); - Q_DECLARE_PRIVATE(KoCreatePathTool) Q_PRIVATE_SLOT(d_func(), void angleDeltaChanged(int)) Q_PRIVATE_SLOT(d_func(), void angleSnapChanged(int)) diff --git a/libs/basicflakes/tools/KoCreatePathTool.cpp b/libs/basicflakes/tools/KoCreatePathTool.cpp --- a/libs/basicflakes/tools/KoCreatePathTool.cpp +++ b/libs/basicflakes/tools/KoCreatePathTool.cpp @@ -22,16 +22,18 @@ #include "KoCreatePathTool.h" #include "KoCreatePathTool_p.h" +#include #include "KoPointerEvent.h" #include "KoPathShape.h" #include "KoSelection.h" #include "KoDocumentResourceManager.h" #include "KoShapePaintingContext.h" #include "KoShapeStroke.h" -#include "KoStrokeConfigWidget.h" #include "KoCanvasBase.h" #include "kis_int_parse_spin_box.h" #include +#include "kis_canvas_resource_provider.h" +#include #include @@ -57,62 +59,39 @@ if (pathStarted()) { - KoShapeStroke *stroke(createStroke()); - - if (stroke) { - d->shape->setStroke(stroke); - } - painter.save(); paintPath(*(d->shape), painter, converter); painter.restore(); - painter.save(); - - painter.setTransform(d->shape->absoluteTransformation(&converter) * painter.transform()); - - KoShape::applyConversion(painter, converter); + KisHandlePainterHelper helper = + KoShape::createHandlePainterHelper(&painter, d->shape, converter, d->handleRadius); - QPen pen(QBrush(Qt::blue), 1); - pen.setCosmetic(true); - painter.setPen(pen); - painter.setBrush(Qt::white); + const bool firstPointActive = d->firstPoint == d->activePoint; - const bool firstPoint = (d->firstPoint == d->activePoint); - - if (d->pointIsDragged || firstPoint) { + if (d->pointIsDragged || firstPointActive) { const bool onlyPaintActivePoints = false; KoPathPoint::PointTypes paintFlags = KoPathPoint::ControlPoint2; if (d->activePoint->activeControlPoint1()) { paintFlags |= KoPathPoint::ControlPoint1; } - d->activePoint->paint(painter, d->handleRadius, paintFlags, onlyPaintActivePoints); - } - - // check if we have to color the first point - if (d->mouseOverFirstPoint) { - painter.setBrush(Qt::red); - } else { - painter.setBrush(Qt::white); + helper.setHandleStyle(KisHandleStyle::highlightedPrimaryHandles()); + d->activePoint->paint(helper, paintFlags, onlyPaintActivePoints); } - d->firstPoint->paint(painter, d->handleRadius, KoPathPoint::Node); - - painter.restore(); + if (!firstPointActive) { + helper.setHandleStyle(d->mouseOverFirstPoint ? + KisHandleStyle::highlightedPrimaryHandles() : + KisHandleStyle::primarySelection()); + d->firstPoint->paint(helper, KoPathPoint::Node); + } } if (d->hoveredPoint) { - painter.save(); - painter.setTransform(d->hoveredPoint->parent()->absoluteTransformation(&converter), true); - KoShape::applyConversion(painter, converter); - QPen pen(QBrush(Qt::blue), 1); - pen.setCosmetic(true); - painter.setPen(pen); - painter.setBrush(Qt::white); - d->hoveredPoint->paint(painter, d->handleRadius, KoPathPoint::Node); - painter.restore(); + KisHandlePainterHelper helper = KoShape::createHandlePainterHelper(&painter, d->hoveredPoint->parent(), converter, d->handleRadius); + helper.setHandleStyle(KisHandleStyle::highlightedPrimaryHandles()); + d->hoveredPoint->paint(helper, KoPathPoint::Node); } painter.save(); @@ -199,7 +178,10 @@ d->shape = pathShape; pathShape->setShapeId(KoPathShapeId); - KoShapeStroke *stroke = new KoShapeStroke(canvas()->resourceManager()->activeStroke()); + KoShapeStrokeSP stroke(new KoShapeStroke()); + const qreal size = canvas()->resourceManager()->resource(KisCanvasResourceProvider::Size).toReal(); + + stroke->setLineWidth(canvas()->unit().fromUserValue(size)); stroke->setColor(canvas()->resourceManager()->foregroundColor().toQColor()); pathShape->setStroke(stroke); @@ -218,7 +200,7 @@ canvas()->updateCanvas(handlePaintRect(point)); canvas()->updateCanvas(canvas()->snapGuide()->boundingRect()); - canvas()->snapGuide()->setEditedShape(pathShape); + canvas()->snapGuide()->setAdditionalEditedShape(pathShape); d->angleSnapStrategy = new AngleSnapStrategy(d->angleSnappingDelta, d->angleSnapStatus); canvas()->snapGuide()->addCustomSnapStrategy(d->angleSnapStrategy); @@ -407,8 +389,10 @@ } } -void KoCreatePathTool::activate(ToolActivation, const QSet &) +void KoCreatePathTool::activate(ToolActivation activation, const QSet &shapes) { + KoToolBase::activate(activation, shapes); + Q_D(KoCreatePathTool); useCursor(Qt::ArrowCursor); @@ -423,6 +407,7 @@ void KoCreatePathTool::deactivate() { cancelPath(); + KoToolBase::deactivate(); } void KoCreatePathTool::documentResourceChanged(int key, const QVariant & res) @@ -451,7 +436,6 @@ d->existingStartPoint.validate(canvas()); d->existingEndPoint.validate(canvas()); - pathShape->setStroke(createStroke()); if (d->connectPaths(pathShape, d->existingStartPoint, d->existingEndPoint)) { if (d->existingStartPoint.isValid()) { startShape = d->existingStartPoint.path; @@ -506,28 +490,11 @@ angleWidget->setWindowTitle(i18n("Angle Constraints")); list.append(angleWidget); - d->strokeWidget = new KoStrokeConfigWidget(0); - d->strokeWidget->setWindowTitle(i18n("Line")); - d->strokeWidget->setCanvas(canvas()); - d->strokeWidget->setActive(false); - list.append(d->strokeWidget); - connect(angleEdit, SIGNAL(valueChanged(int)), this, SLOT(angleDeltaChanged(int))); connect(angleSnap, SIGNAL(stateChanged(int)), this, SLOT(angleSnapChanged(int))); return list; } -KoShapeStroke *KoCreatePathTool::createStroke() -{ - Q_D(KoCreatePathTool); - - KoShapeStroke *stroke = 0; - if (d->strokeWidget) { - stroke = d->strokeWidget->createShapeStroke(); - } - return stroke; -} - //have to include this because of Q_PRIVATE_SLOT #include diff --git a/libs/basicflakes/tools/KoCreatePathTool_p.h b/libs/basicflakes/tools/KoCreatePathTool_p.h --- a/libs/basicflakes/tools/KoCreatePathTool_p.h +++ b/libs/basicflakes/tools/KoCreatePathTool_p.h @@ -190,8 +190,8 @@ listeningToModifiers(false), angleSnapStrategy(0), angleSnappingDelta(15), - angleSnapStatus(false), - strokeWidget(0) { + angleSnapStatus(false) + { } KoPathShape *shape; @@ -209,7 +209,6 @@ AngleSnapStrategy *angleSnapStrategy; int angleSnappingDelta; bool angleSnapStatus; - KoStrokeConfigWidget *strokeWidget; void repaintActivePoint() const { const bool isFirstPoint = (activePoint == firstPoint); diff --git a/libs/basicflakes/tools/KoPencilTool.h b/libs/basicflakes/tools/KoPencilTool.h --- a/libs/basicflakes/tools/KoPencilTool.h +++ b/libs/basicflakes/tools/KoPencilTool.h @@ -20,6 +20,7 @@ #ifndef _KOPENCILTOOL_H_ #define _KOPENCILTOOL_H_ +#include "KoFlakeTypes.h" #include "KoToolBase.h" class KoPathShape; @@ -44,7 +45,7 @@ void mouseReleaseEvent(KoPointerEvent *event); void keyPressEvent(QKeyEvent *event); - virtual void activate(ToolActivation toolActivation, const QSet &shapes); + virtual void activate(ToolActivation activation, const QSet &shapes); void deactivate(); protected: @@ -57,14 +58,18 @@ */ virtual void addPathShape(KoPathShape* path, bool closePath); - KoShapeStroke* createStroke(); + KoShapeStrokeSP createStroke(); void setFittingError(qreal fittingError); qreal getFittingError(); private Q_SLOTS: void selectMode(int mode); void setOptimize(int state); void setDelta(double delta); + +protected Q_SLOTS: + virtual void slotUpdatePencilCursor(); + private: qreal lineAngle(const QPointF &p1, const QPointF &p2); diff --git a/libs/basicflakes/tools/KoPencilTool.cpp b/libs/basicflakes/tools/KoPencilTool.cpp --- a/libs/basicflakes/tools/KoPencilTool.cpp +++ b/libs/basicflakes/tools/KoPencilTool.cpp @@ -34,7 +34,8 @@ #include #include #include -#include +#include +#include #include @@ -64,6 +65,7 @@ , m_existingStartPoint(0) , m_existingEndPoint(0) , m_hoveredPoint(0) + , m_strokeWidget(0) { } @@ -93,15 +95,11 @@ } if (m_hoveredPoint) { - painter.save(); - painter.setTransform(m_hoveredPoint->parent()->absoluteTransformation(&converter), true); - KoShape::applyConversion(painter, converter); - - painter.setPen(QPen(Qt::blue, 0)); //TODO make configurable - painter.setBrush(Qt::white); //TODO make configurable - m_hoveredPoint->paint(painter, handleRadius(), KoPathPoint::Node); + KisHandlePainterHelper helper = + KoShape::createHandlePainterHelper(&painter, m_hoveredPoint->parent(), converter, handleRadius()); - painter.restore(); + helper.setHandleStyle(KisHandleStyle::primarySelection()); + m_hoveredPoint->paint(helper, KoPathPoint::Node); } } @@ -111,7 +109,9 @@ void KoPencilTool::mousePressEvent(KoPointerEvent *event) { - if (! m_shape) { + KoShapeStrokeSP stroke = createStroke(); + + if (!m_shape && stroke && stroke->isVisible()) { m_shape = new KoPathShape(); m_shape->setShapeId(KoPathShapeId); m_shape->setStroke(createStroke()); @@ -178,11 +178,17 @@ } } -void KoPencilTool::activate(ToolActivation, const QSet &) +void KoPencilTool::activate(ToolActivation activation, const QSet &shapes) { + KoToolBase::activate(activation, shapes); + m_points.clear(); m_close = false; - useCursor(Qt::ArrowCursor); + slotUpdatePencilCursor(); + + if (m_strokeWidget) { + m_strokeWidget->activate(); + } } void KoPencilTool::deactivate() @@ -193,6 +199,18 @@ m_existingStartPoint = 0; m_existingEndPoint = 0; m_hoveredPoint = 0; + + if (m_strokeWidget) { + m_strokeWidget->deactivate(); + } + + KoToolBase::deactivate(); +} + +void KoPencilTool::slotUpdatePencilCursor() +{ + KoShapeStrokeSP stroke = createStroke(); + useCursor((stroke && stroke->isVisible()) ? Qt::ArrowCursor : Qt::ForbiddenCursor); } void KoPencilTool::addPoint(const QPointF & point) @@ -353,9 +371,13 @@ optionWidget->setWindowTitle(i18n("Pencil")); widgets.append(optionWidget); - m_strokeWidget = new KoStrokeConfigWidget(0); + m_strokeWidget = new KoStrokeConfigWidget(canvas(), 0); + m_strokeWidget->setNoSelectionTrackingMode(true); m_strokeWidget->setWindowTitle(i18n("Line")); - m_strokeWidget->setCanvas(canvas()); + connect(m_strokeWidget, SIGNAL(sigStrokeChanged()), SLOT(slotUpdatePencilCursor())); + if (isActivated()) { + m_strokeWidget->activate(); + } widgets.append(m_strokeWidget); return widgets; } @@ -417,9 +439,9 @@ m_combineAngle = delta; } -KoShapeStroke* KoPencilTool::createStroke() +KoShapeStrokeSP KoPencilTool::createStroke() { - KoShapeStroke *stroke = 0; + KoShapeStrokeSP stroke; if (m_strokeWidget) { stroke = m_strokeWidget->createShapeStroke(); } diff --git a/libs/command/CMakeLists.txt b/libs/command/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/libs/command/CMakeLists.txt @@ -0,0 +1,28 @@ +set(kritacommand_LIB_SRCS + kundo2stack.cpp + kundo2group.cpp + kundo2view.cpp + kundo2model.cpp + kundo2magicstring.cpp + kundo2commandextradata.cpp + kis_undo_store.cpp + kis_undo_stores.cpp + kis_command_utils.cpp +) + +add_library(kritacommand SHARED ${kritacommand_LIB_SRCS}) +generate_export_header(kritacommand BASE_NAME kritacommand) + +target_link_libraries(kritacommand + PUBLIC + kritawidgetutils + KF5::I18n + KF5::ConfigGui + Qt5::Core + Qt5::Widgets +) + +set_target_properties(kritacommand PROPERTIES + VERSION ${GENERIC_KRITA_LIB_VERSION} SOVERSION ${GENERIC_KRITA_LIB_SOVERSION} +) +install(TARGETS kritacommand ${INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/libs/kundo2/Mainpage.dox b/libs/command/Mainpage.dox rename from libs/kundo2/Mainpage.dox rename to libs/command/Mainpage.dox diff --git a/libs/image/tests/kis_algebra_2d_test.h b/libs/command/kis_command_ids.h copy from libs/image/tests/kis_algebra_2d_test.h copy to libs/command/kis_command_ids.h --- a/libs/image/tests/kis_algebra_2d_test.h +++ b/libs/command/kis_command_ids.h @@ -16,21 +16,26 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_ALGEBRA_2D_TEST_H -#define __KIS_ALGEBRA_2D_TEST_H +#ifndef KIS_COMMAND_IDS_H +#define KIS_COMMAND_IDS_H -#include +namespace KisCommandUtils { +enum CommandId { + MoveShapeId = 9999, + ResizeShapeId, + TransformShapeId, + ChangeShapeTransparencyId, + ChangeShapeBackgroundId, + ChangeShapeStrokeId, + ChangeShapeMarkersId, + ChangeShapeParameterId, + ChangeEllipseShapeId, + ChangeRectangleShapeId, + ChangePathShapePointId, + ChangePathShapeControlPointId +}; -class KisAlgebra2DTest : public QObject -{ - Q_OBJECT -private Q_SLOTS: - void testHalfPlane(); - void testOuterCircle(); +} - void testQuadraticEquation(); - void testIntersections(); - void testWeirdIntersections(); -}; +#endif // KIS_COMMAND_IDS_H -#endif /* __KIS_ALGEBRA_2D_TEST_H */ diff --git a/libs/image/kis_command_utils.h b/libs/command/kis_command_utils.h rename from libs/image/kis_command_utils.h rename to libs/command/kis_command_utils.h --- a/libs/image/kis_command_utils.h +++ b/libs/command/kis_command_utils.h @@ -21,11 +21,11 @@ #include "kundo2command.h" #include "kis_undo_stores.h" -#include "kritaimage_export.h" +#include "kritacommand_export.h" namespace KisCommandUtils { - struct KRITAIMAGE_EXPORT AggregateCommand : public KUndo2Command { + struct KRITACOMMAND_EXPORT AggregateCommand : public KUndo2Command { AggregateCommand(); void redo(); @@ -40,17 +40,35 @@ KisSurrogateUndoStore m_store; }; - struct KRITAIMAGE_EXPORT SkipFirstRedoWrapper : public KUndo2Command { + struct KRITACOMMAND_EXPORT SkipFirstRedoWrapper : public KUndo2Command { SkipFirstRedoWrapper(KUndo2Command *child = 0, KUndo2Command *parent = 0); - void redo(); - void undo(); + void redo() override; + void undo() override; private: bool m_firstRedo; QScopedPointer m_child; }; - struct KRITAIMAGE_EXPORT FlipFlopCommand : public KUndo2Command { + struct KRITACOMMAND_EXPORT SkipFirstRedoBase : public KUndo2Command { + SkipFirstRedoBase(bool skipFirstRedo, KUndo2Command *parent = 0); + SkipFirstRedoBase(bool skipFirstRedo, const KUndo2MagicString &text, KUndo2Command *parent = 0); + + void redo() final; + void undo() final; + + void setSkipOneRedo(bool value); + + protected: + virtual void redoImpl() = 0; + virtual void undoImpl() = 0; + + private: + bool m_firstRedo; + }; + + + struct KRITACOMMAND_EXPORT FlipFlopCommand : public KUndo2Command { FlipFlopCommand(bool finalize, KUndo2Command *parent = 0); void redo(); @@ -67,7 +85,7 @@ bool m_firstRedo; }; - struct KRITAIMAGE_EXPORT CompositeCommand : public KUndo2Command { + struct KRITACOMMAND_EXPORT CompositeCommand : public KUndo2Command { CompositeCommand(KUndo2Command *parent = 0); ~CompositeCommand(); diff --git a/libs/image/kis_command_utils.cpp b/libs/command/kis_command_utils.cpp rename from libs/image/kis_command_utils.cpp rename to libs/command/kis_command_utils.cpp --- a/libs/image/kis_command_utils.cpp +++ b/libs/command/kis_command_utils.cpp @@ -45,7 +45,7 @@ } SkipFirstRedoWrapper::SkipFirstRedoWrapper(KUndo2Command *child, KUndo2Command *parent) - : KUndo2Command(parent), m_firstRedo(true), m_child(child) {} + : KUndo2Command(child->text(), parent), m_firstRedo(true), m_child(child) {} void SkipFirstRedoWrapper::redo() { @@ -67,6 +67,39 @@ } } + SkipFirstRedoBase::SkipFirstRedoBase(bool skipFirstRedo, KUndo2Command *parent) + : KUndo2Command(parent), + m_firstRedo(skipFirstRedo) + { + } + + SkipFirstRedoBase::SkipFirstRedoBase(bool skipFirstRedo, const KUndo2MagicString &text, KUndo2Command *parent) + : KUndo2Command(text, parent), + m_firstRedo(skipFirstRedo) + { + } + + void SkipFirstRedoBase::redo() + { + if (m_firstRedo) { + m_firstRedo = false; + } else { + redoImpl(); + KUndo2Command::redo(); + } + } + + void SkipFirstRedoBase::undo() + { + KUndo2Command::undo(); + undoImpl(); + } + + void SkipFirstRedoBase::setSkipOneRedo(bool value) + { + m_firstRedo = true; + } + FlipFlopCommand::FlipFlopCommand(bool finalize, KUndo2Command *parent) : KUndo2Command(parent), m_finalize(finalize), diff --git a/libs/image/kis_undo_store.h b/libs/command/kis_undo_store.h rename from libs/image/kis_undo_store.h rename to libs/command/kis_undo_store.h --- a/libs/image/kis_undo_store.h +++ b/libs/command/kis_undo_store.h @@ -22,7 +22,7 @@ #include #include -#include +#include class KUndo2Command; class KUndo2MagicString; @@ -54,7 +54,7 @@ * KisDocument::createUndoStore() is just a factory method, the document * doesn't store the undo store itself. */ -class KRITAIMAGE_EXPORT KisUndoStore +class KRITACOMMAND_EXPORT KisUndoStore { public: KisUndoStore(); diff --git a/libs/image/kis_undo_store.cpp b/libs/command/kis_undo_store.cpp rename from libs/image/kis_undo_store.cpp rename to libs/command/kis_undo_store.cpp diff --git a/libs/image/kis_undo_stores.h b/libs/command/kis_undo_stores.h rename from libs/image/kis_undo_stores.h rename to libs/command/kis_undo_stores.h --- a/libs/image/kis_undo_stores.h +++ b/libs/command/kis_undo_stores.h @@ -30,7 +30,7 @@ * internal stack. Used for wrapping around legacy code into * a single command. */ -class KRITAIMAGE_EXPORT KisSurrogateUndoStore : public KisUndoStore +class KRITACOMMAND_EXPORT KisSurrogateUndoStore : public KisUndoStore { public: KisSurrogateUndoStore(); @@ -60,7 +60,7 @@ * @brief The KisDumbUndoStore class doesn't actually save commands, * so you cannot undo or redo! */ -class KRITAIMAGE_EXPORT KisDumbUndoStore : public KisUndoStore +class KRITACOMMAND_EXPORT KisDumbUndoStore : public KisUndoStore { public: const KUndo2Command* presentCommand(); diff --git a/libs/image/kis_undo_stores.cpp b/libs/command/kis_undo_stores.cpp rename from libs/image/kis_undo_stores.cpp rename to libs/command/kis_undo_stores.cpp diff --git a/libs/kundo2/kundo2command.h b/libs/command/kundo2command.h rename from libs/kundo2/kundo2command.h rename to libs/command/kundo2command.h diff --git a/libs/kundo2/kundo2commandextradata.h b/libs/command/kundo2commandextradata.h rename from libs/kundo2/kundo2commandextradata.h rename to libs/command/kundo2commandextradata.h --- a/libs/kundo2/kundo2commandextradata.h +++ b/libs/command/kundo2commandextradata.h @@ -19,10 +19,10 @@ #ifndef __KUNDO2COMMANDEXTRADATA_H #define __KUNDO2COMMANDEXTRADATA_H -#include "kritaundo2_export.h" +#include "kritacommand_export.h" -class KRITAUNDO2_EXPORT KUndo2CommandExtraData +class KRITACOMMAND_EXPORT KUndo2CommandExtraData { public: virtual ~KUndo2CommandExtraData(); diff --git a/libs/kundo2/kundo2commandextradata.cpp b/libs/command/kundo2commandextradata.cpp rename from libs/kundo2/kundo2commandextradata.cpp rename to libs/command/kundo2commandextradata.cpp diff --git a/libs/kundo2/kundo2group.h b/libs/command/kundo2group.h rename from libs/kundo2/kundo2group.h rename to libs/command/kundo2group.h --- a/libs/kundo2/kundo2group.h +++ b/libs/command/kundo2group.h @@ -45,15 +45,15 @@ #include #include -#include "kritaundo2_export.h" +#include "kritacommand_export.h" class KUndo2GroupPrivate; class KUndo2QStack; class QAction; #ifndef QT_NO_UNDOGROUP -class KRITAUNDO2_EXPORT KUndo2Group : public QObject +class KRITACOMMAND_EXPORT KUndo2Group : public QObject { Q_OBJECT Q_DECLARE_PRIVATE(KUndo2Group) diff --git a/libs/kundo2/kundo2group.cpp b/libs/command/kundo2group.cpp rename from libs/kundo2/kundo2group.cpp rename to libs/command/kundo2group.cpp diff --git a/libs/kundo2/kundo2magicstring.h b/libs/command/kundo2magicstring.h rename from libs/kundo2/kundo2magicstring.h rename to libs/command/kundo2magicstring.h --- a/libs/kundo2/kundo2magicstring.h +++ b/libs/command/kundo2magicstring.h @@ -26,7 +26,7 @@ #include -#include "kritaundo2_export.h" +#include "kritacommand_export.h" /** * \class KUndo2MagicString is a special wrapper for a string that is @@ -48,7 +48,7 @@ * because in many languages you cannot combine words without * knowing the proper case. */ -class KRITAUNDO2_EXPORT KUndo2MagicString +class KRITACOMMAND_EXPORT KUndo2MagicString { public: /** diff --git a/libs/kundo2/kundo2magicstring.cpp b/libs/command/kundo2magicstring.cpp rename from libs/kundo2/kundo2magicstring.cpp rename to libs/command/kundo2magicstring.cpp diff --git a/libs/kundo2/kundo2model.h b/libs/command/kundo2model.h rename from libs/kundo2/kundo2model.h rename to libs/command/kundo2model.h diff --git a/libs/kundo2/kundo2model.cpp b/libs/command/kundo2model.cpp rename from libs/kundo2/kundo2model.cpp rename to libs/command/kundo2model.cpp diff --git a/libs/kundo2/kundo2qstack.h b/libs/command/kundo2qstack.h rename from libs/kundo2/kundo2qstack.h rename to libs/command/kundo2qstack.h diff --git a/libs/kundo2/kundo2stack.h b/libs/command/kundo2stack.h rename from libs/kundo2/kundo2stack.h rename to libs/command/kundo2stack.h --- a/libs/kundo2/kundo2stack.h +++ b/libs/command/kundo2stack.h @@ -68,7 +68,7 @@ #include -#include "kritaundo2_export.h" +#include "kritacommand_export.h" class QAction; class KUndo2CommandPrivate; @@ -94,7 +94,7 @@ * from QObject only for the sake of signal/slots capabilities. * Nothing else. */ -class KRITAUNDO2_EXPORT KUndo2Command +class KRITACOMMAND_EXPORT KUndo2Command { KUndo2CommandPrivate *d; int timedID; @@ -162,7 +162,7 @@ #ifndef QT_NO_UNDOSTACK -class KRITAUNDO2_EXPORT KUndo2QStack : public QObject +class KRITACOMMAND_EXPORT KUndo2QStack : public QObject { Q_OBJECT // Q_DECLARE_PRIVATE(KUndo2QStack) @@ -256,7 +256,7 @@ friend class KUndo2Group; }; -class KRITAUNDO2_EXPORT KUndo2Stack : public KUndo2QStack +class KRITACOMMAND_EXPORT KUndo2Stack : public KUndo2QStack { public: explicit KUndo2Stack(QObject *parent = 0); diff --git a/libs/kundo2/kundo2stack.cpp b/libs/command/kundo2stack.cpp rename from libs/kundo2/kundo2stack.cpp rename to libs/command/kundo2stack.cpp diff --git a/libs/kundo2/kundo2stack_p.h b/libs/command/kundo2stack_p.h rename from libs/kundo2/kundo2stack_p.h rename to libs/command/kundo2stack_p.h diff --git a/libs/kundo2/kundo2view.h b/libs/command/kundo2view.h rename from libs/kundo2/kundo2view.h rename to libs/command/kundo2view.h --- a/libs/kundo2/kundo2view.h +++ b/libs/command/kundo2view.h @@ -62,16 +62,16 @@ #include #include -#include "kritaundo2_export.h" +#include "kritacommand_export.h" #ifndef QT_NO_UNDOVIEW class KUndo2ViewPrivate; class KUndo2QStack; class KUndo2Group; class QIcon; -class KRITAUNDO2_EXPORT KUndo2View : public QListView +class KRITACOMMAND_EXPORT KUndo2View : public QListView { Q_OBJECT Q_PROPERTY(QString emptyLabel READ emptyLabel WRITE setEmptyLabel) diff --git a/libs/kundo2/kundo2view.cpp b/libs/command/kundo2view.cpp rename from libs/kundo2/kundo2view.cpp rename to libs/command/kundo2view.cpp diff --git a/libs/flake/CMakeLists.txt b/libs/flake/CMakeLists.txt --- a/libs/flake/CMakeLists.txt +++ b/libs/flake/CMakeLists.txt @@ -29,23 +29,21 @@ KoPathPoint.cpp KoPathSegment.cpp KoSelection.cpp + KoSelectedShapesProxy.cpp + KoSelectedShapesProxySimple.cpp KoShape.cpp KoShapeAnchor.cpp KoShapeBasedDocumentBase.cpp KoShapeApplicationData.cpp KoShapeContainer.cpp KoShapeContainerModel.cpp - KoShapeContainerDefaultModel.cpp KoShapeGroup.cpp - KoShapeManagerPaintingStrategy.cpp KoShapeManager.cpp KoShapePaintingContext.cpp KoFrameShape.cpp KoUnavailShape.cpp - KoMarkerData.cpp KoMarker.cpp KoMarkerCollection.cpp - KoMarkerSharedLoadingData.cpp KoToolBase.cpp KoCanvasController.cpp KoCanvasControllerWidget.cpp @@ -79,11 +77,12 @@ KoOdfGradientBackground.cpp KoHatchBackground.cpp KoPatternBackground.cpp + KoVectorPatternBackground.cpp KoShapeConfigWidgetBase.cpp KoDrag.cpp + KoSvgPaste.cpp KoDragOdfSaveHelper.cpp KoShapeOdfSaveHelper.cpp - KoShapePaste.cpp KoConnectionPoint.cpp KoConnectionShape.cpp KoConnectionShapeLoadingUpdater.cpp @@ -115,6 +114,8 @@ KoTosContainer.cpp KoTosContainerModel.cpp KoClipPath.cpp + KoClipMask.cpp + KoClipMaskPainter.cpp KoCurveFit.cpp commands/KoShapeGroupCommand.cpp commands/KoShapeAlignCommand.cpp @@ -124,6 +125,7 @@ commands/KoShapeDistributeCommand.cpp commands/KoShapeLockCommand.cpp commands/KoShapeMoveCommand.cpp + commands/KoShapeResizeCommand.cpp commands/KoShapeShearCommand.cpp commands/KoShapeSizeCommand.cpp commands/KoShapeStrokeCommand.cpp @@ -157,6 +159,8 @@ commands/KoShapeUnclipCommand.cpp commands/KoPathShapeMarkerCommand.cpp commands/KoShapeConnectionChangeCommand.cpp + commands/KoMultiPathPointMergeCommand.cpp + commands/KoMultiPathPointJoinCommand.cpp tools/KoCreateShapeStrategy.cpp tools/KoPathToolFactory.cpp tools/KoPathTool.cpp @@ -176,6 +180,7 @@ tools/KoPanToolFactory.cpp tools/KoInteractionTool.cpp tools/KoInteractionStrategy.cpp + tools/KoInteractionStrategyFactory.cpp tools/KoCreateShapesTool.cpp tools/KoCreateShapesToolFactory.cpp tools/KoShapeRubberSelectStrategy.cpp @@ -190,12 +195,12 @@ svg/SvgParser.cpp svg/SvgStyleParser.cpp svg/SvgGradientHelper.cpp - svg/SvgPatternHelper.cpp svg/SvgFilterHelper.cpp svg/SvgCssHelper.cpp svg/SvgClipPathHelper.cpp svg/SvgLoadingContext.cpp svg/SvgShapeFactory.cpp + svg/parsers/SvgTransformParser.cpp FlakeDebug.cpp tests/MockShapes.cpp @@ -217,7 +222,7 @@ $ ) -target_link_libraries(kritaflake kritapigment kritawidgetutils kritaodf kritaundo2 KF5::WidgetsAddons Qt5::Svg) +target_link_libraries(kritaflake kritapigment kritawidgetutils kritaodf kritacommand KF5::WidgetsAddons Qt5::Svg) set_target_properties(kritaflake PROPERTIES VERSION ${GENERIC_KRITA_LIB_VERSION} SOVERSION ${GENERIC_KRITA_LIB_SOVERSION} diff --git a/libs/flake/KoBakedShapeRenderer.h b/libs/flake/KoBakedShapeRenderer.h new file mode 100644 --- /dev/null +++ b/libs/flake/KoBakedShapeRenderer.h @@ -0,0 +1,133 @@ +/* + * 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 KOBAKEDSHAPERENDERER_H +#define KOBAKEDSHAPERENDERER_H + +#include +#include +#include +#include + +#include +#include + + +struct KoBakedShapeRenderer { + KoBakedShapeRenderer(const QPainterPath &dstShapeOutline, const QTransform &dstShapeTransform, + const QTransform &bakedTransform, + const QRectF &referenceRect, + bool contentIsObb, const QRectF &bakedShapeBoundingRect, + bool referenceIsObb, + const QTransform &patternTransform) + : m_dstShapeOutline(dstShapeOutline), + m_dstShapeTransform(dstShapeTransform), + m_contentIsObb(contentIsObb), + m_referenceIsObb(referenceIsObb), + m_patternTransform(patternTransform) + { + KIS_SAFE_ASSERT_RECOVER_NOOP(!contentIsObb || !bakedShapeBoundingRect.isEmpty()); + + const QRectF dstShapeBoundingRect = dstShapeOutline.boundingRect(); + + QTransform relativeToBakedShape; + + if (referenceIsObb || contentIsObb) { + m_relativeToShape = KisAlgebra2D::mapToRect(dstShapeBoundingRect); + relativeToBakedShape = KisAlgebra2D::mapToRect(bakedShapeBoundingRect); + } + + + m_referenceRectUser = + referenceIsObb ? + m_relativeToShape.mapRect(referenceRect).toAlignedRect() : + referenceRect.toAlignedRect(); + + m_patch = QImage(m_referenceRectUser.size(), QImage::Format_ARGB32); + m_patch.fill(0); + m_patchPainter.begin(&m_patch); + + m_patchPainter.translate(-m_referenceRectUser.topLeft()); + m_patchPainter.setClipRect(m_referenceRectUser); + + if (contentIsObb) { + m_patchPainter.setTransform(m_relativeToShape, true); + m_patchPainter.setTransform(relativeToBakedShape.inverted(), true); + } + + m_patchPainter.setTransform(bakedTransform.inverted(), true); + } + + QPainter* bakeShapePainter() { + return &m_patchPainter; + } + + void renderShape(QPainter &painter) { + painter.save(); + + painter.setTransform(m_dstShapeTransform, true); + painter.setClipPath(m_dstShapeOutline); + + QTransform brushTransform; + + QPointF patternOffset = m_referenceRectUser.topLeft(); + + brushTransform = + brushTransform * + QTransform::fromTranslate(patternOffset.x(), patternOffset.y()); + + if (m_contentIsObb) { + brushTransform = brushTransform * m_relativeToShape.inverted(); + } + + brushTransform = brushTransform * m_patternTransform; + + if (m_contentIsObb) { + brushTransform = brushTransform * m_relativeToShape; + } + + QBrush brush(m_patch); + brush.setTransform(brushTransform); + + painter.setBrush(brush); + painter.drawPath(m_dstShapeOutline); + + painter.restore(); + } + + QImage patchImage() const { + return m_patch; + } + + +private: + QPainterPath m_dstShapeOutline; + QTransform m_dstShapeTransform; + + bool m_contentIsObb; + bool m_referenceIsObb; + const QTransform &m_patternTransform; + + QImage m_patch; + QPainter m_patchPainter; + + QTransform m_relativeToShape; + QRect m_referenceRectUser; +}; + +#endif // KOBAKEDSHAPERENDERER_H diff --git a/libs/flake/KoCanvasBase.h b/libs/flake/KoCanvasBase.h --- a/libs/flake/KoCanvasBase.h +++ b/libs/flake/KoCanvasBase.h @@ -39,6 +39,7 @@ class KoCanvasController; class KoShape; class KoSnapGuide; +class KoSelectedShapesProxy; class QWidget; class QCursor; @@ -110,12 +111,24 @@ virtual void addCommand(KUndo2Command *command) = 0; /** - * return the current shapeManager + * Return the current shapeManager. WARNING: the shape manager can switch + * in time, e.g. when a layer is changed. Please don't keep any persistent + * connections to it. Instead please use selectedShapesProxy(), + * which is guaranteed to be the same throughout the life of the canvas. + * * @return the current shapeManager */ virtual KoShapeManager *shapeManager() const = 0; /** + * @brief selectedShapesProxy() is a special interface for keeping a persistent connections + * to selectionChanged() and selectionContentChanged() signals. While shapeManager() can change + * throughout the life time of the cavas, selectedShapesProxy() is guaranteed to stay the same. + * @return persistent KoSelectedShapesProxy object + */ + virtual KoSelectedShapesProxy *selectedShapesProxy() const = 0; + + /** * Tell the canvas to repaint the specified rectangle. The coordinates * are document coordinates, not view coordinates. */ diff --git a/libs/flake/KoCanvasBase.cpp b/libs/flake/KoCanvasBase.cpp --- a/libs/flake/KoCanvasBase.cpp +++ b/libs/flake/KoCanvasBase.cpp @@ -31,6 +31,7 @@ #include "KoShapeManager.h" #include "KoToolProxy.h" #include "KoSelection.h" +#include "KoSelectedShapesProxy.h" class Q_DECL_HIDDEN KoCanvasBase::Private { @@ -94,6 +95,7 @@ if (resourceManager()) resourceManager()->disconnect(object); if (shapeManager()) shapeManager()->disconnect(object); if (toolProxy()) toolProxy()->disconnect(object); + if (selectedShapesProxy()) selectedShapesProxy()->disconnect(object); } diff --git a/libs/flake/KoCanvasController.h b/libs/flake/KoCanvasController.h --- a/libs/flake/KoCanvasController.h +++ b/libs/flake/KoCanvasController.h @@ -299,6 +299,12 @@ 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; @@ -465,6 +471,7 @@ virtual void updateDocumentSize(const QSize &/*sz*/, bool /*recalculateCenter*/) {} virtual void setZoomWithWheel(bool /*zoom*/) {} virtual void setVastScrolling(qreal /*factor*/) {} + QPointF currentCursorPosition() const override { return QPointF(); } }; diff --git a/libs/flake/KoCanvasControllerWidget.h b/libs/flake/KoCanvasControllerWidget.h --- a/libs/flake/KoCanvasControllerWidget.h +++ b/libs/flake/KoCanvasControllerWidget.h @@ -139,6 +139,8 @@ virtual void setVastScrolling(qreal factor); + QPointF currentCursorPosition() const override; + /** * \internal */ @@ -171,8 +173,6 @@ /// reimplemented from QWidget virtual void wheelEvent(QWheelEvent *event); /// reimplemented from QWidget - virtual void keyPressEvent(QKeyEvent *event); - /// reimplemented from QWidget virtual bool focusNextPrevChild(bool next); private: diff --git a/libs/flake/KoCanvasControllerWidget.cpp b/libs/flake/KoCanvasControllerWidget.cpp --- a/libs/flake/KoCanvasControllerWidget.cpp +++ b/libs/flake/KoCanvasControllerWidget.cpp @@ -484,6 +484,13 @@ d->vastScrollingFactor = factor; } +QPointF KoCanvasControllerWidget::currentCursorPosition() const +{ + QWidget *canvasWidget = d->canvas->canvasWidget(); + const KoViewConverter *converter = d->canvas->viewConverter(); + return converter->viewToDocument(canvasWidget->mapFromGlobal(QCursor::pos()) + d->canvas->canvasController()->documentOffset() - canvasWidget->pos()); +} + void KoCanvasControllerWidget::pan(const QPoint &distance) { QPoint sourcePoint = scrollBarValue(); @@ -531,11 +538,6 @@ d->viewportWidget->handleDragLeaveEvent(event); } -void KoCanvasControllerWidget::keyPressEvent(QKeyEvent *event) -{ - KoToolManager::instance()->priv()->switchToolByShortcut(event); -} - void KoCanvasControllerWidget::wheelEvent(QWheelEvent *event) { if (d->zoomWithWheel != ((event->modifiers() & Qt::ControlModifier) == Qt::ControlModifier)) { diff --git a/libs/flake/KoCanvasControllerWidgetViewport_p.cpp b/libs/flake/KoCanvasControllerWidgetViewport_p.cpp --- a/libs/flake/KoCanvasControllerWidgetViewport_p.cpp +++ b/libs/flake/KoCanvasControllerWidgetViewport_p.cpp @@ -42,7 +42,6 @@ #include "KoSelection.h" #include "KoCanvasBase.h" #include "KoShapeLayer.h" -#include "KoShapePaste.h" #include "KoShapePaintingContext.h" #include "KoToolProxy.h" #include "KoCanvasControllerWidget.h" @@ -97,19 +96,22 @@ m_drawShadow = drawShadow; } - void Viewport::handleDragEnterEvent(QDragEnterEvent *event) { // if not a canvas set then ignore this, makes it possible to assume // we have a canvas in all the support methods. - if (!(m_parent->canvas() && m_parent->canvas()->canvasWidget())) + if (!(m_parent->canvas() && m_parent->canvas()->canvasWidget())) { + event->ignore(); return; + } // only allow dropping when active layer is editable KoSelection *selection = m_parent->canvas()->shapeManager()->selection(); KoShapeLayer *activeLayer = selection->activeLayer(); - if (activeLayer && (!activeLayer->isEditable() || activeLayer->isGeometryProtected())) + if (activeLayer && (!activeLayer->isEditable() || activeLayer->isGeometryProtected())) { + event->ignore(); return; + } const QMimeData *data = event->mimeData(); if (data->hasFormat(SHAPETEMPLATE_MIMETYPE) || @@ -161,19 +163,6 @@ m_draggedShape->setAbsolutePosition(correctPosition(event->pos())); m_parent->canvas()->shapeManager()->addShape(m_draggedShape); - } - else if (data->hasFormat(KoOdf::mimeType(KoOdf::Text))) { - KoShapeManager *sm = m_parent->canvas()->shapeManager(); - KoShapePaste paste(m_parent->canvas(), sm->selection()->activeLayer()); - if (paste.paste(KoOdf::Text, data)) { - QList shapes = paste.pastedShapes(); - if (shapes.count() == 1) { - m_draggedShape = shapes.first(); - m_draggedShape->setZIndex(KoShapePrivate::MaxZIndex); - event->setDropAction(Qt::CopyAction); - } - event->accept(); - } } else { event->ignore(); } diff --git a/libs/flake/KoCanvasResourceManager.h b/libs/flake/KoCanvasResourceManager.h --- a/libs/flake/KoCanvasResourceManager.h +++ b/libs/flake/KoCanvasResourceManager.h @@ -68,7 +68,6 @@ enum CanvasResource { ForegroundColor, ///< The active forground color selected for this canvas. BackgroundColor, ///< The active background color selected for this canvas. - ActiveStroke, ///< The active stroke 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 @@ -166,12 +165,6 @@ */ KoColor backgroundColor() const; - /// Sets the stroke resource - void setActiveStroke(const KoShapeStroke &stroke); - - /// Returns the stroke resource - KoShapeStroke activeStroke() const; - /** * Return the resource determined by param key as a boolean. * @param key the indentifying key for the resource diff --git a/libs/flake/KoCanvasResourceManager.cpp b/libs/flake/KoCanvasResourceManager.cpp --- a/libs/flake/KoCanvasResourceManager.cpp +++ b/libs/flake/KoCanvasResourceManager.cpp @@ -119,23 +119,6 @@ return resource(key).value(); } - -void KoCanvasResourceManager::setActiveStroke(const KoShapeStroke &stroke) -{ - QVariant v; - v.setValue(stroke); - setResource(ActiveStroke, v); -} - -KoShapeStroke KoCanvasResourceManager::activeStroke() const -{ - if (!d->manager.hasResource(ActiveStroke)) { - KoShapeStroke empty; - return empty; - } - return resource(ActiveStroke).value(); -} - bool KoCanvasResourceManager::boolResource(int key) const { return d->manager.boolResource(key); diff --git a/libs/ui/kis_aspect_ratio_locker.h b/libs/flake/KoClipMask.h copy from libs/ui/kis_aspect_ratio_locker.h copy to libs/flake/KoClipMask.h --- a/libs/ui/kis_aspect_ratio_locker.h +++ b/libs/flake/KoClipMask.h @@ -16,43 +16,53 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_ASPECT_RATIO_LOCKER_H -#define __KIS_ASPECT_RATIO_LOCKER_H +#ifndef KOCLIPMASK_H +#define KOCLIPMASK_H +#include "kritaflake_export.h" + +#include #include -#include -#include "kritaui_export.h" +#include + +class KoShape; +class QRectF; +class QTransform; +class QPointF; +class QPainter; -class QSpinBox; -class QDoubleSpinBox; -class KisSliderSpinBox; -class KisDoubleSliderSpinBox; -class KoAspectButton; -class KRITAUI_EXPORT KisAspectRatioLocker : public QObject +class KRITAFLAKE_EXPORT KoClipMask { - Q_OBJECT public: - KisAspectRatioLocker(QObject *parent = 0); - ~KisAspectRatioLocker(); + KoClipMask(); + ~KoClipMask(); + + KoClipMask *clone() const; + + KoFlake::CoordinateSystem coordinates() const; + void setCoordinates(KoFlake::CoordinateSystem value); - template - void connectSpinBoxes(SpinBoxType *spinOne, SpinBoxType *spinTwo, KoAspectButton *aspectButton); + KoFlake::CoordinateSystem contentCoordinates() const; + void setContentCoordinates(KoFlake::CoordinateSystem value); - void setBlockUpdateSignalOnDrag(bool block); + QRectF maskRect() const; + void setMaskRect(const QRectF &value); -private Q_SLOTS: - void slotSpinOneChanged(); - void slotSpinTwoChanged(); - void slotAspectButtonChanged(); + QList shapes() const; + void setShapes(const QList &value); -Q_SIGNALS: - void sliderValueChanged(); - void aspectButtonChanged(); + bool isEmpty() const; + + void setExtraShapeOffset(const QPointF &value); + + void drawMask(QPainter *painter, KoShape *shape); private: + KoClipMask(const KoClipMask &rhs); + struct Private; const QScopedPointer m_d; }; -#endif /* __KIS_ASPECT_RATIO_LOCKER_H */ +#endif // KOCLIPMASK_H diff --git a/libs/flake/KoClipMask.cpp b/libs/flake/KoClipMask.cpp new file mode 100644 --- /dev/null +++ b/libs/flake/KoClipMask.cpp @@ -0,0 +1,176 @@ +/* + * 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 "KoClipMask.h" + +#include +#include +#include +#include +#include "kis_algebra_2d.h" + +#include +#include + +struct Q_DECL_HIDDEN KoClipMask::Private { + Private() {} + Private(const Private &rhs) + : coordinates(rhs.coordinates), + contentCoordinates(rhs.contentCoordinates), + maskRect(rhs.maskRect), + extraShapeTransform(rhs.extraShapeTransform) + { + Q_FOREACH (KoShape *shape, rhs.shapes) { + KoShape *clonedShape = shape->cloneShape(); + KIS_ASSERT_RECOVER(clonedShape) { continue; } + + shapes << clonedShape; + } + } + + ~Private() { + qDeleteAll(shapes); + shapes.clear(); + } + + + KoFlake::CoordinateSystem coordinates = KoFlake::ObjectBoundingBox; + KoFlake::CoordinateSystem contentCoordinates = KoFlake::UserSpaceOnUse; + + QRectF maskRect = QRectF(-0.1, -0.1, 1.2, 1.2); + + QList shapes; + QTransform extraShapeTransform; // TODO: not used anymore, use direct shape transform instead + +}; + +KoClipMask::KoClipMask() + : m_d(new Private) +{ +} + +KoClipMask::~KoClipMask() +{ +} + +KoClipMask::KoClipMask(const KoClipMask &rhs) + : m_d(new Private(*rhs.m_d)) +{ +} + +KoClipMask *KoClipMask::clone() const +{ + return new KoClipMask(*this); +} + +KoFlake::CoordinateSystem KoClipMask::coordinates() const +{ + return m_d->coordinates; +} + +void KoClipMask::setCoordinates(KoFlake::CoordinateSystem value) +{ + m_d->coordinates = value; +} + +KoFlake::CoordinateSystem KoClipMask::contentCoordinates() const +{ + return m_d->contentCoordinates; +} + +void KoClipMask::setContentCoordinates(KoFlake::CoordinateSystem value) +{ + m_d->contentCoordinates = value; +} + +QRectF KoClipMask::maskRect() const +{ + return m_d->maskRect; +} + +void KoClipMask::setMaskRect(const QRectF &value) +{ + m_d->maskRect = value; +} + +QList KoClipMask::shapes() const +{ + return m_d->shapes; +} + +void KoClipMask::setShapes(const QList &value) +{ + m_d->shapes = value; +} + +bool KoClipMask::isEmpty() const +{ + return m_d->shapes.isEmpty(); +} + +void KoClipMask::setExtraShapeOffset(const QPointF &value) +{ + /** + * TODO: when we implement source shapes sharing, please wrap the shapes + * into a group and apply this transform to the group instead + */ + + if (m_d->contentCoordinates == KoFlake::UserSpaceOnUse) { + const QTransform t = QTransform::fromTranslate(value.x(), value.y()); + + Q_FOREACH (KoShape *shape, m_d->shapes) { + shape->applyAbsoluteTransformation(t); + } + } + + if (m_d->coordinates == KoFlake::UserSpaceOnUse) { + m_d->maskRect.translate(value); + } +} + +void KoClipMask::drawMask(QPainter *painter, KoShape *shape) +{ + painter->save(); + + QPainterPath clipPathInShapeSpace; + + if (m_d->coordinates == KoFlake::ObjectBoundingBox) { + QTransform relativeToShape = KisAlgebra2D::mapToRect(shape->outlineRect()); + clipPathInShapeSpace.addPolygon(relativeToShape.map(m_d->maskRect)); + } else { + clipPathInShapeSpace.addRect(m_d->maskRect); + clipPathInShapeSpace = m_d->extraShapeTransform.map(clipPathInShapeSpace); + } + + painter->setClipPath(clipPathInShapeSpace, Qt::IntersectClip); + + if (m_d->contentCoordinates == KoFlake::ObjectBoundingBox) { + QTransform relativeToShape = KisAlgebra2D::mapToRect(shape->outlineRect()); + + painter->setTransform(relativeToShape, true); + } else { + painter->setTransform(m_d->extraShapeTransform, true); + } + + KoViewConverter converter; + KoShapePainter p; + p.setShapes(m_d->shapes); + p.paint(*painter, converter); + + painter->restore(); +} diff --git a/libs/image/tests/kis_algebra_2d_test.h b/libs/flake/KoClipMaskPainter.h copy from libs/image/tests/kis_algebra_2d_test.h copy to libs/flake/KoClipMaskPainter.h --- a/libs/image/tests/kis_algebra_2d_test.h +++ b/libs/flake/KoClipMaskPainter.h @@ -16,21 +16,31 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_ALGEBRA_2D_TEST_H -#define __KIS_ALGEBRA_2D_TEST_H +#ifndef KOCLIPMASKPAINTER_H +#define KOCLIPMASKPAINTER_H -#include +#include "kritaflake_export.h" -class KisAlgebra2DTest : public QObject +#include + +class QPainter; +class QRectF; + + +class KRITAFLAKE_EXPORT KoClipMaskPainter { - Q_OBJECT -private Q_SLOTS: - void testHalfPlane(); - void testOuterCircle(); - - void testQuadraticEquation(); - void testIntersections(); - void testWeirdIntersections(); +public: + KoClipMaskPainter(QPainter *painter, const QRectF &globalClipRect); + ~KoClipMaskPainter(); + + QPainter* shapePainter(); + QPainter* maskPainter(); + + void renderOnGlobalPainter(); + +private: + struct Private; + const QScopedPointer m_d; }; -#endif /* __KIS_ALGEBRA_2D_TEST_H */ +#endif // KOCLIPMASKPAINTER_H diff --git a/libs/flake/KoClipMaskPainter.cpp b/libs/flake/KoClipMaskPainter.cpp new file mode 100644 --- /dev/null +++ b/libs/flake/KoClipMaskPainter.cpp @@ -0,0 +1,134 @@ +/* + * 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 "KoClipMaskPainter.h" + +#include +#include + +#include "kis_assert.h" + +struct Q_DECL_HIDDEN KoClipMaskPainter::Private +{ + QPainter *globalPainter; + + QImage shapeImage; + QImage maskImage; + + QPainter shapePainter; + QPainter maskPainter; + + QRect alignedGlobalClipRect; +}; + +KoClipMaskPainter::KoClipMaskPainter(QPainter *painter, const QRectF &globalClipRect) + : m_d(new Private) +{ + m_d->globalPainter = painter; + m_d->alignedGlobalClipRect = globalClipRect.toAlignedRect(); + + m_d->shapeImage = QImage(m_d->alignedGlobalClipRect.size(), QImage::Format_ARGB32); + m_d->maskImage = QImage(m_d->alignedGlobalClipRect.size(), QImage::Format_ARGB32); + + m_d->shapeImage.fill(0); + m_d->maskImage.fill(0); + + QTransform moveToBufferTransform = + QTransform::fromTranslate(-m_d->alignedGlobalClipRect.x(), + -m_d->alignedGlobalClipRect.y()); + + m_d->shapePainter.begin(&m_d->shapeImage); + m_d->shapePainter.setTransform(moveToBufferTransform); + m_d->shapePainter.setTransform(painter->transform(), true); + if (painter->hasClipping()) { + m_d->shapePainter.setClipPath(painter->clipPath()); + } + m_d->shapePainter.setOpacity(painter->opacity()); + m_d->shapePainter.setBrush(painter->brush()); + m_d->shapePainter.setPen(painter->pen()); + + m_d->maskPainter.begin(&m_d->maskImage); + m_d->maskPainter.setTransform(moveToBufferTransform); + m_d->maskPainter.setTransform(painter->transform(), true); + if (painter->hasClipping()) { + m_d->maskPainter.setClipPath(painter->clipPath()); + } + m_d->maskPainter.setOpacity(painter->opacity()); + m_d->maskPainter.setBrush(painter->brush()); + m_d->maskPainter.setPen(painter->pen()); +} + +KoClipMaskPainter::~KoClipMaskPainter() +{ +} + +QPainter *KoClipMaskPainter::shapePainter() +{ + return &m_d->shapePainter; +} + +QPainter *KoClipMaskPainter::maskPainter() +{ + return &m_d->maskPainter; +} + +void KoClipMaskPainter::renderOnGlobalPainter() +{ + KIS_ASSERT_RECOVER_RETURN(m_d->maskImage.size() == m_d->shapeImage.size()); + + for (int y = 0; y < m_d->maskImage.height(); y++) { + QRgb *shapeData = reinterpret_cast(m_d->shapeImage.scanLine(y)); + QRgb *maskData = reinterpret_cast(m_d->maskImage.scanLine(y)); + + for (int x = 0; x < m_d->maskImage.width(); x++) { + + const qreal normCoeff = 1.0 / 255.0 * 255.0; + + qreal maskValue = qreal(qAlpha(*maskData)) * + (0.2125 * qRed(*maskData) + + 0.7154 * qGreen(*maskData) + + 0.0721 * qBlue(*maskData)); + + int alpha = qRound(maskValue * qAlpha(*shapeData) * normCoeff); + + *shapeData = (alpha << 24) | (*shapeData & 0x00ffffff); + + shapeData++; + maskData++; + } + } + + KIS_ASSERT_RECOVER_RETURN(m_d->shapeImage.size() == m_d->alignedGlobalClipRect.size()); + QPainterPath globalClipPath; + + if (m_d->globalPainter->hasClipping()) { + globalClipPath = m_d->globalPainter->transform().map(m_d->globalPainter->clipPath()); + } + + m_d->globalPainter->save(); + + m_d->globalPainter->setTransform(QTransform()); + + if (!globalClipPath.isEmpty()) { + m_d->globalPainter->setClipPath(globalClipPath); + } + + m_d->globalPainter->drawImage(m_d->alignedGlobalClipRect.topLeft(), m_d->shapeImage); + m_d->globalPainter->restore(); +} + diff --git a/libs/flake/KoClipPath.h b/libs/flake/KoClipPath.h --- a/libs/flake/KoClipPath.h +++ b/libs/flake/KoClipPath.h @@ -21,9 +21,11 @@ #define KOCLIPPATH_H #include "kritaflake_export.h" + +#include #include -#include #include +#include class KoShape; class KoPathShape; @@ -33,43 +35,24 @@ class QPainterPath; class QSizeF; -/// Shared clip path data -class KRITAFLAKE_EXPORT KoClipData : public QSharedData -{ -public: - /// Creates clip path data from a single path shape, takes ownership of the path shape - explicit KoClipData(KoPathShape *clipPathShape); - - /// Creates clip path data from multiple path shapes, takes ownership of the path shapes - explicit KoClipData(const QList &clipPathShapes); - - /// Destroys the clip path data - ~KoClipData(); - - /// Returns the clip path shapes - QList clipPathShapes() const; - - /// Gives up ownership of clip path shapes - void removeClipShapesOwnership(); - -private: - class Private; - Private * const d; -}; - /// Clip path used to clip shapes class KRITAFLAKE_EXPORT KoClipPath { public: + /** * Create a new shape clipping using the given clip data - * @param clippedShape the shape to clip - * @param clipData shared clipping data containing the clip paths + * @param clipShapes define the clipping shapes, owned by KoClipPath! + * @param coordinates shows if ObjectBoundingBox or UserSpaceOnUse coordinate + * system is used. */ - KoClipPath(KoShape *clippedShape, KoClipData *clipData); - + KoClipPath(QList clipShapes, KoFlake::CoordinateSystem coordinates); ~KoClipPath(); + KoClipPath *clone() const; + + KoFlake::CoordinateSystem coordinates() const; + /// Sets the clip rule to be used for the clip path void setClipRule(Qt::FillRule clipRule); @@ -85,6 +68,8 @@ /// Returns the clip path shapes QList clipPathShapes() const; + QList clipShapes() const; + /** * Returns the transformation from the clip data path shapes to the * current document coordinates of the specified clipped shape. @@ -98,8 +83,11 @@ static void applyClipping(KoShape *clippedShape, QPainter &painter, const KoViewConverter &converter); private: + KoClipPath(const KoClipPath &rhs); + +private: class Private; - Private * const d; + const QScopedPointer d; }; #endif // KOCLIPPATH_H diff --git a/libs/flake/KoClipPath.cpp b/libs/flake/KoClipPath.cpp --- a/libs/flake/KoClipPath.cpp +++ b/libs/flake/KoClipPath.cpp @@ -20,144 +20,161 @@ #include "KoClipPath.h" #include "KoPathShape.h" #include "KoViewConverter.h" +#include "KoShapeGroup.h" #include #include #include #include +#include + + QTransform scaleToPercent(const QSizeF &size) { const qreal w = qMax(static_cast(1e-5), size.width()); const qreal h = qMax(static_cast(1e-5), size.height()); - return QTransform().scale(100/w, 100/h); + return QTransform().scale(1.0/w, 1.0/h); } QTransform scaleFromPercent(const QSizeF &size) { const qreal w = qMax(static_cast(1e-5), size.width()); const qreal h = qMax(static_cast(1e-5), size.height()); - return QTransform().scale(w/100, h/100); + return QTransform().scale(w/1.0, h/1.0); } -class Q_DECL_HIDDEN KoClipData::Private +class Q_DECL_HIDDEN KoClipPath::Private { public: - Private() : deleteClipShapes(true) + Private() + {} + + Private(const Private &rhs) + : clipPath(rhs.clipPath), + clipRule(rhs.clipRule), + coordinates(rhs.coordinates), + initialTransformToShape(rhs.initialTransformToShape), + initialShapeSize(rhs.initialShapeSize) { + Q_FOREACH (KoShape *shape, rhs.shapes) { + KoShape *clonedShape = shape->cloneShape(); + KIS_ASSERT_RECOVER(clonedShape) { continue; } + + shapes.append(clonedShape); + } } ~Private() { - if (deleteClipShapes) - qDeleteAll(clipPathShapes); + qDeleteAll(shapes); + shapes.clear(); } - QList clipPathShapes; - bool deleteClipShapes; -}; - -KoClipData::KoClipData(KoPathShape *clipPathShape) - : d(new Private()) -{ - Q_ASSERT(clipPathShape); - d->clipPathShapes.append(clipPathShape); -} - -KoClipData::KoClipData(const QList &clipPathShapes) - : d(new Private()) -{ - Q_ASSERT(clipPathShapes.count()); - d->clipPathShapes = clipPathShapes; -} - -KoClipData::~KoClipData() -{ - delete d; -} - -QList KoClipData::clipPathShapes() const -{ - return d->clipPathShapes; -} - -void KoClipData::removeClipShapesOwnership() -{ - d->deleteClipShapes = false; -} - -class Q_DECL_HIDDEN KoClipPath::Private -{ -public: - Private(KoClipData *data) - : clipData(data) - {} - - ~Private() - { + void collectShapePath(QPainterPath *result, const KoShape *shape) { + if (const KoPathShape *pathShape = dynamic_cast(shape)) { + // different shapes add up to the final path using Windind Fill rule (acc. to SVG 1.1) + QTransform t = pathShape->absoluteTransformation(0); + result->addPath(t.map(pathShape->outline())); + } else if (const KoShapeGroup *groupShape = dynamic_cast(shape)) { + QList shapes = groupShape->shapes(); + qSort(shapes.begin(), shapes.end(), KoShape::compareShapeZIndex); + + Q_FOREACH (const KoShape *child, shapes) { + collectShapePath(result, child); + } + } } - void compileClipPath(KoShape *clippedShape) + + void compileClipPath() { - QList clipShapes = clipData->clipPathShapes(); - if (!clipShapes.count()) + QList clipShapes = this->shapes; + if (clipShapes.isEmpty()) return; - initialShapeSize = clippedShape->outline().boundingRect().size(); - initialTransformToShape = clippedShape->absoluteTransformation(0).inverted(); + clipPath = QPainterPath(); + clipPath.setFillRule(Qt::WindingFill); + + qSort(clipShapes.begin(), clipShapes.end(), KoShape::compareShapeZIndex); - QTransform transformToShape = initialTransformToShape * scaleToPercent(initialShapeSize); + Q_FOREACH (KoShape *path, clipShapes) { + if (!path) continue; - Q_FOREACH (KoPathShape *path, clipShapes) { - if (!path) - continue; - // map clip path to shape coordinates of clipped shape - QTransform m = path->absoluteTransformation(0) * transformToShape; - if (clipPath.isEmpty()) - clipPath = m.map(path->outline()); - else - clipPath |= m.map(path->outline()); + collectShapePath(&clipPath, path); } } - QExplicitlySharedDataPointer clipData; ///< the clip path data + QList shapes; QPainterPath clipPath; ///< the compiled clip path in shape coordinates of the clipped shape + Qt::FillRule clipRule = Qt::WindingFill; + KoFlake::CoordinateSystem coordinates = KoFlake::ObjectBoundingBox; QTransform initialTransformToShape; ///< initial transformation to shape coordinates of the clipped shape QSizeF initialShapeSize; ///< initial size of clipped shape }; -KoClipPath::KoClipPath(KoShape *clippedShape, KoClipData *clipData) - : d( new Private(clipData) ) +KoClipPath::KoClipPath(QList clipShapes, KoFlake::CoordinateSystem coordinates) + : d(new Private()) +{ + d->shapes = clipShapes; + d->coordinates = coordinates; + d->compileClipPath(); +} + +KoClipPath::KoClipPath(const KoClipPath &rhs) + : d(new Private(*rhs.d)) { - d->compileClipPath(clippedShape); } KoClipPath::~KoClipPath() { - delete d; +} + +KoClipPath *KoClipPath::clone() const +{ + return new KoClipPath(*this); } void KoClipPath::setClipRule(Qt::FillRule clipRule) { - d->clipPath.setFillRule(clipRule); + d->clipRule = clipRule; } Qt::FillRule KoClipPath::clipRule() const { - return d->clipPath.fillRule(); + return d->clipRule; +} + +KoFlake::CoordinateSystem KoClipPath::coordinates() const +{ + return d->coordinates; } void KoClipPath::applyClipping(KoShape *clippedShape, QPainter &painter, const KoViewConverter &converter) { QPainterPath clipPath; KoShape *shape = clippedShape; while (shape) { if (shape->clipPath()) { - QTransform m = scaleFromPercent(shape->outline().boundingRect().size()) * shape->absoluteTransformation(0); - if (clipPath.isEmpty()) - clipPath = m.map(shape->clipPath()->path()); - else - clipPath |= m.map(shape->clipPath()->path()); + QPainterPath path = shape->clipPath()->path(); + + QTransform t; + + if (shape->clipPath()->coordinates() == KoFlake::ObjectBoundingBox) { + const QRectF shapeLocalBoundingRect = shape->outline().boundingRect(); + t = KisAlgebra2D::mapToRect(shapeLocalBoundingRect) * shape->absoluteTransformation(0); + + } else { + t = shape->absoluteTransformation(0); + } + + path = t.map(path); + + if (clipPath.isEmpty()) { + clipPath = path; + } else { + clipPath &= path; + } } shape = shape->parent(); } @@ -183,7 +200,23 @@ QList KoClipPath::clipPathShapes() const { - return d->clipData->clipPathShapes(); + // TODO: deprecate this method! + + QList shapes; + + Q_FOREACH (KoShape *shape, d->shapes) { + KoPathShape *pathShape = dynamic_cast(shape); + if (pathShape) { + shapes << pathShape; + } + } + + return shapes; +} + +QList KoClipPath::clipShapes() const +{ + return d->shapes; } QTransform KoClipPath::clipDataTransformation(KoShape *clippedShape) const diff --git a/libs/flake/KoColorBackground.h b/libs/flake/KoColorBackground.h --- a/libs/flake/KoColorBackground.h +++ b/libs/flake/KoColorBackground.h @@ -38,6 +38,8 @@ virtual ~KoColorBackground(); + bool compareTo(const KoShapeBackground *other) const override; + /// Returns the background color QColor color() const; diff --git a/libs/flake/KoColorBackground.cpp b/libs/flake/KoColorBackground.cpp --- a/libs/flake/KoColorBackground.cpp +++ b/libs/flake/KoColorBackground.cpp @@ -52,6 +52,14 @@ { } +bool KoColorBackground::compareTo(const KoShapeBackground *other) const +{ + Q_D(const KoColorBackground); + + const KoColorBackground *bg = dynamic_cast(other); + return bg && bg->color() == d->color; +} + QColor KoColorBackground::color() const { Q_D(const KoColorBackground); diff --git a/libs/flake/KoConnectionShape.h b/libs/flake/KoConnectionShape.h --- a/libs/flake/KoConnectionShape.h +++ b/libs/flake/KoConnectionShape.h @@ -54,6 +54,8 @@ KoConnectionShape(); virtual ~KoConnectionShape(); + KoShape* cloneShape() const override; + // reimplemented virtual void saveOdf(KoShapeSavingContext &context) const; @@ -124,6 +126,9 @@ void updateConnections(); protected: + KoConnectionShape(const KoConnectionShape &rhs); + + /// reimplemented void moveHandleAction(int handleId, const QPointF &point, Qt::KeyboardModifiers modifiers = Qt::NoModifier); diff --git a/libs/flake/KoConnectionShape.cpp b/libs/flake/KoConnectionShape.cpp --- a/libs/flake/KoConnectionShape.cpp +++ b/libs/flake/KoConnectionShape.cpp @@ -49,6 +49,20 @@ { } +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); @@ -59,7 +73,7 @@ KoConnectionPoint::EscapeDirection ed = attachedShape->connectionPoint(connectionPointId).escapeDirection; if (ed == KoConnectionPoint::AllDirections) { QPointF handlePoint = q->shapeToDocument(handles[handleId]); - QPointF centerPoint = attachedShape->absolutePosition(KoFlake::CenteredPosition); + QPointF centerPoint = attachedShape->absolutePosition(KoFlake::Center); /* * Determine the best escape direction from the position of the handle point @@ -74,11 +88,11 @@ * of the orthogonal direction. */ // define our edge points in the right order - const KoFlake::Position corners[4] = { - KoFlake::BottomRightCorner, - KoFlake::BottomLeftCorner, - KoFlake::TopLeftCorner, - KoFlake::TopRightCorner + const KoFlake::AnchorPosition corners[4] = { + KoFlake::BottomRight, + KoFlake::BottomLeft, + KoFlake::TopLeft, + KoFlake::TopRight }; QPointF vHandle = handlePoint-centerPoint; @@ -116,15 +130,15 @@ } } else if (ed == KoConnectionPoint::HorizontalDirections) { QPointF handlePoint = q->shapeToDocument(handles[handleId]); - QPointF centerPoint = attachedShape->absolutePosition(KoFlake::CenteredPosition); + 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::CenteredPosition); + 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); @@ -299,7 +313,7 @@ } KoConnectionShape::KoConnectionShape() - : KoParameterShape(*(new KoConnectionShapePrivate(this))) + : KoParameterShape(new KoConnectionShapePrivate(this)) { Q_D(KoConnectionShape); d->handles.push_back(QPointF(0, 0)); @@ -313,6 +327,12 @@ clearConnectionPoints(); } +KoConnectionShape::KoConnectionShape(const KoConnectionShape &rhs) + : KoParameterShape(new KoConnectionShapePrivate(*rhs.d_func(), this)) +{ +} + + KoConnectionShape::~KoConnectionShape() { Q_D(KoConnectionShape); @@ -322,6 +342,11 @@ d->shape2->removeDependee(this); } +KoShape *KoConnectionShape::cloneShape() const +{ + return new KoConnectionShape(*this); +} + void KoConnectionShape::saveOdf(KoShapeSavingContext & context) const { Q_D(const KoConnectionShape); @@ -445,7 +470,7 @@ if (d->hasCustomPath) { KoPathShapeLoader loader(this); loader.parseSvg(element.attributeNS(KoXmlNS::svg, "d"), true); - if (m_subpaths.size() > 0) { + if (d->subpaths.size() > 0) { QRectF viewBox = loadOdfViewbox(element); if (viewBox.isEmpty()) { // there should be a viewBox to transform the path data @@ -503,8 +528,8 @@ p2 = d->handles[EndHandle]; } - QPointF relativeBegin = m_subpaths.first()->first()->point(); - QPointF relativeEnd = m_subpaths.last()->last()->point(); + QPointF relativeBegin = d->subpaths.first()->first()->point(); + QPointF relativeEnd = d->subpaths.last()->last()->point(); QPointF diffRelative(relativeBegin - relativeEnd); QPointF diffAbsolute(p1 - p2); diff --git a/libs/flake/KoConnectionShapeFactory.cpp b/libs/flake/KoConnectionShapeFactory.cpp --- a/libs/flake/KoConnectionShapeFactory.cpp +++ b/libs/flake/KoConnectionShapeFactory.cpp @@ -31,6 +31,8 @@ #include #include +#include "kis_pointer_utils.h" + KoConnectionShapeFactory::KoConnectionShapeFactory() : KoShapeFactoryBase(KOCONNECTIONSHAPEID, i18n("Tie")) { @@ -44,7 +46,7 @@ KoShape* KoConnectionShapeFactory::createDefaultShape(KoDocumentResourceManager *) const { KoConnectionShape * shape = new KoConnectionShape(); - shape->setStroke(new KoShapeStroke()); + shape->setStroke(toQShared(new KoShapeStroke())); shape->setShapeId(KoPathShapeId); return shape; } diff --git a/libs/flake/KoConnectionShape_p.h b/libs/flake/KoConnectionShape_p.h --- a/libs/flake/KoConnectionShape_p.h +++ b/libs/flake/KoConnectionShape_p.h @@ -25,6 +25,7 @@ { public: explicit KoConnectionShapePrivate(KoConnectionShape *q); + explicit KoConnectionShapePrivate(const KoConnectionShapePrivate &rhs, KoConnectionShape *q); /// Returns escape direction of given handle QPointF escapeDirection(int handleId) const; diff --git a/libs/flake/KoDocumentResourceManager.h b/libs/flake/KoDocumentResourceManager.h --- a/libs/flake/KoDocumentResourceManager.h +++ b/libs/flake/KoDocumentResourceManager.h @@ -73,8 +73,6 @@ UndoStack, ///< The document-wide undo stack (KUndo2Stack) ImageCollection, ///< The KoImageCollection for the document OdfDocument, ///< The document this canvas shows (KoDocumentBase) - PasteOffset, ///< Application wide paste offset - PasteAtCursor, ///< Application wide paste at cursor setting 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 @@ -222,20 +220,6 @@ /// Returns the actual grab sensitivity int grabSensitivity() const; - /** - * Offset used for pasting shapes to a document. - */ - void setPasteOffset(qreal pasteOffset); - /// Returns the current paste offset - qreal pasteOffset() const; - - /** - * Enables/disables pasting shape at cursor position - */ - void enablePasteAtCursor(bool enable); - /// Returns current state of paste at cursor setting - bool pasteAtCursor() const; - KUndo2Stack *undoStack() const; void setUndoStack(KUndo2Stack *undoStack); diff --git a/libs/flake/KoDocumentResourceManager.cpp b/libs/flake/KoDocumentResourceManager.cpp --- a/libs/flake/KoDocumentResourceManager.cpp +++ b/libs/flake/KoDocumentResourceManager.cpp @@ -125,53 +125,32 @@ void KoDocumentResourceManager::setHandleRadius(int handleRadius) { // do not allow arbitrary small handles - if (handleRadius < 3) - handleRadius = 3; + if (handleRadius < 5) + handleRadius = 5; setResource(HandleRadius, QVariant(handleRadius)); } int KoDocumentResourceManager::handleRadius() const { if (hasResource(HandleRadius)) return intResource(HandleRadius); - return 3; // default value. + return 5; // default value (and is used just about everywhere) } void KoDocumentResourceManager::setGrabSensitivity(int grabSensitivity) { // do not allow arbitrary small grab sensitivity - if (grabSensitivity < 3) - grabSensitivity = 3; + if (grabSensitivity < 5) + grabSensitivity = 5; setResource(GrabSensitivity, QVariant(grabSensitivity)); } int KoDocumentResourceManager::grabSensitivity() const { if (hasResource(GrabSensitivity)) return intResource(GrabSensitivity); - return 3; // default value + return 5; // default value (and is used just about everywhere) } -void KoDocumentResourceManager::setPasteOffset(qreal pasteOffset) -{ - setResource(PasteOffset, QVariant(pasteOffset)); -} - -qreal KoDocumentResourceManager::pasteOffset() const -{ - return resource(PasteOffset).toDouble(); -} - -void KoDocumentResourceManager::enablePasteAtCursor(bool enable) -{ - setResource(PasteAtCursor, QVariant(enable)); -} - -bool KoDocumentResourceManager::pasteAtCursor() const -{ - return resource(PasteAtCursor).toBool(); -} - - void KoDocumentResourceManager::setUndoStack(KUndo2Stack *undoStack) { QVariant variant; diff --git a/libs/flake/KoDrag.h b/libs/flake/KoDrag.h --- a/libs/flake/KoDrag.h +++ b/libs/flake/KoDrag.h @@ -22,11 +22,14 @@ #include "kritaflake_export.h" +#include + class QMimeData; class QString; class QByteArray; class KoDragOdfSaveHelper; class KoDragPrivate; +class KoShape; /** * Class for simplifying adding a odf to the clip board @@ -42,15 +45,11 @@ KoDrag(); ~KoDrag(); + /** - * Set odf mime type - * - * This calls helper.writeBody(); - * - * @param mimeType used for creating the odf document - * @param helper helper for saving the body of the odf document + * Load SVG data into the current mime data */ - bool setOdf(const char *mimeType, KoDragOdfSaveHelper &helper); + bool setSvg(const QList shapes); /** * Add additional mimeTypes diff --git a/libs/flake/KoDrag.cpp b/libs/flake/KoDrag.cpp --- a/libs/flake/KoDrag.cpp +++ b/libs/flake/KoDrag.cpp @@ -38,6 +38,11 @@ #include #include "KoShapeSavingContext.h" +#include +#include +#include + + class KoDragPrivate { public: KoDragPrivate() : mimeData(0) { } @@ -55,73 +60,31 @@ delete d; } -bool KoDrag::setOdf(const char *mimeType, KoDragOdfSaveHelper &helper) +bool KoDrag::setSvg(const QList originalShapes) { - struct Finally { - Finally(KoStore *s) : store(s) { } - ~Finally() { - delete store; - } - KoStore *store; - }; - - QBuffer buffer; - KoStore *store = KoStore::createStore(&buffer, KoStore::Write, mimeType); - Finally finally(store); // delete store when we exit this scope - Q_ASSERT(store); - Q_ASSERT(!store->bad()); - - KoOdfWriteStore odfStore(store); - KoEmbeddedDocumentSaver embeddedSaver; - - KoXmlWriter *manifestWriter = odfStore.manifestWriter(mimeType); - KoXmlWriter *contentWriter = odfStore.contentWriter(); - - if (!contentWriter) { - return false; - } - - KoGenStyles mainStyles; - KoXmlWriter *bodyWriter = odfStore.bodyWriter(); - KoShapeSavingContext *context = helper.context(bodyWriter, mainStyles, embeddedSaver); + QRectF boundingRect; + QList shapes; - if (!helper.writeBody()) { - return false; + Q_FOREACH (KoShape *shape, originalShapes) { + boundingRect |= shape->boundingRect(); + shapes.append(shape->cloneShape()); } - mainStyles.saveOdfStyles(KoGenStyles::DocumentAutomaticStyles, contentWriter); + qSort(shapes.begin(), shapes.end(), KoShape::compareShapeZIndex); - odfStore.closeContentWriter(); - - //add manifest line for content.xml - manifestWriter->addManifestEntry("content.xml", "text/xml"); - - - if (!mainStyles.saveOdfStylesDotXml(store, manifestWriter)) { - return false; - } - - if (!context->saveDataCenter(store, manifestWriter)) { - debugFlake << "save data centers failed"; - return false; - } + QBuffer buffer; + QLatin1String mimeType("image/svg+xml"); - // Save embedded objects - KoDocumentBase::SavingContext documentContext(odfStore, embeddedSaver); - if (!embeddedSaver.saveEmbeddedDocuments(documentContext)) { - debugFlake << "save embedded documents failed"; - return false; - } + buffer.open(QIODevice::WriteOnly); - // Write out manifest file - if (!odfStore.closeManifestWriter()) { - return false; - } + const QSizeF pageSize(boundingRect.right(), boundingRect.bottom()); + SvgWriter writer(shapes, pageSize); + writer.save(buffer); - delete store; // make sure the buffer if fully flushed. - finally.store = 0; - setData(mimeType, buffer.buffer()); + buffer.close(); + qDeleteAll(shapes); + setData(mimeType, buffer.data()); return true; } diff --git a/libs/flake/KoFlake.h b/libs/flake/KoFlake.h --- a/libs/flake/KoFlake.h +++ b/libs/flake/KoFlake.h @@ -24,14 +24,38 @@ #include "kritaflake_export.h" class QGradient; +class QRectF; class QPointF; class QSizeF; +class KoShape; +class QTransform; + +#include + /** * Flake reference */ namespace KoFlake { + enum FillVariant { + Fill, + StrokeFill + }; + + enum FillType { + None, + Solid, + Gradient, + Pattern + }; + + enum MarkerPosition { + StartMarker, + MidMarker, + EndMarker + }; + /// the selection type for KoSelection::selectedObjects() enum SelectionType { FullSelection, ///< Create a list of all user-shapes in the selection. This excludes KoShapeGroup grouping objects that may be selected. @@ -62,26 +86,39 @@ ShapeOnTop ///< return the shape highest z-ordering, regardless of selection. }; - /// position. See KoShape::absolutePosition() - enum Position { - TopLeftCorner, ///< the top left corner - TopRightCorner, ///< the top right corner - BottomLeftCorner, ///< the bottom left corner - BottomRightCorner, ///< the bottom right corner - CenteredPosition ///< the centred corner - }; - /** * Used to see which style type is active */ enum StyleType { Background, ///< the background / fill style is active Foreground ///< the foreground / stroke style is active }; + enum AnchorPosition { + TopLeft, + Top, + TopRight, + Left, + Center, + Right, + BottomLeft, + Bottom, + BottomRight, + NoAnchor, + NumAnchorPositions + }; + + KRITAFLAKE_EXPORT QPointF anchorToPoint(AnchorPosition anchor, const QRectF rect, bool *valid = 0); + + enum CanvasResource { + HotPosition = 1410100299 + }; + /// clones the given gradient KRITAFLAKE_EXPORT QGradient *cloneGradient(const QGradient *gradient); + KRITAFLAKE_EXPORT QGradient *mergeGradient(const QGradient *coordsSource, const QGradient *fillSource); + /** * Convert absolute to relative position * @@ -101,6 +138,13 @@ * @return absolute position */ KRITAFLAKE_EXPORT QPointF toAbsolute(const QPointF &relative, const QSizeF &size); + + KRITAFLAKE_EXPORT Qt::Orientation significantScaleOrientation(qreal scaleX, qreal scaleY); + + KRITAFLAKE_EXPORT void resizeShape(KoShape *shape, qreal scaleX, qreal scaleY, + const QPointF &absoluteStillPoint, + bool useGlobalMode, + bool usePostScaling, const QTransform &postScalingCoveringTransform); } #endif diff --git a/libs/flake/KoFlake.cpp b/libs/flake/KoFlake.cpp --- a/libs/flake/KoFlake.cpp +++ b/libs/flake/KoFlake.cpp @@ -25,6 +25,7 @@ #include #include +#include "kis_global.h" QGradient *KoFlake::cloneGradient(const QGradient *gradient) { @@ -63,6 +64,67 @@ return clone; } +QGradient *KoFlake::mergeGradient(const QGradient *coordsSource, const QGradient *fillSource) +{ + QPointF start; + QPointF end; + QPointF focalPoint; + + switch (coordsSource->type()) { + case QGradient::LinearGradient: { + const QLinearGradient *lg = static_cast(coordsSource); + start = lg->start(); + focalPoint = start; + end = lg->finalStop(); + break; + } + case QGradient::RadialGradient: { + const QRadialGradient *rg = static_cast(coordsSource); + start = rg->center(); + end = start + QPointF(rg->radius(), 0); + focalPoint = rg->focalPoint(); + break; + } + case QGradient::ConicalGradient: { + const QConicalGradient *cg = static_cast(coordsSource); + + start = cg->center(); + focalPoint = start; + + QLineF l (start, start + QPointF(1.0, 0)); + l.setAngle(cg->angle()); + end = l.p2(); + break; + } + default: + return 0; + } + + QGradient *clone = 0; + + switch (fillSource->type()) { + case QGradient::LinearGradient: + clone = new QLinearGradient(start, end); + break; + case QGradient::RadialGradient: + clone = new QRadialGradient(start, kisDistance(start, end), focalPoint); + break; + case QGradient::ConicalGradient: { + QLineF l(start, end); + clone = new QConicalGradient(l.p1(), l.angle()); + break; + } + default: + return 0; + } + + clone->setCoordinateMode(fillSource->coordinateMode()); + clone->setSpread(fillSource->spread()); + clone->setStops(fillSource->stops()); + + return clone; +} + QPointF KoFlake::toRelative(const QPointF &absolute, const QSizeF &size) { return QPointF(size.width() == 0 ? 0: absolute.x() / size.width(), @@ -73,3 +135,202 @@ { return QPointF(relative.x() * size.width(), relative.y() * size.height()); } + +#include +#include +#include "kis_debug.h" +#include "kis_algebra_2d.h" + +namespace { + +qreal getScaleByPointsPair(qreal x1, qreal x2, qreal expX1, qreal expX2) +{ + static const qreal eps = 1e-10; + + const qreal diff = x2 - x1; + const qreal expDiff = expX2 - expX1; + + return qAbs(diff) > eps ? expDiff / diff : 1.0; +} + +void findMinMaxPoints(const QPolygonF &poly, int *minPoint, int *maxPoint, std::function dimension) +{ + KIS_ASSERT_RECOVER_RETURN(minPoint); + KIS_ASSERT_RECOVER_RETURN(maxPoint); + + qreal minValue = dimension(poly[*minPoint]); + qreal maxValue = dimension(poly[*maxPoint]); + + for (int i = 0; i < poly.size(); i++) { + const qreal value = dimension(poly[i]); + + if (value < minValue) { + *minPoint = i; + minValue = value; + } + + if (value > maxValue) { + *maxPoint = i; + maxValue = value; + } + } +} + +} + + +Qt::Orientation KoFlake::significantScaleOrientation(qreal scaleX, qreal scaleY) +{ + const qreal scaleXDeviation = qAbs(1.0 - scaleX); + const qreal scaleYDeviation = qAbs(1.0 - scaleY); + + return scaleXDeviation > scaleYDeviation ? Qt::Horizontal : Qt::Vertical; +} + +void KoFlake::resizeShape(KoShape *shape, qreal scaleX, qreal scaleY, + const QPointF &absoluteStillPoint, + bool useGlobalMode, + bool usePostScaling, const QTransform &postScalingCoveringTransform) +{ + QPointF localStillPoint = shape->absoluteTransformation(0).inverted().map(absoluteStillPoint); + + QPointF relativeStillPoint = KisAlgebra2D::absoluteToRelative(localStillPoint, shape->outlineRect()); + QPointF parentalStillPointBefore = shape->transformation().map(localStillPoint); + + if (usePostScaling) { + const QTransform scale = QTransform::fromScale(scaleX, scaleY); + + if (!useGlobalMode) { + shape->setTransformation(shape->transformation() * + postScalingCoveringTransform.inverted() * + scale * postScalingCoveringTransform); + } else { + const QTransform uniformGlobalTransform = + shape->absoluteTransformation(0) * + scale * + shape->absoluteTransformation(0).inverted() * + shape->transformation(); + + shape->setTransformation(uniformGlobalTransform); + } + } else { + using namespace KisAlgebra2D; + + if (useGlobalMode) { + const QTransform scale = QTransform::fromScale(scaleX, scaleY); + const QTransform uniformGlobalTransform = + shape->absoluteTransformation(0) * + scale * + shape->absoluteTransformation(0).inverted(); + + const QRectF rect = shape->outlineRect(); + + /** + * The basic idea of such global scaling: + * + * 1) We choose two the most distant points of the original outline rect + * 2) Calculate their expected position if transformed using `uniformGlobalTransform` + * 3) NOTE1: we do not transform the entire shape using `uniformGlobalTransform`, + * because it will cause massive shearing. We transform only two points + * and adjust other points using dumb scaling. + * 4) NOTE2: given that `scale` transform is much more simpler than + * `uniformGlobalTransform`, we cannot guarantee equivalent changes on + * both globalScaleX and globalScaleY at the same time. We can guarantee + * only one of them. Therefore we select the most "important" axis and + * guarantee scael along it. The scale along the other direction is not + * controlled. + * 5) After we have the two most distant points, we can just calculate the scale + * by dividing difference between their expected and original positions. This + * formula can be derived from equation: + * + * localPoint_i * ScaleMatrix = localPoint_i * UniformGlobalTransform = expectedPoint_i + */ + + // choose the most significant scale direction + Qt::Orientation significantOrientation = significantScaleOrientation(scaleX, scaleY); + + std::function dimension; + + if (significantOrientation == Qt::Horizontal) { + dimension = [] (const QPointF &pt) { + return pt.x(); + }; + + } else { + dimension = [] (const QPointF &pt) { + return pt.y(); + }; + } + + // find min and max points (in absolute coordinates), + // by default use top-left and bottom-right + QPolygonF localPoints(rect); + QPolygonF globalPoints = shape->absoluteTransformation(0).map(localPoints); + + int minPointIndex = 0; + int maxPointIndex = 2; + + findMinMaxPoints(globalPoints, &minPointIndex, &maxPointIndex, dimension); + + // calculate the scale using the extremum points + const QPointF minPoint = localPoints[minPointIndex]; + const QPointF maxPoint = localPoints[maxPointIndex]; + + const QPointF minPointExpected = uniformGlobalTransform.map(minPoint); + const QPointF maxPointExpected = uniformGlobalTransform.map(maxPoint); + + scaleX = getScaleByPointsPair(minPoint.x(), maxPoint.x(), + minPointExpected.x(), maxPointExpected.x()); + scaleY = getScaleByPointsPair(minPoint.y(), maxPoint.y(), + minPointExpected.y(), maxPointExpected.y()); + } + + const QSizeF oldSize(shape->size()); + const QSizeF newSize(oldSize.width() * qAbs(scaleX), oldSize.height() * qAbs(scaleY)); + + const QTransform mirrorTransform = QTransform::fromScale(signPZ(scaleX), signPZ(scaleY)); + + shape->setSize(newSize); + + if (!mirrorTransform.isIdentity()) { + shape->setTransformation(mirrorTransform * shape->transformation()); + } + + } + + QPointF newLocalStillPoint = KisAlgebra2D::relativeToAbsolute(relativeStillPoint, shape->outlineRect()); + QPointF parentalStillPointAfter = shape->transformation().map(newLocalStillPoint); + + QPointF diff = parentalStillPointBefore - parentalStillPointAfter; + shape->setTransformation(shape->transformation() * QTransform::fromTranslate(diff.x(), diff.y())); +} + +QPointF KoFlake::anchorToPoint(AnchorPosition anchor, const QRectF rect, bool *valid) +{ + static QVector anchorTable; + + if (anchorTable.isEmpty()) { + anchorTable << QPointF(0.0,0.0); + anchorTable << QPointF(0.5,0.0); + anchorTable << QPointF(1.0,0.0); + + anchorTable << QPointF(0.0,0.5); + anchorTable << QPointF(0.5,0.5); + anchorTable << QPointF(1.0,0.5); + + anchorTable << QPointF(0.0,1.0); + anchorTable << QPointF(0.5,1.0); + anchorTable << QPointF(1.0,1.0); + } + + if (anchor == NoAnchor) { + if (valid) { + *valid = false; + } + return rect.topLeft(); + } else if (valid) { + *valid = true; + } + + return KisAlgebra2D::relativeToAbsolute(anchorTable[int(anchor)], rect); +} diff --git a/libs/image/tests/kis_algebra_2d_test.h b/libs/flake/KoFlakeCoordinateSystem.h copy from libs/image/tests/kis_algebra_2d_test.h copy to libs/flake/KoFlakeCoordinateSystem.h --- a/libs/image/tests/kis_algebra_2d_test.h +++ b/libs/flake/KoFlakeCoordinateSystem.h @@ -16,21 +16,39 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_ALGEBRA_2D_TEST_H -#define __KIS_ALGEBRA_2D_TEST_H +#ifndef KOFLAKECOORDINATE_SYSTEM_H +#define KOFLAKECOORDINATE_SYSTEM_H -#include +#include -class KisAlgebra2DTest : public QObject -{ - Q_OBJECT -private Q_SLOTS: - void testHalfPlane(); - void testOuterCircle(); - - void testQuadraticEquation(); - void testIntersections(); - void testWeirdIntersections(); +namespace KoFlake { + +enum CoordinateSystem { + UserSpaceOnUse, + ObjectBoundingBox }; -#endif /* __KIS_ALGEBRA_2D_TEST_H */ +inline CoordinateSystem coordinatesFromString(const QString &value, CoordinateSystem defaultValue) +{ + CoordinateSystem result = defaultValue; + + if (value == "userSpaceOnUse") { + result = UserSpaceOnUse; + } else if (value == "objectBoundingBox") { + result = ObjectBoundingBox; + } + + return result; +} + +inline QString coordinateToString(CoordinateSystem value) +{ + return + value == ObjectBoundingBox? + "objectBoundingBox" : + "userSpaceOnUse"; +} +} + +#endif // KOFLAKECOORDINATE_SYSTEM_H + diff --git a/libs/image/tests/kis_algebra_2d_test.h b/libs/flake/KoFlakeTypes.h copy from libs/image/tests/kis_algebra_2d_test.h copy to libs/flake/KoFlakeTypes.h --- a/libs/image/tests/kis_algebra_2d_test.h +++ b/libs/flake/KoFlakeTypes.h @@ -16,21 +16,16 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_ALGEBRA_2D_TEST_H -#define __KIS_ALGEBRA_2D_TEST_H +#ifndef KOFLAKETYPES_H +#define KOFLAKETYPES_H -#include +class KoShapeStroke; +class KoShapeStrokeModel; -class KisAlgebra2DTest : public QObject -{ - Q_OBJECT -private Q_SLOTS: - void testHalfPlane(); - void testOuterCircle(); +template class QSharedPointer; - void testQuadraticEquation(); - void testIntersections(); - void testWeirdIntersections(); -}; +typedef QSharedPointer KoShapeStrokeModelSP; +typedef QSharedPointer KoShapeStrokeSP; + +#endif // KOFLAKETYPES_H -#endif /* __KIS_ALGEBRA_2D_TEST_H */ diff --git a/libs/flake/KoFlakeUtils.h b/libs/flake/KoFlakeUtils.h new file mode 100644 --- /dev/null +++ b/libs/flake/KoFlakeUtils.h @@ -0,0 +1,89 @@ +/* + * 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 KOFLAKEUTILS_H +#define KOFLAKEUTILS_H + +#include +#include +#include + +#include "kis_global.h" +#include "KoShapeStrokeCommand.h" + + +namespace KoFlake { + +template + auto modifyShapesStrokes(QList shapes, ModifyFunction modifyFunction) + -> decltype(modifyFunction(KoShapeStrokeSP()), (KUndo2Command*)(0)) + { + if (shapes.isEmpty()) return 0; + + QList newStrokes; + + Q_FOREACH(KoShape *shape, shapes) { + KoShapeStrokeSP shapeStroke = shape->stroke() ? + qSharedPointerDynamicCast(shape->stroke()) : + KoShapeStrokeSP(); + + KoShapeStrokeSP newStroke = + toQShared(shapeStroke ? + new KoShapeStroke(*shapeStroke) : + new KoShapeStroke()); + + modifyFunction(newStroke); + + newStrokes << newStroke; + } + + return new KoShapeStrokeCommand(shapes, newStrokes); +} + +template +bool compareShapePropertiesEqual(const QList shapes, const Policy &policy) +{ + if (shapes.size() == 1) return true; + + typename Policy::PointerType bg = + policy.getProperty(shapes.first()); + + Q_FOREACH (KoShape *shape, shapes) { + if ( + !( + (!bg && !policy.getProperty(shape)) || + (bg && policy.compareTo(bg, policy.getProperty(shape))) + )) { + + return false; + } + } + + return true; +} + +template +bool compareShapePropertiesEqual(const QList shapes) +{ + return compareShapePropertiesEqual(shapes, Policy()); +} + +} + +#endif // KOFLAKEUTILS_H + diff --git a/libs/flake/KoGradientBackground.h b/libs/flake/KoGradientBackground.h --- a/libs/flake/KoGradientBackground.h +++ b/libs/flake/KoGradientBackground.h @@ -47,6 +47,8 @@ /// Destroys the background virtual ~KoGradientBackground(); + bool compareTo(const KoShapeBackground *other) const override; + /// Sets the transform matrix void setTransform(const QTransform &matrix); diff --git a/libs/flake/KoGradientBackground.cpp b/libs/flake/KoGradientBackground.cpp --- a/libs/flake/KoGradientBackground.cpp +++ b/libs/flake/KoGradientBackground.cpp @@ -69,6 +69,16 @@ 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); @@ -101,10 +111,40 @@ { Q_D(const KoGradientBackground); if (!d->gradient) return; - QBrush brush(*d->gradient); - brush.setTransform(d->matrix); - painter.setBrush(brush); + 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! + * + * 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); } diff --git a/libs/flake/KoGradientHelper.h b/libs/flake/KoGradientHelper.h --- a/libs/flake/KoGradientHelper.h +++ b/libs/flake/KoGradientHelper.h @@ -34,6 +34,6 @@ /// Calculates color at given position from given gradient stops KRITAFLAKE_EXPORT QColor colorAt(qreal position, const QGradientStops &stops); -}; +} #endif diff --git a/libs/flake/KoImageData.h b/libs/flake/KoImageData.h --- a/libs/flake/KoImageData.h +++ b/libs/flake/KoImageData.h @@ -68,6 +68,8 @@ inline bool operator!=(const KoImageData &other) const { return !operator==(other); } bool operator==(const KoImageData &other) const; + KoShapeUserData* clone() const override; + void setImage(const QString &location, KoStore *store, KoImageCollection *collection = 0); /** diff --git a/libs/flake/KoImageData.cpp b/libs/flake/KoImageData.cpp --- a/libs/flake/KoImageData.cpp +++ b/libs/flake/KoImageData.cpp @@ -350,6 +350,11 @@ return *this; } +KoShapeUserData *KoImageData::clone() const +{ + return new KoImageData(*this); +} + qint64 KoImageData::key() const { return d->key; diff --git a/libs/flake/KoMarker.h b/libs/flake/KoMarker.h --- a/libs/flake/KoMarker.h +++ b/libs/flake/KoMarker.h @@ -24,52 +24,93 @@ #include #include "kritaflake_export.h" +#include class KoXmlElement; class KoShapeLoadingContext; class KoShapeSavingContext; class QString; class QPainterPath; +class KoShape; +class QPainter; +class KoShapeStroke; class KRITAFLAKE_EXPORT KoMarker : public QSharedData { public: KoMarker(); ~KoMarker(); /** - * Load the marker + * Display name of the marker * - * @param element The xml element containing the marker - * @param context The shape loading context + * @return Display name of the marker */ - bool loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context); + QString name() const; + + KoMarker(const KoMarker &rhs); + bool operator==(const KoMarker &other) const; + + enum MarkerCoordinateSystem { + StrokeWidth, + UserSpaceOnUse + }; + + void setCoordinateSystem(MarkerCoordinateSystem value); + MarkerCoordinateSystem coordinateSystem() const; + + static MarkerCoordinateSystem coordinateSystemFromString(const QString &value); + static QString coordinateSystemToString(MarkerCoordinateSystem value); + + void setReferencePoint(const QPointF &value); + QPointF referencePoint() const; + + void setReferenceSize(const QSizeF &size); + QSizeF referenceSize() const; + + bool hasAutoOtientation() const; + void setAutoOrientation(bool value); + + // measured in radians! + qreal explicitOrientation() const; + + // measured in radians! + void setExplicitOrientation(qreal value); + + void setShapes(const QList &shapes); + QList shapes() const; /** - * Save the marker - * - * @return The reference of the marker. + * @brief paintAtOrigin paints the marker at the position \p pos. + * Scales and rotates the masrker if needed. */ - QString saveOdf(KoShapeSavingContext &context) const; + void paintAtPosition(QPainter *painter, const QPointF &pos, qreal strokeWidth, qreal nodeAngle); /** - * Display name of the marker - * - * @return Display name of the marker + * Return maximum distance that the marker can take outside the shape itself */ - QString name() const; + qreal maxInset(qreal strokeWidth) const; /** - * Get the path of the marker - * - * It calculates the offset depending on the line width - * - * @param The width of the line the marker is attached to. - * @return the path of the marker + * Bounding rect of the marker in local coordinates. It is assumed that the marker + * is painted with the reference point placed at position (0,0) */ - QPainterPath path(qreal width) const; + QRectF boundingRect(qreal strokeWidth, qreal nodeAngle) const; - bool operator==(const KoMarker &other) const; + /** + * Outline of the marker in local coordinates. It is assumed that the marker + * is painted with the reference point placed at position (0,0) + */ + QPainterPath outline(qreal strokeWidth, qreal nodeAngle) const; + + /** + * Draws a preview of the marker in \p previewRect of \p painter + */ + void drawPreview(QPainter *painter, const QRectF &previewRect, + const QPen &pen, KoFlake::MarkerPosition position); + + + void applyShapeStroke(KoShape *shape, KoShapeStroke *stroke, const QPointF &pos, qreal strokeWidth, qreal nodeAngle); private: class Private; diff --git a/libs/flake/KoMarker.cpp b/libs/flake/KoMarker.cpp --- a/libs/flake/KoMarker.cpp +++ b/libs/flake/KoMarker.cpp @@ -28,21 +28,108 @@ #include "KoShapeLoadingContext.h" #include "KoShapeSavingContext.h" #include "KoOdfWorkaround.h" +#include "KoShapePainter.h" +#include "KoViewConverter.h" +#include +#include +#include + #include #include #include +#include + +#include "kis_global.h" +#include "kis_algebra_2d.h" + class Q_DECL_HIDDEN KoMarker::Private { public: Private() + : coordinateSystem(StrokeWidth), + referenceSize(3,3), + hasAutoOrientation(false), + explicitOrientation(0) {} + ~Private() { + qDeleteAll(shapes); + } + + bool operator==(const KoMarker::Private &other) const + { + // WARNING: comparison of shapes is extremely fuzzy! Don't + // trust it in life-critical cases! + + return name == other.name && + coordinateSystem == other.coordinateSystem && + referencePoint == other.referencePoint && + referenceSize == other.referenceSize && + hasAutoOrientation == other.hasAutoOrientation && + explicitOrientation == other.explicitOrientation && + compareShapesTo(other.shapes); + } + + Private(const Private &rhs) + : name(rhs.name), + coordinateSystem(rhs.coordinateSystem), + referencePoint(rhs.referencePoint), + referenceSize(rhs.referenceSize), + hasAutoOrientation(rhs.hasAutoOrientation), + explicitOrientation(rhs.explicitOrientation) + { + Q_FOREACH (KoShape *shape, rhs.shapes) { + shapes << shape->cloneShape(); + } + } + QString name; - QString d; - QPainterPath path; - QRect viewBox; + MarkerCoordinateSystem coordinateSystem; + QPointF referencePoint; + QSizeF referenceSize; + + bool hasAutoOrientation; + qreal explicitOrientation; + + QList shapes; + QScopedPointer shapePainter; + + bool compareShapesTo(const QList other) const { + if (shapes.size() != other.size()) return false; + + for (int i = 0; i < shapes.size(); i++) { + if (shapes[i]->outline() != other[i]->outline() || + shapes[i]->absoluteTransformation(0) != other[i]->absoluteTransformation(0)) { + + return false; + } + } + + return true; + } + + QTransform markerTransform(qreal strokeWidth, qreal nodeAngle, const QPointF &pos = QPointF()) { + const QTransform translate = QTransform::fromTranslate(-referencePoint.x(), -referencePoint.y()); + + QTransform t = translate; + + if (coordinateSystem == StrokeWidth) { + t *= QTransform::fromScale(strokeWidth, strokeWidth); + } + + const qreal angle = hasAutoOrientation ? nodeAngle : explicitOrientation; + if (angle != 0.0) { + QTransform r; + r.rotateRadians(angle); + t *= r; + } + + t *= QTransform::fromTranslate(pos.x(), pos.y()); + + return t; + } }; KoMarker::KoMarker() @@ -55,71 +142,276 @@ delete d; } -bool KoMarker::loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context) +QString KoMarker::name() const +{ + return d->name; +} + +KoMarker::KoMarker(const KoMarker &rhs) + : QSharedData(rhs), + d(new Private(*rhs.d)) +{ +} + +bool KoMarker::operator==(const KoMarker &other) const { - Q_UNUSED(context); - // A shape uses draw:marker-end="Arrow" draw:marker-end-width="0.686cm" draw:marker-end-center="true" which marker and how the marker is used + return *d == *other.d; +} - // - // +void KoMarker::setCoordinateSystem(KoMarker::MarkerCoordinateSystem value) +{ + d->coordinateSystem = value; +} - d->d =element.attributeNS(KoXmlNS::svg, "d"); - if (d->d.isEmpty()) { - return false; +KoMarker::MarkerCoordinateSystem KoMarker::coordinateSystem() const +{ + return d->coordinateSystem; +} + +KoMarker::MarkerCoordinateSystem KoMarker::coordinateSystemFromString(const QString &value) +{ + MarkerCoordinateSystem result = StrokeWidth; + + if (value == "userSpaceOnUse") { + result = UserSpaceOnUse; } -#ifndef NWORKAROUND_ODF_BUGS - KoOdfWorkaround::fixMarkerPath(d->d); -#endif + return result; +} + +QString KoMarker::coordinateSystemToString(KoMarker::MarkerCoordinateSystem value) +{ + return + value == StrokeWidth ? + "strokeWidth" : + "userSpaceOnUse"; +} + +void KoMarker::setReferencePoint(const QPointF &value) +{ + d->referencePoint = value; +} - KoPathShape pathShape; - KoPathShapeLoader loader(&pathShape); - loader.parseSvg(d->d, true); +QPointF KoMarker::referencePoint() const +{ + return d->referencePoint; +} - d->path = pathShape.outline(); - d->viewBox = KoPathShape::loadOdfViewbox(element); +void KoMarker::setReferenceSize(const QSizeF &size) +{ + d->referenceSize = size; +} - QString displayName(element.attributeNS(KoXmlNS::draw, "display-name")); - if (displayName.isEmpty()) { - displayName = element.attributeNS(KoXmlNS::draw, "name"); +QSizeF KoMarker::referenceSize() const +{ + return d->referenceSize; +} + +bool KoMarker::hasAutoOtientation() const +{ + return d->hasAutoOrientation; +} + +void KoMarker::setAutoOrientation(bool value) +{ + d->hasAutoOrientation = value; +} + +qreal KoMarker::explicitOrientation() const +{ + return d->explicitOrientation; +} + +void KoMarker::setExplicitOrientation(qreal value) +{ + d->explicitOrientation = value; +} + +void KoMarker::setShapes(const QList &shapes) +{ + d->shapes = shapes; + + if (d->shapePainter) { + d->shapePainter->setShapes(shapes); } - d->name = displayName; - return true; } -QString KoMarker::saveOdf(KoShapeSavingContext &context) const +QList KoMarker::shapes() const { - KoGenStyle style(KoGenStyle::MarkerStyle); - style.addAttribute("draw:display-name", d->name); - style.addAttribute("svg:d", d->d); - const QString viewBox = QString::fromLatin1("%1 %2 %3 %4") - .arg(d->viewBox.x()).arg(d->viewBox.y()) - .arg(d->viewBox.width()).arg(d->viewBox.height()); - style.addAttribute(QLatin1String("svg:viewBox"), viewBox); - QString name = QString(QUrl::toPercentEncoding(d->name, "", " ")).replace('%', '_'); - return context.mainStyles().insert(style, name, KoGenStyles::DontAddNumberToName); + return d->shapes; } -QString KoMarker::name() const +void KoMarker::paintAtPosition(QPainter *painter, const QPointF &pos, qreal strokeWidth, qreal nodeAngle) { - return d->name; + QTransform oldTransform = painter->transform(); + + KoViewConverter converter; + + if (!d->shapePainter) { + d->shapePainter.reset(new KoShapePainter()); + d->shapePainter->setShapes(d->shapes); + } + + painter->setTransform(d->markerTransform(strokeWidth, nodeAngle, pos), true); + d->shapePainter->paint(*painter, converter); + + painter->setTransform(oldTransform); } -QPainterPath KoMarker::path(qreal width) const +qreal KoMarker::maxInset(qreal strokeWidth) const { - if (!d->viewBox.isValid() || width == 0) { - return QPainterPath(); + QRectF shapesBounds = boundingRect(strokeWidth, 0.0); // normalized to 0,0 + qreal result = 0.0; + + result = qMax(KisAlgebra2D::norm(shapesBounds.topLeft()), result); + result = qMax(KisAlgebra2D::norm(shapesBounds.topRight()), result); + result = qMax(KisAlgebra2D::norm(shapesBounds.bottomLeft()), result); + result = qMax(KisAlgebra2D::norm(shapesBounds.bottomRight()), result); + + if (d->coordinateSystem == StrokeWidth) { + result *= strokeWidth; } - // TODO: currently the , properties of viewbox are ignored, why? OOo-compat? - qreal height = width * d->viewBox.height() / d->viewBox.width(); + return result; +} + +QRectF KoMarker::boundingRect(qreal strokeWidth, qreal nodeAngle) const +{ + QRectF shapesBounds = KoShape::boundingRect(d->shapes); + + const QTransform t = d->markerTransform(strokeWidth, nodeAngle); - QTransform transform; - transform.scale(width / d->viewBox.width(), height / d->viewBox.height()); - return transform.map(d->path); + if (!t.isIdentity()) { + shapesBounds = t.mapRect(shapesBounds); + } + + return shapesBounds; } -bool KoMarker::operator==(const KoMarker &other) const +QPainterPath KoMarker::outline(qreal strokeWidth, qreal nodeAngle) const { - return (d->d == other.d->d && d->viewBox ==other.d->viewBox); + QPainterPath outline; + Q_FOREACH (KoShape *shape, d->shapes) { + outline |= shape->absoluteTransformation(0).map(shape->outline()); + } + + const QTransform t = d->markerTransform(strokeWidth, nodeAngle); + + if (!t.isIdentity()) { + outline = t.map(outline); + } + + return outline; +} + +void KoMarker::drawPreview(QPainter *painter, const QRectF &previewRect, const QPen &pen, KoFlake::MarkerPosition position) +{ + const QRectF outlineRect = outline(pen.widthF(), 0).boundingRect(); // normalized to 0,0 + QPointF marker; + QPointF start; + QPointF end; + + if (position == KoFlake::StartMarker) { + marker = QPointF(-outlineRect.left() + previewRect.left(), previewRect.center().y()); + start = marker; + end = QPointF(previewRect.right(), start.y()); + } else if (position == KoFlake::MidMarker) { + start = QPointF(previewRect.left(), previewRect.center().y()); + marker = QPointF(-outlineRect.center().x() + previewRect.center().x(), start.y()); + end = QPointF(previewRect.right(), start.y()); + } else if (position == KoFlake::EndMarker) { + start = QPointF(previewRect.left(), previewRect.center().y()); + marker = QPointF(-outlineRect.right() + previewRect.right(), start.y()); + end = marker; + } + + painter->save(); + painter->setPen(pen); + painter->setClipRect(previewRect); + + painter->drawLine(start, end); + paintAtPosition(painter, marker, pen.widthF(), 0); + + painter->restore(); +} + +void KoMarker::applyShapeStroke(KoShape *parentShape, KoShapeStroke *stroke, const QPointF &pos, qreal strokeWidth, qreal nodeAngle) +{ + const QGradient *originalGradient = stroke->lineBrush().gradient(); + + if (!originalGradient) { + QList linearizedShapes = KoShape::linearizeSubtree(d->shapes); + Q_FOREACH(KoShape *shape, linearizedShapes) { + // update the stroke + KoShapeStrokeSP shapeStroke = shape->stroke() ? + qSharedPointerDynamicCast(shape->stroke()) : + KoShapeStrokeSP(); + + if (shapeStroke) { + shapeStroke = toQShared(new KoShapeStroke(*shapeStroke)); + + shapeStroke->setLineBrush(QBrush()); + shapeStroke->setColor(stroke->color()); + + shape->setStroke(shapeStroke); + } + + // update the background + if (shape->background()) { + QSharedPointer bg(new KoColorBackground(stroke->color())); + shape->setBackground(bg); + } + } + } else { + QScopedPointer g(KoFlake::cloneGradient(originalGradient)); + KIS_ASSERT_RECOVER_RETURN(g); + + const QTransform markerTransformInverted = + d->markerTransform(strokeWidth, nodeAngle, pos).inverted(); + + QTransform gradientToUser; + + // Unwrap the gradient to work in global mode + if (g->coordinateMode() == QGradient::ObjectBoundingMode) { + QRectF boundingRect = + parentShape ? + parentShape->outline().boundingRect() : + this->boundingRect(strokeWidth, nodeAngle); + + boundingRect = KisAlgebra2D::ensureRectNotSmaller(boundingRect, QSizeF(1.0, 1.0)); + + gradientToUser = QTransform(boundingRect.width(), 0, 0, boundingRect.height(), + boundingRect.x(), boundingRect.y()); + + g->setCoordinateMode(QGradient::LogicalMode); + } + + QList linearizedShapes = KoShape::linearizeSubtree(d->shapes); + Q_FOREACH(KoShape *shape, linearizedShapes) { + // shape-unwinding transform + QTransform t = gradientToUser * markerTransformInverted * shape->absoluteTransformation(0).inverted(); + + // update the stroke + KoShapeStrokeSP shapeStroke = shape->stroke() ? + qSharedPointerDynamicCast(shape->stroke()) : + KoShapeStrokeSP(); + + if (shapeStroke) { + shapeStroke = toQShared(new KoShapeStroke(*shapeStroke)); + + QBrush brush(*g); + brush.setTransform(t); + shapeStroke->setLineBrush(brush); + shapeStroke->setColor(Qt::transparent); + shape->setStroke(shapeStroke); + } + + // update the background + if (shape->background()) { + + QSharedPointer bg(new KoGradientBackground(KoFlake::cloneGradient(g.data()), t)); + shape->setBackground(bg); + } + } + } } diff --git a/libs/flake/KoMarkerCollection.h b/libs/flake/KoMarkerCollection.h --- a/libs/flake/KoMarkerCollection.h +++ b/libs/flake/KoMarkerCollection.h @@ -38,10 +38,6 @@ explicit KoMarkerCollection(QObject *parent = 0); virtual ~KoMarkerCollection(); - bool loadOdf(KoShapeLoadingContext &context); - // For now we only save the used markers and that is done with a KoSharedSavingData when a marker usage is encountered. - //void saveOdf(KoShapeSavingContext &context) const; - QList markers() const; /** @@ -57,10 +53,11 @@ */ KoMarker * addMarker(KoMarker *marker); + void loadMarkersFromFile(const QString &svgFile); + private: /// load the markers that are available per default. void loadDefaultMarkers(); - void loadOdfMarkers(const QHash &markers, KoShapeLoadingContext &context, QHash &lookupTable); class Private; Private * const d; diff --git a/libs/flake/KoMarkerCollection.cpp b/libs/flake/KoMarkerCollection.cpp --- a/libs/flake/KoMarkerCollection.cpp +++ b/libs/flake/KoMarkerCollection.cpp @@ -21,15 +21,22 @@ #include +#include #include "KoMarker.h" -#include "KoMarkerSharedLoadingData.h" #include -#include -#include -#include -#include -#include #include +#include +#include +#include +#include +#include + +#include "kis_debug.h" + +// WARNING: there is a bug in GCC! It doesn't warn that we are +// deleting an uninitialized type here! +#include + class Q_DECL_HIDDEN KoMarkerCollection::Private { @@ -56,66 +63,58 @@ delete d; } -bool KoMarkerCollection::loadOdf(KoShapeLoadingContext &context) +void KoMarkerCollection::loadMarkersFromFile(const QString &svgFile) { - debugFlake; - QHash lookupTable; + QFile file(svgFile); + if (!file.exists()) return; - const QHash markers = context.odfLoadingContext().stylesReader().drawStyles("marker"); - loadOdfMarkers(markers, context, lookupTable); + if (!file.open(QIODevice::ReadOnly)) return; - KoMarkerSharedLoadingData * sharedMarkerData = new KoMarkerSharedLoadingData(lookupTable); - context.addSharedData(MARKER_SHARED_LOADING_ID, sharedMarkerData); + QXmlStreamReader reader(&file); + reader.setNamespaceProcessing(false); - return true; -} + QString errorMsg; + int errorLine = 0; + int errorColumn; -void KoMarkerCollection::loadDefaultMarkers() -{ - // use the same mechanism for loading the markers that are available - // per default as when loading the normal markers. - KoOdfStylesReader markerReader; - KoOdfLoadingContext odfContext(markerReader, 0); - KoShapeLoadingContext shapeContext(odfContext, 0); KoXmlDocument doc; - QString filePath = QStandardPaths::locate(QStandardPaths::AppDataLocation, "styles/markers.xml"); + bool ok = doc.setContent(&reader, &errorMsg, &errorLine, &errorColumn); + if (!ok) { + errKrita << "Parsing error in " << svgFile << "! Aborting!" << endl + << " In line: " << errorLine << ", column: " << errorColumn << endl + << " Error message: " << errorMsg << endl; + errKrita << i18n("Parsing error in the main document at line %1, column %2\nError message: %3" + , errorLine , errorColumn , errorMsg); + return; + } - if (!filePath.isEmpty()) { - QFile file(filePath); - QString errorMessage; - if (KoOdfReadStore::loadAndParse(&file, doc, errorMessage, filePath)) { - markerReader.createStyleMap(doc, true); + KoDocumentResourceManager manager; + SvgParser parser(&manager); + parser.setResolution(QRectF(0,0,100,100), 72); // initialize with default values + parser.setXmlBaseDir(QFileInfo(svgFile).absolutePath()); - QHash lookupTable; - const QHash defaultMarkers = markerReader.drawStyles("marker"); - loadOdfMarkers(defaultMarkers, shapeContext, lookupTable); - } - else { - warnFlake << "reading of" << filePath << "failed:" << errorMessage; - } - } - else { - debugFlake << "markers.xml not found"; + parser.setFileFetcher( + [](const QString &fileName) { + QFile file(fileName); + if (!file.exists()) return QByteArray(); + + file.open(QIODevice::ReadOnly); + return file.readAll(); + }); + + QSizeF fragmentSize; + QList shapes = parser.parseSvg(doc.documentElement(), &fragmentSize); + qDeleteAll(shapes); + + Q_FOREACH (const QExplicitlySharedDataPointer &marker, parser.knownMarkers()) { + addMarker(marker.data()); } } -void KoMarkerCollection::loadOdfMarkers(const QHash &markers, KoShapeLoadingContext &context, QHash &lookupTable) +void KoMarkerCollection::loadDefaultMarkers() { - QHash::const_iterator it(markers.constBegin()); - for (; it != markers.constEnd(); ++it) { - KoMarker *marker = new KoMarker(); - if (marker->loadOdf(*(it.value()), context)) { - KoMarker *m = addMarker(marker); - lookupTable.insert(it.key(), m); - debugFlake << "loaded marker" << it.key() << marker << m; - if (m != marker) { - delete marker; - } - } - else { - delete marker; - } - } + QString filePath = KoResourcePaths::findResource("data", "styles/markers.svg"); + loadMarkersFromFile(filePath); } QList KoMarkerCollection::markers() const diff --git a/libs/flake/KoMarkerData.h b/libs/flake/KoMarkerData.h deleted file mode 100644 --- a/libs/flake/KoMarkerData.h +++ /dev/null @@ -1,136 +0,0 @@ -/* This file is part of the KDE project - Copyright (C) 2011 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 KOMARKERDATA_H -#define KOMARKERDATA_H - -#include - -#include "kritaflake_export.h" - -class KoGenStyle; -class KoMarker; -class KoShapeLoadingContext; -class KoShapeSavingContext; - -class KRITAFLAKE_EXPORT KoMarkerData -{ -public: - /// Property enum - enum MarkerPosition { - MarkerStart, ///< it is the marker where the Path starts - MarkerEnd ///< it is the marker where the Path ends - }; - - KoMarkerData(KoMarker *marker, qreal width, MarkerPosition position, bool center); - explicit KoMarkerData(MarkerPosition position); - KoMarkerData(const KoMarkerData &other); - ~KoMarkerData(); - - /** - * Get the marker - * - * @return the marker or 0 if no marker is set - */ - KoMarker *marker() const; - - /** - * Set the marker - * - * @param marker The marker that is set or 0 to remove the marker - */ - void setMarker(KoMarker *marker); - - /** - * Get the with of the marker according to the pen width - */ - qreal width(qreal penWidth) const; - - /** - * Set the width of the marker - * - * This calculates a base width for the marker so the width of the marker changes - * with the width of the line. - * - * @param width The width of the marker - * @param penWidth The width of the used pen - */ - void setWidth(qreal width, qreal penWidth); - - /** - * Get the position of the marker - * - * @return Position of the marker - */ - MarkerPosition position() const; - - /** - * Set the position of the marker - * - * @param position Position of the marker - */ - void setPosition(MarkerPosition position); - - /** - * Get the center property of the marker - * - * If the marker is centered at the start of the stroke the line will get longer. - * - * @return Returns true if the marker is centered at the start of the stroke. - */ - bool center() const; - - /** - * Set the center property of the marker - * - * @param center If set to true the marker should be centered at the start of the stroke. - */ - void setCenter(bool center); - - /** - * Compare the marker data - */ - KoMarkerData &operator=(const KoMarkerData &other); - - /** - * Load the marker data - * - * @param penWidth the used pen width of the line - * @param context The shape loading context - */ - bool loadOdf(qreal penWidth, KoShapeLoadingContext &context); - - /** - * Save the marker data to the style - * - * @param style The style that we add the marker data to - * @param penWidth the used pen width of the line - * @param context The shape saving context - */ - void saveStyle(KoGenStyle &style, qreal penWidth, KoShapeSavingContext &context) const; - -private: - // make private to be sure it is not used - KoMarkerData(); - - class Private; - Private * const d; -}; - -#endif /* KOMARKERDATA_H */ diff --git a/libs/flake/KoMarkerData.cpp b/libs/flake/KoMarkerData.cpp deleted file mode 100644 --- a/libs/flake/KoMarkerData.cpp +++ /dev/null @@ -1,173 +0,0 @@ -/* This file is part of the KDE project - Copyright (C) 2011 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. -*/ - -#include "KoMarkerData.h" - -#include -#include -#include -#include -#include -#include -#include "KoShapeLoadingContext.h" -#include "KoMarker.h" -#include "KoMarkerSharedLoadingData.h" - -/** - * This defines the factor the width of the arrow is widened - * when the width of the line is changed. - */ -static const qreal ResizeFactor = 1.5; - -static const struct { - const char * m_markerPositionLoad; - const char * m_markerWidthLoad; - const char * m_markerCenterLoad; - const char * m_markerPositionSave; - const char * m_markerWidthSave; - const char * m_markerCenterSave; -} markerOdfData[] = { - { "marker-start", "marker-start-width", "marker-start-center", "draw:marker-start", "draw:marker-start-width", "draw:marker-start-center" }, - { "marker-end" , "marker-end-width", "marker-end-center", "draw:marker-end" , "draw:marker-end-width", "draw:marker-end-center" } -}; - -class Q_DECL_HIDDEN KoMarkerData::Private -{ -public: - Private(KoMarker *marker, qreal baseWidth, KoMarkerData::MarkerPosition position, bool center) - : marker(marker) - , baseWidth(baseWidth) - , position(position) - , center(center) - {} - - QExplicitlySharedDataPointer marker; - qreal baseWidth; - MarkerPosition position; - bool center; -}; - -KoMarkerData::KoMarkerData(KoMarker *marker, qreal width, MarkerPosition position, bool center) -: d(new Private(marker, width, position, center)) -{ -} - -KoMarkerData::KoMarkerData(MarkerPosition position) -: d(new Private(0, 0, position, false)) -{ -} - -KoMarkerData::KoMarkerData() -: d(0) -{ - Q_ASSERT(0); -} - -KoMarkerData::KoMarkerData(const KoMarkerData &other) -: d(new Private(other.d->marker.data(), other.d->baseWidth, other.d->position, other.d->center)) -{ -} - -KoMarkerData::~KoMarkerData() -{ - delete d; -} - - -KoMarker *KoMarkerData::marker() const -{ - return d->marker.data(); -} - -void KoMarkerData::setMarker(KoMarker *marker) -{ - d->marker = QExplicitlySharedDataPointer(marker); -} - -qreal KoMarkerData::width(qreal penWidth) const -{ - return d->baseWidth + penWidth * ResizeFactor; -} - -void KoMarkerData::setWidth(qreal width, qreal penWidth) -{ - d->baseWidth = qMax(qreal(0.0), width - penWidth * ResizeFactor); -} - -KoMarkerData::MarkerPosition KoMarkerData::position() const -{ - return d->position; -} - -void KoMarkerData::setPosition(MarkerPosition position) -{ - d->position = position; -} - -bool KoMarkerData::center() const -{ - return d->center; -} - -void KoMarkerData::setCenter(bool center) -{ - d->center = center; -} - -KoMarkerData &KoMarkerData::operator=(const KoMarkerData &other) -{ - if (this != &other) { - d->marker = other.d->marker; - d->baseWidth = other.d->baseWidth; - d->position = other.d->position; - d->center = other.d->center; - } - return (*this); -} - -bool KoMarkerData::loadOdf(qreal penWidth, KoShapeLoadingContext &context) -{ - KoMarkerSharedLoadingData *markerShared = dynamic_cast(context.sharedData(MARKER_SHARED_LOADING_ID)); - if (markerShared) { - KoStyleStack &styleStack = context.odfLoadingContext().styleStack(); - // draw:marker-end="Arrow" draw:marker-end-width="0.686cm" draw:marker-end-center="true" - const QString markerStart(styleStack.property(KoXmlNS::draw, markerOdfData[d->position].m_markerPositionLoad)); - const QString markerStartWidth(styleStack.property(KoXmlNS::draw, markerOdfData[d->position].m_markerWidthLoad)); - if (!markerStart.isEmpty() && !markerStartWidth.isEmpty()) { - KoMarker *marker = markerShared->marker(markerStart); - if (marker) { - setMarker(marker); - qreal markerWidth = KoUnit::parseValue(markerStartWidth); - setWidth(markerWidth, penWidth); - setCenter(styleStack.property(KoXmlNS::draw, markerOdfData[d->position].m_markerCenterLoad) == "true"); - } - } - } - return true; -} - -void KoMarkerData::saveStyle(KoGenStyle &style, qreal penWidth, KoShapeSavingContext &context) const -{ - if (d->marker) { - QString markerRef = d->marker->saveOdf(context); - style.addProperty(markerOdfData[d->position].m_markerPositionSave, markerRef, KoGenStyle::GraphicType); - style.addPropertyPt(markerOdfData[d->position].m_markerWidthSave, width(penWidth), KoGenStyle::GraphicType); - style.addProperty(markerOdfData[d->position].m_markerCenterSave, d->center, KoGenStyle::GraphicType); - } -} diff --git a/libs/flake/KoMarkerSharedLoadingData.h b/libs/flake/KoMarkerSharedLoadingData.h deleted file mode 100644 --- a/libs/flake/KoMarkerSharedLoadingData.h +++ /dev/null @@ -1,45 +0,0 @@ -/* This file is part of the KDE project - Copyright (C) 2011 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 KOMARKERSHAREDLOADINGDATA_H -#define KOMARKERSHAREDLOADINGDATA_H - -#include "KoSharedLoadingData.h" - -#include - -#define MARKER_SHARED_LOADING_ID "KoMarkerShareadLoadingId" - -class KoMarker; -class QString; - -class KoMarkerSharedLoadingData : public KoSharedLoadingData -{ -public: - KoMarkerSharedLoadingData(const QHash &lookupTable); - virtual ~KoMarkerSharedLoadingData(); - - KoMarker *marker(const QString &name) const; - -private: - class Private; - Private * const d; -}; - -#endif /* KOMARKERSHAREDLOADINGDATA_H */ diff --git a/libs/flake/KoMarkerSharedLoadingData.cpp b/libs/flake/KoMarkerSharedLoadingData.cpp deleted file mode 100644 --- a/libs/flake/KoMarkerSharedLoadingData.cpp +++ /dev/null @@ -1,44 +0,0 @@ -/* This file is part of the KDE project - Copyright (C) 2011 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. -*/ - -#include "KoMarkerSharedLoadingData.h" - -#include - -class KoMarkerSharedLoadingData::Private -{ -public: - QHash lookupTable; -}; - -KoMarkerSharedLoadingData::KoMarkerSharedLoadingData(const QHash &lookupTable) -: d(new Private()) -{ - d->lookupTable = lookupTable; -} - -KoMarkerSharedLoadingData::~KoMarkerSharedLoadingData() -{ - delete d; -} - -KoMarker *KoMarkerSharedLoadingData::marker(const QString &name) const -{ - return d->lookupTable.value(name, 0); -} diff --git a/libs/flake/KoOdfGradientBackground.h b/libs/flake/KoOdfGradientBackground.h --- a/libs/flake/KoOdfGradientBackground.h +++ b/libs/flake/KoOdfGradientBackground.h @@ -39,6 +39,8 @@ // destructor virtual ~KoOdfGradientBackground(); + bool compareTo(const KoShapeBackground *other) const override; + /// reimplemented from KoShapeBackground virtual void fillStyle(KoGenStyle& style, KoShapeSavingContext& context); /// reimplemented from KoShapeBackground diff --git a/libs/flake/KoOdfGradientBackground.cpp b/libs/flake/KoOdfGradientBackground.cpp --- a/libs/flake/KoOdfGradientBackground.cpp +++ b/libs/flake/KoOdfGradientBackground.cpp @@ -65,6 +65,12 @@ } +bool KoOdfGradientBackground::compareTo(const KoShapeBackground *other) const +{ + Q_UNUSED(other); + return false; +} + bool KoOdfGradientBackground::loadOdf(const KoXmlElement& e) { diff --git a/libs/flake/KoParameterShape.h b/libs/flake/KoParameterShape.h --- a/libs/flake/KoParameterShape.h +++ b/libs/flake/KoParameterShape.h @@ -25,6 +25,7 @@ #include "kritaflake_export.h" class KoParameterShapePrivate; +class KisHandlePainterHelper; /** * KoParameterShape is the base class for all parametric shapes @@ -83,7 +84,7 @@ * @param converter the view converter for applying the actual zoom * @param handleRadius the radius of the handles used for painting */ - void paintHandles(QPainter &painter, const KoViewConverter &converter, int handleRadius); + void paintHandles(KisHandlePainterHelper &handlesHelper); /** * @brief Paint the given handles @@ -93,7 +94,7 @@ * @param handleId of the handle which should be repainted * @param handleRadius the radius of the handle used for painting */ - void paintHandle(QPainter &painter, const KoViewConverter &converter, int handleId, int handleRadius); + void paintHandle(KisHandlePainterHelper &handlesHelper, int handleId); /// reimplemented from KoShape virtual void setSize(const QSizeF &size); @@ -137,7 +138,7 @@ void setHandles(const QList &handles); /// constructor - KoParameterShape(KoParameterShapePrivate &); + KoParameterShape(KoParameterShapePrivate *); /** * @brief Updates the internal state of a KoParameterShape. @@ -157,7 +158,7 @@ */ virtual void updatePath(const QSizeF &size) = 0; -private: +protected: Q_DECLARE_PRIVATE(KoParameterShape) }; diff --git a/libs/flake/KoParameterShape.cpp b/libs/flake/KoParameterShape.cpp --- a/libs/flake/KoParameterShape.cpp +++ b/libs/flake/KoParameterShape.cpp @@ -21,15 +21,29 @@ #include "KoParameterShape.h" #include "KoParameterShape_p.h" +#include + #include #include +KoParameterShapePrivate::KoParameterShapePrivate(KoParameterShape *shape) + : KoPathShapePrivate(shape), + parametric(true) +{ +} + +KoParameterShapePrivate::KoParameterShapePrivate(const KoParameterShapePrivate &rhs, KoParameterShape *q) + : KoPathShapePrivate(rhs, q), + handles(rhs.handles) +{ +} + KoParameterShape::KoParameterShape() - : KoPathShape(*(new KoParameterShapePrivate(this))) + : KoPathShape(new KoParameterShapePrivate(this)) { } -KoParameterShape::KoParameterShape(KoParameterShapePrivate &dd) +KoParameterShape::KoParameterShape(KoParameterShapePrivate *dd) : KoPathShape(dd) { } @@ -52,7 +66,6 @@ updatePath(size()); update(); - d->shapeChanged(ParameterChanged); } @@ -76,42 +89,20 @@ return d->handles.value(handleId); } -void KoParameterShape::paintHandles(QPainter & painter, const KoViewConverter & converter, int handleRadius) +void KoParameterShape::paintHandles(KisHandlePainterHelper &handlesHelper) { Q_D(KoParameterShape); - applyConversion(painter, converter); - - QTransform worldMatrix = painter.worldTransform(); - painter.setTransform(QTransform()); - - QTransform matrix; - matrix.rotate(45.0); - QPolygonF poly(d->handleRect(QPointF(0, 0), handleRadius)); - poly = matrix.map(poly); QList::const_iterator it(d->handles.constBegin()); for (; it != d->handles.constEnd(); ++it) { - QPointF moveVector = worldMatrix.map(*it); - poly.translate(moveVector.x(), moveVector.y()); - painter.drawPolygon(poly); - poly.translate(-moveVector.x(), -moveVector.y()); + handlesHelper.drawGradientHandle(*it); } } -void KoParameterShape::paintHandle(QPainter & painter, const KoViewConverter & converter, int handleId, int handleRadius) +void KoParameterShape::paintHandle(KisHandlePainterHelper &handlesHelper, int handleId) { Q_D(KoParameterShape); - applyConversion(painter, converter); - - QTransform worldMatrix = painter.worldTransform(); - painter.setTransform(QTransform()); - - QTransform matrix; - matrix.rotate(45.0); - QPolygonF poly(d->handleRect(QPointF(0, 0), handleRadius)); - poly = matrix.map(poly); - poly.translate(worldMatrix.map(d->handles[handleId])); - painter.drawPolygon(poly); + handlesHelper.drawGradientHandle(d->handles[handleId]); } void KoParameterShape::setSize(const QSizeF &newSize) @@ -163,6 +154,8 @@ { Q_D(KoParameterShape); d->handles = handles; + + d->shapeChanged(ParameterChanged); } int KoParameterShape::handleCount() const diff --git a/libs/flake/KoParameterShape_p.h b/libs/flake/KoParameterShape_p.h --- a/libs/flake/KoParameterShape_p.h +++ b/libs/flake/KoParameterShape_p.h @@ -21,19 +21,20 @@ #ifndef KOPARAMETERSHAPE_P_H #define KOPARAMETERSHAPE_P_H +#include "kritaflake_export.h" +#include #include "KoPathShape_p.h" #include #include -class KoParameterShapePrivate : public KoPathShapePrivate +class KoParameterShape; + +class KRITAFLAKE_EXPORT KoParameterShapePrivate : public KoPathShapePrivate { public: - explicit KoParameterShapePrivate(KoParameterShape *shape) - : KoPathShapePrivate(shape), - parametric(true) - { - } + explicit KoParameterShapePrivate(KoParameterShape *shape); + explicit KoParameterShapePrivate(const KoParameterShapePrivate &rhs, KoParameterShape *q); bool parametric; diff --git a/libs/flake/KoPathPoint.h b/libs/flake/KoPathPoint.h --- a/libs/flake/KoPathPoint.h +++ b/libs/flake/KoPathPoint.h @@ -31,6 +31,7 @@ class QTransform; class QRectF; class QPainter; +class KisHandlePainterHelper; /** * @brief A KoPathPoint represents a point in a path. @@ -80,6 +81,7 @@ * @brief Copy Constructor */ KoPathPoint(const KoPathPoint &pathPoint); + KoPathPoint(const KoPathPoint &pathPoint, KoPathShape *newParent); /** * @brief Assignment operator. @@ -212,7 +214,7 @@ * @param active If true only the given active points are painted * If false all given points are used. */ - void paint(QPainter &painter, int handleRadius, PointTypes types, bool active = true); + void paint(KisHandlePainterHelper &handlesHelper, PointTypes types, bool active = true); /** * @brief Sets the parent path shape. diff --git a/libs/flake/KoPathPoint.cpp b/libs/flake/KoPathPoint.cpp --- a/libs/flake/KoPathPoint.cpp +++ b/libs/flake/KoPathPoint.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include @@ -51,15 +52,21 @@ KoPathPoint::KoPathPoint(const KoPathPoint &pathPoint) : d(new Private()) { - d->shape = pathPoint.d->shape; + d->shape = 0; d->point = pathPoint.d->point; d->controlPoint1 = pathPoint.d->controlPoint1; d->controlPoint2 = pathPoint.d->controlPoint2; d->properties = pathPoint.d->properties; d->activeControlPoint1 = pathPoint.d->activeControlPoint1; d->activeControlPoint2 = pathPoint.d->activeControlPoint2; } +KoPathPoint::KoPathPoint(const KoPathPoint &pathPoint, KoPathShape *newParent) + : KoPathPoint(pathPoint) +{ + d->shape = newParent; +} + KoPathPoint::KoPathPoint() : d(new Private()) { @@ -255,49 +262,40 @@ d->shape->notifyChanged(); } -void KoPathPoint::paint(QPainter &painter, int handleRadius, PointTypes types, bool active) +void KoPathPoint::paint(KisHandlePainterHelper &handlesHelper, PointTypes types, bool active) { - QRectF handle(-handleRadius, -handleRadius, 2*handleRadius, 2*handleRadius); - bool drawControlPoint1 = types & ControlPoint1 && (!active || activeControlPoint1()); bool drawControlPoint2 = types & ControlPoint2 && (!active || activeControlPoint2()); // draw lines at the bottom - if (drawControlPoint2) - painter.drawLine(point(), controlPoint2()); - - if (drawControlPoint1) - painter.drawLine(point(), controlPoint1()); - - - QTransform worldMatrix = painter.worldTransform(); + if (drawControlPoint2) { + handlesHelper.drawConnectionLine(point(), controlPoint2()); + } - painter.setWorldTransform(QTransform()); + if (drawControlPoint1) { + handlesHelper.drawConnectionLine(point(), controlPoint1()); + } // the point is lowest if (types & Node) { - if (properties() & IsSmooth) - painter.drawRect(handle.translated(worldMatrix.map(point()))); - else if (properties() & IsSymmetric) { - QTransform matrix; - matrix.rotate(45.0); - QPolygonF poly(handle); - poly = matrix.map(poly); - poly.translate(worldMatrix.map(point())); - painter.drawPolygon(poly); - } else - painter.drawEllipse(handle.translated(worldMatrix.map(point()))); + if (properties() & IsSmooth) { + handlesHelper.drawHandleRect(point()); + } else if (properties() & IsSymmetric) { + handlesHelper.drawGradientHandle(point()); + } else { + handlesHelper.drawHandleCircle(point()); + } } // then comes control point 2 - if (drawControlPoint2) - painter.drawEllipse(handle.translated(worldMatrix.map(controlPoint2()))); + if (drawControlPoint2) { + handlesHelper.drawHandleSmallCircle(controlPoint2()); + } // then comes control point 1 - if (drawControlPoint1) - painter.drawEllipse(handle.translated(worldMatrix.map(controlPoint1()))); - - painter.setWorldTransform(worldMatrix); + if (drawControlPoint1) { + handlesHelper.drawHandleSmallCircle(controlPoint1()); + } } void KoPathPoint::setParent(KoPathShape* parent) diff --git a/libs/flake/KoPathPointData.h b/libs/flake/KoPathPointData.h --- a/libs/flake/KoPathPointData.h +++ b/libs/flake/KoPathPointData.h @@ -22,11 +22,12 @@ #define KOPATHPOINTDATA_H #include "KoPathShape.h" +#include /** * @brief Describe a KoPathPoint by a KoPathShape and its indices */ -class KoPathPointData +class KoPathPointData : public boost::equality_comparable { public: /// contructor diff --git a/libs/flake/KoPathShape.h b/libs/flake/KoPathShape.h --- a/libs/flake/KoPathShape.h +++ b/libs/flake/KoPathShape.h @@ -29,14 +29,14 @@ #include #include "KoTosContainer.h" -#include "KoMarkerData.h" #define KoPathShapeId "KoPathShape" class KoPathSegment; class KoPathPoint; class KoPathShapePrivate; class KoMarker; +class KisHandlePainterHelper; typedef QPair KoPathPointIndex; @@ -85,15 +85,20 @@ */ virtual ~KoPathShape(); + KoShape *cloneShape() const; + /// reimplemented virtual void paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext); - virtual void paintPoints(QPainter &painter, const KoViewConverter &converter, int handleRadius); + virtual void paintPoints(KisHandlePainterHelper &handlesHelper); + + /// reimplemented + QRectF outlineRect() const override; /// reimplemented - virtual QPainterPath outline() const; + QPainterPath outline() const override; /// reimplemented - virtual QRectF boundingRect() const; + QRectF boundingRect() const override; /// reimplemented - virtual QSizeF size() const; + QSizeF size() const override; QPainterPath pathStroke(const QPen &pen) const; /** @@ -409,9 +414,9 @@ /** * @brief Combines two path shapes by appending the data of the specified path. * @param path the path to combine with - * @return true if combining was successful, else false + * @return index of the first segment inserted or -1 on failure */ - bool combine(KoPathShape *path); + int combine(KoPathShape *path); /** * @brief Creates separate path shapes, one for each existing subpath. @@ -450,20 +455,22 @@ /// Returns the viewbox from the given xml element. static QRect loadOdfViewbox(const KoXmlElement &element); - /// Marker setter - void setMarker(const KoMarkerData &markerData); + void setMarker(KoMarker *marker, KoFlake::MarkerPosition pos); + KoMarker* marker(KoFlake::MarkerPosition pos) const; + bool hasMarkers() const; - /// Marker setter - void setMarker(KoMarker *marker, KoMarkerData::MarkerPosition position); + bool autoFillMarkers() const; + void setAutoFillMarkers(bool value); - /// returns the list of all the markers of the path - KoMarker *marker(KoMarkerData::MarkerPosition position) const; - - KoMarkerData markerData(KoMarkerData::MarkerPosition position) const; +private: + /// constructor: to be used in cloneShape(), not in descendants! + /// \internal + KoPathShape(const KoPathShape &rhs); protected: - /// constructor \internal - KoPathShape(KoPathShapePrivate &); + /// constructor:to be used in descendant shapes + /// \internal + KoPathShape(KoPathShapePrivate *); /// reimplemented virtual QString saveStyle(KoGenStyle &style, KoShapeSavingContext &context) const; @@ -494,8 +501,6 @@ */ QTransform resizeMatrix( const QSizeF &newSize ) const; - KoSubpathList m_subpaths; - private: Q_DECLARE_PRIVATE(KoPathShape) }; diff --git a/libs/flake/KoPathShape.cpp b/libs/flake/KoPathShape.cpp --- a/libs/flake/KoPathShape.cpp +++ b/libs/flake/KoPathShape.cpp @@ -36,7 +36,6 @@ #include "KoShapeContainer.h" #include "KoFilterEffectStack.h" #include "KoMarker.h" -#include "KoMarkerSharedLoadingData.h" #include "KoShapeStroke.h" #include "KoInsets.h" @@ -51,6 +50,8 @@ #include #include +#include "kis_global.h" + #include // for qIsNaN static bool qIsNaNPoint(const QPointF &p) { return qIsNaN(p.x()) || qIsNaN(p.y()); @@ -60,11 +61,27 @@ KoPathShapePrivate::KoPathShapePrivate(KoPathShape *q) : KoTosContainerPrivate(q), fillRule(Qt::OddEvenFill), - startMarker(KoMarkerData::MarkerStart), - endMarker(KoMarkerData::MarkerEnd) + 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); @@ -97,29 +114,39 @@ } KoPathShape::KoPathShape() - :KoTosContainer(*(new KoPathShapePrivate(this))) + :KoTosContainer(new KoPathShapePrivate(this)) { } -KoPathShape::KoPathShape(KoPathShapePrivate &dd) +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 (m_subpaths.length() <= 1) { + if (d->subpaths.length() <= 1) { QTransform matrix; matrix.scale(scaleFactor.width(), scaleFactor.height()); QString points; - KoSubpath *subPath = m_subpaths.first(); + KoSubpath *subPath = d->subpaths.first(); KoSubpath::const_iterator pointIt(subPath->constBegin()); KoPathPoint *currPoint= 0; @@ -303,13 +330,13 @@ style.addProperty("svg:fill-rule", d->fillRule == Qt::OddEvenFill ? "evenodd" : "nonzero"); - KoShapeStroke *lineBorder = dynamic_cast(stroke()); + QSharedPointer lineBorder = qSharedPointerDynamicCast(stroke()); qreal lineWidth = 0; if (lineBorder) { lineWidth = lineBorder->lineWidth(); } - d->startMarker.saveStyle(style, lineWidth, context); - d->endMarker.saveStyle(style, lineWidth, context); + + Q_UNUSED(lineWidth) return KoTosContainer::saveStyle(style, context); } @@ -332,14 +359,13 @@ #endif } - KoShapeStroke *lineBorder = dynamic_cast(stroke()); + QSharedPointer lineBorder = qSharedPointerDynamicCast(stroke()); qreal lineWidth = 0; if (lineBorder) { lineWidth = lineBorder->lineWidth(); } - d->startMarker.loadOdf(lineWidth, context); - d->endMarker.loadOdf(lineWidth, context); + Q_UNUSED(lineWidth); } QRect KoPathShape::loadOdfViewbox(const KoXmlElement & element) @@ -361,12 +387,14 @@ void KoPathShape::clear() { - Q_FOREACH (KoSubpath *subpath, m_subpaths) { + Q_D(KoPathShape); + + Q_FOREACH (KoSubpath *subpath, d->subpaths) { Q_FOREACH (KoPathPoint *point, *subpath) delete point; delete subpath; } - m_subpaths.clear(); + d->subpaths.clear(); } void KoPathShape::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) @@ -387,13 +415,13 @@ void KoPathShapePrivate::paintDebug(QPainter &painter) { Q_Q(KoPathShape); - KoSubpathList::const_iterator pathIt(q->m_subpaths.constBegin()); + KoSubpathList::const_iterator pathIt(subpaths.constBegin()); int i = 0; QPen pen(Qt::black, 0); painter.save(); painter.setPen(pen); - for (; pathIt != q->m_subpaths.constEnd(); ++pathIt) { + for (; pathIt != subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) { ++i; @@ -422,33 +450,40 @@ void KoPathShapePrivate::debugPath() const { Q_Q(const KoPathShape); - KoSubpathList::const_iterator pathIt(q->m_subpaths.constBegin()); - for (; pathIt != q->m_subpaths.constEnd(); ++pathIt) { + 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(QPainter &painter, const KoViewConverter &converter, int handleRadius) +void KoPathShape::paintPoints(KisHandlePainterHelper &handlesHelper) { - applyConversion(painter, converter); + Q_D(KoPathShape); - KoSubpathList::const_iterator pathIt(m_subpaths.constBegin()); + KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); - for (; pathIt != m_subpaths.constEnd(); ++pathIt) { + for (; pathIt != d->subpaths.constEnd(); ++pathIt) { KoSubpath::const_iterator it((*pathIt)->constBegin()); for (; it != (*pathIt)->constEnd(); ++it) - (*it)->paint(painter, handleRadius, KoPathPoint::Node); + (*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, m_subpaths) { + Q_FOREACH (KoSubpath * subpath, d->subpaths) { KoPathPoint * lastPoint = subpath->first(); bool activeCP = false; Q_FOREACH (KoPathPoint * currPoint, *subpath) { @@ -513,7 +548,7 @@ QTransform transform = absoluteTransformation(0); // calculate the bounding rect of the transformed outline QRectF bb; - KoShapeStroke *lineBorder = dynamic_cast(stroke()); + const QSharedPointer lineBorder = qSharedPointerDynamicCast(stroke()); QPen pen; if (lineBorder) { pen.setWidthF(lineBorder->lineWidth()); @@ -533,6 +568,10 @@ qreal top = qMin(tl.y(),br.y()); qreal bottom = qMax(tl.y(),br.y()); bb.adjust(left, top, right, bottom); + + // TODO: take care about transformations! + // take care about markers! + bb = kisGrowRect(bb, stroke()->strokeMaxMarkersInset(this)); } if (shadow()) { KoInsets insets; @@ -550,7 +589,7 @@ { // don't call boundingRect here as it uses absoluteTransformation // which itself uses size() -> leads to infinite reccursion - return outline().boundingRect().size(); + return outlineRect().size(); } void KoPathShape::setSize(const QSizeF &newSize) @@ -585,65 +624,69 @@ 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); - m_subpaths.push_back(path); + d->subpaths.push_back(path); return point; } KoPathPoint * KoPathShape::lineTo(const QPointF &p) { Q_D(KoPathShape); - if (m_subpaths.empty()) { + if (d->subpaths.empty()) { moveTo(QPointF(0, 0)); } KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); - KoPathPoint * lastPoint = m_subpaths.last()->last(); + KoPathPoint * lastPoint = d->subpaths.last()->last(); d->updateLast(&lastPoint); - m_subpaths.last()->push_back(point); + d->subpaths.last()->push_back(point); return point; } KoPathPoint * KoPathShape::curveTo(const QPointF &c1, const QPointF &c2, const QPointF &p) { Q_D(KoPathShape); - if (m_subpaths.empty()) { + if (d->subpaths.empty()) { moveTo(QPointF(0, 0)); } - KoPathPoint * lastPoint = m_subpaths.last()->last(); + KoPathPoint * lastPoint = d->subpaths.last()->last(); d->updateLast(&lastPoint); lastPoint->setControlPoint2(c1); KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); point->setControlPoint1(c2); - m_subpaths.last()->push_back(point); + d->subpaths.last()->push_back(point); return point; } KoPathPoint * KoPathShape::curveTo(const QPointF &c, const QPointF &p) { Q_D(KoPathShape); - if (m_subpaths.empty()) + if (d->subpaths.empty()) moveTo(QPointF(0, 0)); - KoPathPoint * lastPoint = m_subpaths.last()->last(); + KoPathPoint * lastPoint = d->subpaths.last()->last(); d->updateLast(&lastPoint); lastPoint->setControlPoint2(c); KoPathPoint * point = new KoPathPoint(this, p, KoPathPoint::StopSubpath); - m_subpaths.last()->push_back(point); + d->subpaths.last()->push_back(point); return point; } KoPathPoint * KoPathShape::arcTo(qreal rx, qreal ry, qreal startAngle, qreal sweepAngle) { - if (m_subpaths.empty()) { + Q_D(KoPathShape); + + if (d->subpaths.empty()) { moveTo(QPointF(0, 0)); } - KoPathPoint * lastPoint = m_subpaths.last()->last(); + KoPathPoint * lastPoint = d->subpaths.last()->last(); if (lastPoint->properties() & KoPathPoint::CloseSubpath) { - lastPoint = m_subpaths.last()->first(); + lastPoint = d->subpaths.last()->first(); } QPointF startpoint(lastPoint->point()); @@ -662,12 +705,10 @@ int pointCnt = 0; // check Parameters - if (sweepAngle == 0) + if (sweepAngle == 0.0) return pointCnt; - if (sweepAngle > 360) - sweepAngle = 360; - else if (sweepAngle < -360) - sweepAngle = - 360; + + sweepAngle = qBound(-360.0, sweepAngle, 360.0); if (rx == 0 || ry == 0) { //TODO @@ -720,19 +761,19 @@ void KoPathShape::close() { Q_D(KoPathShape); - if (m_subpaths.empty()) { + if (d->subpaths.empty()) { return; } - d->closeSubpath(m_subpaths.last()); + d->closeSubpath(d->subpaths.last()); } void KoPathShape::closeMerge() { Q_D(KoPathShape); - if (m_subpaths.empty()) { + if (d->subpaths.empty()) { return; } - d->closeMergeSubpath(m_subpaths.last()); + d->closeMergeSubpath(d->subpaths.last()); } QPointF KoPathShape::normalize() @@ -752,8 +793,8 @@ void KoPathShapePrivate::map(const QTransform &matrix) { Q_Q(KoPathShape); - KoSubpathList::const_iterator pathIt(q->m_subpaths.constBegin()); - for (; pathIt != q->m_subpaths.constEnd(); ++pathIt) { + 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); @@ -768,15 +809,15 @@ if ((*lastPoint)->properties() & KoPathPoint::StopSubpath && (*lastPoint)->properties() & KoPathPoint::CloseSubpath) { // get the first point of the subpath - KoPathPoint *subpathStart = q->m_subpaths.last()->first(); + KoPathPoint *subpathStart = subpaths.last()->first(); // clone the first point of the subpath... - KoPathPoint * newLastPoint = new KoPathPoint(*subpathStart); + 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); - q->m_subpaths.push_back(path); + subpaths.push_back(path); *lastPoint = newLastPoint; } else { // the subpath was not closed so the formerly last point @@ -788,10 +829,12 @@ QList KoPathShape::pointsAt(const QRectF &r) const { + Q_D(const KoPathShape); + QList result; - KoSubpathList::const_iterator pathIt(m_subpaths.constBegin()); - for (; pathIt != m_subpaths.constEnd(); ++pathIt) { + 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())) @@ -807,10 +850,12 @@ QList KoPathShape::segmentsAt(const QRectF &r) const { + Q_D(const KoPathShape); + QList segments; - int subpathCount = m_subpaths.count(); + int subpathCount = d->subpaths.count(); for (int subpathIndex = 0; subpathIndex < subpathCount; ++subpathIndex) { - KoSubpath * subpath = m_subpaths[subpathIndex]; + KoSubpath * subpath = d->subpaths[subpathIndex]; int pointCount = subpath->count(); bool subpathClosed = isClosedSubpath(subpathIndex); for (int pointIndex = 0; pointIndex < pointCount; ++pointIndex) { @@ -832,8 +877,10 @@ KoPathPointIndex KoPathShape::pathPointIndex(const KoPathPoint *point) const { - for (int subpathIndex = 0; subpathIndex < m_subpaths.size(); ++subpathIndex) { - KoSubpath * subpath = m_subpaths.at(subpathIndex); + 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); @@ -879,18 +926,22 @@ int KoPathShape::pointCount() const { + Q_D(const KoPathShape); + int i = 0; - KoSubpathList::const_iterator pathIt(m_subpaths.constBegin()); - for (; pathIt != m_subpaths.constEnd(); ++pathIt) { + KoSubpathList::const_iterator pathIt(d->subpaths.constBegin()); + for (; pathIt != d->subpaths.constEnd(); ++pathIt) { i += (*pathIt)->size(); } return i; } int KoPathShape::subpathCount() const { - return m_subpaths.count(); + Q_D(const KoPathShape); + + return d->subpaths.count(); } int KoPathShape::subpathPointCount(int subpathIndex) const @@ -968,6 +1019,7 @@ 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) { @@ -1018,7 +1070,7 @@ subpath->last()->setProperty(KoPathPoint::StopSubpath); // insert the new subpath after the broken one - m_subpaths.insert(pointIndex.first + 1, newSubpath); + d->subpaths.insert(pointIndex.first + 1, newSubpath); return true; } @@ -1043,7 +1095,7 @@ subpath->append(p); // remove the nextSubpath from path - m_subpaths.removeAt(subpathIndex + 1); + d->subpaths.removeAt(subpathIndex + 1); // delete it as it is no longer possible to use it delete nextSubpath; @@ -1056,14 +1108,14 @@ Q_D(KoPathShape); KoSubpath *subpath = d->subPath(oldSubpathIndex); - if (subpath == 0 || newSubpathIndex >= m_subpaths.size()) + if (subpath == 0 || newSubpathIndex >= d->subpaths.size()) return false; if (oldSubpathIndex == newSubpathIndex) return true; - m_subpaths.removeAt(oldSubpathIndex); - m_subpaths.insert(newSubpathIndex, subpath); + d->subpaths.removeAt(oldSubpathIndex); + d->subpaths.insert(newSubpathIndex, subpath); return true; } @@ -1162,54 +1214,70 @@ Q_D(KoPathShape); KoSubpath *subpath = d->subPath(subpathIndex); - if (subpath != 0) - m_subpaths.removeAt(subpathIndex); + if (subpath != 0) { + Q_FOREACH (KoPathPoint* point, *subpath) { + point->setParent(this); + } + + d->subpaths.removeAt(subpathIndex); + } return subpath; } bool KoPathShape::addSubpath(KoSubpath * subpath, int subpathIndex) { - if (subpathIndex < 0 || subpathIndex > m_subpaths.size()) + Q_D(KoPathShape); + + if (subpathIndex < 0 || subpathIndex > d->subpaths.size()) return false; - m_subpaths.insert(subpathIndex, subpath); + Q_FOREACH (KoPathPoint* point, *subpath) { + point->setParent(this); + } + + d->subpaths.insert(subpathIndex, subpath); return true; } - -bool KoPathShape::combine(KoPathShape *path) +int KoPathShape::combine(KoPathShape *path) { - if (! path) - return false; + 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->m_subpaths) { + Q_FOREACH (KoSubpath* subpath, path->d_func()->subpaths) { KoSubpath *newSubpath = new KoSubpath(); Q_FOREACH (KoPathPoint* point, *subpath) { - KoPathPoint *newPoint = new KoPathPoint(*point); + KoPathPoint *newPoint = new KoPathPoint(*point, this); newPoint->map(pathMatrix); newPoint->map(myMatrix); - newPoint->setParent(this); newSubpath->append(newPoint); } - m_subpaths.append(newSubpath); + d->subpaths.append(newSubpath); + + if (insertSegmentPosition < 0) { + insertSegmentPosition = d->subpaths.size() - 1; + } } normalize(); - return true; + return insertSegmentPosition; } bool KoPathShape::separate(QList & separatedPaths) { - if (! m_subpaths.size()) + Q_D(KoPathShape); + + if (! d->subpaths.size()) return false; QTransform myMatrix = absoluteTransformation(0); - Q_FOREACH (KoSubpath* subpath, m_subpaths) { + Q_FOREACH (KoSubpath* subpath, d->subpaths) { KoPathShape *shape = new KoPathShape(); if (! shape) continue; @@ -1219,11 +1287,11 @@ KoSubpath *newSubpath = new KoSubpath(); Q_FOREACH (KoPathPoint* point, *subpath) { - KoPathPoint *newPoint = new KoPathPoint(*point); + KoPathPoint *newPoint = new KoPathPoint(*point, shape); newPoint->map(myMatrix); newSubpath->append(newPoint); } - shape->m_subpaths.append(newSubpath); + shape->d_func()->subpaths.append(newSubpath); shape->normalize(); separatedPaths.append(shape); } @@ -1269,10 +1337,10 @@ KoSubpath *KoPathShapePrivate::subPath(int subpathIndex) const { Q_Q(const KoPathShape); - if (subpathIndex < 0 || subpathIndex >= q->m_subpaths.size()) + if (subpathIndex < 0 || subpathIndex >= subpaths.size()) return 0; - return q->m_subpaths.at(subpathIndex); + return subpaths.at(subpathIndex); } QString KoPathShape::pathShapeId() const @@ -1282,11 +1350,13 @@ QString KoPathShape::toString(const QTransform &matrix) const { - QString d; + Q_D(const KoPathShape); + + QString pathString; // iterate over all subpaths - KoSubpathList::const_iterator pathIt(m_subpaths.constBegin()); - for (; pathIt != m_subpaths.constEnd(); ++pathIt) { + 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); @@ -1303,7 +1373,7 @@ // are we starting a subpath ? if (currPoint->properties() & KoPathPoint::StartSubpath) { const QPointF p = matrix.map(currPoint->point()); - d += QString("M%1 %2").arg(p.x()).arg(p.y()); + pathString += QString("M%1 %2").arg(p.x()).arg(p.y()); } } // end point of curve segment ? @@ -1315,15 +1385,15 @@ const QPointF cp1 = matrix.map(cubicSeg.first()->controlPoint2()); const QPointF cp2 = matrix.map(cubicSeg.second()->controlPoint1()); const QPointF p = matrix.map(cubicSeg.second()->point()); - d += QString("C%1 %2 %3 %4 %5 %6") + 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()); - d += QString("L%1 %2").arg(p.x()).arg(p.y()); + pathString += QString("L%1 %2").arg(p.x()).arg(p.y()); } // last point closes subpath ? if (currPoint->properties() & KoPathPoint::StopSubpath @@ -1337,20 +1407,20 @@ const QPointF cp1 = matrix.map(cubicSeg.first()->controlPoint2()); const QPointF cp2 = matrix.map(cubicSeg.second()->controlPoint1()); const QPointF p = matrix.map(cubicSeg.second()->point()); - d += QString("C%1 %2 %3 %4 %5 %6") + 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()); } - d += QString("Z"); + pathString += QString("Z"); } activeControlPoint2 = currPoint->activeControlPoint2(); lastPoint = currPoint; } } - return d; + return pathString; } char nodeType(const KoPathPoint * point) @@ -1370,8 +1440,8 @@ { Q_Q(const KoPathShape); QString types; - KoSubpathList::const_iterator pathIt(q->m_subpaths.constBegin()); - for (; pathIt != q->m_subpaths.constEnd(); ++pathIt) { + 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()) { @@ -1407,8 +1477,8 @@ if (element.hasAttributeNS(KoXmlNS::calligra, "nodeTypes")) { QString nodeTypes = element.attributeNS(KoXmlNS::calligra, "nodeTypes"); QString::const_iterator nIt(nodeTypes.constBegin()); - KoSubpathList::const_iterator pathIt(q->m_subpaths.constBegin()); - for (; pathIt != q->m_subpaths.constEnd(); ++pathIt) { + 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 @@ -1467,7 +1537,7 @@ } } - shape->normalize(); + //shape->normalize(); return shape; } @@ -1482,6 +1552,7 @@ 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; @@ -1501,63 +1572,46 @@ return outlinePath.contains(point); } -void KoPathShape::setMarker(const KoMarkerData &markerData) +void KoPathShape::setMarker(KoMarker *marker, KoFlake::MarkerPosition pos) { Q_D(KoPathShape); - if (markerData.position() == KoMarkerData::MarkerStart) { - d->startMarker = markerData; - } - else { - d->endMarker = markerData; + if (!marker && d->markersNew.contains(pos)) { + d->markersNew.remove(pos); + } else { + d->markersNew[pos] = marker; } } -void KoPathShape::setMarker(KoMarker *marker, KoMarkerData::MarkerPosition position) +KoMarker *KoPathShape::marker(KoFlake::MarkerPosition pos) const { - Q_D(KoPathShape); - - if (position == KoMarkerData::MarkerStart) { - if (!d->startMarker.marker()) { - d->startMarker.setWidth(MM_TO_POINT(DefaultMarkerWidth), qreal(0.0)); - } - d->startMarker.setMarker(marker); - } - else { - if (!d->endMarker.marker()) { - d->endMarker.setWidth(MM_TO_POINT(DefaultMarkerWidth), qreal(0.0)); - } - d->endMarker.setMarker(marker); - } + Q_D(const KoPathShape); + return d->markersNew[pos].data(); } -KoMarker *KoPathShape::marker(KoMarkerData::MarkerPosition position) const +bool KoPathShape::hasMarkers() const { Q_D(const KoPathShape); - - if (position == KoMarkerData::MarkerStart) { - return d->startMarker.marker(); - } - else { - return d->endMarker.marker(); - } + return !d->markersNew.isEmpty(); } -KoMarkerData KoPathShape::markerData(KoMarkerData::MarkerPosition position) const +bool KoPathShape::autoFillMarkers() const { Q_D(const KoPathShape); + return d->autoFillMarkers; +} - if (position == KoMarkerData::MarkerStart) { - return d->startMarker; - } - else { - return d->endMarker; - } +void KoPathShape::setAutoFillMarkers(bool value) +{ + Q_D(KoPathShape); + d->autoFillMarkers = value; } QPainterPath KoPathShape::pathStroke(const QPen &pen) const { - if (m_subpaths.isEmpty()) { + Q_D(const KoPathShape); + + if (d->subpaths.isEmpty()) { return QPainterPath(); } QPainterPath pathOutline; @@ -1574,126 +1628,7 @@ KoPathPoint *secondPoint = 0; KoPathPoint *preLastPoint = 0; - KoSubpath *firstSubpath = m_subpaths.first(); - bool twoPointPath = subpathPointCount(0) == 2; - bool closedPath = isClosedSubpath(0); - - /* - * The geometry is horizontally centered. It is vertically positioned relative to an offset value which - * is specified by a draw:marker-start-center attribute for markers referenced by a - * draw:marker-start attribute, and by the draw:marker-end-center attribute for markers - * referenced by a draw:marker-end attribute. The attribute value true defines an offset of 0.5 - * and the attribute value false defines an offset of 0.3, which is also the default value. The offset - * specifies the marker's vertical position in a range from 0.0 to 1.0, where the value 0.0 means the - * geometry's bottom bound is aligned to the X axis of the local coordinate system of the marker - * geometry, and where the value 1.0 means the top bound to be aligned to the X axis of the local - * coordinate system of the marker geometry. - * - * The shorten factor to use results of the 0.3 which means we need to start at 0.7 * height of the marker - */ - static const qreal shortenFactor = 0.7; - - KoMarkerData mdStart = markerData(KoMarkerData::MarkerStart); - KoMarkerData mdEnd = markerData(KoMarkerData::MarkerEnd); - if (mdStart.marker() && !closedPath) { - QPainterPath markerPath = mdStart.marker()->path(mdStart.width(pen.widthF())); - - KoPathSegment firstSegment = segmentByIndex(KoPathPointIndex(0, 0)); - if (firstSegment.isValid()) { - QRectF pathBoundingRect = markerPath.boundingRect(); - qreal shortenLength = pathBoundingRect.height() * shortenFactor; - debugFlake << "length" << firstSegment.length() << shortenLength; - qreal t = firstSegment.paramAtLength(shortenLength); - firstSegments = firstSegment.splitAt(t); - // transform the marker so that it goes from the first point of the first segment to the second point of the first segment - QPointF startPoint = firstSegments.first.first()->point(); - QPointF newStartPoint = firstSegments.first.second()->point(); - QLineF vector(newStartPoint, startPoint); - qreal angle = -vector.angle() + 90; - QTransform transform; - transform.translate(startPoint.x(), startPoint.y()) - .rotate(angle) - .translate(-pathBoundingRect.width() / 2.0, 0); - - markerPath = transform.map(markerPath); - QPainterPath startOutline = stroker.createStroke(markerPath); - startOutline = startOutline.united(markerPath); - pathOutline.addPath(startOutline); - firstPoint = firstSubpath->first(); - if (firstPoint->properties() & KoPathPoint::StartSubpath) { - firstSegments.second.first()->setProperty(KoPathPoint::StartSubpath); - } - debugFlake << "start marker" << angle << startPoint << newStartPoint << firstPoint->point(); - - if (!twoPointPath) { - if (firstSegment.second()->activeControlPoint2()) { - firstSegments.second.second()->setControlPoint2(firstSegment.second()->controlPoint2()); - } - secondPoint = (*firstSubpath)[1]; - } - else if (!mdEnd.marker()) { - // in case it is two point path with no end marker we need to modify the last point via the secondPoint - secondPoint = (*firstSubpath)[1]; - } - } - } - if (mdEnd.marker() && !closedPath) { - QPainterPath markerPath = mdEnd.marker()->path(mdEnd.width(pen.widthF())); - - KoPathSegment lastSegment; - - /* - * if the path consits only of 2 point and it it has an marker on both ends - * use the firstSegments.second as that is the path that needs to be shortened - */ - if (twoPointPath && firstPoint) { - lastSegment = firstSegments.second; - } - else { - lastSegment = segmentByIndex(KoPathPointIndex(0, firstSubpath->count() - 2)); - } - - if (lastSegment.isValid()) { - QRectF pathBoundingRect = markerPath.boundingRect(); - qreal shortenLength = lastSegment.length() - pathBoundingRect.height() * shortenFactor; - qreal t = lastSegment.paramAtLength(shortenLength); - lastSegments = lastSegment.splitAt(t); - // transform the marker so that it goes from the last point of the first segment to the previous point of the last segment - QPointF startPoint = lastSegments.second.second()->point(); - QPointF newStartPoint = lastSegments.second.first()->point(); - QLineF vector(newStartPoint, startPoint); - qreal angle = -vector.angle() + 90; - QTransform transform; - transform.translate(startPoint.x(), startPoint.y()).rotate(angle).translate(-pathBoundingRect.width() / 2.0, 0); - - markerPath = transform.map(markerPath); - QPainterPath endOutline = stroker.createStroke(markerPath); - endOutline = endOutline.united(markerPath); - pathOutline.addPath(endOutline); - lastPoint = firstSubpath->last(); - debugFlake << "end marker" << angle << startPoint << newStartPoint << lastPoint->point(); - if (twoPointPath) { - if (firstSegments.second.isValid()) { - if (lastSegments.first.first()->activeControlPoint2()) { - firstSegments.second.first()->setControlPoint2(lastSegments.first.first()->controlPoint2()); - } - } - else { - // if there is no start marker we need the first point needs to be changed via the preLastPoint - // the flag needs to be set so the moveTo is done - lastSegments.first.first()->setProperty(KoPathPoint::StartSubpath); - preLastPoint = (*firstSubpath)[firstSubpath->count()-2]; - } - } - else { - if (lastSegment.first()->activeControlPoint1()) { - lastSegments.first.first()->setControlPoint1(lastSegment.first()->controlPoint1()); - } - preLastPoint = (*firstSubpath)[firstSubpath->count()-2]; - } - } - } - + KoSubpath *firstSubpath = d->subpaths.first(); stroker.setWidth(pen.widthF()); stroker.setJoinStyle(pen.joinStyle()); diff --git a/libs/flake/KoPathShapeFactory.cpp b/libs/flake/KoPathShapeFactory.cpp --- a/libs/flake/KoPathShapeFactory.cpp +++ b/libs/flake/KoPathShapeFactory.cpp @@ -32,6 +32,8 @@ #include #include +#include "kis_pointer_utils.h" + KoPathShapeFactory::KoPathShapeFactory(const QStringList&) : KoShapeFactoryBase(KoPathShapeId, i18n("Simple path shape")) { @@ -50,7 +52,7 @@ path->curveTo(QPointF(0, 120), QPointF(50, 120), QPointF(50, 50)); path->curveTo(QPointF(50, -20), QPointF(100, -20), QPointF(100, 50)); path->normalize(); - path->setStroke(new KoShapeStroke(1.0)); + path->setStroke(toQShared(new KoShapeStroke(1.0))); return path; } diff --git a/libs/flake/KoPathShape_p.h b/libs/flake/KoPathShape_p.h --- a/libs/flake/KoPathShape_p.h +++ b/libs/flake/KoPathShape_p.h @@ -19,13 +19,15 @@ #ifndef KOPATHSHAPEPRIVATE_H #define KOPATHSHAPEPRIVATE_H +#include "KoPathShape.h" #include "KoTosContainer_p.h" -#include "KoMarkerData.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 @@ -89,8 +91,10 @@ Q_DECLARE_PUBLIC(KoPathShape) - KoMarkerData startMarker; - KoMarkerData endMarker; + KoSubpathList subpaths; + + QMap> markersNew; + bool autoFillMarkers; }; #endif diff --git a/libs/flake/KoPatternBackground.h b/libs/flake/KoPatternBackground.h --- a/libs/flake/KoPatternBackground.h +++ b/libs/flake/KoPatternBackground.h @@ -61,6 +61,8 @@ virtual ~KoPatternBackground(); + bool compareTo(const KoShapeBackground *other) const override; + /// Sets the transform matrix void setTransform(const QTransform &matrix); diff --git a/libs/flake/KoPatternBackground.cpp b/libs/flake/KoPatternBackground.cpp --- a/libs/flake/KoPatternBackground.cpp +++ b/libs/flake/KoPatternBackground.cpp @@ -144,6 +144,12 @@ { } +bool KoPatternBackground::compareTo(const KoShapeBackground *other) const +{ + Q_UNUSED(other); + return false; +} + void KoPatternBackground::setTransform(const QTransform &matrix) { Q_D(KoPatternBackground); diff --git a/libs/flake/KoRTree.h b/libs/flake/KoRTree.h --- a/libs/flake/KoRTree.h +++ b/libs/flake/KoRTree.h @@ -31,6 +31,7 @@ #include #include +#include "kis_assert.h" // #define CALLIGRA_RTREE_DEBUG #ifdef CALLIGRA_RTREE_DEBUG @@ -75,6 +76,12 @@ 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 @@ -105,6 +112,16 @@ 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(). * @@ -162,6 +179,7 @@ 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; @@ -252,6 +270,7 @@ virtual void intersects(const QRectF& rect, QMap & result) const; virtual void contains(const QPointF & point, QMap & result) const; + virtual void contained(const QRectF & point, QMap & result) const; virtual void keys(QList & result) const; virtual void values(QMap & result) const; @@ -286,6 +305,7 @@ virtual void intersects(const QRectF& rect, QMap & result) const; virtual void contains(const QPointF & point, QMap & result) const; + virtual void contained(const QRectF & point, QMap & result) const; virtual void keys(QList & result) const; virtual void values(QMap & result) const; @@ -351,6 +371,9 @@ 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++); } @@ -419,17 +442,32 @@ } template +bool KoRTree::contains(const T &data) +{ + return m_leafMap[data]; +} + + +template void KoRTree::remove(const T&data) { //qDebug() << "KoRTree remove"; LeafNode * leaf = m_leafMap[data]; - if (leaf == 0) { - qWarning() << "KoRTree::remove( const T&data) data not found"; - return; - } + + // Trying to remove unexistent 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); @@ -471,6 +509,15 @@ } 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; @@ -686,6 +733,14 @@ //qDebug() << " 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 guarenteed + * to be readded in remove(). + */ + } else { //qDebug() << " update BB parent is root" << parent->isRoot(); parent->setChildBoundingBox(node->place(), node->boundingBox()); @@ -871,6 +926,16 @@ } 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) { @@ -1041,6 +1106,16 @@ } 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) { diff --git a/libs/flake/KoSelectedShapesProxy.h b/libs/flake/KoSelectedShapesProxy.h new file mode 100644 --- /dev/null +++ b/libs/flake/KoSelectedShapesProxy.h @@ -0,0 +1,59 @@ +/* + * 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 KOSELECTEDSHAPESPROXY_H +#define KOSELECTEDSHAPESPROXY_H + +#include +#include "kritaflake_export.h" + +class KoSelection; +class KoShapeLayer; + +/** + * @brief The KoSelectedShapesProxy class is a special interface of KoCanvasBase to + * have a stable connection to shape selection signals in an environment when the + * active shape manager can switch (e.g. when shape layers are switched in Krita) + */ + +class KRITAFLAKE_EXPORT KoSelectedShapesProxy : public QObject +{ + Q_OBJECT +public: + explicit KoSelectedShapesProxy(QObject *parent = 0); + + /** + * Returns a pointer to a currently active shape selection. Don't connect to the + * selection, unless you really know what you are doing. Use the signals provided + * by KoSelectedShapesProxy itself. They are guaranteed to be valid all the time. + */ + virtual KoSelection *selection() = 0; + +Q_SIGNALS: + + // forwards a corresponding signal of KoShapeManager + void selectionChanged(); + + // forwards a corresponding signal of KoShapeManager + void selectionContentChanged(); + + // forwards a corresponding signal of KoSelection + void currentLayerChanged(const KoShapeLayer *layer); +}; + +#endif // KOSELECTEDSHAPESPROXY_H diff --git a/libs/kundo2/kundo2commandextradata.cpp b/libs/flake/KoSelectedShapesProxy.cpp rename from libs/kundo2/kundo2commandextradata.cpp rename to libs/flake/KoSelectedShapesProxy.cpp --- a/libs/kundo2/kundo2commandextradata.cpp +++ b/libs/flake/KoSelectedShapesProxy.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,9 +16,10 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kundo2commandextradata.h" +#include "KoSelectedShapesProxy.h" - -KUndo2CommandExtraData::~KUndo2CommandExtraData() +KoSelectedShapesProxy::KoSelectedShapesProxy(QObject *parent) + : QObject(parent) { } + diff --git a/libs/image/tests/kis_algebra_2d_test.h b/libs/flake/KoSelectedShapesProxySimple.h copy from libs/image/tests/kis_algebra_2d_test.h copy to libs/flake/KoSelectedShapesProxySimple.h --- a/libs/image/tests/kis_algebra_2d_test.h +++ b/libs/flake/KoSelectedShapesProxySimple.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,21 +16,24 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_ALGEBRA_2D_TEST_H -#define __KIS_ALGEBRA_2D_TEST_H +#ifndef KOSELECTEDSHAPESPROXYSIMPLE_H +#define KOSELECTEDSHAPESPROXYSIMPLE_H -#include +#include +#include -class KisAlgebra2DTest : public QObject +class KoShapeManager; + + +class KRITAFLAKE_EXPORT KoSelectedShapesProxySimple : public KoSelectedShapesProxy { Q_OBJECT -private Q_SLOTS: - void testHalfPlane(); - void testOuterCircle(); +public: + KoSelectedShapesProxySimple(KoShapeManager *shapeManager); + KoSelection *selection(); - void testQuadraticEquation(); - void testIntersections(); - void testWeirdIntersections(); +private: + QPointer m_shapeManager; }; -#endif /* __KIS_ALGEBRA_2D_TEST_H */ +#endif // KOSELECTEDSHAPESPROXYSIMPLE_H diff --git a/libs/flake/KoSelectedShapesProxySimple.cpp b/libs/flake/KoSelectedShapesProxySimple.cpp new file mode 100644 --- /dev/null +++ b/libs/flake/KoSelectedShapesProxySimple.cpp @@ -0,0 +1,40 @@ +/* + * 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 "KoSelectedShapesProxySimple.h" + +#include "kis_assert.h" +#include +#include + +KoSelectedShapesProxySimple::KoSelectedShapesProxySimple(KoShapeManager *shapeManager) + : m_shapeManager(shapeManager) +{ + KIS_ASSERT_RECOVER_RETURN(m_shapeManager); + + connect(m_shapeManager.data(), SIGNAL(selectionChanged()), SIGNAL(selectionChanged())); + connect(m_shapeManager.data(), SIGNAL(selectionContentChanged()), SIGNAL(selectionContentChanged())); + connect(m_shapeManager->selection(), SIGNAL(currentLayerChanged(const KoShapeLayer*)), SIGNAL(currentLayerChanged(const KoShapeLayer*))); +} + +KoSelection *KoSelectedShapesProxySimple::selection() +{ + KIS_ASSERT_RECOVER_RETURN_VALUE(m_shapeManager, 0); + return m_shapeManager->selection(); +} + diff --git a/libs/flake/KoSelection.h b/libs/flake/KoSelection.h --- a/libs/flake/KoSelection.h +++ b/libs/flake/KoSelection.h @@ -48,16 +48,20 @@ * A selection, however, should not be selectable. We need to think * a little about the interaction here. */ -class KRITAFLAKE_EXPORT KoSelection : public QObject, public KoShape +class KRITAFLAKE_EXPORT KoSelection : public QObject, public KoShape, public KoShape::ShapeChangeListener { Q_OBJECT public: KoSelection(); virtual ~KoSelection(); - virtual void paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintcontext); + void paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintcontext) override; + void setSize(const QSizeF &size) override; + QSizeF size() const override; + QRectF outlineRect() const override; + QRectF boundingRect() const override; /** * Adds a shape to the selection. @@ -73,7 +77,7 @@ * @param shape the shape to add to the selection * @param recursive enables recursively selecting shapes of parent groups */ - void select(KoShape *shape, bool recursive = true); + void select(KoShape *shape); /** * Removes a selected shape. @@ -89,7 +93,7 @@ * @param shape the shape to remove from the selection * @param recursive enables recursively deselecting shapes of parent groups */ - void deselect(KoShape *shape, bool recursive = true); + void deselect(KoShape *shape); /// clear the selections list void deselectAll(); @@ -100,14 +104,26 @@ * @param strip if StrippedSelection, the returned list will not include any children * of a container shape if the container-parent is itself also in the set. */ - const QList selectedShapes(KoFlake::SelectionType strip = KoFlake::FullSelection) const; + const QList selectedShapes() const; + + /** + * Same as selectedShapes() but only for editable shapes. Used by + * the algorithms that modify the image + */ + const QList selectedEditableShapes() const; + + /** + * Same as selectedEditableShapes() but also includes shapes delegates. + * Used for + */ + const QList selectedEditableShapesAndDelegates() const; /** * Return the first selected shape, or 0 if there is nothing selected. * @param strip if StrippedSelection, the returned list will not include any children * of a grouped shape if the group-parent is itself also in the set. */ - KoShape *firstSelectedShape(KoFlake::SelectionType strip = KoFlake::FullSelection) const; + KoShape *firstSelectedShape() const; /// return true if the shape is selected bool isSelected(const KoShape *shape) const; @@ -117,8 +133,6 @@ virtual bool hitTest(const QPointF &position) const; - virtual QRectF boundingRect() const; - /** * Sets the currently active layer. * @param layer the new active layer @@ -132,8 +146,7 @@ */ KoShapeLayer *activeLayer() const; - /// Updates the size and position of the selection - void updateSizeAndPosition(); + void notifyShapeChanged(ChangeType type, KoShape *shape); Q_SIGNALS: /// emitted when the selection is changed @@ -146,7 +159,6 @@ virtual void saveOdf(KoShapeSavingContext &) const; virtual bool loadOdf(const KoXmlElement &, KoShapeLoadingContext &); - Q_PRIVATE_SLOT(d_func(), void selectionChangedEvent()) Q_DECLARE_PRIVATE_D(KoShape::d_ptr, KoSelection) }; diff --git a/libs/flake/KoSelection.cpp b/libs/flake/KoSelection.cpp --- a/libs/flake/KoSelection.cpp +++ b/libs/flake/KoSelection.cpp @@ -27,295 +27,202 @@ #include "KoShapeGroup.h" #include "KoPointerEvent.h" #include "KoShapePaintingContext.h" +#include "kis_algebra_2d.h" +#include "krita_container_utils.h" #include -#include -QRectF KoSelectionPrivate::sizeRect() -{ - bool first = true; - QRectF bb; - - QTransform invSelectionTransform = q->absoluteTransformation(0).inverted(); - - QRectF bound; - - if (!selectedShapes.isEmpty()) { - QList::const_iterator it = selectedShapes.constBegin(); - for (; it != selectedShapes.constEnd(); ++it) { - if (dynamic_cast(*it)) - continue; - - const QTransform shapeTransform = (*it)->absoluteTransformation(0); - const QRectF shapeRect(QRectF(QPointF(), (*it)->size())); - - if (first) { - bb = (shapeTransform * invSelectionTransform).mapRect(shapeRect); - bound = shapeTransform.mapRect(shapeRect); - first = false; - } else { - bb = bb.united((shapeTransform * invSelectionTransform).mapRect(shapeRect)); - bound = bound.united(shapeTransform.mapRect(shapeRect)); - } - } - } - - globalBound = bound; - return bb; -} +#include "kis_debug.h" -void KoSelectionPrivate::requestSelectionChangedEvent() +KoSelection::KoSelection() + : KoShape(new KoSelectionPrivate(this)) { - if (eventTriggered) - return; - eventTriggered = true; - QTimer::singleShot(0, q, SLOT(selectionChangedEvent())); + Q_D(KoSelection); + connect(&d->selectionChangedCompressor, SIGNAL(timeout()), SIGNAL(selectionChanged())); } -void KoSelectionPrivate::selectionChangedEvent() +KoSelection::~KoSelection() { - eventTriggered = false; - emit q->selectionChanged(); } -void KoSelectionPrivate::selectGroupChildren(KoShapeGroup *group) +void KoSelection::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintcontext) { - if (! group) - return; - - Q_FOREACH (KoShape *shape, group->shapes()) { - if (selectedShapes.contains(shape)) - continue; - selectedShapes << shape; - - KoShapeGroup *childGroup = dynamic_cast(shape); - if (childGroup) - selectGroupChildren(childGroup); - } + Q_UNUSED(painter); + Q_UNUSED(converter); + Q_UNUSED(paintcontext); } -void KoSelectionPrivate::deselectGroupChildren(KoShapeGroup *group) +void KoSelection::setSize(const QSizeF &size) { - if (! group) - return; - - Q_FOREACH (KoShape *shape, group->shapes()) { - if (selectedShapes.contains(shape)) - selectedShapes.removeAll(shape); - - KoShapeGroup *childGroup = dynamic_cast(shape); - if (childGroup) - deselectGroupChildren(childGroup); - } + Q_UNUSED(size); + qWarning() << "WARNING: KoSelection::setSize() should never be used!"; } -//////////// - -KoSelection::KoSelection() - : KoShape(*(new KoSelectionPrivate(this))) +QSizeF KoSelection::size() const { + return outlineRect().size(); } -KoSelection::~KoSelection() +QRectF KoSelection::outlineRect() const { + Q_D(const KoSelection); + + QPolygonF globalPolygon; + Q_FOREACH (KoShape *shape, d->selectedShapes) { + globalPolygon = globalPolygon.united( + shape->absoluteTransformation(0).map(QPolygonF(shape->outlineRect()))); + } + const QPolygonF localPolygon = transformation().inverted().map(globalPolygon); + + return localPolygon.boundingRect(); } -void KoSelection::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintcontext) +QRectF KoSelection::boundingRect() const { - Q_UNUSED(painter); - Q_UNUSED(converter); - Q_UNUSED(paintcontext); + Q_D(const KoSelection); + return KoShape::boundingRect(d->selectedShapes); } -void KoSelection::select(KoShape *shape, bool recursive) +void KoSelection::select(KoShape *shape) { Q_D(KoSelection); - Q_ASSERT(shape != this); - Q_ASSERT(shape); - if (!shape->isSelectable() || !shape->isVisible(true)) + KIS_SAFE_ASSERT_RECOVER_RETURN(shape != this); + KIS_SAFE_ASSERT_RECOVER_RETURN(shape); + + if (!shape->isSelectable() || !shape->isVisible(true)) { return; + } - // save old number of selected shapes - int oldSelectionCount = d->selectedShapes.count(); + // check recursively + if (isSelected(shape)) { + return; + } - if (!d->selectedShapes.contains(shape)) - d->selectedShapes << shape; - - // automatically recursively select all child shapes downwards in the hierarchy - KoShapeGroup *group = dynamic_cast(shape); - if (group) - d->selectGroupChildren(group); - - if (recursive) { - // recursively select all parents and their children upwards the hierarchy - KoShapeContainer *parent = shape->parent(); - while (parent) { - KoShapeGroup *parentGroup = dynamic_cast(parent); - if (! parentGroup) break; - if (! d->selectedShapes.contains(parentGroup)) { - d->selectedShapes << parentGroup; - d->selectGroupChildren(parentGroup); - } - parent = parentGroup->parent(); - } + // find the topmost parent to select + while (KoShapeGroup *parentGroup = dynamic_cast(shape->parent())) { + shape = parentGroup; } - if (d->selectedShapes.count() == 1) { + d->selectedShapes << shape; + shape->addShapeChangeListener(this); + + d->savedMatrices = d->fetchShapesMatrices(); + + if (d->selectedShapes.size() == 1) { setTransformation(shape->absoluteTransformation(0)); - updateSizeAndPosition(); } else { - // reset global bound if there were no shapes selected before - if (!oldSelectionCount) - d->globalBound = QRectF(); - setTransformation(QTransform()); - // we are resetting the transformation here anyway, - // so we can just add the newly selected shapes to the bounding box - // in document coordinates and then use that size and position - int newSelectionCount = d->selectedShapes.count(); - for (int i = oldSelectionCount; i < newSelectionCount; ++i) { - KoShape *shape = d->selectedShapes[i]; - - // don't add the rect of the group rect, as it can be invalid - if (dynamic_cast(shape)) { - continue; - } - const QTransform shapeTransform = shape->absoluteTransformation(0); - const QRectF shapeRect(QRectF(QPointF(), shape->size())); - - d->globalBound = d->globalBound.united(shapeTransform.mapRect(shapeRect)); - } - setSize(d->globalBound.size()); - setPosition(d->globalBound.topLeft()); } - d->requestSelectionChangedEvent(); + d->selectionChangedCompressor.start(); } -void KoSelection::deselect(KoShape *shape, bool recursive) +void KoSelection::deselect(KoShape *shape) { Q_D(KoSelection); - if (! d->selectedShapes.contains(shape)) + if (!d->selectedShapes.contains(shape)) return; d->selectedShapes.removeAll(shape); + shape->removeShapeChangeListener(this); + d->savedMatrices = d->fetchShapesMatrices(); - KoShapeGroup *group = dynamic_cast(shape); - if (recursive) { - // recursively find the top group upwards int the hierarchy - KoShapeGroup *parentGroup = dynamic_cast(shape->parent()); - while (parentGroup) { - group = parentGroup; - parentGroup = dynamic_cast(parentGroup->parent()); - } + if (d->selectedShapes.size() == 1) { + setTransformation(d->selectedShapes.first()->absoluteTransformation(0)); } - if (group) - d->deselectGroupChildren(group); - - if (count() == 1) - setTransformation(firstSelectedShape()->absoluteTransformation(0)); - - updateSizeAndPosition(); - d->requestSelectionChangedEvent(); + d->selectionChangedCompressor.start(); } void KoSelection::deselectAll() { Q_D(KoSelection); - // reset the transformation matrix of the selection - setTransformation(QTransform()); if (d->selectedShapes.isEmpty()) return; + + Q_FOREACH (KoShape *shape, d->selectedShapes) { + shape->removeShapeChangeListener(this); + } + d->savedMatrices = d->fetchShapesMatrices(); + + // reset the transformation matrix of the selection + setTransformation(QTransform()); + d->selectedShapes.clear(); - d->requestSelectionChangedEvent(); + d->selectionChangedCompressor.start(); } int KoSelection::count() const { Q_D(const KoSelection); - int count = 0; - Q_FOREACH (KoShape *shape, d->selectedShapes) - if (dynamic_cast(shape) == 0) - ++count; - return count; + return d->selectedShapes.size(); } bool KoSelection::hitTest(const QPointF &position) const { Q_D(const KoSelection); - if (count() > 1) { - QRectF bb(boundingRect()); - return bb.contains(position); - } else if (count() == 1) { - return (*d->selectedShapes.begin())->hitTest(position); - } else { // count == 0 - return false; + + Q_FOREACH (KoShape *shape, d->selectedShapes) { + if (shape->hitTest(position)) return true; } + + return false; } -void KoSelection::updateSizeAndPosition() + +const QList KoSelection::selectedShapes() const { - Q_D(KoSelection); - QRectF bb = d->sizeRect(); - QTransform matrix = absoluteTransformation(0); - setSize(bb.size()); - QPointF p = matrix.map(bb.topLeft() + matrix.inverted().map(position())); - setPosition(p); + Q_D(const KoSelection); + return d->selectedShapes; } -QRectF KoSelection::boundingRect() const +const QList KoSelection::selectedEditableShapes() const { - return absoluteTransformation(0).mapRect(QRectF(QPointF(), size())); + Q_D(const KoSelection); + + QList shapes = selectedShapes(); + + KritaUtils::filterContainer (shapes, [](KoShape *shape) { + return shape->isEditable(); + }); + + return shapes; } -const QList KoSelection::selectedShapes(KoFlake::SelectionType strip) const +const QList KoSelection::selectedEditableShapesAndDelegates() const { - Q_D(const KoSelection); - QList answer; - // strip the child objects when there is also a parent included. - bool doStripping = strip == KoFlake::StrippedSelection; - Q_FOREACH (KoShape *shape, d->selectedShapes) { - KoShapeContainer *container = shape->parent(); - if (strip != KoFlake::TopLevelSelection && dynamic_cast(shape)) - // since a KoShapeGroup - // guarentees all its children are selected at the same time as itself - // is selected we will only return its children. - continue; - bool add = true; - while (doStripping && add && container) { - if (dynamic_cast(container) == 0 && d->selectedShapes.contains(container)) - add = false; - container = container->parent(); + QList shapes; + Q_FOREACH (KoShape *shape, selectedShapes()) { + QSet delegates = shape->toolDelegates(); + if (delegates.isEmpty()) { + shapes.append(shape); + } else { + Q_FOREACH (KoShape *delegatedShape, delegates) { + shapes.append(delegatedShape); + } } - if (strip == KoFlake::TopLevelSelection && container && d->selectedShapes.contains(container)) - add = false; - if (add) - answer << shape; } - return answer; + return shapes; } bool KoSelection::isSelected(const KoShape *shape) const { Q_D(const KoSelection); if (shape == this) return true; - foreach (KoShape *s, d->selectedShapes) { - if (s == shape) - return true; + const KoShape *tmpShape = shape; + while (tmpShape && std::find(d->selectedShapes.begin(), d->selectedShapes.end(), tmpShape) == d->selectedShapes.end()/*d->selectedShapes.contains(tmpShape)*/) { + tmpShape = tmpShape->parent(); } - return false; + return tmpShape; } -KoShape *KoSelection::firstSelectedShape(KoFlake::SelectionType strip) const +KoShape *KoSelection::firstSelectedShape() const { - QList set = selectedShapes(strip); - if (set.isEmpty()) - return 0; - return *(set.begin()); + Q_D(const KoSelection); + return !d->selectedShapes.isEmpty() ? d->selectedShapes.first() : 0; } void KoSelection::setActiveLayer(KoShapeLayer *layer) @@ -331,6 +238,30 @@ return d->activeLayer; } +void KoSelection::notifyShapeChanged(KoShape::ChangeType type, KoShape *shape) +{ + Q_UNUSED(shape); + Q_D(KoSelection); + + if (type == KoShape::Deleted) { + deselect(shape); + // HACK ALERT: the caller will also remove the listener, so re-add it here + shape->addShapeChangeListener(this); + + } else if (type >= KoShape::PositionChanged && type <= KoShape::GenericMatrixChange) { + QList matrices = d->fetchShapesMatrices(); + + QTransform newTransform; + if (d->checkMatricesConsistent(matrices, &newTransform)) { + d->savedMatrices = matrices; + setTransformation(newTransform); + } else { + d->savedMatrices = matrices; + setTransformation(QTransform()); + } + } +} + void KoSelection::saveOdf(KoShapeSavingContext &) const { } @@ -340,5 +271,39 @@ return true; } -//have to include this because of Q_PRIVATE_SLOT -#include "moc_KoSelection.cpp" + +QList KoSelectionPrivate::fetchShapesMatrices() const +{ + QList result; + Q_FOREACH (KoShape *shape, selectedShapes) { + result << shape->absoluteTransformation(0); + } + return result; +} + +bool KoSelectionPrivate::checkMatricesConsistent(const QList &matrices, QTransform *newTransform) +{ + Q_Q(KoSelection); + + QTransform commonDiff; + bool haveCommonDiff = false; + + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(matrices.size() == selectedShapes.size(), false); + + for (int i = 0; i < matrices.size(); i++) { + QTransform t = savedMatrices[i]; + QTransform diff = t.inverted() * matrices[i]; + + + if (haveCommonDiff) { + if (!KisAlgebra2D::fuzzyMatrixCompare(commonDiff, diff, 1e-5)) { + return false; + } + } else { + commonDiff = diff; + } + } + + *newTransform = q->transformation() * commonDiff; + return true; +} diff --git a/libs/flake/KoSelection_p.h b/libs/flake/KoSelection_p.h --- a/libs/flake/KoSelection_p.h +++ b/libs/flake/KoSelection_p.h @@ -21,28 +21,30 @@ #include "KoShape_p.h" +#include "kis_signal_compressor.h" + class KoShapeGroup; class KoSelectionPrivate : public KoShapePrivate { public: explicit KoSelectionPrivate(KoSelection *parent) - : KoShapePrivate(parent), eventTriggered(false), activeLayer(0), q(parent) {} + : KoShapePrivate(parent), + activeLayer(0), + selectionChangedCompressor(1, KisSignalCompressor::FIRST_INACTIVE) + {} QList selectedShapes; - bool eventTriggered; - KoShapeLayer *activeLayer; - void requestSelectionChangedEvent(); - void selectGroupChildren(KoShapeGroup *group); - void deselectGroupChildren(KoShapeGroup *group); + KisSignalCompressor selectionChangedCompressor; + - void selectionChangedEvent(); + QList savedMatrices; - QRectF sizeRect(); + QList fetchShapesMatrices() const; + bool checkMatricesConsistent(const QList &matrices, QTransform *newTransform); - KoSelection *q; - QRectF globalBound; + Q_DECLARE_PUBLIC(KoSelection) }; #endif diff --git a/libs/flake/KoShape.h b/libs/flake/KoShape.h --- a/libs/flake/KoShape.h +++ b/libs/flake/KoShape.h @@ -24,6 +24,7 @@ #define KOSHAPE_H #include "KoFlake.h" +#include "KoFlakeTypes.h" #include "KoConnectionPoint.h" #include @@ -52,11 +53,13 @@ class KoFilterEffectStack; class KoSnapData; class KoClipPath; +class KoClipMask; class KoShapePaintingContext; class KoShapeAnchor; class KoBorder; struct KoInsets; class KoShapeBackground; +class KisHandlePainterHelper; /** @@ -113,6 +116,7 @@ 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 @@ -125,7 +129,8 @@ 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 + ClipPathChanged, ///< the shapes clip path has changed + TransparencyChanged ///< the shapetransparency value has changed }; /// The behavior text should do when intersecting this shape. @@ -165,6 +170,12 @@ virtual ~KoShape(); /** + * @brief creates a deep copy of thie shape or shapes subtree + * @return a cloned shape + */ + virtual KoShape* cloneShape() const; + + /** * @brief Paint the shape * 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 @@ -321,6 +332,26 @@ 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 @@ -651,6 +682,15 @@ */ 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; + /** * Request a repaint to be queued. * The repaint will be of the entire Shape, including its selection handles should this @@ -731,13 +771,13 @@ * Returns the currently set stroke, or 0 if there is no stroke. * @return the currently set stroke, or 0 if there is no stroke. */ - KoShapeStrokeModel *stroke() const; + KoShapeStrokeModelSP stroke() const; /** * Set a new stroke, removing the old one. * @param stroke the new stroke, or 0 if there should be no stroke. */ - void setStroke(KoShapeStrokeModel *stroke); + void setStroke(KoShapeStrokeModelSP stroke); /** * Return the insets of the stroke. @@ -763,6 +803,12 @@ /// 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/hight ratio intact so as not to distort shapes that rely on that @@ -785,7 +831,7 @@ * @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::Position anchor = KoFlake::CenteredPosition) const; + QPointF absolutePosition(KoFlake::AnchorPosition anchor = KoFlake::Center) const; /** * Move this shape to an absolute position where the end location will be the same @@ -801,7 +847,7 @@ * @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::Position anchor = KoFlake::CenteredPosition); + void setAbsolutePosition(const QPointF &newPosition, KoFlake::AnchorPosition anchor = KoFlake::Center); /** * Set a data object on the shape to be used by an application. @@ -816,19 +862,6 @@ KoShapeUserData *userData() const; /** - * Set a data object on the shape to be used by an application. - * This is specifically useful when an application wants to have data that is per shape - * and should be deleted when the shape is destructed. - * @param applicationData the new application data, or 0 to delete the current one. - */ - void setApplicationData(KoShapeApplicationData *applicationData); - - /** - * Return the current applicationData. - */ - KoShapeApplicationData *applicationData() 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 @@ -901,6 +934,13 @@ 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 @@ -1090,9 +1130,31 @@ */ KoShapePrivate *priv(); +public: + + struct 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 &); + KoShape(KoShapePrivate *); /* ** loading saving helper methods */ /// attributes from ODF 1.1 chapter 9.2.15 Common Drawing Shape Attributes @@ -1155,7 +1217,7 @@ virtual void loadStyle(const KoXmlElement &element, KoShapeLoadingContext &context); /// Loads the stroke style - KoShapeStrokeModel *loadOdfStroke(const KoXmlElement &element, KoShapeLoadingContext &context) const; + KoShapeStrokeModelSP loadOdfStroke(const KoXmlElement &element, KoShapeLoadingContext &context) const; /// Loads the fill style QSharedPointer loadOdfFill(KoShapeLoadingContext &context) const; diff --git a/libs/flake/KoShape.cpp b/libs/flake/KoShape.cpp --- a/libs/flake/KoShape.cpp +++ b/libs/flake/KoShape.cpp @@ -70,21 +70,20 @@ #include #include +#include "kis_assert.h" + #include #include "KoOdfGradientBackground.h" +#include // KoShapePrivate KoShapePrivate::KoShapePrivate(KoShape *shape) : q_ptr(shape), size(50, 50), parent(0), - userData(0), - appData(0), - stroke(0), shadow(0), border(0), - clipPath(0), filterEffectStack(0), transparency(0.0), zIndex(0), @@ -102,52 +101,99 @@ textRunAroundDistanceRight(0.0), textRunAroundDistanceBottom(0.0), textRunAroundThreshold(0.0), - textRunAroundContour(KoShape::ContourFull), - anchor(0), - minimumHeight(0.0) + textRunAroundContour(KoShape::ContourFull) { connectors[KoConnectionPoint::TopConnectionPoint] = KoConnectionPoint::defaultConnectionPoint(KoConnectionPoint::TopConnectionPoint); connectors[KoConnectionPoint::RightConnectionPoint] = KoConnectionPoint::defaultConnectionPoint(KoConnectionPoint::RightConnectionPoint); connectors[KoConnectionPoint::BottomConnectionPoint] = KoConnectionPoint::defaultConnectionPoint(KoConnectionPoint::BottomConnectionPoint); connectors[KoConnectionPoint::LeftConnectionPoint] = KoConnectionPoint::defaultConnectionPoint(KoConnectionPoint::LeftConnectionPoint); connectors[KoConnectionPoint::FirstCustomConnectionPoint] = KoConnectionPoint(QPointF(0.5, 0.5), KoConnectionPoint::AllDirections, KoConnectionPoint::AlignCenter); } +KoShapePrivate::KoShapePrivate(const KoShapePrivate &rhs, KoShape *q) + : q_ptr(q), + size(rhs.size), + shapeId(rhs.shapeId), + name(rhs.name), + localMatrix(rhs.localMatrix), + connectors(rhs.connectors), + parent(0), // to be initialized later + shapeManagers(), // to be initialized later + toolDelegates(), // FIXME: how to initialize them? + userData(rhs.userData ? rhs.userData->clone() : 0), + stroke(rhs.stroke), + fill(rhs.fill), + dependees(), // FIXME: how to initialize them? + shadow(0), // WARNING: not implemented in Krita + border(0), // WARNING: not implemented in Krita + clipPath(rhs.clipPath ? rhs.clipPath->clone() : 0), + clipMask(rhs.clipMask ? rhs.clipMask->clone() : 0), + additionalAttributes(rhs.additionalAttributes), + additionalStyleAttributes(rhs.additionalStyleAttributes), + filterEffectStack(0), // WARNING: not implemented in Krita + transparency(rhs.transparency), + hyperLink(rhs.hyperLink), + + zIndex(rhs.zIndex), + runThrough(rhs.runThrough), + visible(rhs.visible), + printable(rhs.visible), + geometryProtected(rhs.geometryProtected), + keepAspect(rhs.keepAspect), + selectable(rhs.selectable), + detectCollision(rhs.detectCollision), + protectContent(rhs.protectContent), + + textRunAroundSide(rhs.textRunAroundSide), + textRunAroundDistanceLeft(rhs.textRunAroundDistanceLeft), + textRunAroundDistanceTop(rhs.textRunAroundDistanceTop), + textRunAroundDistanceRight(rhs.textRunAroundDistanceRight), + textRunAroundDistanceBottom(rhs.textRunAroundDistanceBottom), + textRunAroundThreshold(rhs.textRunAroundThreshold), + textRunAroundContour(rhs.textRunAroundContour) +{ +} + KoShapePrivate::~KoShapePrivate() { Q_Q(KoShape); - if (parent) + if (parent) { parent->removeShape(q); + } + Q_FOREACH (KoShapeManager *manager, shapeManagers) { - manager->remove(q); - manager->removeAdditional(q); + manager->shapeInterface()->notifyShapeDestructed(q); } - delete userData; - delete appData; - if (stroke && !stroke->deref()) - delete stroke; + shapeManagers.clear(); + if (shadow && !shadow->deref()) delete shadow; if (filterEffectStack && !filterEffectStack->deref()) delete filterEffectStack; - delete clipPath; } void KoShapePrivate::shapeChanged(KoShape::ChangeType type) { Q_Q(KoShape); if (parent) parent->model()->childChanged(q, type); + q->shapeChanged(type); - Q_FOREACH (KoShape * shape, dependees) + + Q_FOREACH (KoShape * shape, dependees) { shape->shapeChanged(type, q); + } + + Q_FOREACH (KoShape::ShapeChangeListener *listener, listeners) { + listener->notifyShapeChangedImpl(type, q); + } } void KoShapePrivate::updateStroke() { Q_Q(KoShape); - if (stroke == 0) - return; + if (!stroke) return; + KoInsets insets; stroke->strokeInsets(q, insets); QSizeF inner = q->size(); @@ -271,8 +317,8 @@ notifyChanged(); } -KoShape::KoShape(KoShapePrivate &dd) - : d_ptr(&dd) +KoShape::KoShape(KoShapePrivate *dd) + : d_ptr(dd) { } @@ -283,6 +329,11 @@ delete d_ptr; } +KoShape *KoShape::cloneShape() const +{ + return 0; +} + void KoShape::scale(qreal sx, qreal sy) { Q_D(KoShape); @@ -406,6 +457,29 @@ return bb; } +QRectF KoShape::boundingRect(const QList &shapes) +{ + QRectF boundingRect; + Q_FOREACH (KoShape *shape, shapes) { + boundingRect |= shape->boundingRect(); + } + return boundingRect; +} + +QRectF KoShape::absoluteOutlineRect(KoViewConverter *converter) const +{ + return absoluteTransformation(converter).map(outline()).boundingRect(); +} + +QRectF KoShape::absoluteOutlineRect(const QList &shapes, KoViewConverter *converter) +{ + QRectF absoluteOutlineRect; + Q_FOREACH (KoShape *shape, shapes) { + absoluteOutlineRect |= shape->absoluteOutlineRect(converter); + } + return absoluteOutlineRect; +} + QTransform KoShape::absoluteTransformation(const KoViewConverter *converter) const { Q_D(const KoShape); @@ -474,6 +548,19 @@ bool KoShape::compareShapeZIndex(KoShape *s1, KoShape *s2) { + /** + * WARNING: Our definition of zIndex is not yet compatible with SVG2's + * definition. In SVG stacking context of groups with the same + * zIndex are **merged**, while in Krita the contents of groups + * is never merged. One group will always below than the other. + * Therefore, when zIndex of two groups inside the same parent + * coinside, the resulting painting order in Krita is + * **UNDEFINED**. + * + * To avoid this trouble we use KoShapeReorderCommand::mergeInShape() + * inside KoShapeCreateCommand. + */ + // First sort according to runThrough which is sort of a master level KoShape *parentShapeS1 = s1->parent(); KoShape *parentShapeS2 = s2->parent(); @@ -555,20 +642,51 @@ void KoShape::setParent(KoShapeContainer *parent) { Q_D(KoShape); - if (d->parent == parent) + + if (d->parent == parent) { return; + } + KoShapeContainer *oldParent = d->parent; d->parent = 0; // avoids recursive removing - if (oldParent) - oldParent->removeShape(this); + + if (oldParent) { + oldParent->shapeInterface()->removeShape(this); + } + + KIS_SAFE_ASSERT_RECOVER_NOOP(parent != this); + if (parent && parent != this) { d->parent = parent; - parent->addShape(this); + parent->shapeInterface()->addShape(this); } + notifyChanged(); d->shapeChanged(ParentChanged); } +bool KoShape::inheritsTransformFromAny(const QList ancestorsInQuestion) const +{ + bool result = false; + + KoShape *shape = const_cast(this); + while (shape) { + KoShapeContainer *parent = shape->parent(); + if (parent && !parent->inheritsTransform(shape)) { + break; + } + + if (ancestorsInQuestion.contains(shape)) { + result = true; + break; + } + + shape = parent; + } + + return result; +} + int KoShape::zIndex() const { Q_D(const KoShape); @@ -628,20 +746,22 @@ return QPainterPath(); } -QPointF KoShape::absolutePosition(KoFlake::Position anchor) const +QPointF KoShape::absolutePosition(KoFlake::AnchorPosition anchor) const { - QPointF point; - switch (anchor) { - case KoFlake::TopLeftCorner: break; - case KoFlake::TopRightCorner: point = QPointF(size().width(), 0.0); break; - case KoFlake::BottomLeftCorner: point = QPointF(0.0, size().height()); break; - case KoFlake::BottomRightCorner: point = QPointF(size().width(), size().height()); break; - case KoFlake::CenteredPosition: point = QPointF(size().width() / 2.0, size().height() / 2.0); break; + const QRectF rc = outlineRect(); + + QPointF point = rc.topLeft(); + + bool valid = false; + QPointF anchoredPoint = KoFlake::anchorToPoint(anchor, rc, &valid); + if (valid) { + point = anchoredPoint; } + return absoluteTransformation(0).map(point); } -void KoShape::setAbsolutePosition(const QPointF &newPosition, KoFlake::Position anchor) +void KoShape::setAbsolutePosition(const QPointF &newPosition, KoFlake::AnchorPosition anchor) { Q_D(KoShape); QPointF currentAbsPosition = absolutePosition(anchor); @@ -687,27 +807,13 @@ void KoShape::setUserData(KoShapeUserData *userData) { Q_D(KoShape); - delete d->userData; - d->userData = userData; + d->userData.reset(userData); } KoShapeUserData *KoShape::userData() const { Q_D(const KoShape); - return d->userData; -} - -void KoShape::setApplicationData(KoShapeApplicationData *appData) -{ - Q_D(KoShape); - // appdata is deleted by the application. - d->appData = appData; -} - -KoShapeApplicationData *KoShape::applicationData() const -{ - Q_D(const KoShape); - return d->appData; + return d->userData.data(); } bool KoShape::hasTransparency() const @@ -723,6 +829,9 @@ { Q_D(KoShape); d->transparency = qBound(0.0, transparency, 1.0); + + d->shapeChanged(TransparencyChanged); + notifyChanged(); } qreal KoShape::transparency(bool recursive) const @@ -776,7 +885,7 @@ QPointF KoShape::position() const { Q_D(const KoShape); - QPointF center(0.5*size().width(), 0.5*size().height()); + QPointF center = outlineRect().center(); return d->localMatrix.map(center) - center; } @@ -973,31 +1082,6 @@ d->textRunAroundContour = contour; } -void KoShape::setAnchor(KoShapeAnchor *anchor) -{ - Q_D(KoShape); - d->anchor = anchor; -} - -KoShapeAnchor *KoShape::anchor() const -{ - Q_D(const KoShape); - return d->anchor; -} - -void KoShape::setMinimumHeight(qreal height) -{ - Q_D(KoShape); - d->minimumHeight = height; -} - -qreal KoShape::minimumHeight() const -{ - Q_D(const KoShape); - return d->minimumHeight; -} - - void KoShape::setBackground(QSharedPointer fill) { Q_D(KoShape); @@ -1119,6 +1203,9 @@ { Q_D(KoShape); d->keepAspect = keepAspect; + + d->shapeChanged(KeepAspectRatioChange); + notifyChanged(); } bool KoShape::keepAspectRatio() const @@ -1151,22 +1238,22 @@ return d->detectCollision; } -KoShapeStrokeModel *KoShape::stroke() const +KoShapeStrokeModelSP KoShape::stroke() const { Q_D(const KoShape); return d->stroke; } -void KoShape::setStroke(KoShapeStrokeModel *stroke) +void KoShape::setStroke(KoShapeStrokeModelSP stroke) { Q_D(KoShape); - if (stroke) - stroke->ref(); + + // TODO: check if it really updates stuff d->updateStroke(); - if (d->stroke) - d->stroke->deref(); + d->stroke = stroke; d->updateStroke(); + d->shapeChanged(StrokeChanged); notifyChanged(); } @@ -1212,15 +1299,27 @@ void KoShape::setClipPath(KoClipPath *clipPath) { Q_D(KoShape); - d->clipPath = clipPath; + d->clipPath.reset(clipPath); d->shapeChanged(ClipPathChanged); notifyChanged(); } KoClipPath * KoShape::clipPath() const { Q_D(const KoShape); - return d->clipPath; + return d->clipPath.data(); +} + +void KoShape::setClipMask(KoClipMask *clipMask) +{ + Q_D(KoShape); + d->clipMask.reset(clipMask); +} + +KoClipMask* KoShape::clipMask() const +{ + Q_D(const KoShape); + return d->clipMask.data(); } QTransform KoShape::transform() const @@ -1279,7 +1378,7 @@ { Q_D(const KoShape); // and fill the style - KoShapeStrokeModel *sm = stroke(); + KoShapeStrokeModelSP sm = stroke(); if (sm) { sm->fillStyle(style, context); } @@ -1403,10 +1502,8 @@ styleStack.setTypeProperties("graphic"); d->fill.clear(); - if (d->stroke && !d->stroke->deref()) { - delete d->stroke; - d->stroke = 0; - } + d->stroke.clear(); + if (d->shadow && !d->shadow->deref()) { delete d->shadow; d->shadow = 0; @@ -1623,16 +1720,16 @@ return bg; } -KoShapeStrokeModel *KoShape::loadOdfStroke(const KoXmlElement &element, KoShapeLoadingContext &context) const +KoShapeStrokeModelSP KoShape::loadOdfStroke(const KoXmlElement &element, KoShapeLoadingContext &context) const { KoStyleStack &styleStack = context.odfLoadingContext().styleStack(); KoOdfStylesReader &stylesReader = context.odfLoadingContext().stylesReader(); QString stroke = KoShapePrivate::getStyleProperty("stroke", context); if (stroke == "solid" || stroke == "dash") { QPen pen = KoOdfGraphicStyles::loadOdfStrokeStyle(styleStack, stroke, stylesReader); - KoShapeStroke *stroke = new KoShapeStroke(); + QSharedPointer stroke(new KoShapeStroke()); if (styleStack.hasProperty(KoXmlNS::calligra, "stroke-gradient")) { QString gradientName = styleStack.property(KoXmlNS::calligra, "stroke-gradient"); @@ -1655,7 +1752,7 @@ } else if (stroke.isEmpty()) { QPen pen = KoOdfGraphicStyles::loadOdfStrokeStyle(styleStack, "solid", stylesReader); if (KoOdfWorkaround::fixMissingStroke(pen, element, context, this)) { - KoShapeStroke *stroke = new KoShapeStroke(); + QSharedPointer stroke(new KoShapeStroke()); #ifndef NWORKAROUND_ODF_BUGS KoOdfWorkaround::fixPenWidth(pen, context); @@ -1671,7 +1768,7 @@ #endif } - return 0; + return KoShapeStrokeModelSP(); } KoShapeShadow *KoShapePrivate::loadOdfShadow(KoShapeLoadingContext &context) const @@ -1834,9 +1931,8 @@ ps->loadContourOdf(child, context, scaleFactor); ps->setTransformation(transformation()); - KoClipData *cd = new KoClipData(ps); - KoClipPath *clipPath = new KoClipPath(this, cd); - d->clipPath = clipPath; + KoClipPath *clipPath = new KoClipPath({ps}, KoFlake::UserSpaceOnUse); + d->clipPath.reset(clipPath); } } @@ -1893,14 +1989,14 @@ scaleMatrix.scale(params[0].toDouble(), params[0].toDouble()); matrix = matrix * scaleMatrix; } else if (cmd == "skewx") { - QPointF p = absolutePosition(KoFlake::TopLeftCorner); + QPointF p = absolutePosition(KoFlake::TopLeft); QTransform shearMatrix; shearMatrix.translate(p.x(), p.y()); shearMatrix.shear(tan(-params[0].toDouble()), 0.0F); shearMatrix.translate(-p.x(), -p.y()); matrix = matrix * shearMatrix; } else if (cmd == "skewy") { - QPointF p = absolutePosition(KoFlake::TopLeftCorner); + QPointF p = absolutePosition(KoFlake::TopLeft); QTransform shearMatrix; shearMatrix.translate(p.x(), p.y()); shearMatrix.shear(0.0F, tan(-params[0].toDouble())); @@ -2136,6 +2232,17 @@ painter.scale(zoomX, zoomY); } +KisHandlePainterHelper KoShape::createHandlePainterHelper(QPainter *painter, KoShape *shape, const KoViewConverter &converter, qreal handleRadius) +{ + const QTransform originalPainterTransform = painter->transform(); + + painter->setTransform(shape->absoluteTransformation(&converter) * painter->transform()); + KoShape::applyConversion(*painter, converter); + + // move c-tor + return KisHandlePainterHelper(painter, originalPainterTransform, handleRadius); +} + QPointF KoShape::shapeToDocument(const QPointF &point) const { return absoluteTransformation(0).map(point); @@ -2286,3 +2393,67 @@ Q_D(KoShape); return d; } + +KoShape::ShapeChangeListener::~ShapeChangeListener() +{ + Q_FOREACH(KoShape *shape, m_registeredShapes) { + shape->removeShapeChangeListener(this); + } +} + +void KoShape::ShapeChangeListener::registerShape(KoShape *shape) +{ + KIS_SAFE_ASSERT_RECOVER_RETURN(!m_registeredShapes.contains(shape)); + m_registeredShapes.append(shape); +} + +void KoShape::ShapeChangeListener::unregisterShape(KoShape *shape) +{ + KIS_SAFE_ASSERT_RECOVER_RETURN(m_registeredShapes.contains(shape)); + m_registeredShapes.removeAll(shape); +} + +void KoShape::ShapeChangeListener::notifyShapeChangedImpl(KoShape::ChangeType type, KoShape *shape) +{ + KIS_SAFE_ASSERT_RECOVER_RETURN(m_registeredShapes.contains(shape)); + + notifyShapeChanged(type, shape); + + if (type == KoShape::Deleted) { + unregisterShape(shape); + } +} + +void KoShape::addShapeChangeListener(KoShape::ShapeChangeListener *listener) +{ + Q_D(KoShape); + + KIS_SAFE_ASSERT_RECOVER_RETURN(!d->listeners.contains(listener)); + listener->registerShape(this); + d->listeners.append(listener); +} + +void KoShape::removeShapeChangeListener(KoShape::ShapeChangeListener *listener) +{ + Q_D(KoShape); + + KIS_SAFE_ASSERT_RECOVER_RETURN(d->listeners.contains(listener)); + d->listeners.removeAll(listener); + listener->unregisterShape(this); +} + +QList KoShape::linearizeSubtree(const QList &shapes) +{ + QList result; + + Q_FOREACH (KoShape *shape, shapes) { + result << shape; + + KoShapeContainer *container = dynamic_cast(shape); + if (container) { + result << linearizeSubtree(container->shapes()); + } + } + + return result; +} diff --git a/libs/flake/KoShapeBackground.h b/libs/flake/KoShapeBackground.h --- a/libs/flake/KoShapeBackground.h +++ b/libs/flake/KoShapeBackground.h @@ -51,6 +51,8 @@ /// Returns if the background has some transparency. virtual bool hasTransparency() const; + virtual bool compareTo(const KoShapeBackground *other) const = 0; + /** * Fills the style object * @param style object diff --git a/libs/flake/KoShapeBasedDocumentBase.h b/libs/flake/KoShapeBasedDocumentBase.h --- a/libs/flake/KoShapeBasedDocumentBase.h +++ b/libs/flake/KoShapeBasedDocumentBase.h @@ -27,6 +27,7 @@ #include +class QRectF; class KoShape; class KoShapeBasedDocumentBasePrivate; class KoDocumentResourceManager; @@ -78,6 +79,22 @@ */ 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. + */ + virtual qreal pixelsPerInch() const = 0; + private: KoShapeBasedDocumentBasePrivate * const d; }; diff --git a/libs/flake/KoShapeBasedDocumentBase.cpp b/libs/flake/KoShapeBasedDocumentBase.cpp --- a/libs/flake/KoShapeBasedDocumentBase.cpp +++ b/libs/flake/KoShapeBasedDocumentBase.cpp @@ -19,6 +19,7 @@ * Boston, MA 02110-1301, USA. */ +#include #include #include "KoShapeBasedDocumentBase.h" @@ -43,17 +44,11 @@ } // read persistent application wide resources KSharedConfigPtr config = KSharedConfig::openConfig(); - if (config->hasGroup("Misc")) { - KConfigGroup miscGroup = config->group("Misc"); - const qreal pasteOffset = miscGroup.readEntry("CopyOffset", 10.0); - resourceManager->setPasteOffset(pasteOffset); - const bool pasteAtCursor = miscGroup.readEntry("PasteAtCursor", true); - resourceManager->enablePasteAtCursor(pasteAtCursor); - const uint grabSensitivity = miscGroup.readEntry("GrabSensitivity", 3); - resourceManager->setGrabSensitivity(grabSensitivity); - const uint handleRadius = miscGroup.readEntry("HandleRadius", 3); - resourceManager->setHandleRadius(handleRadius); - } + KConfigGroup miscGroup = config->group("Misc"); + const uint grabSensitivity = miscGroup.readEntry("GrabSensitivity", 3); + resourceManager->setGrabSensitivity(grabSensitivity); + const uint handleRadius = miscGroup.readEntry("HandleRadius", 3); + resourceManager->setHandleRadius(handleRadius); } ~KoShapeBasedDocumentBasePrivate() @@ -82,3 +77,11 @@ { return d->resourceManager; } + +QRectF KoShapeBasedDocumentBase::documentRect() const +{ + const qreal pxToPt = 72.0 / pixelsPerInch(); + + QTransform t = QTransform::fromScale(pxToPt, pxToPt); + return t.mapRect(documentRectInPixels()); +} diff --git a/libs/flake/KoShapeContainer.h b/libs/flake/KoShapeContainer.h --- a/libs/flake/KoShapeContainer.h +++ b/libs/flake/KoShapeContainer.h @@ -105,13 +105,6 @@ void removeShape(KoShape *shape); /** - * Remove all children to be completely separated from the container. - * - * All the shapes will only be removed from the container but not be deleted. - */ - void removeAllShapes(); - - /** * Return the current number of children registered. * @return the current number of children registered. */ @@ -211,6 +204,40 @@ */ KoShapeContainerModel *model() const; + + /** + * A special interface for KoShape to use during setParent call. Don't use + * these method directly for managing shapes hierarchy! Use shape->setParent() + * instead. + */ + struct ShapeInterface { + ShapeInterface(KoShapeContainer *_q); + + /** + * Add a child to this container. + * + * This container will NOT take over ownership of the shape. The caller or those creating + * the shape is responsible to delete it if not needed any longer. + * + * @param shape the child to be managed in the container. + */ + void addShape(KoShape *shape); + + /** + * Remove a child to be completely separated from the container. + * + * The shape will only be removed from this container but not be deleted. + * + * @param shape the child to be removed. + */ + void removeShape(KoShape *shape); + + protected: + KoShapeContainer *q; + }; + + ShapeInterface* shapeInterface(); + protected: /** * This hook is for inheriting classes that need to do something on adding/removing @@ -223,7 +250,7 @@ virtual void shapeChanged(ChangeType type, KoShape *shape = 0); /// constructor - KoShapeContainer(KoShapeContainerPrivate &); + KoShapeContainer(KoShapeContainerPrivate *); private: Q_DECLARE_PRIVATE(KoShapeContainer) diff --git a/libs/flake/KoShapeContainer.cpp b/libs/flake/KoShapeContainer.cpp --- a/libs/flake/KoShapeContainer.cpp +++ b/libs/flake/KoShapeContainer.cpp @@ -21,94 +21,74 @@ #include "KoShapeContainer_p.h" #include "KoShapeContainerModel.h" #include "KoShapeStrokeModel.h" -#include "KoShapeContainerDefaultModel.h" +#include "SimpleShapeContainerModel.h" #include "KoShapeSavingContext.h" #include "KoViewConverter.h" #include #include #include #include "kis_painting_tweaks.h" +#include "kis_assert.h" KoShapeContainerPrivate::KoShapeContainerPrivate(KoShapeContainer *q) : KoShapePrivate(q), - model(0) + shapeInterface(q), + model(0) { } KoShapeContainerPrivate::~KoShapeContainerPrivate() { delete model; } +KoShapeContainerPrivate::KoShapeContainerPrivate(const KoShapeContainerPrivate &rhs, KoShapeContainer *q) + : KoShapePrivate(rhs, q), + shapeInterface(q), + model(0) +{ +} + KoShapeContainer::KoShapeContainer(KoShapeContainerModel *model) - : KoShape(*(new KoShapeContainerPrivate(this))) + : KoShape(new KoShapeContainerPrivate(this)) { Q_D(KoShapeContainer); d->model = model; } -KoShapeContainer::KoShapeContainer(KoShapeContainerPrivate &dd) +KoShapeContainer::KoShapeContainer(KoShapeContainerPrivate *dd) : KoShape(dd) { + Q_D(KoShapeContainer); + + // HACK ALERT: the shapes are copied inside the model, + // but we still need to connect the to the + // hierarchy here! + if (d->model) { + Q_FOREACH (KoShape *shape, d->model->shapes()) { + shape->setParent(this); + } + } } KoShapeContainer::~KoShapeContainer() { Q_D(KoShapeContainer); if (d->model) { - Q_FOREACH (KoShape *shape, d->model->shapes()) { - delete shape; - } + d->model->deleteOwnedShapes(); } } void KoShapeContainer::addShape(KoShape *shape) { - Q_D(KoShapeContainer); - Q_ASSERT(shape); - if (shape->parent() == this && shapes().contains(shape)) - return; - // TODO add a method to create a default model depending on the shape container - if (d->model == 0) - d->model = new KoShapeContainerDefaultModel(); - if (shape->parent() && shape->parent() != this) - shape->parent()->removeShape(shape); - d->model->add(shape); shape->setParent(this); } void KoShapeContainer::removeShape(KoShape *shape) { - Q_D(KoShapeContainer); - Q_ASSERT(shape); - if (d->model == 0) - return; - d->model->remove(shape); shape->setParent(0); - - KoShapeContainer * grandparent = parent(); - if (grandparent) { - grandparent->model()->childChanged(this, KoShape::ChildChanged); - } -} - -void KoShapeContainer::removeAllShapes() -{ - Q_D(KoShapeContainer); - if (d->model == 0) - return; - for(int i = d->model->shapes().count() - 1; i >= 0; --i) { - KoShape *shape = d->model->shapes()[i]; - d->model->remove(shape); - shape->setParent(0); - } - - KoShapeContainer * grandparent = parent(); - if (grandparent) { - grandparent->model()->childChanged(this, KoShape::ChildChanged); - } } int KoShapeContainer::shapeCount() const @@ -153,60 +133,13 @@ void KoShapeContainer::paint(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintcontext) { + // Shape container paints only its internal component part. All the children are rendered + // by the shape manager itself + Q_D(KoShapeContainer); painter.save(); paintComponent(painter, converter, paintcontext); painter.restore(); - if (d->model == 0 || d->model->count() == 0) - return; - - QList sortedObjects = d->model->shapes(); - qSort(sortedObjects.begin(), sortedObjects.end(), KoShape::compareShapeZIndex); - - // Do the following to revert the absolute transformation of the container - // that is re-applied in shape->absoluteTransformation() later on. The transformation matrix - // of the container has already been applied once before this function is called. - QTransform baseMatrix = absoluteTransformation(&converter).inverted() * painter.transform(); - - // clip the children to the parent outline. - QTransform m; - qreal zoomX, zoomY; - converter.zoom(&zoomX, &zoomY); - m.scale(zoomX, zoomY); - painter.setClipPath(m.map(outline()), Qt::IntersectClip); - - QRectF toPaintRect = converter.viewToDocument(KisPaintingTweaks::safeClipBoundingRect(painter)); - toPaintRect = transform().mapRect(toPaintRect); - // We'll use this clipRect to see if our child shapes lie within it. - // Because shape->boundingRect() uses absoluteTransformation(0) we'll - // use that as well to have the same (absolute) reference transformation - // of our and the child's bounding boxes. - QTransform absTrans = absoluteTransformation(0); - QRectF clipRect = absTrans.map(outline()).boundingRect(); - - - Q_FOREACH (KoShape *shape, sortedObjects) { - //debugFlake <<"KoShapeContainer::painting shape:" << shape->shapeId() <<"," << shape->boundingRect(); - if (!shape->isVisible()) - continue; - if (!isClipped(shape)) // the shapeManager will have to draw those, or else we can't do clipRects - continue; - // don't try to draw a child shape that is not in the clipping rect of the painter. - if (!clipRect.intersects(shape->boundingRect())) - - continue; - - painter.save(); - painter.setTransform(shape->absoluteTransformation(&converter) * baseMatrix); - shape->paint(painter, converter, paintcontext); - painter.restore(); - if (shape->stroke()) { - painter.save(); - painter.setTransform(shape->absoluteTransformation(&converter) * baseMatrix); - shape->stroke()->paint(shape, painter, converter); - painter.restore(); - } - } } void KoShapeContainer::shapeChanged(ChangeType type, KoShape* shape) @@ -254,3 +187,54 @@ Q_D(const KoShapeContainer); return d->model; } + +KoShapeContainer::ShapeInterface *KoShapeContainer::shapeInterface() +{ + Q_D(KoShapeContainer); + return &d->shapeInterface; +} + +KoShapeContainer::ShapeInterface::ShapeInterface(KoShapeContainer *_q) + : q(_q) +{ +} + +void KoShapeContainer::ShapeInterface::addShape(KoShape *shape) +{ + KoShapeContainerPrivate * const d = q->d_func(); + + KIS_SAFE_ASSERT_RECOVER_RETURN(shape); + + if (shape->parent() == q && q->shapes().contains(shape)) { + return; + } + + // TODO add a method to create a default model depending on the shape container + if (!d->model) { + d->model = new SimpleShapeContainerModel(); + } + + if (shape->parent() && shape->parent() != q) { + shape->parent()->shapeInterface()->removeShape(shape); + } + + d->model->add(shape); + d->model->shapeHasBeenAddedToHierarchy(shape, q); +} + +void KoShapeContainer::ShapeInterface::removeShape(KoShape *shape) +{ + KoShapeContainerPrivate * const d = q->d_func(); + + KIS_SAFE_ASSERT_RECOVER_RETURN(shape); + KIS_SAFE_ASSERT_RECOVER_RETURN(d->model); + KIS_SAFE_ASSERT_RECOVER_RETURN(d->model->shapes().contains(shape)); + + d->model->shapeToBeRemovedFromHierarchy(shape, q); + d->model->remove(shape); + + KoShapeContainer *grandparent = q->parent(); + if (grandparent) { + grandparent->model()->childChanged(q, KoShape::ChildChanged); + } +} diff --git a/libs/flake/KoShapeContainerDefaultModel.h b/libs/flake/KoShapeContainerDefaultModel.h deleted file mode 100644 --- a/libs/flake/KoShapeContainerDefaultModel.h +++ /dev/null @@ -1,65 +0,0 @@ -/* This file is part of the KDE project - * Copyright (C) 2006-2007, 2010 Thomas Zander - * 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 KOSHAPECONTAINERDEFAULTMODEL_H -#define KOSHAPECONTAINERDEFAULTMODEL_H - -#include "KoShapeContainerModel.h" - -#include "kritaflake_export.h" - -/** - * A default implementation of the KoShapeContainerModel. - */ -class KRITAFLAKE_EXPORT KoShapeContainerDefaultModel : public KoShapeContainerModel -{ -public: - KoShapeContainerDefaultModel(); - virtual ~KoShapeContainerDefaultModel(); - - virtual void add(KoShape *shape); - - virtual void proposeMove(KoShape *shape, QPointF &move); - - virtual void setClipped(const KoShape *shape, bool clipping); - - virtual bool isClipped(const KoShape *shape) const; - - virtual void setInheritsTransform(const KoShape *shape, bool inherit); - - virtual bool inheritsTransform(const KoShape *shape) const; - - virtual void remove(KoShape *shape); - - virtual int count() const; - - virtual QList shapes() const; - - virtual bool isChildLocked(const KoShape *child) const; - - /// empty implementation. - virtual void containerChanged(KoShapeContainer *container, KoShape::ChangeType type); - -private: - class Private; - Private * const d; -}; - -#endif diff --git a/libs/flake/KoShapeContainerDefaultModel.cpp b/libs/flake/KoShapeContainerDefaultModel.cpp deleted file mode 100644 --- a/libs/flake/KoShapeContainerDefaultModel.cpp +++ /dev/null @@ -1,170 +0,0 @@ -/* This file is part of the KDE project - * Copyright (C) 2006-2007, 2010 Thomas Zander - * Copyright (C) 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. - */ - -#include "KoShapeContainerDefaultModel.h" - -#include "KoShapeContainer.h" - -class Q_DECL_HIDDEN KoShapeContainerDefaultModel::Private -{ -public: - class Relation - { - public: - explicit Relation(KoShape *child) - : inside(false), - inheritsTransform(false), - m_child(child) - {} - - KoShape* child() - { - return m_child; - } - - uint inside : 1; ///< if true, the child will be clipped by the parent. - uint inheritsTransform : 1; - - private: - KoShape *m_child; - }; - - ~Private() - { - qDeleteAll(relations); - } - - Relation* findRelation(const KoShape *child) const - { - foreach (Relation *relation, relations) { - if (relation->child() == child) { - return relation; - } - } - return 0; - } - - - // TODO use a QMap instead this should speed things up a bit - QList relations; -}; - -KoShapeContainerDefaultModel::KoShapeContainerDefaultModel() -: d(new Private()) -{ -} - -KoShapeContainerDefaultModel::~KoShapeContainerDefaultModel() -{ - delete d; -} - -void KoShapeContainerDefaultModel::add(KoShape *child) -{ - Private::Relation *r = new Private::Relation(child); - d->relations.append(r); -} - -void KoShapeContainerDefaultModel::proposeMove(KoShape *shape, QPointF &move) -{ - KoShapeContainer *parent = shape->parent(); - bool allowedToMove = true; - while (allowedToMove && parent) { - allowedToMove = parent->isEditable(); - parent = parent->parent(); - } - if (! allowedToMove) { - move.setX(0); - move.setY(0); - } -} - - -void KoShapeContainerDefaultModel::setClipped(const KoShape *child, bool clipping) -{ - Private::Relation *relation = d->findRelation(child); - if (relation == 0) - return; - if (relation->inside == clipping) - return; - relation->child()->update(); // mark old canvas-location as in need of repaint (aggregated) - relation->inside = clipping; - relation->child()->notifyChanged(); - relation->child()->update(); // mark new area as in need of repaint -} - -bool KoShapeContainerDefaultModel::isClipped(const KoShape *child) const -{ - Private::Relation *relation = d->findRelation(child); - return relation ? relation->inside: false; -} - -void KoShapeContainerDefaultModel::remove(KoShape *child) -{ - Private::Relation *relation = d->findRelation(child); - if (relation == 0) - return; - d->relations.removeAll(relation); - delete relation; -} - -int KoShapeContainerDefaultModel::count() const -{ - return d->relations.count(); -} - -QList KoShapeContainerDefaultModel::shapes() const -{ - QList answer; - Q_FOREACH (Private::Relation *relation, d->relations) { - answer.append(relation->child()); - } - return answer; -} - -bool KoShapeContainerDefaultModel::isChildLocked(const KoShape *child) const -{ - return child->isGeometryProtected(); -} - -void KoShapeContainerDefaultModel::containerChanged(KoShapeContainer *, KoShape::ChangeType) -{ -} - -void KoShapeContainerDefaultModel::setInheritsTransform(const KoShape *shape, bool inherit) -{ - Private::Relation *relation = d->findRelation(shape); - if (relation == 0) - return; - if (relation->inheritsTransform == inherit) - return; - relation->child()->update(); // mark old canvas-location as in need of repaint (aggregated) - relation->inheritsTransform = inherit; - relation->child()->notifyChanged(); - relation->child()->update(); // mark new area as in need of repaint -} - -bool KoShapeContainerDefaultModel::inheritsTransform(const KoShape *shape) const -{ - Private::Relation *relation = d->findRelation(shape); - return relation ? relation->inheritsTransform: false; -} - diff --git a/libs/flake/KoShapeContainerModel.h b/libs/flake/KoShapeContainerModel.h --- a/libs/flake/KoShapeContainerModel.h +++ b/libs/flake/KoShapeContainerModel.h @@ -46,6 +46,8 @@ /// destructor virtual ~KoShapeContainerModel(); + void deleteOwnedShapes(); + /** * Add a shape to this models store. * @param shape the shape to be managed in the container. @@ -168,6 +170,12 @@ * @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/KoShapeContainerModel.cpp b/libs/flake/KoShapeContainerModel.cpp --- a/libs/flake/KoShapeContainerModel.cpp +++ b/libs/flake/KoShapeContainerModel.cpp @@ -21,14 +21,28 @@ #include "KoShapeContainer.h" +#include "kis_assert.h" + KoShapeContainerModel::KoShapeContainerModel() { } KoShapeContainerModel::~KoShapeContainerModel() { } +void KoShapeContainerModel::deleteOwnedShapes() +{ + QList ownedShapes = this->shapes(); + + Q_FOREACH (KoShape *shape, ownedShapes) { + shape->setParent(0); + delete shape; + } + + KIS_SAFE_ASSERT_RECOVER_NOOP(!this->count()); +} + void KoShapeContainerModel::proposeMove(KoShape *child, QPointF &move) { Q_UNUSED(child); @@ -48,3 +62,24 @@ } } } + +void KoShapeContainerModel::shapeHasBeenAddedToHierarchy(KoShape *shape, KoShapeContainer *addedToSubtree) +{ + KoShapeContainer *parent = addedToSubtree->parent(); + if (parent) { + parent->model()->shapeHasBeenAddedToHierarchy(shape, parent); + } +} + +void KoShapeContainerModel::shapeToBeRemovedFromHierarchy(KoShape *shape, KoShapeContainer *removedFromSubtree) +{ + KoShapeContainer *parent = removedFromSubtree->parent(); + if (parent) { + parent->model()->shapeToBeRemovedFromHierarchy(shape, parent); + } +} + +KoShapeContainerModel::KoShapeContainerModel(const KoShapeContainerModel &rhs) +{ + Q_UNUSED(rhs); +} diff --git a/libs/flake/KoShapeContainer_p.h b/libs/flake/KoShapeContainer_p.h --- a/libs/flake/KoShapeContainer_p.h +++ b/libs/flake/KoShapeContainer_p.h @@ -20,6 +20,7 @@ #define KOSHAPECONTAINERPRIVATE_H #include "KoShape_p.h" +#include "KoShapeContainer.h" #include "kritaflake_export.h" class KoShapeContainerModel; @@ -33,6 +34,9 @@ explicit KoShapeContainerPrivate(KoShapeContainer *q); virtual ~KoShapeContainerPrivate(); + KoShapeContainerPrivate(const KoShapeContainerPrivate &rhs, KoShapeContainer *q); + + KoShapeContainer::ShapeInterface shapeInterface; KoShapeContainerModel *model; }; diff --git a/libs/flake/KoShapeController.h b/libs/flake/KoShapeController.h --- a/libs/flake/KoShapeController.h +++ b/libs/flake/KoShapeController.h @@ -119,6 +119,22 @@ void setShapeControllerBase(KoShapeBasedDocumentBase *shapeBasedDocument); /** + * 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. + */ + 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 diff --git a/libs/flake/KoShapeController.cpp b/libs/flake/KoShapeController.cpp --- a/libs/flake/KoShapeController.cpp +++ b/libs/flake/KoShapeController.cpp @@ -178,6 +178,21 @@ d->shapeBasedDocument = shapeBasedDocument; } +QRectF KoShapeController::documentRectInPixels() const +{ + return d->shapeBasedDocument ? d->shapeBasedDocument->documentRectInPixels() : QRectF(0,0,1920,1080); +} + +qreal KoShapeController::pixelsPerInch() const +{ + return d->shapeBasedDocument ? d->shapeBasedDocument->pixelsPerInch() : 72.0; +} + +QRectF KoShapeController::documentRect() const +{ + return d->shapeBasedDocument ? d->shapeBasedDocument->documentRect() : documentRectInPixels(); +} + KoDocumentResourceManager *KoShapeController::resourceManager() const { if (!d->shapeBasedDocument) diff --git a/libs/flake/KoShapeGroup.h b/libs/flake/KoShapeGroup.h --- a/libs/flake/KoShapeGroup.h +++ b/libs/flake/KoShapeGroup.h @@ -52,19 +52,26 @@ KoShapeGroup(); /// destructor virtual ~KoShapeGroup(); + + KoShape* cloneShape() const override; + /// This implementation is empty since a group is itself not visible. virtual void paintComponent(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintcontext); /// always returns false since the group itself can't be selected or hit virtual bool hitTest(const QPointF &position) const; - /// a group in flake doesn't have a size, this function just returns QSizeF(0,0) - virtual QSizeF size() const; + QSizeF size() const override; + void setSize(const QSizeF &size) override; + QRectF outlineRect() const override; /// a group's boundingRect - virtual QRectF boundingRect() const; + QRectF boundingRect() const override; /// reimplemented from KoShape virtual void saveOdf(KoShapeSavingContext &context) const; // reimplemented virtual bool loadOdf(const KoXmlElement &element, KoShapeLoadingContext &context); +private: + friend class ShapeGroupContainerModel; + /** * @brief Invalidate the size cache of the group * @@ -75,6 +82,9 @@ void invalidateSizeCache(); private: + KoShapeGroup(const KoShapeGroup &rhs); + +private: virtual void shapeChanged(ChangeType type, KoShape *shape = 0); Q_DECLARE_PRIVATE(KoShapeGroup) diff --git a/libs/flake/KoShapeGroup.cpp b/libs/flake/KoShapeGroup.cpp --- a/libs/flake/KoShapeGroup.cpp +++ b/libs/flake/KoShapeGroup.cpp @@ -42,6 +42,12 @@ ShapeGroupContainerModel(KoShapeGroup *group) : m_group(group) {} ~ShapeGroupContainerModel() override {} + ShapeGroupContainerModel(const ShapeGroupContainerModel &rhs, KoShapeGroup *group) + : SimpleShapeContainerModel(rhs), + m_group(group) + { + } + void add(KoShape *child) override { SimpleShapeContainerModel::add(child); @@ -82,28 +88,50 @@ { public: KoShapeGroupPrivate(KoShapeGroup *q) - : KoShapeContainerPrivate(q) + : KoShapeContainerPrivate(q) { model = new ShapeGroupContainerModel(q); } + KoShapeGroupPrivate(const KoShapeGroupPrivate &rhs, KoShapeGroup *q) + : KoShapeContainerPrivate(rhs, q) + { + ShapeGroupContainerModel *otherModel = dynamic_cast(rhs.model); + KIS_ASSERT_RECOVER_RETURN(otherModel); + model = new ShapeGroupContainerModel(*otherModel, q); + } + ~KoShapeGroupPrivate() override { } - mutable bool sizeCached; + mutable QRectF savedOutlineRect; + mutable bool sizeCached = false; + + void tryUpdateCachedSize() const; + + Q_DECLARE_PUBLIC(KoShapeGroup) }; KoShapeGroup::KoShapeGroup() - : KoShapeContainer(*(new KoShapeGroupPrivate(this))) + : KoShapeContainer(new KoShapeGroupPrivate(this)) +{ +} + +KoShapeGroup::KoShapeGroup(const KoShapeGroup &rhs) + : KoShapeContainer(new KoShapeGroupPrivate(*rhs.d_func(), this)) { - setSize(QSizeF(0, 0)); } KoShapeGroup::~KoShapeGroup() { } +KoShape *KoShapeGroup::cloneShape() const +{ + return new KoShapeGroup(*this); +} + void KoShapeGroup::paintComponent(QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &) { Q_UNUSED(painter); @@ -116,38 +144,53 @@ return false; } -QSizeF KoShapeGroup::size() const +void KoShapeGroupPrivate::tryUpdateCachedSize() const { - Q_D(const KoShapeGroup); - //debugFlake << "size" << d->size; - if (!d->sizeCached) { + Q_Q(const KoShapeGroup); + + if (!sizeCached) { QRectF bound; - Q_FOREACH (KoShape *shape, shapes()) { - if (bound.isEmpty()) - bound = shape->transformation().mapRect(shape->outlineRect()); - else - bound |= shape->transformation().mapRect(shape->outlineRect()); + Q_FOREACH (KoShape *shape, q->shapes()) { + bound |= shape->transformation().mapRect(shape->outlineRect()); } - d->size = bound.size(); - d->sizeCached = true; - debugFlake << "recalculated size" << d->size; + savedOutlineRect = bound; + size = bound.size(); + sizeCached = true; } +} + +QSizeF KoShapeGroup::size() const +{ + Q_D(const KoShapeGroup); + d->tryUpdateCachedSize(); return d->size; } +void KoShapeGroup::setSize(const QSizeF &size) +{ + QSizeF oldSize = this->size(); + if (!shapeCount() || oldSize.isNull()) return; + + const QTransform scale = + QTransform::fromScale(size.width() / oldSize.width(), size.height() / oldSize.height()); + + setTransformation(scale * transformation()); + + KoShapeContainer::setSize(size); +} + +QRectF KoShapeGroup::outlineRect() const +{ + Q_D(const KoShapeGroup); + + d->tryUpdateCachedSize(); + return d->savedOutlineRect; +} + QRectF KoShapeGroup::boundingRect() const { - bool first = true; - QRectF groupBound; - Q_FOREACH (KoShape* shape, shapes()) { - if (first) { - groupBound = shape->boundingRect(); - first = false; - } else { - groupBound = groupBound.united(shape->boundingRect()); - } - } + QRectF groupBound = KoShape::boundingRect(shapes()); if (shadow()) { KoInsets insets; @@ -228,23 +271,16 @@ KoShapeContainer::shapeChanged(type, shape); switch (type) { case KoShape::StrokeChanged: - { - KoShapeStrokeModel *str = stroke(); - if (str) { - if (str->deref()) - delete str; - setStroke(0); - } break; - } default: break; } + + invalidateSizeCache(); } void KoShapeGroup::invalidateSizeCache() { Q_D(KoShapeGroup); d->sizeCached = false; } - diff --git a/libs/flake/KoShapeLayer.cpp b/libs/flake/KoShapeLayer.cpp --- a/libs/flake/KoShapeLayer.cpp +++ b/libs/flake/KoShapeLayer.cpp @@ -48,16 +48,7 @@ QRectF KoShapeLayer::boundingRect() const { - QRectF bb; - - Q_FOREACH (KoShape* shape, shapes()) { - if (bb.isEmpty()) - bb = shape->boundingRect(); - else - bb = bb.united(shape->boundingRect()); - } - - return bb; + return KoShape::boundingRect(shapes()); } void KoShapeLayer::saveOdf(KoShapeSavingContext & context) const diff --git a/libs/flake/KoShapeLoadingContext.cpp b/libs/flake/KoShapeLoadingContext.cpp --- a/libs/flake/KoShapeLoadingContext.cpp +++ b/libs/flake/KoShapeLoadingContext.cpp @@ -75,7 +75,7 @@ if (d->documentResources) { KoMarkerCollection *markerCollection = d->documentResources->resource(KoDocumentResourceManager::MarkerCollection).value(); if (markerCollection) { - markerCollection->loadOdf(*this); + //markerCollection->loadOdf(*this); } } } diff --git a/libs/flake/KoShapeManager.h b/libs/flake/KoShapeManager.h --- a/libs/flake/KoShapeManager.h +++ b/libs/flake/KoShapeManager.h @@ -34,7 +34,6 @@ class KoViewConverter; class KoCanvasBase; class KoPointerEvent; -class KoShapeManagerPaintingStrategy; class KoShapePaintingContext; @@ -100,26 +99,13 @@ */ void addShape(KoShape *shape, KoShapeManager::Repaint repaint = PaintShapeOnAdd); - /** - * Add an additional shape to the manager. - * - * For additional shapes only updates are handled - */ - void addAdditional(KoShape *shape); /** * Remove a KoShape from this manager * @param shape the shape to remove */ void remove(KoShape *shape); - /** - * Remove an additional shape - * - * For additional shapes only updates are handled - */ - void removeAdditional(KoShape *shape); - public: /// return the selection shapes for this shapeManager KoSelection *selection() const; @@ -147,7 +133,7 @@ * @param rect the rectangle in the document coordinate system. * @param omitHiddenShapes if true, only visible shapes are considered */ - QList shapesAt(const QRectF &rect, bool omitHiddenShapes = true); + QList shapesAt(const QRectF &rect, bool omitHiddenShapes = true, bool containedMode = false); /** * Request a repaint to be queued. @@ -173,46 +159,51 @@ void notifyShapeChanged(KoShape *shape); /** - * Switch to editing the shape that is at the position of the event. - * This method will check select a shape at the event position and switch to the default tool - * for that shape, or switch to the default tool if there is no shape at the position. - * @param event the event that holds the point where to look for a shape. - */ - void suggestChangeTool(KoPointerEvent *event); - - /** * Paint a shape * * @param shape the shape to paint * @param painter the painter to paint to. * @param converter to convert between document and view coordinates. */ - void paintShape(KoShape *shape, QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext); + static void paintShape(KoShape *shape, QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext); /** - * Set the strategy of the KoShapeManager - * - * This can be used to change the behaviour of the painting of the shapes. - * @param strategy the new strategy. The ownership of the argument \p - * strategy will be taken by the shape manager. + * @brief renderSingleShape renders a shape on \p painter. This method includes all the + * needed steps for painting a single shape: setting transformations, clipping and masking. */ - void setPaintingStrategy(KoShapeManagerPaintingStrategy *strategy); + static void renderSingleShape(KoShape *shape, QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext); + + /** + * A special interface for KoShape to use during shape destruction. Don't use this + * interface directly unless you are KoShape. + */ + struct ShapeInterface { + ShapeInterface(KoShapeManager *_q); + + /** + * Called by a shape when it is destructed. Please note that you cannot access + * any shape's method type or information during this call because the shape might be + * semi-destroyed. + */ + void notifyShapeDestructed(KoShape *shape); + + protected: + KoShapeManager *q; + }; + + ShapeInterface* shapeInterface(); Q_SIGNALS: /// emitted when the selection is changed void selectionChanged(); /// emitted when an object in the selection is changed (moved/rotated etc) void selectionContentChanged(); /// emitted when any object changed (moved/rotated etc) void contentChanged(); - /// emitted when a shape is removed. - void shapeRemoved(KoShape *); /// emitted when any shape changed. void shapeChanged(KoShape *); private: - - friend class KoShapeManagerPaintingStrategy; KoCanvasBase *canvas(); diff --git a/libs/flake/KoShapeManager.cpp b/libs/flake/KoShapeManager.cpp --- a/libs/flake/KoShapeManager.cpp +++ b/libs/flake/KoShapeManager.cpp @@ -32,24 +32,29 @@ #include "KoShapeStrokeModel.h" #include "KoShapeGroup.h" #include "KoToolProxy.h" -#include "KoShapeManagerPaintingStrategy.h" #include "KoShapeShadow.h" #include "KoShapeLayer.h" #include "KoFilterEffect.h" #include "KoFilterEffectStack.h" #include "KoFilterEffectRenderContext.h" #include "KoShapeBackground.h" #include #include "KoClipPath.h" +#include "KoClipMaskPainter.h" #include "KoShapePaintingContext.h" #include "KoViewConverter.h" +#include "KisQPainterStateSaver.h" #include #include #include #include "kis_painting_tweaks.h" +bool KoShapeManager::Private::shapeUsedInRenderingTree(KoShape *shape) +{ + return !dynamic_cast(shape) && !dynamic_cast(shape); +} void KoShapeManager::Private::updateTree() { @@ -65,21 +70,22 @@ } foreach (KoShape *shape, aggregate4update) { + if (!shapeUsedInRenderingTree(shape)) continue; + tree.remove(shape); QRectF br(shape->boundingRect()); - strategy->adapt(shape, br); tree.insert(br, shape); } // do it again to see which shapes we intersect with _after_ moving. - foreach (KoShape *shape, aggregate4update) + foreach (KoShape *shape, aggregate4update) { detector.detect(tree, shape, shapeIndexesBeforeUpdate[shape]); + } aggregate4update.clear(); shapeIndexesBeforeUpdate.clear(); detector.fireSignals(); if (selectionModified) { - selection->updateSizeAndPosition(); emit q->selectionContentChanged(); } if (anyModified) { @@ -100,7 +106,7 @@ paintGroup(childGroup, painter, converter, paintContext); } else { painter.save(); - strategy->paint(child, painter, converter, paintContext); + KoShapeManager::renderSingleShape(child, painter, converter, paintContext); painter.restore(); } } @@ -132,7 +138,6 @@ delete d; } - void KoShapeManager::setShapes(const QList &shapes, Repaint repaint) { //clear selection @@ -143,6 +148,7 @@ d->aggregate4update.clear(); d->tree.clear(); d->shapes.clear(); + Q_FOREACH (KoShape *shape, shapes) { addShape(shape, repaint); } @@ -154,10 +160,12 @@ return; shape->priv()->addShapeManager(this); d->shapes.append(shape); - if (! dynamic_cast(shape) && ! dynamic_cast(shape)) { + + if (d->shapeUsedInRenderingTree(shape)) { QRectF br(shape->boundingRect()); d->tree.insert(br, shape); } + if (repaint == PaintShapeOnAdd) { shape->update(); } @@ -176,17 +184,6 @@ detector.fireSignals(); } -void KoShapeManager::addAdditional(KoShape *shape) -{ - if (shape) { - if (d->additionalShapes.contains(shape)) { - return; - } - shape->priv()->addShapeManager(this); - d->additionalShapes.append(shape); - } -} - void KoShapeManager::remove(KoShape *shape) { Private::DetectCollision detector; @@ -197,7 +194,10 @@ shape->priv()->removeShapeManager(this); d->selection->deselect(shape); d->aggregate4update.remove(shape); - d->tree.remove(shape); + + if (d->shapeUsedInRenderingTree(shape)) { + d->tree.remove(shape); + } d->shapes.removeAll(shape); // remove the children of a KoShapeContainer @@ -207,20 +207,34 @@ remove(containerShape); } } +} - // This signal is used in the annotation shape. - // FIXME: Is this really what we want? (and shouldn't it be called shapeDeleted()?) - shapeRemoved(shape); +KoShapeManager::ShapeInterface::ShapeInterface(KoShapeManager *_q) + : q(_q) +{ } -void KoShapeManager::removeAdditional(KoShape *shape) +void KoShapeManager::ShapeInterface::notifyShapeDestructed(KoShape *shape) { - if (shape) { - shape->priv()->removeShapeManager(this); - d->additionalShapes.removeAll(shape); + q->d->selection->deselect(shape); + q->d->aggregate4update.remove(shape); + + // we cannot access RTTI of the semi-destructed shape, so just + // unlink it lazily + if (q->d->tree.contains(shape)) { + q->d->tree.remove(shape); } + + q->d->shapes.removeAll(shape); +} + + +KoShapeManager::ShapeInterface *KoShapeManager::shapeInterface() +{ + return &d->shapeInterface; } + void KoShapeManager::paint(QPainter &painter, const KoViewConverter &converter, bool forPrint) { d->updateTree(); @@ -264,20 +278,10 @@ qSort(sortedShapes.begin(), sortedShapes.end(), KoShape::compareShapeZIndex); - foreach (KoShape *shape, sortedShapes) { - if (shape->parent() != 0 && shape->parent()->isClipped(shape)) - continue; + KoShapePaintingContext paintContext(d->canvas, forPrint); //FIXME - painter.save(); - - // apply shape clipping - KoClipPath::applyClipping(shape, painter, converter); - - // let the painting strategy paint the shape - KoShapePaintingContext paintContext(d->canvas, forPrint); //FIXME - d->strategy->paint(shape, painter, converter, paintContext); - - painter.restore(); + foreach (KoShape *shape, sortedShapes) { + renderSingleShape(shape, painter, converter, paintContext); } #ifdef CALLIGRA_RTREE_DEBUG @@ -297,6 +301,20 @@ } } +void KoShapeManager::renderSingleShape(KoShape *shape, QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) +{ + KisQPainterStateSaver saver(&painter); + + // apply shape clipping + KoClipPath::applyClipping(shape, painter, converter); + + // apply transformation + painter.setTransform(shape->absoluteTransformation(&converter) * painter.transform()); + + // paint the shape + paintShape(shape, painter, converter, paintContext); +} + void KoShapeManager::paintShape(KoShape *shape, QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) { qreal transparency = shape->transparency(true); @@ -310,15 +328,33 @@ painter.restore(); } if (!shape->filterEffectStack() || shape->filterEffectStack()->isEmpty()) { - painter.save(); - shape->paint(painter, converter, paintContext); - painter.restore(); + + QScopedPointer clipMaskPainter; + QPainter *shapePainter = &painter; + + KoClipMask *clipMask = shape->clipMask(); + if (clipMask) { + clipMaskPainter.reset(new KoClipMaskPainter(&painter, shape->boundingRect())); + shapePainter = clipMaskPainter->shapePainter(); + } + + shapePainter->save(); + shape->paint(*shapePainter, converter, paintContext); + shapePainter->restore(); if (shape->stroke()) { - painter.save(); - shape->stroke()->paint(shape, painter, converter); - painter.restore(); + shapePainter->save(); + shape->stroke()->paint(shape, *shapePainter, converter); + shapePainter->restore(); + } + + if (clipMask) { + shape->clipMask()->drawMask(clipMaskPainter->maskPainter(), shape); + clipMaskPainter->renderOnGlobalPainter(); } + } else { + // TODO: clipping mask is not implemented for this case! + // There are filter effects, then we need to prerender the shape on an image, to filter it QRectF shapeBound(QPointF(), shape->size()); // First step, compute the rectangle used for the image @@ -350,7 +386,7 @@ // the childrens matrix contains the groups matrix as well // so we have to compensate for that before painting the children imagePainter.setTransform(group->absoluteTransformation(&converter).inverted(), true); - d->paintGroup(group, imagePainter, converter, paintContext); + Private::paintGroup(group, imagePainter, converter, paintContext); } else { imagePainter.save(); shape->paint(imagePainter, converter, paintContext); @@ -479,25 +515,36 @@ return 0; // missed everything } -QList KoShapeManager::shapesAt(const QRectF &rect, bool omitHiddenShapes) +QList KoShapeManager::shapesAt(const QRectF &rect, bool omitHiddenShapes, bool containedMode) { d->updateTree(); - QList intersectedShapes(d->tree.intersects(rect)); - - for (int count = intersectedShapes.count() - 1; count >= 0; count--) { + QList shapes(containedMode ? d->tree.contained(rect) : d->tree.intersects(rect)); + + for (int count = shapes.count() - 1; count >= 0; count--) { - KoShape *shape = intersectedShapes.at(count); + KoShape *shape = shapes.at(count); - if (omitHiddenShapes && ! shape->isVisible(true)) { - intersectedShapes.removeAt(count); + if (omitHiddenShapes && !shape->isVisible(true)) { + shapes.removeAt(count); } else { const QPainterPath outline = shape->absoluteTransformation(0).map(shape->outline()); - if (! outline.intersects(rect) && ! outline.contains(rect)) { - intersectedShapes.removeAt(count); + + if (!containedMode && !outline.intersects(rect) && !outline.contains(rect)) { + shapes.removeAt(count); + + } else if (containedMode) { + + QPainterPath containingPath; + containingPath.addRect(rect); + + if (!containingPath.contains(outline)) { + shapes.removeAt(count); + } } } } - return intersectedShapes; + + return shapes; } void KoShapeManager::update(QRectF &rect, const KoShape *shape, bool selectionHandles) @@ -551,40 +598,6 @@ return d->selection; } -void KoShapeManager::suggestChangeTool(KoPointerEvent *event) -{ - QList shapes; - - KoShape *clicked = shapeAt(event->point); - if (clicked) { - if (! selection()->isSelected(clicked)) { - selection()->deselectAll(); - selection()->select(clicked); - } - shapes.append(clicked); - } - - QList shapes2; - foreach (KoShape *shape, shapes) { - QSet delegates = shape->toolDelegates(); - if (delegates.isEmpty()) { - shapes2.append(shape); - } else { - foreach (KoShape *delegatedShape, delegates) { - shapes2.append(delegatedShape); - } - } - } - KoToolManager::instance()->switchToolRequested( - KoToolManager::instance()->preferredToolForSelection(shapes2)); -} - -void KoShapeManager::setPaintingStrategy(KoShapeManagerPaintingStrategy *strategy) -{ - delete d->strategy; - d->strategy = strategy; -} - KoCanvasBase *KoShapeManager::canvas() { return d->canvas; diff --git a/libs/flake/KoShapeManagerPaintingStrategy.h b/libs/flake/KoShapeManagerPaintingStrategy.h deleted file mode 100644 --- a/libs/flake/KoShapeManagerPaintingStrategy.h +++ /dev/null @@ -1,81 +0,0 @@ -/* This file is part of the KDE project - - Copyright (C) 2007,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 KOSHAPEMANAGERPAINTINGSTRATEGY_H -#define KOSHAPEMANAGERPAINTINGSTRATEGY_H - -#include "kritaflake_export.h" - -class KoShapeManager; -class KoShape; -class KoViewConverter; -class KoShapePaintingContext; -class QPainter; -class QRectF; - -/** - * This implements the painting strategy for the KoShapeManager - * - * This is done to make it possible to have e.g. animations in kpresenter. - * - * This class implements the default strategy which is normally used. - */ -class KRITAFLAKE_EXPORT KoShapeManagerPaintingStrategy -{ -public: - explicit KoShapeManagerPaintingStrategy(KoShapeManager *shapeManager); - virtual ~KoShapeManagerPaintingStrategy(); - - /** - * Paint the shape - * - * @param shape the shape to paint - * @param painter the painter to paint to. - * @param converter to convert between document and view coordinates. - * @param forPrint if true, make sure only actual content is drawn and no decorations. - */ - virtual void paint(KoShape *shape, QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext); - - /** - * Adapt the rect the shape occupies - * - * @param rect rect which will be updated to give the rect the shape occupies. - */ - virtual void adapt(KoShape *shape, QRectF &rect); - - /** - * Set the shape manager - * - * This is needed in case you cannot set the shape manager when creating the paiting strategy. - * It needs to be set before you paint otherwise nothing will be painted. - * - * @param shapeManager The shape manager to use in the painting startegy - */ - void setShapeManager(KoShapeManager *shapeManager); - -protected: - KoShapeManager *shapeManager(); - -private: - class Private; - Private * const d; -}; - -#endif /* KOSHAPEMANAGERPAINTINGSTRATEGY_H */ diff --git a/libs/flake/KoShapeManagerPaintingStrategy.cpp b/libs/flake/KoShapeManagerPaintingStrategy.cpp deleted file mode 100644 --- a/libs/flake/KoShapeManagerPaintingStrategy.cpp +++ /dev/null @@ -1,71 +0,0 @@ -/* This file is part of the KDE project - - Copyright (C) 2007,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. -*/ - -#include "KoShapeManagerPaintingStrategy.h" - -#include "KoShape.h" -#include "KoShapeManager.h" -#include - -class Q_DECL_HIDDEN KoShapeManagerPaintingStrategy::Private -{ -public: - Private(KoShapeManager * manager) - : shapeManager(manager) - {} - - KoShapeManager * shapeManager; -}; - -KoShapeManagerPaintingStrategy::KoShapeManagerPaintingStrategy(KoShapeManager * shapeManager) -: d(new KoShapeManagerPaintingStrategy::Private(shapeManager)) -{ -} - -KoShapeManagerPaintingStrategy::~KoShapeManagerPaintingStrategy() -{ - delete d; -} - -void KoShapeManagerPaintingStrategy::paint(KoShape * shape, QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext) -{ - if (d->shapeManager) { - painter.save(); - painter.setTransform(shape->absoluteTransformation(&converter) * painter.transform()); - d->shapeManager->paintShape(shape, painter, converter, paintContext); - painter.restore(); // for the matrix - } -} - -void KoShapeManagerPaintingStrategy::adapt(KoShape * shape, QRectF & rect) -{ - Q_UNUSED(shape); - Q_UNUSED(rect); -} - -void KoShapeManagerPaintingStrategy::setShapeManager(KoShapeManager * shapeManager) -{ - d->shapeManager = shapeManager; -} - -KoShapeManager * KoShapeManagerPaintingStrategy::shapeManager() -{ - return d->shapeManager; -} diff --git a/libs/flake/KoShapeManager_p.h b/libs/flake/KoShapeManager_p.h --- a/libs/flake/KoShapeManager_p.h +++ b/libs/flake/KoShapeManager_p.h @@ -26,10 +26,9 @@ #include "KoShape.h" #include "KoShape_p.h" #include "KoShapeContainer.h" -#include "KoShapeManagerPaintingStrategy.h" +#include "KoShapeManager.h" #include -class KoShapeManager; class KoCanvasBase; class KoShapeGroup; class KoShapePaintingContext; @@ -42,14 +41,13 @@ : selection(new KoSelection()), canvas(c), tree(4, 2), - strategy(new KoShapeManagerPaintingStrategy(shapeManager)), - q(shapeManager) + q(shapeManager), + shapeInterface(shapeManager) { } ~Private() { delete selection; - delete strategy; } /** @@ -59,11 +57,17 @@ void updateTree(); /** + * Returns whether the shape should be added to the RTree for collision and ROI + * detection. + */ + bool shapeUsedInRenderingTree(KoShape *shape); + + /** * Recursively paints the given group shape to the specified painter * This is needed for filter effects on group shapes where the filter effect * applies to all the children of the group shape at once */ - void paintGroup(KoShapeGroup *group, QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext); + static void paintGroup(KoShapeGroup *group, QPainter &painter, const KoViewConverter &converter, KoShapePaintingContext &paintContext); class DetectCollision { @@ -104,8 +108,8 @@ KoRTree tree; QSet aggregate4update; QHash shapeIndexesBeforeUpdate; - KoShapeManagerPaintingStrategy *strategy; KoShapeManager *q; + KoShapeManager::ShapeInterface shapeInterface; }; #endif diff --git a/libs/flake/KoShapePaste.h b/libs/flake/KoShapePaste.h deleted file mode 100644 --- a/libs/flake/KoShapePaste.h +++ /dev/null @@ -1,58 +0,0 @@ -/* This file is part of the KDE project - Copyright (C) 2007 Thorsten Zachmann - 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 KOSHAPEPASTE_H -#define KOSHAPEPASTE_H - -#include -#include "kritaflake_export.h" - -#include - -class KoCanvasBase; -class KoShapeLayer; -class KoShape; - -/** - * Class for pasting shapes to the document - */ -class KRITAFLAKE_EXPORT KoShapePaste : public KoOdfPaste -{ -public: - /** - * Contructor - * - * @param canvas The canvas on which the paste is done - * @param parentLayer The layer on which the shapes will be pasted - */ - KoShapePaste(KoCanvasBase *canvas, KoShapeLayer *parentLayer); - virtual ~KoShapePaste(); - - QList pastedShapes() const; - -protected: - /// reimplemented - virtual bool process(const KoXmlElement & body, KoOdfReadStore &odfStore); - - class Private; - Private * const d; -}; - -#endif /* KOSHAPEPASTE_H */ diff --git a/libs/flake/KoShapePaste.cpp b/libs/flake/KoShapePaste.cpp deleted file mode 100644 --- a/libs/flake/KoShapePaste.cpp +++ /dev/null @@ -1,193 +0,0 @@ -/* This file is part of the KDE project - Copyright (C) 2007 Thorsten Zachmann - Copyright (C) 2009 Thomas Zander - Copyright (C) 2010-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 "KoShapePaste.h" - -#include -#include - -#include -#include -#include - -#include "KoCanvasBase.h" -#include "KoShapeController.h" -#include "KoShape.h" -#include "KoShapeLayer.h" -#include "KoShapeLoadingContext.h" -#include "KoShapeManager.h" -#include "KoShapeBasedDocumentBase.h" -#include "KoShapeRegistry.h" -#include "KoCanvasController.h" -#include "KoDocumentResourceManager.h" -#include "commands/KoShapeCreateCommand.h" -#include "KoViewConverter.h" - -class Q_DECL_HIDDEN KoShapePaste::Private -{ -public: - Private(KoCanvasBase *cb, KoShapeLayer *l) : canvas(cb), layer(l) {} - - KoCanvasBase *canvas; - KoShapeLayer *layer; - QList pastedShapes; -}; - -KoShapePaste::KoShapePaste(KoCanvasBase *canvas, KoShapeLayer *layer) - : d(new Private(canvas, layer)) -{ -} - -KoShapePaste::~KoShapePaste() -{ - delete d; -} - -bool KoShapePaste::process(const KoXmlElement & body, KoOdfReadStore & odfStore) -{ - d->pastedShapes.clear(); - KoOdfLoadingContext loadingContext(odfStore.styles(), odfStore.store()); - KoShapeLoadingContext context(loadingContext, d->canvas->shapeController()->resourceManager()); - - QList shapes(d->layer ? d->layer->shapes(): d->canvas->shapeManager()->topLevelShapes()); - - int zIndex = 0; - if (!shapes.isEmpty()) { - zIndex = shapes.first()->zIndex(); - foreach (KoShape * shape, shapes) { - zIndex = qMax(zIndex, shape->zIndex()); - } - ++zIndex; - } - context.setZIndex(zIndex); - - KoDocumentResourceManager *rm = d->canvas->shapeController()->resourceManager(); - Q_ASSERT(rm); - - QPointF pasteOffset(rm->pasteOffset(), rm->pasteOffset()); - const bool pasteAtCursor = rm->pasteAtCursor(); - - // get hold of the canvas' shape manager - KoShapeManager *sm = d->canvas->shapeManager(); - Q_ASSERT(sm); - - // TODO if this is a text create a text shape and load the text inside the new shape. - // create the shape from the clipboard - KoXmlElement element; - forEachElement(element, body) { - debugFlake << "loading shape" << element.localName(); - - KoShape * shape = KoShapeRegistry::instance()->createShapeFromOdf(element, context); - if (shape) { - d->pastedShapes << shape; - } - } - - if (d->pastedShapes.isEmpty()) - return true; - - // position shapes - if (pasteAtCursor) { - QRectF bbox; - // determine bounding rect of all pasted shapes - foreach (KoShape *shape, d->pastedShapes) { - if (bbox.isEmpty()) - bbox = shape->boundingRect(); - else - bbox |= shape->boundingRect(); - } - // where is the cursor now? - QWidget *canvasWidget = d->canvas->canvasWidget(); - KoCanvasController *cc = d->canvas->canvasController(); - // map mouse screen position to the canvas widget coordinates - QPointF mouseCanvasPos = canvasWidget->mapFromGlobal(QCursor::pos()); - // apply the canvas offset - mouseCanvasPos -= QPoint(cc->canvasOffsetX(), cc->canvasOffsetY()); - // apply offset of document origin - mouseCanvasPos -= d->canvas->documentOrigin(); - // convert to document coordinates - QPointF mouseDocumentPos = d->canvas->viewConverter()->viewToDocument(mouseCanvasPos); - // now we can determine the offset to apply, with the center of the pasted shapes - // bounding rect at the current mouse position - QPointF pasteOffset = mouseDocumentPos - bbox.center(); - foreach (KoShape *shape, d->pastedShapes) { - QPointF move(pasteOffset); - d->canvas->clipToDocument(shape, move); - if (move.x() != 0 || move.y() != 0) { - shape->setPosition(shape->position() + move); - } - } - } else { - foreach (KoShape *shape, d->pastedShapes) { - bool done = true; - do { - // find a nice place for our shape. - done = true; - foreach (const KoShape *s, sm->shapesAt(shape->boundingRect()) + d->pastedShapes) { - if (d->layer && s->parent() != d->layer) - continue; - if (s->name() != shape->name()) - continue; - if (qAbs(s->position().x() - shape->position().x()) > 0.001) - continue; - if (qAbs(s->position().y() - shape->position().y()) > 0.001) - continue; - if (qAbs(s->size().width() - shape->size().width()) > 0.001) - continue; - if (qAbs(s->size().height() - shape->size().height()) > 0.001) - continue; - // move it and redo our iteration. - QPointF move(pasteOffset); - d->canvas->clipToDocument(shape, move); - if (move.x() != 0 || move.y() != 0) { - shape->setPosition(shape->position() + move); - done = false; - break; - } - } - } while (!done); - } - } - - KUndo2Command *cmd = new KUndo2Command(kundo2_i18n("Paste Shapes")); - if (!cmd) { - qDeleteAll(d->pastedShapes); - d->pastedShapes.clear(); - return false; - } - - // add shapes to the document - foreach (KoShape *shape, d->pastedShapes) { - if (!shape->parent()) { - shape->setParent(d->layer); - } - d->canvas->shapeController()->addShapeDirect(shape, cmd); - } - - d->canvas->addCommand(cmd); - - return true; -} - -QList KoShapePaste::pastedShapes() const -{ - return d->pastedShapes; -} diff --git a/libs/flake/KoShapeSavingContext.cpp b/libs/flake/KoShapeSavingContext.cpp --- a/libs/flake/KoShapeSavingContext.cpp +++ b/libs/flake/KoShapeSavingContext.cpp @@ -261,12 +261,13 @@ QString KoShapeSavingContext::markerRef(const KoMarker *marker) { - QMap::iterator it = d->markerRefs.find(marker); - if (it == d->markerRefs.end()) { - it = d->markerRefs.insert(marker, marker->saveOdf(*this)); - } +// QMap::iterator it = d->markerRefs.find(marker); +// if (it == d->markerRefs.end()) { +// it = d->markerRefs.insert(marker, marker->saveOdf(*this)); +// } +// return it.value(); - return it.value(); + return QString(); } void KoShapeSavingContext::addDataCenter(KoDataCenterBase * dataCenter) diff --git a/libs/flake/KoShapeStroke.h b/libs/flake/KoShapeStroke.h --- a/libs/flake/KoShapeStroke.h +++ b/libs/flake/KoShapeStroke.h @@ -24,6 +24,7 @@ #ifndef KOSHAPESTROKE_H #define KOSHAPESTROKE_H +#include "KoFlakeTypes.h" #include "KoShapeStrokeModel.h" #include "kritaflake_export.h" @@ -101,9 +102,13 @@ // pure virtuals from KoShapeStrokeModel implemented here. virtual void fillStyle(KoGenStyle &style, KoShapeSavingContext &context) const; virtual void strokeInsets(const KoShape *shape, KoInsets &insets) const; + virtual qreal strokeMaxMarkersInset(const KoShape *shape) const; virtual bool hasTransparency() const; virtual void paint(KoShape *shape, QPainter &painter, const KoViewConverter &converter); - virtual void paint(KoShape *shape, QPainter &painter, const KoViewConverter &converter, const QColor &color); + + virtual bool compareFillTo(const KoShapeStrokeModel *other); + virtual bool compareStyleTo(const KoShapeStrokeModel *other); + virtual bool isVisible() const; private: class Private; diff --git a/libs/flake/KoShapeStroke.cpp b/libs/flake/KoShapeStroke.cpp --- a/libs/flake/KoShapeStroke.cpp +++ b/libs/flake/KoShapeStroke.cpp @@ -40,52 +40,154 @@ #include "KoShape.h" #include "KoShapeSavingContext.h" #include "KoPathShape.h" -#include "KoMarkerData.h" +#include "KoMarker.h" #include "KoInsets.h" +#include +#include +#include +#include "kis_global.h" class Q_DECL_HIDDEN KoShapeStroke::Private { public: + Private(KoShapeStroke *_q) : q(_q) {} + KoShapeStroke *q; + void paintBorder(KoShape *shape, QPainter &painter, const QPen &pen) const; QColor color; QPen pen; QBrush brush; }; +namespace { +QPair anglesForSegment(KoPathSegment segment) { + const qreal eps = 1e-6; + + if (segment.degree() < 3) { + segment = segment.toCubic(); + } + + QList points = segment.controlPoints(); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(points.size() == 4, qMakePair(0.0, 0.0)); + QPointF vec1 = points[1] - points[0]; + QPointF vec2 = points[3] - points[2]; + + if (vec1.manhattanLength() < eps) { + points[1] = segment.pointAt(eps); + vec1 = points[1] - points[0]; + } + + if (vec2.manhattanLength() < eps) { + points[2] = segment.pointAt(1.0 - eps); + vec2 = points[3] - points[2]; + } + + const qreal angle1 = std::atan2(vec1.y(), vec1.x()); + const qreal angle2 = std::atan2(vec2.y(), vec2.x()); + return qMakePair(angle1, angle2); +} +} + void KoShapeStroke::Private::paintBorder(KoShape *shape, QPainter &painter, const QPen &pen) const { if (!pen.isCosmetic() && pen.style() != Qt::NoPen) { KoPathShape *pathShape = dynamic_cast(shape); if (pathShape) { QPainterPath path = pathShape->pathStroke(pen); painter.fillPath(path, pen.brush()); + + if (!pathShape->hasMarkers()) return; + + const bool autoFillMarkers = pathShape->autoFillMarkers(); + KoMarker *startMarker = pathShape->marker(KoFlake::StartMarker); + KoMarker *midMarker = pathShape->marker(KoFlake::MidMarker); + KoMarker *endMarker = pathShape->marker(KoFlake::EndMarker); + + for (int i = 0; i < pathShape->subpathCount(); i++) { + const int numSubPoints = pathShape->subpathPointCount(i); + if (numSubPoints < 2) continue; + + const bool isClosedSubpath = pathShape->isClosedSubpath(i); + + qreal firstAngle = 0.0; + { + KoPathSegment segment = pathShape->segmentByIndex(KoPathPointIndex(i, 0)); + firstAngle= anglesForSegment(segment).first; + } + + const int numSegments = isClosedSubpath ? numSubPoints : numSubPoints - 1; + + qreal lastAngle = 0.0; + { + KoPathSegment segment = pathShape->segmentByIndex(KoPathPointIndex(i, numSegments - 1)); + lastAngle = anglesForSegment(segment).second; + } + + qreal previousAngle = 0.0; + + for (int j = 0; j < numSegments; j++) { + KoPathSegment segment = pathShape->segmentByIndex(KoPathPointIndex(i, j)); + QPair angles = anglesForSegment(segment); + + const qreal angle1 = angles.first; + const qreal angle2 = angles.second; + + if (j == 0 && startMarker) { + const qreal angle = isClosedSubpath ? bisectorAngle(firstAngle, lastAngle) : firstAngle; + if (autoFillMarkers) { + startMarker->applyShapeStroke(shape, q, segment.first()->point(), pen.widthF(), angle); + } + startMarker->paintAtPosition(&painter, segment.first()->point(), pen.widthF(), angle); + } + + if (j > 0 && midMarker) { + const qreal angle = bisectorAngle(previousAngle, angle1); + if (autoFillMarkers) { + midMarker->applyShapeStroke(shape, q, segment.first()->point(), pen.widthF(), angle); + } + midMarker->paintAtPosition(&painter, segment.first()->point(), pen.widthF(), angle); + } + + if (j == numSegments - 1 && endMarker) { + const qreal angle = isClosedSubpath ? bisectorAngle(firstAngle, lastAngle) : lastAngle; + if (autoFillMarkers) { + endMarker->applyShapeStroke(shape, q, segment.second()->point(), pen.widthF(), angle); + } + endMarker->paintAtPosition(&painter, segment.second()->point(), pen.widthF(), angle); + } + + previousAngle = angle2; + } + } + return; } + painter.strokePath(shape->outline(), pen); } } KoShapeStroke::KoShapeStroke() - : d(new Private()) + : d(new Private(this)) { d->color = QColor(Qt::black); // we are not rendering stroke with zero width anymore // so lets use a default width of 1.0 d->pen.setWidthF(1.0); } KoShapeStroke::KoShapeStroke(const KoShapeStroke &other) - : KoShapeStrokeModel(), d(new Private()) + : KoShapeStrokeModel(), d(new Private(this)) { d->color = other.d->color; d->pen = other.d->pen; d->brush = other.d->brush; } KoShapeStroke::KoShapeStroke(qreal lineWidth, const QColor &color) - : d(new Private()) + : d(new Private(this)) { d->pen.setWidthF(qMax(qreal(0.0), lineWidth)); d->pen.setJoinStyle(Qt::MiterJoin); @@ -144,6 +246,29 @@ insets.right = lineWidth; } +qreal KoShapeStroke::strokeMaxMarkersInset(const KoShape *shape) const +{ + qreal result = 0.0; + + const KoPathShape *pathShape = dynamic_cast(shape); + if (pathShape && pathShape->hasMarkers()) { + const qreal lineWidth = d->pen.widthF(); + + QVector markers; + markers << pathShape->marker(KoFlake::StartMarker); + markers << pathShape->marker(KoFlake::MidMarker); + markers << pathShape->marker(KoFlake::EndMarker); + + Q_FOREACH (const KoMarker *marker, markers) { + if (marker) { + result = qMax(result, marker->maxInset(lineWidth)); + } + } + } + + return result; +} + bool KoShapeStroke::hasTransparency() const { return d->color.alpha() > 0; @@ -163,14 +288,38 @@ d->paintBorder(shape, painter, pen); } -void KoShapeStroke::paint(KoShape *shape, QPainter &painter, const KoViewConverter &converter, const QColor &color) +bool KoShapeStroke::compareFillTo(const KoShapeStrokeModel *other) { - KoShape::applyConversion(painter, converter); + if (!other) return false; - QPen pen = d->pen; - pen.setColor(color); + const KoShapeStroke *stroke = dynamic_cast(other); + if (!stroke) return false; - d->paintBorder(shape, painter, pen); + return (d->brush.gradient() && d->brush == stroke->d->brush) || + (!d->brush.gradient() && d->color == stroke->d->color); +} + +bool KoShapeStroke::compareStyleTo(const KoShapeStrokeModel *other) +{ + if (!other) return false; + + const KoShapeStroke *stroke = dynamic_cast(other); + if (!stroke) return false; + + QPen pen1 = d->pen; + QPen pen2 = stroke->d->pen; + + // just a random color top avoid comparison of that property + pen1.setColor(Qt::magenta); + pen2.setColor(Qt::magenta); + + return pen1 == pen2; +} + +bool KoShapeStroke::isVisible() const +{ + return d->pen.widthF() > 0 && + (d->brush.gradient() || d->color.alpha() > 0); } void KoShapeStroke::setCapStyle(Qt::PenCapStyle style) @@ -225,10 +374,11 @@ void KoShapeStroke::setLineStyle(Qt::PenStyle style, const QVector &dashes) { - if (style < Qt::CustomDashLine) + if (style < Qt::CustomDashLine) { d->pen.setStyle(style); - else + } else { d->pen.setDashPattern(dashes); + } } Qt::PenStyle KoShapeStroke::lineStyle() const diff --git a/libs/flake/KoShapeStrokeModel.h b/libs/flake/KoShapeStrokeModel.h --- a/libs/flake/KoShapeStrokeModel.h +++ b/libs/flake/KoShapeStrokeModel.h @@ -25,6 +25,8 @@ #include "kritaflake_export.h" +#include + class KoShape; class KoGenStyle; class KoShapeSavingContext; @@ -62,6 +64,13 @@ * @param insets the insets object that will be filled and returned. */ virtual void strokeInsets(const KoShape *shape, KoInsets &insets) const = 0; + + /** + * Return a maximum distance that the markers of the shape can take outside the + * shape itself + */ + virtual qreal strokeMaxMarkersInset(const KoShape *shape) const = 0; + /** * Returns true if there is some transparency, false if the stroke is fully opaque. * @return if the stroke is transparent. @@ -77,18 +86,9 @@ */ virtual void paint(KoShape *shape, QPainter &painter, const KoViewConverter &converter) = 0; - /** - * Paint the stroke in the given color - * - * This method should paint the stroke around the shape in the given color. - * - * @param shape the shape to paint around - * @param painter the painter to paint to, the painter will have the topleft of the - * shape as its start coordinate. - * @param converter to convert between internal and view coordinates. - * @param color to use to paint the stroke. - */ - virtual void paint(KoShape *shape, QPainter &painter, const KoViewConverter &converter, const QColor &color) = 0; + virtual bool compareFillTo(const KoShapeStrokeModel *other) = 0; + virtual bool compareStyleTo(const KoShapeStrokeModel *other) = 0; + virtual bool isVisible() const = 0; /** * Increments the use-value. diff --git a/libs/flake/KoShapeUserData.h b/libs/flake/KoShapeUserData.h --- a/libs/flake/KoShapeUserData.h +++ b/libs/flake/KoShapeUserData.h @@ -49,6 +49,11 @@ /// Constructor explicit KoShapeUserData(QObject *parent = 0); virtual ~KoShapeUserData(); + + virtual KoShapeUserData* clone() const = 0; + +protected: + KoShapeUserData(const KoShapeUserData &rhs); }; #endif diff --git a/libs/flake/KoShapeUserData.cpp b/libs/flake/KoShapeUserData.cpp --- a/libs/flake/KoShapeUserData.cpp +++ b/libs/flake/KoShapeUserData.cpp @@ -27,3 +27,9 @@ KoShapeUserData::~KoShapeUserData() { } + +KoShapeUserData::KoShapeUserData(const KoShapeUserData &rhs) + : QObject() +{ + Q_UNUSED(rhs); +} diff --git a/libs/flake/KoShape_p.h b/libs/flake/KoShape_p.h --- a/libs/flake/KoShape_p.h +++ b/libs/flake/KoShape_p.h @@ -25,6 +25,9 @@ #include #include #include +#include + +#include class KoBorder; class KoShapeManager; @@ -35,6 +38,9 @@ public: explicit KoShapePrivate(KoShape *shape); virtual ~KoShapePrivate(); + + explicit KoShapePrivate(const KoShapePrivate &rhs, KoShape *q); + /** * Notify the shape that a change was done. To be used by inheriting shapes. * @param type the change type @@ -58,6 +64,7 @@ /// calls update on the shape where the stroke is. void updateStroke(); +public: // Members KoShape *q_ptr; // Points the shape that owns this class. @@ -73,14 +80,15 @@ KoShapeContainer *parent; QSet shapeManagers; QSet toolDelegates; - KoShapeUserData *userData; - KoShapeApplicationData *appData; - KoShapeStrokeModel *stroke; ///< points to a stroke, or 0 if there is no stroke + QScopedPointer userData; + QSharedPointer stroke; ///< points to a stroke, or 0 if there is no stroke QSharedPointer fill; ///< Stands for the background color / fill etc. QList dependees; ///< list of shape dependent on this shape + QList listeners; KoShapeShadow * shadow; ///< the current shape shadow KoBorder *border; ///< the current shape border - KoClipPath * clipPath; ///< the current clip path + QScopedPointer clipPath; ///< the current clip path + QScopedPointer clipMask; ///< the current clip mask QMap additionalAttributes; QMap additionalStyleAttributes; KoFilterEffectStack *filterEffectStack; ///< stack of filter effects applied to the shape @@ -105,8 +113,10 @@ qreal textRunAroundDistanceBottom; qreal textRunAroundThreshold; KoShape::TextRunAroundContour textRunAroundContour; - KoShapeAnchor *anchor; - qreal minimumHeight; + + +public: + /// Connection point converters /// Convert connection point position from shape coordinates, taking alignment into account void convertFromShapeCoordinates(KoConnectionPoint &point, const QSizeF &shapeSize) const; diff --git a/libs/flake/KoSnapGuide.h b/libs/flake/KoSnapGuide.h --- a/libs/flake/KoSnapGuide.h +++ b/libs/flake/KoSnapGuide.h @@ -90,10 +90,10 @@ QRectF boundingRect(); /// Adds an additional shape to snap to (useful when creating a path) - void setEditedShape(KoShape *shape); + void setAdditionalEditedShape(KoShape *shape); /// returns the extra shapes to use - KoShape *editedShape() const; + KoShape *additionalEditedShape() const; void enableSnapStrategy(Strategy type, bool value); bool isStrategyEnabled(Strategy type) const; diff --git a/libs/flake/KoSnapGuide.cpp b/libs/flake/KoSnapGuide.cpp --- a/libs/flake/KoSnapGuide.cpp +++ b/libs/flake/KoSnapGuide.cpp @@ -39,7 +39,7 @@ { public: Private(KoCanvasBase *parentCanvas) - : canvas(parentCanvas), editedShape(0), currentStrategy(0), + : canvas(parentCanvas), additionalEditedShape(0), currentStrategy(0), active(true), snapDistance(10) { @@ -51,7 +51,7 @@ } KoCanvasBase *canvas; - KoShape *editedShape; + KoShape *additionalEditedShape; typedef QSharedPointer KoSnapStrategySP; typedef QList StrategiesList; @@ -80,14 +80,14 @@ { } -void KoSnapGuide::setEditedShape(KoShape *shape) +void KoSnapGuide::setAdditionalEditedShape(KoShape *shape) { - d->editedShape = shape; + d->additionalEditedShape = shape; } -KoShape *KoSnapGuide::editedShape() const +KoShape *KoSnapGuide::additionalEditedShape() const { - return d->editedShape; + return d->additionalEditedShape; } void KoSnapGuide::enableSnapStrategy(Strategy type, bool value) @@ -267,7 +267,7 @@ void KoSnapGuide::reset() { d->currentStrategy.clear(); - d->editedShape = 0; + d->additionalEditedShape = 0; d->ignoredPoints.clear(); d->ignoredShapes.clear(); // remove all custom strategies diff --git a/libs/flake/KoSnapProxy.h b/libs/flake/KoSnapProxy.h --- a/libs/flake/KoSnapProxy.h +++ b/libs/flake/KoSnapProxy.h @@ -38,16 +38,16 @@ KoSnapProxy(KoSnapGuide *snapGuide); /// returns list of points in given rectangle in document coordinates - QList pointsInRect(const QRectF &rect); + QList pointsInRect(const QRectF &rect, bool omitEditedShape); /// returns list of shape in given rectangle in document coordinates QList shapesInRect(const QRectF &rect, bool omitEditedShape = false); /// returns list of points from given shape QList pointsFromShape(KoShape *shape); /// returns list of points in given rectangle in document coordinates - QList segmentsInRect(const QRectF &rect); + QList segmentsInRect(const QRectF &rect, bool omitEditedShape); /// returns list of all shapes QList shapes(bool omitEditedShape = false); diff --git a/libs/flake/KoSnapProxy.cpp b/libs/flake/KoSnapProxy.cpp --- a/libs/flake/KoSnapProxy.cpp +++ b/libs/flake/KoSnapProxy.cpp @@ -30,10 +30,10 @@ { } -QList KoSnapProxy::pointsInRect(const QRectF &rect) +QList KoSnapProxy::pointsInRect(const QRectF &rect, bool omitEditedShape) { QList points; - QList shapes = shapesInRect(rect); + QList shapes = shapesInRect(rect, omitEditedShape); Q_FOREACH (KoShape * shape, shapes) { Q_FOREACH (const QPointF & point, pointsFromShape(shape)) { if (rect.contains(point)) @@ -48,14 +48,26 @@ { QList shapes = m_snapGuide->canvas()->shapeManager()->shapesAt(rect); Q_FOREACH (KoShape * shape, m_snapGuide->ignoredShapes()) { - int index = shapes.indexOf(shape); - if (index >= 0) + const int index = shapes.indexOf(shape); + if (index >= 0) { shapes.removeAt(index); + } } - if (! omitEditedShape && m_snapGuide->editedShape()) { - QRectF bound = m_snapGuide->editedShape()->boundingRect(); + + + if (omitEditedShape) { + Q_FOREACH (KoPathPoint *point, m_snapGuide->ignoredPathPoints()) { + const int index = shapes.indexOf(point->parent()); + if (index >= 0) { + shapes.removeAt(index); + } + } + } + + if (!omitEditedShape && m_snapGuide->additionalEditedShape()) { + QRectF bound = m_snapGuide->additionalEditedShape()->boundingRect(); if (rect.intersects(bound) || rect.contains(bound)) - shapes.append(m_snapGuide->editedShape()); + shapes.append(m_snapGuide->additionalEditedShape()); } return shapes; } @@ -101,10 +113,10 @@ return snapPoints; } -QList KoSnapProxy::segmentsInRect(const QRectF &rect) +QList KoSnapProxy::segmentsInRect(const QRectF &rect, bool omitEditedShape) { - QList shapes = shapesInRect(rect, true); + QList shapes = shapesInRect(rect, omitEditedShape); QList ignoredPoints = m_snapGuide->ignoredPathPoints(); QList segments; @@ -145,16 +157,25 @@ // filter all hidden and ignored shapes Q_FOREACH (KoShape * shape, allShapes) { - - if (! shape->isVisible(true)) - continue; - if (ignoredShapes.contains(shape)) - continue; + if (shape->isVisible(true) && + !ignoredShapes.contains(shape)) { + + filteredShapes.append(shape); + } + } + + if (omitEditedShape) { + Q_FOREACH (KoPathPoint *point, m_snapGuide->ignoredPathPoints()) { + const int index = filteredShapes.indexOf(point->parent()); + if (index >= 0) { + filteredShapes.removeAt(index); + } + } + } - filteredShapes.append(shape); + if (!omitEditedShape && m_snapGuide->additionalEditedShape()) { + filteredShapes.append(m_snapGuide->additionalEditedShape()); } - if (! omitEditedShape && m_snapGuide->editedShape()) - filteredShapes.append(m_snapGuide->editedShape()); return filteredShapes; } diff --git a/libs/flake/KoSnapStrategy.cpp b/libs/flake/KoSnapStrategy.cpp --- a/libs/flake/KoSnapStrategy.cpp +++ b/libs/flake/KoSnapStrategy.cpp @@ -145,7 +145,7 @@ QRectF rect(-maxSnapDistance, -maxSnapDistance, maxSnapDistance, maxSnapDistance); rect.moveCenter(mousePosition); - QList points = proxy->pointsInRect(rect); + QList points = proxy->pointsInRect(rect, false); QPointF snappedPoint = mousePosition; foreach (const QPointF &point, points) { @@ -387,7 +387,7 @@ rect.moveCenter(mousePosition); QPointF snappedPoint = mousePosition; - QList segments = proxy->segmentsInRect(rect); + QList segments = proxy->segmentsInRect(rect, false); int segmentCount = segments.count(); for (int i = 0; i < segmentCount; ++i) { const KoPathSegment &s1 = segments[i]; @@ -510,12 +510,12 @@ rect.moveCenter(mousePosition); QPointF snappedPoint = mousePosition; - KoFlake::Position pointId[5] = { - KoFlake::TopLeftCorner, - KoFlake::TopRightCorner, - KoFlake::BottomRightCorner, - KoFlake::BottomLeftCorner, - KoFlake::CenteredPosition + KoFlake::AnchorPosition pointId[5] = { + KoFlake::TopLeft, + KoFlake::TopRight, + KoFlake::BottomRight, + KoFlake::BottomLeft, + KoFlake::Center }; QList shapes = proxy->shapesInRect(rect, true); diff --git a/libs/kundo2/kundo2commandextradata.h b/libs/flake/KoSvgPaste.h copy from libs/kundo2/kundo2commandextradata.h copy to libs/flake/KoSvgPaste.h --- a/libs/kundo2/kundo2commandextradata.h +++ b/libs/flake/KoSvgPaste.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,16 +16,23 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KUNDO2COMMANDEXTRADATA_H -#define __KUNDO2COMMANDEXTRADATA_H +#ifndef KOSVGPASTE_H +#define KOSVGPASTE_H -#include "kritaundo2_export.h" +#include "kritaflake_export.h" +#include +class KoShape; +class QRectF; +class QSizeF; -class KRITAUNDO2_EXPORT KUndo2CommandExtraData +class KRITAFLAKE_EXPORT KoSvgPaste { public: - virtual ~KUndo2CommandExtraData(); + KoSvgPaste(); + + bool hasShapes() const; + QList fetchShapes(const QRectF viewportInPx, qreal resolutionPPI, QSizeF *fragmentSize = 0) const; }; -#endif /* __KUNDO2COMMANDEXTRADATA_H */ +#endif // KOSVGPASTE_H diff --git a/libs/flake/KoSvgPaste.cpp b/libs/flake/KoSvgPaste.cpp new file mode 100644 --- /dev/null +++ b/libs/flake/KoSvgPaste.cpp @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2017 Dmitry Kazakov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "KoSvgPaste.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +KoSvgPaste::KoSvgPaste() +{ +} + +bool KoSvgPaste::hasShapes() const +{ + const QMimeData *mimeData = QApplication::clipboard()->mimeData(); + return mimeData && mimeData->hasFormat("image/svg+xml"); +} + +QList KoSvgPaste::fetchShapes(const QRectF viewportInPx, qreal resolutionPPI, QSizeF *fragmentSize) const +{ + QList shapes; + + const QMimeData *mimeData = QApplication::clipboard()->mimeData(); + if (!mimeData) return shapes; + + QByteArray data = mimeData->data("image/svg+xml"); + if (data.isEmpty()) return shapes; + + KoXmlDocument doc; + + QString errorMsg; + int errorLine = 0; + int errorColumn = 0; + + const bool documentValid = doc.setContent(data, false, &errorMsg, &errorLine, &errorColumn); + + if (!documentValid) { + errorFlake << "Failed to process an SVG file at" + << errorLine << ":" << errorColumn << "->" << errorMsg; + return shapes; + } + + KoDocumentResourceManager resourceManager; + SvgParser parser(&resourceManager); + parser.setResolution(viewportInPx, resolutionPPI); + + shapes = parser.parseSvg(doc.documentElement(), fragmentSize); + + return shapes; +} diff --git a/libs/flake/KoTextShapeDataBase.h b/libs/flake/KoTextShapeDataBase.h --- a/libs/flake/KoTextShapeDataBase.h +++ b/libs/flake/KoTextShapeDataBase.h @@ -133,7 +133,7 @@ protected: /// constructor - KoTextShapeDataBase(KoTextShapeDataBasePrivate &); + KoTextShapeDataBase(KoTextShapeDataBasePrivate *); KoTextShapeDataBasePrivate *d_ptr; diff --git a/libs/flake/KoTextShapeDataBase.cpp b/libs/flake/KoTextShapeDataBase.cpp --- a/libs/flake/KoTextShapeDataBase.cpp +++ b/libs/flake/KoTextShapeDataBase.cpp @@ -20,19 +20,28 @@ #include "KoTextShapeDataBase.h" #include "KoTextShapeDataBase_p.h" +#include + KoTextShapeDataBasePrivate::KoTextShapeDataBasePrivate() - : document(0) - , textAlignment(Qt::AlignLeft | Qt::AlignTop) + : textAlignment(Qt::AlignLeft | Qt::AlignTop) , resizeMethod(KoTextShapeDataBase::NoResize) { } +KoTextShapeDataBasePrivate::KoTextShapeDataBasePrivate(const KoTextShapeDataBasePrivate &rhs) + : document(rhs.document->clone()), + margins(rhs.margins), + textAlignment(rhs.textAlignment), + resizeMethod(rhs.resizeMethod) +{ +} + KoTextShapeDataBasePrivate::~KoTextShapeDataBasePrivate() { } -KoTextShapeDataBase::KoTextShapeDataBase(KoTextShapeDataBasePrivate &dd) - : d_ptr(&dd) +KoTextShapeDataBase::KoTextShapeDataBase(KoTextShapeDataBasePrivate *dd) + : d_ptr(dd) { } @@ -44,7 +53,7 @@ QTextDocument *KoTextShapeDataBase::document() const { Q_D(const KoTextShapeDataBase); - return d->document; + return d->document.data(); } void KoTextShapeDataBase::setShapeMargins(const KoInsets &margins) diff --git a/libs/flake/KoTextShapeDataBase_p.h b/libs/flake/KoTextShapeDataBase_p.h --- a/libs/flake/KoTextShapeDataBase_p.h +++ b/libs/flake/KoTextShapeDataBase_p.h @@ -31,7 +31,9 @@ KoTextShapeDataBasePrivate(); virtual ~KoTextShapeDataBasePrivate(); - QTextDocument *document; + KoTextShapeDataBasePrivate(const KoTextShapeDataBasePrivate &rhs); + + QScopedPointer document; KoInsets margins; Qt::Alignment textAlignment; KoTextShapeDataBase::ResizeMethod resizeMethod; diff --git a/libs/flake/KoToolBase.h b/libs/flake/KoToolBase.h --- a/libs/flake/KoToolBase.h +++ b/libs/flake/KoToolBase.h @@ -50,6 +50,7 @@ class QDragLeaveEvent; class QDropEvent; class QTouchEvent; +class QMenu; /** * Abstract base class for all tools. Tools can create or manipulate @@ -189,6 +190,13 @@ virtual void touchEvent(QTouchEvent *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. @@ -209,29 +217,6 @@ 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, @@ -302,18 +287,11 @@ * 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. - * If you reimplement this function make sure to also reimplement supportedPasteMimeTypes(). * @return will return true if pasting succeeded. False if nothing happened. */ virtual bool paste(); /** - * Returns the mimetypes that this tool's paste() function can handle - * @return QStringList containing the mimetypes that's supported by paste() - */ - virtual QStringList supportedPasteMimeTypes() const; - - /** * 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 @@ -339,9 +317,10 @@ virtual void dropEvent(QDropEvent *event, const QPointF &point); /** - * @return A list of actions to be used for a popup. + * @return a menu with context-aware actions for the currect selection. If + * the returned value is null, no context menu is shown. */ - QList popupActionList() const; + virtual QMenu* popupActionsMenu(); /// Returns the canvas the tool is working on KoCanvasBase *canvas() const; @@ -353,6 +332,31 @@ */ bool isInTextMode() const; + /** + * 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(); + public Q_SLOTS: /** @@ -375,7 +379,7 @@ * and should emit done when it is done. * @see deactivate() */ - virtual void activate(ToolActivation toolActivation, const QSet &shapes) = 0; + virtual void activate(ToolActivation toolActivation, const QSet &shapes); /** * This method is called whenever this tool is no longer the @@ -476,13 +480,6 @@ */ void addAction(const QString &name, QAction *action); - /** - * Set the list of actions to be used as popup menu. - * @param list the list of actions. - * @see popupActionList - */ - void setPopupActionList(const QList &list); - /// Convenience function to get the current handle radius uint handleRadius() const; @@ -520,6 +517,11 @@ */ void setMaskSyntheticEvents(bool value); + /** + * Returns true if activate() has been called (more times than deactivate :) ) + */ + bool isActivated() const; + protected: KoToolBase(KoToolBasePrivate &dd); diff --git a/libs/flake/KoToolBase.cpp b/libs/flake/KoToolBase.cpp --- a/libs/flake/KoToolBase.cpp +++ b/libs/flake/KoToolBase.cpp @@ -114,8 +114,24 @@ } } + +bool KoToolBase::isActivated() const +{ + Q_D(const KoToolBase); + return d->isActivated; +} + + +void KoToolBase::activate(KoToolBase::ToolActivation toolActivation, const QSet &shapes) +{ + Q_D(KoToolBase); + d->isActivated = true; +} + void KoToolBase::deactivate() { + Q_D(KoToolBase); + d->isActivated = false; } void KoToolBase::canvasResourceChanged(int key, const QVariant & res) @@ -165,6 +181,10 @@ event->ignore(); } +void KoToolBase::explicitUserStrokeEndRequest() +{ +} + QVariant KoToolBase::inputMethodQuery(Qt::InputMethodQuery query, const KoViewConverter &) const { Q_D(const KoToolBase); @@ -190,21 +210,6 @@ event->accept(); } -void KoToolBase::customPressEvent(KoPointerEvent * event) -{ - event->ignore(); -} - -void KoToolBase::customReleaseEvent(KoPointerEvent * event) -{ - event->ignore(); -} - -void KoToolBase::customMoveEvent(KoPointerEvent * event) -{ - event->ignore(); -} - bool KoToolBase::wantsTouch() const { return false; @@ -292,16 +297,9 @@ deleteSelection(); } -QList KoToolBase::popupActionList() const +QMenu *KoToolBase::popupActionsMenu() { - Q_D(const KoToolBase); - return d->popupActionList; -} - -void KoToolBase::setPopupActionList(const QList &list) -{ - Q_D(KoToolBase); - d->popupActionList = list; + return 0; } KoCanvasBase * KoToolBase::canvas() const @@ -363,11 +361,6 @@ d->isInTextMode=value; } -QStringList KoToolBase::supportedPasteMimeTypes() const -{ - return QStringList(); -} - bool KoToolBase::paste() { return false; @@ -415,6 +408,22 @@ return d->isInTextMode; } +void KoToolBase::requestUndoDuringStroke() +{ + /** + * Default implementation just cancells the stroke + */ + requestStrokeCancellation(); +} + +void KoToolBase::requestStrokeCancellation() +{ +} + +void KoToolBase::requestStrokeEnd() +{ +} + bool KoToolBase::maskSyntheticEvents() const { Q_D(const KoToolBase); diff --git a/libs/flake/KoToolBase_p.h b/libs/flake/KoToolBase_p.h --- a/libs/flake/KoToolBase_p.h +++ b/libs/flake/KoToolBase_p.h @@ -41,7 +41,8 @@ : currentCursor(Qt::ArrowCursor), q(qq), canvas(canvas_), - isInTextMode(false) + isInTextMode(false), + isActivated(false) { } @@ -78,11 +79,11 @@ QCursor currentCursor; QHash actions; QString toolId; - QList popupActionList; KoToolBase *q; KoCanvasBase *canvas; ///< the canvas interface this tool will work for. bool isInTextMode; bool maskSyntheticEvents{false}; ///< Whether this tool masks synthetic events + bool isActivated; }; #endif diff --git a/libs/flake/KoToolManager.h b/libs/flake/KoToolManager.h --- a/libs/flake/KoToolManager.h +++ b/libs/flake/KoToolManager.h @@ -230,9 +230,6 @@ /// Request tool activation for the given canvas controller void requestToolActivation(KoCanvasController *controller); - /// Injects an input event from a plugin based input device - void injectDeviceEvent(KoInputDeviceHandlerEvent *event); - /// Returns the toolId of the currently active tool QString activeToolId() const; diff --git a/libs/flake/KoToolManager.cpp b/libs/flake/KoToolManager.cpp --- a/libs/flake/KoToolManager.cpp +++ b/libs/flake/KoToolManager.cpp @@ -33,6 +33,7 @@ #include "KoShapeLayer.h" #include "KoShapeRegistry.h" #include "KoShapeManager.h" +#include "KoSelectedShapesProxy.h" #include "KoCanvasBase.h" #include "KoInputDeviceHandlerRegistry.h" #include "KoInputDeviceHandlerEvent.h" @@ -43,6 +44,8 @@ #include "kis_action_registry.h" #include "KoToolFactoryBase.h" +#include + // Qt + kde #include #include @@ -373,45 +376,36 @@ QString KoToolManager::preferredToolForSelection(const QList &shapes) { QList types; - Q_FOREACH (KoShape *shape, shapes) - if (! types.contains(shape->shapeId())) - types.append(shape->shapeId()); + Q_FOREACH (KoShape *shape, shapes) { + types << shape->shapeId(); + } + + KritaUtils::makeContainerUnique(types); QString toolType = KoInteractionTool_ID; int prio = INT_MAX; Q_FOREACH (ToolHelper *helper, d->tools) { + if (helper->id() == KoCreateShapesTool_ID) continue; + if (helper->priority() >= prio) continue; - if (helper->section() == KoToolFactoryBase::mainToolType()) - continue; bool toolWillWork = false; foreach (const QString &type, types) { if (helper->activationShapeId().split(',').contains(type)) { toolWillWork = true; break; } } + if (toolWillWork) { toolType = helper->id(); prio = helper->priority(); } } return toolType; } -void KoToolManager::injectDeviceEvent(KoInputDeviceHandlerEvent * event) -{ - if (d->canvasData && d->canvasData->canvas->canvas()) { - if (static_cast(event->type()) == KoInputDeviceHandlerEvent::ButtonPressed) - d->canvasData->activeTool->customPressEvent(event->pointerEvent()); - else if (static_cast(event->type()) == KoInputDeviceHandlerEvent::ButtonReleased) - d->canvasData->activeTool->customReleaseEvent(event->pointerEvent()); - else if (static_cast(event->type()) == KoInputDeviceHandlerEvent::PositionChanged) - d->canvasData->activeTool->customMoveEvent(event->pointerEvent()); - } -} - void KoToolManager::addDeferredToolFactory(KoToolFactoryBase *toolFactory) { ToolHelper *tool = new ToolHelper(toolFactory); @@ -693,14 +687,7 @@ KoSelection *selection = canvasData->activeTool->canvas()->shapeManager()->selection(); Q_ASSERT(selection); - Q_FOREACH (KoShape *shape, selection->selectedShapes()) { - QSet delegates = shape->toolDelegates(); - if (delegates.isEmpty()) { // no delegates, just the orig shape - shapesToOperateOn << shape; - } else { - shapesToOperateOn += delegates; - } - } + shapesToOperateOn = QSet::fromList(selection->selectedEditableShapesAndDelegates()); } if (canvasData->canvas->canvas()) { @@ -881,7 +868,7 @@ Connector *connector = new Connector(controller->canvas()->shapeManager()); connect(connector, SIGNAL(selectionChanged(QList)), q, SLOT(selectionChanged(QList))); - connect(controller->canvas()->shapeManager()->selection(), + connect(controller->canvas()->selectedShapesProxy(), SIGNAL(currentLayerChanged(const KoShapeLayer*)), q, SLOT(currentLayerChanged(const KoShapeLayer*))); @@ -1068,16 +1055,5 @@ } } -void KoToolManager::Private::switchToolByShortcut(QKeyEvent *event) -{ - QKeySequence item(event->key() | ((Qt::ControlModifier | Qt::AltModifier) & event->modifiers())); - - if (event->key() == Qt::Key_Space && event->modifiers() == 0) { - switchTool(KoPanTool_ID, true); - } else if (event->key() == Qt::Key_Escape && event->modifiers() == 0) { - switchTool(KoInteractionTool_ID, false); - } -} - //have to include this because of Q_PRIVATE_SLOT #include "moc_KoToolManager.cpp" diff --git a/libs/flake/KoToolManager_p.h b/libs/flake/KoToolManager_p.h --- a/libs/flake/KoToolManager_p.h +++ b/libs/flake/KoToolManager_p.h @@ -86,9 +86,6 @@ */ void registerToolProxy(KoToolProxy *proxy, KoCanvasBase *canvas); - void switchToolByShortcut(QKeyEvent *event); - - KoToolManager *q; QList tools; // list of all available tools via their factories. diff --git a/libs/flake/KoToolProxy.h b/libs/flake/KoToolProxy.h --- a/libs/flake/KoToolProxy.h +++ b/libs/flake/KoToolProxy.h @@ -44,6 +44,7 @@ class QTouchEvent; class QPainter; class QPointF; +class QMenu; /** * Tool proxy object which allows an application to address the current tool. @@ -110,13 +111,16 @@ void wheelEvent(KoPointerEvent *event); /// Forwarded to the current KoToolBase + void explicitUserStrokeEndRequest(); + + /// Forwarded to the current KoToolBase QVariant inputMethodQuery(Qt::InputMethodQuery query, const KoViewConverter &converter) const; /// Forwarded to the current KoToolBase void inputMethodEvent(QInputMethodEvent *event); /// Forwarded to the current KoToolBase - QList popupActionList() const; + QMenu* popupActionsMenu(); /// Forwarded to the current KoToolBase void deleteSelection(); @@ -144,9 +148,6 @@ bool paste(); /// Forwarded to the current KoToolBase - QStringList supportedPasteMimeTypes() const; - - /// Forwarded to the current KoToolBase void dragMoveEvent(QDragMoveEvent *event, const QPointF &point); /// Forwarded to the current KoToolBase @@ -161,6 +162,16 @@ /// \internal KoToolProxyPrivate *priv(); +protected Q_SLOTS: + /// Forwarded to the current KoToolBase + void requestUndoDuringStroke(); + + /// Forwarded to the current KoToolBase + void requestStrokeCancellation(); + + /// Forwarded to the current KoToolBase + void requestStrokeEnd(); + Q_SIGNALS: /** * A tool can have a selection that is copy-able, this signal is emitted when that status changes. diff --git a/libs/flake/KoToolProxy.cpp b/libs/flake/KoToolProxy.cpp --- a/libs/flake/KoToolProxy.cpp +++ b/libs/flake/KoToolProxy.cpp @@ -43,7 +43,6 @@ #include "KoShapeManager.h" #include "KoSelection.h" #include "KoShapeLayer.h" -#include "KoShapePaste.h" #include "KoShapeRegistry.h" #include "KoShapeController.h" #include "KoOdf.h" @@ -321,27 +320,12 @@ { // let us handle it as any other mousepress (where we then detect multi clicks mousePressEvent(event); - if (!event->isAccepted() && d->activeTool) - d->activeTool->canvas()->shapeManager()->suggestChangeTool(event); } void KoToolProxy::mouseMoveEvent(QMouseEvent *event, const QPointF &point) { - if (d->mouseLeaveWorkaround) { - d->mouseLeaveWorkaround = false; - return; - } - KoInputDevice id; - KoToolManager::instance()->priv()->switchInputDevice(id); - if (d->activeTool == 0) { - event->ignore(); - return; - } - KoPointerEvent ev(event, point); - d->activeTool->mouseMoveEvent(&ev); - - d->checkAutoScroll(ev); + mouseMoveEvent(&ev); } void KoToolProxy::mouseMoveEvent(KoPointerEvent *event) @@ -364,38 +348,8 @@ void KoToolProxy::mouseReleaseEvent(QMouseEvent *event, const QPointF &point) { - d->mouseLeaveWorkaround = false; - KoInputDevice id; - KoToolManager::instance()->priv()->switchInputDevice(id); - d->scrollTimer.stop(); - KoPointerEvent ev(event, point); - if (d->activeTool) { - d->activeTool->mouseReleaseEvent(&ev); - - if (! event->isAccepted() && event->button() == Qt::LeftButton && event->modifiers() == 0 - && qAbs(d->mouseDownPoint.x() - event->x()) < 5 - && qAbs(d->mouseDownPoint.y() - event->y()) < 5) { - // we potentially will change the selection - Q_ASSERT(d->activeTool->canvas()); - KoShapeManager *manager = d->activeTool->canvas()->shapeManager(); - Q_ASSERT(manager); - // only change the selection if that will not lead to losing a complex selection - if (manager->selection() && manager->selection()->count() <= 1) { - KoShape *shape = manager->shapeAt(point); - if (shape && !manager->selection()->isSelected(shape)) { // make the clicked shape the active one - manager->selection()->deselectAll(); - manager->selection()->select(shape); - QList shapes; - shapes << shape; - QString tool = KoToolManager::instance()->preferredToolForSelection(shapes); - KoToolManager::instance()->switchToolRequested(tool); - } - } - } - } else { - event->ignore(); - } + mouseReleaseEvent(&ev); } void KoToolProxy::mouseReleaseEvent(KoPointerEvent* event) @@ -407,27 +361,6 @@ if (d->activeTool) { d->activeTool->mouseReleaseEvent(event); - - if (!event->isAccepted() && event->button() == Qt::LeftButton && event->modifiers() == 0 - && qAbs(d->mouseDownPoint.x() - event->x()) < 5 - && qAbs(d->mouseDownPoint.y() - event->y()) < 5) { - // we potentially will change the selection - Q_ASSERT(d->activeTool->canvas()); - KoShapeManager *manager = d->activeTool->canvas()->shapeManager(); - Q_ASSERT(manager); - // only change the selection if that will not lead to losing a complex selection - if (manager->selection() && manager->selection()->count() <= 1) { - KoShape *shape = manager->shapeAt(event->point); - if (shape && !manager->selection()->isSelected(shape)) { // make the clicked shape the active one - manager->selection()->deselectAll(); - manager->selection()->select(shape); - QList shapes; - shapes << shape; - QString tool = KoToolManager::instance()->preferredToolForSelection(shapes); - KoToolManager::instance()->switchToolRequested(tool); - } - } - } } else { event->ignore(); } @@ -466,6 +399,13 @@ event->ignore(); } +void KoToolProxy::explicitUserStrokeEndRequest() +{ + if (d->activeTool) { + d->activeTool->explicitUserStrokeEndRequest(); + } +} + QVariant KoToolProxy::inputMethodQuery(Qt::InputMethodQuery query, const KoViewConverter &converter) const { if (d->activeTool) @@ -478,6 +418,11 @@ if (d->activeTool) d->activeTool->inputMethodEvent(event); } +QMenu *KoToolProxy::popupActionsMenu() +{ + return d->activeTool ? d->activeTool->popupActionsMenu() : 0; +} + void KoToolProxy::setActiveTool(KoToolBase *tool) { if (d->activeTool) @@ -522,23 +467,8 @@ bool success = false; KoCanvasBase *canvas = d->controller->canvas(); - if (d->activeTool && d->isActiveLayerEditable()) + if (d->activeTool && d->isActiveLayerEditable()) { success = d->activeTool->paste(); - - if (!success) { - const QMimeData *data = QApplication::clipboard()->mimeData(); - - if (data->hasFormat(KoOdf::mimeType(KoOdf::Text))) { - KoShapeManager *shapeManager = canvas->shapeManager(); - KoShapePaste paste(canvas, shapeManager->selection()->activeLayer()); - success = paste.paste(KoOdf::Text, data); - if (success) { - shapeManager->selection()->deselectAll(); - Q_FOREACH (KoShape *shape, paste.pastedShapes()) { - shapeManager->selection()->select(shape); - } - } - } } if (!success) { @@ -607,25 +537,10 @@ d->activeTool->dropEvent(event, point); } -QStringList KoToolProxy::supportedPasteMimeTypes() const -{ - if (d->activeTool) - return d->activeTool->supportedPasteMimeTypes(); - - return QStringList(); -} - -QList KoToolProxy::popupActionList() const -{ - if (d->activeTool) - return d->activeTool->popupActionList(); - return QList(); -} - void KoToolProxy::deleteSelection() { if (d->activeTool) - return d->activeTool->deleteSelection(); + d->activeTool->deleteSelection(); } void KoToolProxy::processEvent(QEvent *e) const @@ -639,6 +554,27 @@ } } +void KoToolProxy::requestUndoDuringStroke() +{ + if (d->activeTool) { + d->activeTool->requestUndoDuringStroke(); + } +} + +void KoToolProxy::requestStrokeCancellation() +{ + if (d->activeTool) { + d->activeTool->requestStrokeCancellation(); + } +} + +void KoToolProxy::requestStrokeEnd() +{ + if (d->activeTool) { + d->activeTool->requestStrokeEnd(); + } +} + KoToolProxyPrivate *KoToolProxy::priv() { return d; diff --git a/libs/flake/KoTosContainer.h b/libs/flake/KoTosContainer.h --- a/libs/flake/KoTosContainer.h +++ b/libs/flake/KoTosContainer.h @@ -95,7 +95,7 @@ protected: /// constructor - KoTosContainer(KoTosContainerPrivate &); + KoTosContainer(KoTosContainerPrivate *); //reimplemented void loadStyle(const KoXmlElement &element, KoShapeLoadingContext &context); @@ -125,7 +125,7 @@ virtual void shapeChanged(ChangeType type, KoShape *shape = 0); -private: +protected: Q_DECLARE_PRIVATE(KoTosContainer) }; diff --git a/libs/flake/KoTosContainer.cpp b/libs/flake/KoTosContainer.cpp --- a/libs/flake/KoTosContainer.cpp +++ b/libs/flake/KoTosContainer.cpp @@ -43,22 +43,30 @@ { } +KoTosContainerPrivate::KoTosContainerPrivate(const KoTosContainerPrivate &rhs, KoShapeContainer *q) + : KoShapeContainerPrivate(rhs, q), + resizeBehavior(rhs.resizeBehavior), + preferredTextRect(rhs.preferredTextRect), + alignment(rhs.alignment) +{ +} + KoTosContainerPrivate::~KoTosContainerPrivate() { } KoTosContainer::KoTosContainer() - : KoShapeContainer(*(new KoTosContainerPrivate(this))) + : KoShapeContainer(new KoTosContainerPrivate(this)) { } KoTosContainer::~KoTosContainer() { delete textShape(); } -KoTosContainer::KoTosContainer(KoTosContainerPrivate &dd) +KoTosContainer::KoTosContainer(KoTosContainerPrivate *dd) : KoShapeContainer(dd) { } diff --git a/libs/flake/KoTosContainerModel.cpp b/libs/flake/KoTosContainerModel.cpp --- a/libs/flake/KoTosContainerModel.cpp +++ b/libs/flake/KoTosContainerModel.cpp @@ -40,6 +40,7 @@ KoTextShapeDataBase *shapeData = qobject_cast(shape->userData()); Q_ASSERT(shapeData != 0); if (shapeData) { + delete m_textShape; m_textShape = shape; } } diff --git a/libs/flake/KoTosContainer_p.h b/libs/flake/KoTosContainer_p.h --- a/libs/flake/KoTosContainer_p.h +++ b/libs/flake/KoTosContainer_p.h @@ -1,15 +1,17 @@ #ifndef KOTOSCONTAINER_P_H #define KOTOSCONTAINER_P_H +#include "kritaflake_export.h" #include "KoShapeContainer_p.h" #include "KoTosContainer.h" -class KoTosContainerPrivate : public KoShapeContainerPrivate +class KRITAFLAKE_EXPORT KoTosContainerPrivate : public KoShapeContainerPrivate { public: explicit KoTosContainerPrivate(KoShapeContainer *q); + explicit KoTosContainerPrivate(const KoTosContainerPrivate &rhs, KoShapeContainer *q); virtual ~KoTosContainerPrivate(); diff --git a/libs/flake/KoUnavailShape.cpp b/libs/flake/KoUnavailShape.cpp --- a/libs/flake/KoUnavailShape.cpp +++ b/libs/flake/KoUnavailShape.cpp @@ -44,7 +44,7 @@ #include #include "KoShapeLoadingContext.h" #include "KoShapeSavingContext.h" -#include "KoShapeContainerDefaultModel.h" +#include "SimpleShapeContainerModel.h" #include "KoShapeBackground.h" #include @@ -171,7 +171,7 @@ KoUnavailShape::KoUnavailShape() : KoFrameShape( "", "" ) -, KoShapeContainer(new KoShapeContainerDefaultModel()) +, KoShapeContainer(new SimpleShapeContainerModel()) , d(new Private(this)) { setShapeId(KoUnavailShape_SHAPEID); diff --git a/libs/flake/KoVectorPatternBackground.h b/libs/flake/KoVectorPatternBackground.h new file mode 100644 --- /dev/null +++ b/libs/flake/KoVectorPatternBackground.h @@ -0,0 +1,68 @@ +/* + * 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 KOVECTORPATTERNBACKGROUND_H +#define KOVECTORPATTERNBACKGROUND_H + +#include +#include + +class KoShape; +class QPointF; +class QRectF; +class QTransform; +class KoVectorPatternBackgroundPrivate; + + +class KoVectorPatternBackground : public KoShapeBackground +{ +public: + KoVectorPatternBackground(); + ~KoVectorPatternBackground(); + + bool compareTo(const KoShapeBackground *other) const override; + + void setReferenceCoordinates(KoFlake::CoordinateSystem value); + KoFlake::CoordinateSystem referenceCoordinates() const; + + /** + * In ViewBox just use the same mode as for referenceCoordinates + */ + void setContentCoordinates(KoFlake::CoordinateSystem value); + KoFlake::CoordinateSystem contentCoordinates() const; + + void setReferenceRect(const QRectF &value); + QRectF referenceRect() const; + + void setPatternTransform(const QTransform &value); + QTransform patternTransform() const; + + void setShapes(const QList value); + QList shapes() const; + + void paint(QPainter &painter, const KoViewConverter &converter_Unused, KoShapePaintingContext &context_Unused, const QPainterPath &fillPath) const override; + bool hasTransparency() const override; + void fillStyle(KoGenStyle &style, KoShapeSavingContext &context) override; + bool loadStyle(KoOdfLoadingContext &context, const QSizeF &shapeSize) override; + +private: + Q_DECLARE_PRIVATE(KoVectorPatternBackground) + Q_DISABLE_COPY(KoVectorPatternBackground) +}; + +#endif // KOVECTORPATTERNBACKGROUND_H diff --git a/libs/flake/KoVectorPatternBackground.cpp b/libs/flake/KoVectorPatternBackground.cpp new file mode 100644 --- /dev/null +++ b/libs/flake/KoVectorPatternBackground.cpp @@ -0,0 +1,176 @@ +/* + * 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 "KoVectorPatternBackground.h" + +#include +#include +#include +#include "KoShapeBackground_p.h" +#include +#include + +class KoVectorPatternBackgroundPrivate : public KoShapeBackgroundPrivate +{ +public: + KoVectorPatternBackgroundPrivate() + { + } + + ~KoVectorPatternBackgroundPrivate() + { + qDeleteAll(shapes); + shapes.clear(); + } + + QList shapes; + KoFlake::CoordinateSystem referenceCoordinates = + KoFlake::ObjectBoundingBox; + KoFlake::CoordinateSystem contentCoordinates = + KoFlake::UserSpaceOnUse; + QRectF referenceRect; + QTransform patternTransform; +}; + +KoVectorPatternBackground::KoVectorPatternBackground() + : KoShapeBackground(*(new KoVectorPatternBackgroundPrivate())) +{ +} + +KoVectorPatternBackground::~KoVectorPatternBackground() +{ + +} + +bool KoVectorPatternBackground::compareTo(const KoShapeBackground *other) const +{ + Q_UNUSED(other); + return false; +} + +void KoVectorPatternBackground::setReferenceCoordinates(KoFlake::CoordinateSystem value) +{ + Q_D(KoVectorPatternBackground); + d->referenceCoordinates = value; +} + +KoFlake::CoordinateSystem KoVectorPatternBackground::referenceCoordinates() const +{ + Q_D(const KoVectorPatternBackground); + return d->referenceCoordinates; +} + +void KoVectorPatternBackground::setContentCoordinates(KoFlake::CoordinateSystem value) +{ + Q_D(KoVectorPatternBackground); + d->contentCoordinates = value; +} + +KoFlake::CoordinateSystem KoVectorPatternBackground::contentCoordinates() const +{ + Q_D(const KoVectorPatternBackground); + return d->contentCoordinates; +} + +void KoVectorPatternBackground::setReferenceRect(const QRectF &value) +{ + Q_D(KoVectorPatternBackground); + d->referenceRect = value; +} + +QRectF KoVectorPatternBackground::referenceRect() const +{ + Q_D(const KoVectorPatternBackground); + return d->referenceRect; +} + +void KoVectorPatternBackground::setPatternTransform(const QTransform &value) +{ + Q_D(KoVectorPatternBackground); + d->patternTransform = value; +} + +QTransform KoVectorPatternBackground::patternTransform() const +{ + Q_D(const KoVectorPatternBackground); + return d->patternTransform; +} + +void KoVectorPatternBackground::setShapes(const QList value) +{ + Q_D(KoVectorPatternBackground); + qDeleteAll(d->shapes); + d->shapes.clear(); + + d->shapes = value; +} + +QList KoVectorPatternBackground::shapes() const +{ + Q_D(const KoVectorPatternBackground); + return d->shapes; +} + +void KoVectorPatternBackground::paint(QPainter &painter, const KoViewConverter &converter_Unused, KoShapePaintingContext &context_Unused, const QPainterPath &fillPath) const +{ + Q_UNUSED(context_Unused); + Q_UNUSED(converter_Unused); + + Q_D(const KoVectorPatternBackground); + + const QPainterPath dstShapeOutline = fillPath; + const QRectF dstShapeBoundingBox = dstShapeOutline.boundingRect(); + + KoBakedShapeRenderer renderer(dstShapeOutline, QTransform(), + QTransform(), + d->referenceRect, + d->contentCoordinates != KoFlake::UserSpaceOnUse, + dstShapeBoundingBox, + d->referenceCoordinates != KoFlake::UserSpaceOnUse, + d->patternTransform); + + QPainter *patchPainter = renderer.bakeShapePainter(); + + KoViewConverter converter; + KoShapePainter p; + p.setShapes(d->shapes); + p.paint(*patchPainter, converter); + + // uncomment for debug + // renderer.patchImage().save("dd_patch_image.png"); + + painter.setPen(Qt::NoPen); + renderer.renderShape(painter); +} + +bool KoVectorPatternBackground::hasTransparency() const +{ + return true; +} + +void KoVectorPatternBackground::fillStyle(KoGenStyle &, KoShapeSavingContext &) +{ + // noop +} + +bool KoVectorPatternBackground::loadStyle(KoOdfLoadingContext &, const QSizeF &Size) +{ + Q_UNUSED(Size); + return true; +} + diff --git a/libs/flake/SimpleShapeContainerModel.h b/libs/flake/SimpleShapeContainerModel.h --- a/libs/flake/SimpleShapeContainerModel.h +++ b/libs/flake/SimpleShapeContainerModel.h @@ -21,24 +21,58 @@ #define SIMPLESHAPECONTAINERMODEL_H #include "KoShapeContainerModel.h" +#include /// \internal class SimpleShapeContainerModel: public KoShapeContainerModel { public: SimpleShapeContainerModel() {} ~SimpleShapeContainerModel() {} + + SimpleShapeContainerModel(const SimpleShapeContainerModel &rhs) + : KoShapeContainerModel(rhs), + m_inheritsTransform(rhs.m_inheritsTransform), + m_clipped(rhs.m_clipped) + { + Q_FOREACH (KoShape *shape, rhs.m_members) { + m_members << shape->cloneShape(); + } + + KIS_ASSERT_RECOVER(m_members.size() == m_inheritsTransform.size() && + m_members.size() == m_clipped.size()) + { + qDeleteAll(m_members); + m_members.clear(); + m_inheritsTransform.clear(); + m_clipped.clear(); + } + } + void add(KoShape *child) { if (m_members.contains(child)) return; m_members.append(child); + m_clipped.append(false); + m_inheritsTransform.append(true); } - void setClipped(const KoShape *, bool) { } - bool isClipped(const KoShape *) const { - return false; + void setClipped(const KoShape *shape, bool value) { + const int index = indexOf(shape); + KIS_SAFE_ASSERT_RECOVER_RETURN(index >= 0); + m_clipped[index] = value; } - void remove(KoShape *child) { - m_members.removeAll(child); + bool isClipped(const KoShape *shape) const { + const int index = indexOf(shape); + KIS_SAFE_ASSERT_RECOVER(index >= 0) { return false;} + return m_clipped[index]; + } + void remove(KoShape *shape) { + const int index = indexOf(shape); + KIS_SAFE_ASSERT_RECOVER_RETURN(index >= 0); + + m_members.removeAt(index); + m_clipped.removeAt(index); + m_inheritsTransform.removeAt(index); } int count() const { return m_members.count(); @@ -56,13 +90,41 @@ return child->isGeometryProtected(); } } - void setInheritsTransform(const KoShape *, bool ) { } - bool inheritsTransform(const KoShape *) const { - return false; + void setInheritsTransform(const KoShape *shape, bool value) { + const int index = indexOf(shape); + KIS_SAFE_ASSERT_RECOVER_RETURN(index >= 0); + m_inheritsTransform[index] = value; + } + bool inheritsTransform(const KoShape *shape) const { + const int index = indexOf(shape); + KIS_SAFE_ASSERT_RECOVER(index >= 0) { return true;} + return m_inheritsTransform[index]; + } + + void proposeMove(KoShape *shape, QPointF &move) + { + KoShapeContainer *parent = shape->parent(); + bool allowedToMove = true; + while (allowedToMove && parent) { + allowedToMove = parent->isEditable(); + parent = parent->parent(); + } + if (! allowedToMove) { + move.setX(0); + move.setY(0); + } + } + +private: + int indexOf(const KoShape *shape) const { + // workaround indexOf constness! + return m_members.indexOf(const_cast(shape)); } private: // members QList m_members; + QList m_inheritsTransform; + QList m_clipped; }; #endif diff --git a/libs/flake/commands/KoMultiPathPointJoinCommand.h b/libs/flake/commands/KoMultiPathPointJoinCommand.h new file mode 100644 --- /dev/null +++ b/libs/flake/commands/KoMultiPathPointJoinCommand.h @@ -0,0 +1,38 @@ +/* + * 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 KOMULTIPATHPOINTJOINCOMMAND_H +#define KOMULTIPATHPOINTJOINCOMMAND_H + +#include + +class KRITAFLAKE_EXPORT KoMultiPathPointJoinCommand : public KoMultiPathPointMergeCommand +{ +public: + KoMultiPathPointJoinCommand(const KoPathPointData &pointData1, + const KoPathPointData &pointData2, + KoShapeBasedDocumentBase *controller, + KoSelection *selection, + KUndo2Command *parent = 0); + +protected: + virtual KUndo2Command *createMergeCommand(const KoPathPointData &pointData1, + const KoPathPointData &pointData2); +}; + +#endif // KOMULTIPATHPOINTJOINCOMMAND_H diff --git a/libs/flake/commands/KoMultiPathPointJoinCommand.cpp b/libs/flake/commands/KoMultiPathPointJoinCommand.cpp new file mode 100644 --- /dev/null +++ b/libs/flake/commands/KoMultiPathPointJoinCommand.cpp @@ -0,0 +1,37 @@ +/* + * 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 "KoMultiPathPointJoinCommand.h" + +#include + +KoMultiPathPointJoinCommand::KoMultiPathPointJoinCommand(const KoPathPointData &pointData1, + const KoPathPointData &pointData2, + KoShapeBasedDocumentBase *controller, + KoSelection *selection, + KUndo2Command *parent) + : KoMultiPathPointMergeCommand(pointData1, pointData2, controller, selection, parent) +{ + setText(kundo2_i18n("Join subpaths")); +} + +KUndo2Command *KoMultiPathPointJoinCommand::createMergeCommand(const KoPathPointData &pointData1, const KoPathPointData &pointData2) +{ + return new KoSubpathJoinCommand(pointData1, pointData2); +} + diff --git a/libs/flake/commands/KoMultiPathPointMergeCommand.h b/libs/flake/commands/KoMultiPathPointMergeCommand.h new file mode 100644 --- /dev/null +++ b/libs/flake/commands/KoMultiPathPointMergeCommand.h @@ -0,0 +1,56 @@ +/* + * 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 KOMULTIPATHPOINTMERGECOMMAND_H +#define KOMULTIPATHPOINTMERGECOMMAND_H + +#include + +#include "kritaflake_export.h" +#include + +class KoSelection; +class KoPathShape; +class KoPathPointData; +class KoShapeBasedDocumentBase; + + +class KRITAFLAKE_EXPORT KoMultiPathPointMergeCommand : public KUndo2Command +{ +public: + KoMultiPathPointMergeCommand(const KoPathPointData &pointData1, + const KoPathPointData &pointData2, + KoShapeBasedDocumentBase *controller, + KoSelection *selection, + KUndo2Command *parent = 0); + ~KoMultiPathPointMergeCommand(); + + void undo(); + void redo(); + + KoPathShape *testingCombinedPath() const; + +protected: + virtual KUndo2Command *createMergeCommand(const KoPathPointData &pointData1, + const KoPathPointData &pointData2); +private: + struct Private; + const QScopedPointer m_d; +}; + +#endif // KOMULTIPATHPOINTMERGECOMMAND_H diff --git a/libs/flake/commands/KoMultiPathPointMergeCommand.cpp b/libs/flake/commands/KoMultiPathPointMergeCommand.cpp new file mode 100644 --- /dev/null +++ b/libs/flake/commands/KoMultiPathPointMergeCommand.cpp @@ -0,0 +1,129 @@ +/* + * 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 "KoMultiPathPointMergeCommand.h" +#include + +#include +#include +#include + +#include "kis_assert.h" + + +struct Q_DECL_HIDDEN KoMultiPathPointMergeCommand::Private +{ + Private(const KoPathPointData &_pointData1, const KoPathPointData &_pointData2, KoShapeBasedDocumentBase *_controller, KoSelection *_selection) + : pointData1(_pointData1), + pointData2(_pointData2), + controller(_controller), + selection(_selection) + { + } + + KoPathPointData pointData1; + KoPathPointData pointData2; + KoShapeBasedDocumentBase *controller; + KoSelection *selection; + + + QScopedPointer combineCommand; + QScopedPointer mergeCommand; +}; + +KoMultiPathPointMergeCommand::KoMultiPathPointMergeCommand(const KoPathPointData &pointData1, const KoPathPointData &pointData2, KoShapeBasedDocumentBase *controller, KoSelection *selection, KUndo2Command *parent) + : KUndo2Command(kundo2_i18n("Merge points"), parent), + m_d(new Private(pointData1, pointData2, controller, selection)) +{ +} + +KoMultiPathPointMergeCommand::~KoMultiPathPointMergeCommand() +{ +} + +KUndo2Command *KoMultiPathPointMergeCommand::createMergeCommand(const KoPathPointData &pointData1, const KoPathPointData &pointData2) +{ + return new KoPathPointMergeCommand(pointData1, pointData2); +} + +void KoMultiPathPointMergeCommand::redo() +{ + if (m_d->selection) { + m_d->selection->deselectAll(); + } + + KoShape *mergedShape = 0; + + if (m_d->pointData1.pathShape != m_d->pointData2.pathShape) { + KIS_SAFE_ASSERT_RECOVER_RETURN(m_d->controller); + + QList shapes = {m_d->pointData1.pathShape, m_d->pointData2.pathShape}; + m_d->combineCommand.reset(new KoPathCombineCommand(m_d->controller, shapes)); + m_d->combineCommand->redo(); + + KoPathPointData newPD1 = m_d->combineCommand->originalToCombined(m_d->pointData1); + KoPathPointData newPD2 = m_d->combineCommand->originalToCombined(m_d->pointData2); + + m_d->mergeCommand.reset(createMergeCommand(newPD1, newPD2)); + m_d->mergeCommand->redo(); + + mergedShape = m_d->combineCommand->combinedPath(); + + } else { + m_d->mergeCommand.reset(createMergeCommand(m_d->pointData1, m_d->pointData2)); + m_d->mergeCommand->redo(); + + mergedShape = m_d->pointData1.pathShape; + } + + if (m_d->selection) { + m_d->selection->select(mergedShape); + } + + KUndo2Command::redo(); +} + +KoPathShape *KoMultiPathPointMergeCommand::testingCombinedPath() const +{ + return m_d->combineCommand ? m_d->combineCommand->combinedPath() : 0; +} + +void KoMultiPathPointMergeCommand::undo() +{ + KUndo2Command::undo(); + + if (m_d->selection) { + m_d->selection->deselectAll(); + } + + if (m_d->mergeCommand) { + m_d->mergeCommand->undo(); + m_d->mergeCommand.reset(); + } + + if (m_d->combineCommand) { + m_d->combineCommand->undo(); + m_d->combineCommand.reset(); + } + + if (m_d->selection) { + m_d->selection->select(m_d->pointData1.pathShape); + m_d->selection->select(m_d->pointData2.pathShape); + } +} + diff --git a/libs/flake/commands/KoParameterHandleMoveCommand.h b/libs/flake/commands/KoParameterHandleMoveCommand.h --- a/libs/flake/commands/KoParameterHandleMoveCommand.h +++ b/libs/flake/commands/KoParameterHandleMoveCommand.h @@ -45,9 +45,13 @@ virtual ~KoParameterHandleMoveCommand(); /// redo the command - void redo(); + void redo() override; /// revert the actions done in redo - void undo(); + void undo() override; + + int id() const override; + bool mergeWith(const KUndo2Command *command) override; + private: KoParameterShape *m_shape; int m_handleId; diff --git a/libs/flake/commands/KoParameterHandleMoveCommand.cpp b/libs/flake/commands/KoParameterHandleMoveCommand.cpp --- a/libs/flake/commands/KoParameterHandleMoveCommand.cpp +++ b/libs/flake/commands/KoParameterHandleMoveCommand.cpp @@ -22,6 +22,7 @@ #include "KoParameterHandleMoveCommand.h" #include "KoParameterShape.h" #include +#include "kis_command_ids.h" KoParameterHandleMoveCommand::KoParameterHandleMoveCommand(KoParameterShape *shape, int handleId, const QPointF &startPoint, const QPointF &endPoint, Qt::KeyboardModifiers keyModifiers, KUndo2Command *parent) : KUndo2Command(parent) @@ -56,3 +57,25 @@ m_shape->update(); } +int KoParameterHandleMoveCommand::id() const +{ + return KisCommandUtils::ChangeShapeParameterId; +} + +bool KoParameterHandleMoveCommand::mergeWith(const KUndo2Command *command) +{ + const KoParameterHandleMoveCommand *other = dynamic_cast(command); + + if (!other || + other->m_shape != m_shape || + other->m_handleId != m_handleId || + other->m_keyModifiers != m_keyModifiers) { + + return false; + } + + m_endPoint = other->m_endPoint; + + return true; +} + diff --git a/libs/flake/commands/KoParameterToPathCommand.cpp b/libs/flake/commands/KoParameterToPathCommand.cpp --- a/libs/flake/commands/KoParameterToPathCommand.cpp +++ b/libs/flake/commands/KoParameterToPathCommand.cpp @@ -103,8 +103,7 @@ KoSubpath * subpath = new KoSubpath; for (int pointIndex = 0; pointIndex < pointCount; ++pointIndex) { KoPathPoint * p = source->pointByIndex(KoPathPointIndex(subpathIndex, pointIndex)); - KoPathPoint * c = new KoPathPoint(*p); - c->setParent(destination); + KoPathPoint * c = new KoPathPoint(*p, destination); subpath->append(c); } destination->addSubpath(subpath, subpathIndex); diff --git a/libs/flake/commands/KoPathCombineCommand.h b/libs/flake/commands/KoPathCombineCommand.h --- a/libs/flake/commands/KoPathCombineCommand.h +++ b/libs/flake/commands/KoPathCombineCommand.h @@ -27,6 +27,7 @@ class KoShapeBasedDocumentBase; class KoPathShape; +class KoPathPointData; /// The undo / redo command for combining two or more paths into one class KRITAFLAKE_EXPORT KoPathCombineCommand : public KUndo2Command @@ -45,6 +46,9 @@ /// revert the actions done in redo void undo(); + KoPathShape *combinedPath() const; + KoPathPointData originalToCombined(KoPathPointData pd) const; + private: class Private; Private * const d; diff --git a/libs/flake/commands/KoPathCombineCommand.cpp b/libs/flake/commands/KoPathCombineCommand.cpp --- a/libs/flake/commands/KoPathCombineCommand.cpp +++ b/libs/flake/commands/KoPathCombineCommand.cpp @@ -23,49 +23,64 @@ #include "KoShapeContainer.h" #include "KoPathShape.h" #include +#include "kis_assert.h" +#include + +#include class Q_DECL_HIDDEN KoPathCombineCommand::Private { public: Private(KoShapeBasedDocumentBase *c, const QList &p) : controller(c), paths(p) - , combinedPath(0), combinedPathParent(0) + , combinedPath(0) + , combinedPathParent(0) , isCombined(false) { - foreach (KoPathShape * path, paths) + foreach (KoPathShape * path, paths) { oldParents.append(path->parent()); + } } ~Private() { if (isCombined && controller) { - Q_FOREACH (KoPathShape* path, paths) + Q_FOREACH (KoPathShape* path, paths) { delete path; - } else + } + } else { delete combinedPath; + } } KoShapeBasedDocumentBase *controller; QList paths; QList oldParents; KoPathShape *combinedPath; KoShapeContainer *combinedPathParent; + + QHash shapeStartSegmentIndex; + bool isCombined; }; KoPathCombineCommand::KoPathCombineCommand(KoShapeBasedDocumentBase *controller, const QList &paths, KUndo2Command *parent) -: KUndo2Command(parent) -, d(new Private(controller, paths)) + : KUndo2Command(kundo2_i18n("Combine paths"), parent) + , d(new Private(controller, paths)) { - setText(kundo2_i18n("Combine paths")); + KIS_SAFE_ASSERT_RECOVER_RETURN(!paths.isEmpty()); - d->combinedPath = new KoPathShape(); - d->combinedPath->setStroke(d->paths.first()->stroke()); - d->combinedPath->setShapeId(d->paths.first()->shapeId()); - // combine the paths Q_FOREACH (KoPathShape* path, d->paths) { - d->combinedPath->combine(path); - if (! d->combinedPathParent && path->parent()) + if (!d->combinedPath) { + KoPathShape *clone = dynamic_cast(path->cloneShape()); + KIS_ASSERT_RECOVER_BREAK(clone); + + d->combinedPath = clone; d->combinedPathParent = path->parent(); + d->shapeStartSegmentIndex[path] = 0; + } else { + const int startSegmentIndex = d->combinedPath->combine(path); + d->shapeStartSegmentIndex[path] = startSegmentIndex; + } } } @@ -77,23 +92,16 @@ void KoPathCombineCommand::redo() { KUndo2Command::redo(); - - if (! d->paths.size()) - return; + if (d->paths.isEmpty()) return; d->isCombined = true; if (d->controller) { - QList::iterator parentIt = d->oldParents.begin(); Q_FOREACH (KoPathShape* p, d->paths) { d->controller->removeShape(p); - if (*parentIt) - (*parentIt)->removeShape(p); - ++parentIt; - + p->setParent(0); } - if (d->combinedPathParent) - d->combinedPathParent->addShape(d->combinedPath); + d->combinedPath->setParent(d->combinedPathParent); d->controller->addShape(d->combinedPath); } } @@ -107,15 +115,30 @@ if (d->controller) { d->controller->removeShape(d->combinedPath); - if (d->combinedPath->parent()) - d->combinedPath->parent()->removeShape(d->combinedPath); - QList::iterator parentIt = d->oldParents.begin(); + d->combinedPath->setParent(0); + + auto parentIt = d->oldParents.begin(); Q_FOREACH (KoPathShape* p, d->paths) { - d->controller->addShape(p); p->setParent(*parentIt); + d->controller->addShape(p); ++parentIt; } } KUndo2Command::undo(); } +KoPathShape *KoPathCombineCommand::combinedPath() const +{ + return d->combinedPath; +} + +KoPathPointData KoPathCombineCommand::originalToCombined(KoPathPointData pd) const +{ + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(d->shapeStartSegmentIndex.contains(pd.pathShape), pd); + + const int segmentOffet = d->shapeStartSegmentIndex[pd.pathShape]; + + KoPathPointIndex newIndex(segmentOffet + pd.pointIndex.first, pd.pointIndex.second); + return KoPathPointData(d->combinedPath, newIndex); +} + diff --git a/libs/flake/commands/KoPathControlPointMoveCommand.h b/libs/flake/commands/KoPathControlPointMoveCommand.h --- a/libs/flake/commands/KoPathControlPointMoveCommand.h +++ b/libs/flake/commands/KoPathControlPointMoveCommand.h @@ -41,9 +41,13 @@ KoPathControlPointMoveCommand(const KoPathPointData &pointData, const QPointF &offset, KoPathPoint::PointType pointType, KUndo2Command *parent = 0); /// redo the command - void redo(); + void redo() override; /// revert the actions done in redo - void undo(); + void undo() override; + + int id() const override; + bool mergeWith(const KUndo2Command *command) override; + private: KoPathPointData m_pointData; // the offset in shape coordinates diff --git a/libs/flake/commands/KoPathControlPointMoveCommand.cpp b/libs/flake/commands/KoPathControlPointMoveCommand.cpp --- a/libs/flake/commands/KoPathControlPointMoveCommand.cpp +++ b/libs/flake/commands/KoPathControlPointMoveCommand.cpp @@ -21,6 +21,7 @@ #include "KoPathControlPointMoveCommand.h" #include #include +#include "kis_command_ids.h" KoPathControlPointMoveCommand::KoPathControlPointMoveCommand( const KoPathPointData &pointData, @@ -94,3 +95,23 @@ m_offset *= -1.0; } +int KoPathControlPointMoveCommand::id() const +{ + return KisCommandUtils::ChangePathShapeControlPointId; +} + +bool KoPathControlPointMoveCommand::mergeWith(const KUndo2Command *command) +{ + const KoPathControlPointMoveCommand *other = dynamic_cast(command); + + if (!other || + other->m_pointData != m_pointData || + other->m_pointType != m_pointType) { + + return false; + } + + m_offset += other->m_offset; + + return true; +} diff --git a/libs/flake/commands/KoPathPointMergeCommand.h b/libs/flake/commands/KoPathPointMergeCommand.h --- a/libs/flake/commands/KoPathPointMergeCommand.h +++ b/libs/flake/commands/KoPathPointMergeCommand.h @@ -47,6 +47,8 @@ /// revert the actions done in redo void undo(); + KoPathPointData mergedPointData() const; + private: class Private; diff --git a/libs/flake/commands/KoPathPointMergeCommand.cpp b/libs/flake/commands/KoPathPointMergeCommand.cpp --- a/libs/flake/commands/KoPathPointMergeCommand.cpp +++ b/libs/flake/commands/KoPathPointMergeCommand.cpp @@ -24,6 +24,8 @@ #include "KoPathShape.h" #include #include +#include "kis_assert.h" + class Q_DECL_HIDDEN KoPathPointMergeCommand::Private { @@ -34,6 +36,7 @@ , startPoint(pointData2.pointIndex) , splitIndex(KoPathPointIndex(-1, -1)) , removedPoint(0) + , mergedPointIndex(-1, -1) , reverse(ReverseNone) { } @@ -89,6 +92,7 @@ QPointF oldControlPoint2; KoPathPoint * removedPoint; + KoPathPointIndex mergedPointIndex; enum Reverse { ReverseNone = 0, @@ -107,15 +111,20 @@ KoPathPointMergeCommand::KoPathPointMergeCommand(const KoPathPointData &pointData1, const KoPathPointData &pointData2, KUndo2Command *parent) : KUndo2Command(parent), d(new Private(pointData1, pointData2)) { - Q_ASSERT(pointData1.pathShape == pointData2.pathShape); - Q_ASSERT(d->pathShape); - Q_ASSERT(!d->pathShape->isClosedSubpath(d->endPoint.first)); - Q_ASSERT(d->endPoint.second == 0 || + KIS_ASSERT(pointData1.pathShape == pointData2.pathShape); + KIS_ASSERT(d->pathShape); + + KIS_ASSERT(!d->pathShape->isClosedSubpath(d->endPoint.first)); + KIS_ASSERT(!d->pathShape->isClosedSubpath(d->startPoint.first)); + + KIS_ASSERT(d->endPoint.second == 0 || d->endPoint.second == d->pathShape->subpathPointCount(d->endPoint.first) - 1); - Q_ASSERT(!d->pathShape->isClosedSubpath(d->startPoint.first)); - Q_ASSERT(d->startPoint.second == 0 || + + KIS_ASSERT(d->startPoint.second == 0 || d->startPoint.second == d->pathShape->subpathPointCount(d->startPoint.first) - 1); + KIS_ASSERT(d->startPoint != d->endPoint); + // if we have two different subpaths we might need to reverse them if (d->endPoint.first != d->startPoint.first) { // sort by point index @@ -178,6 +187,9 @@ // set new startpoint of subpath to close the subpath KoPathPointIndex newStartIndex(d->startPoint.first,0); d->pathShape->pointByIndex(newStartIndex)->setProperty(KoPathPoint::CloseSubpath); + + d->mergedPointIndex = d->pathShape->pathPointIndex(endPoint); + } else { // first revert subpaths if needed if (d->reverse & Private::ReverseFirst) { @@ -193,6 +205,8 @@ d->pathShape->join(d->endPoint.first); // change the first point of the points to merge d->removedPoint = d->mergePoints(endPoint, startPoint); + + d->mergedPointIndex = d->pathShape->pathPointIndex(endPoint); } d->pathShape->normalize(); @@ -239,4 +253,10 @@ // reset the removed point d->removedPoint = 0; + d->mergedPointIndex = KoPathPointIndex(-1,-1); +} + +KoPathPointData KoPathPointMergeCommand::mergedPointData() const +{ + return KoPathPointData(d->pathShape, d->mergedPointIndex); } diff --git a/libs/flake/commands/KoPathPointMoveCommand.h b/libs/flake/commands/KoPathPointMoveCommand.h --- a/libs/flake/commands/KoPathPointMoveCommand.h +++ b/libs/flake/commands/KoPathPointMoveCommand.h @@ -54,9 +54,12 @@ ~KoPathPointMoveCommand(); /// redo the command - void redo(); + void redo() override; /// revert the actions done in redo - void undo(); + void undo() override; + + int id() const override; + bool mergeWith(const KUndo2Command *command) override; private: KoPathPointMoveCommandPrivate * const d; diff --git a/libs/flake/commands/KoPathPointMoveCommand.cpp b/libs/flake/commands/KoPathPointMoveCommand.cpp --- a/libs/flake/commands/KoPathPointMoveCommand.cpp +++ b/libs/flake/commands/KoPathPointMoveCommand.cpp @@ -22,14 +22,15 @@ #include "KoPathPointMoveCommand.h" #include "KoPathPoint.h" #include +#include "kis_command_ids.h" +#include "krita_container_utils.h" class KoPathPointMoveCommandPrivate { public: - KoPathPointMoveCommandPrivate() : undoCalled(true) { } + KoPathPointMoveCommandPrivate() { } void applyOffset(qreal factor); - bool undoCalled; // this command stores diffs; so calling undo twice will give wrong results. Guard against that. QMap points; QSet paths; }; @@ -75,21 +76,38 @@ void KoPathPointMoveCommand::redo() { KUndo2Command::redo(); - if (! d->undoCalled) - return; - d->applyOffset(1.0); - d->undoCalled = false; } void KoPathPointMoveCommand::undo() { KUndo2Command::undo(); - if (d->undoCalled) - return; - d->applyOffset(-1.0); - d->undoCalled = true; +} + +int KoPathPointMoveCommand::id() const +{ + return KisCommandUtils::ChangePathShapePointId; +} + +bool KoPathPointMoveCommand::mergeWith(const KUndo2Command *command) +{ + const KoPathPointMoveCommand *other = dynamic_cast(command); + + if (!other || + other->d->paths != d->paths || + !KritaUtils::compareListsUnordered(other->d->points.keys(), d->points.keys())) { + + return false; + } + + auto it = d->points.begin(); + while (it != d->points.end()) { + it.value() += other->d->points[it.key()]; + ++it; + } + + return true; } void KoPathPointMoveCommandPrivate::applyOffset(qreal factor) diff --git a/libs/flake/commands/KoPathShapeMarkerCommand.h b/libs/flake/commands/KoPathShapeMarkerCommand.h --- a/libs/flake/commands/KoPathShapeMarkerCommand.h +++ b/libs/flake/commands/KoPathShapeMarkerCommand.h @@ -23,7 +23,9 @@ #include "kritaflake_export.h" -#include "KoMarkerData.h" +#include + +#include "KoFlake.h" #include #include @@ -41,19 +43,19 @@ * @param position the position - start or end - of the marker on the shape * @param parent the parent command used for macro commands */ - KoPathShapeMarkerCommand(const QList &shapes, KoMarker *marker, KoMarkerData::MarkerPosition position, KUndo2Command *parent = 0); + KoPathShapeMarkerCommand(const QList &shapes, KoMarker *marker, KoFlake::MarkerPosition position, KUndo2Command *parent = 0); + + ~KoPathShapeMarkerCommand(); + + void redo() override; + void undo() override; - virtual ~KoPathShapeMarkerCommand(); - /// redo the command - void redo(); - /// revert the actions done in redo - void undo(); + int id() const override; + bool mergeWith(const KUndo2Command *command) override; private: - QList m_shapes; ///< the shapes to set marker for - QList m_oldMarkers; ///< the old markers, one for each shape - KoMarker* m_marker; ///< the new marker to set - KoMarkerData::MarkerPosition m_position; + struct Private; + const QScopedPointer m_d; }; #endif // KoPathShapeMarkerCommand_H diff --git a/libs/flake/commands/KoPathShapeMarkerCommand.cpp b/libs/flake/commands/KoPathShapeMarkerCommand.cpp --- a/libs/flake/commands/KoPathShapeMarkerCommand.cpp +++ b/libs/flake/commands/KoPathShapeMarkerCommand.cpp @@ -21,20 +21,34 @@ #include "KoPathShapeMarkerCommand.h" #include "KoMarker.h" #include "KoPathShape.h" +#include + +#include "kis_command_ids.h" #include -KoPathShapeMarkerCommand::KoPathShapeMarkerCommand(const QList &shapes, KoMarker *marker, KoMarkerData::MarkerPosition position, KUndo2Command *parent) -: KUndo2Command(parent) -, m_shapes(shapes) -, m_marker(marker) -, m_position(position) +class Q_DECL_HIDDEN KoPathShapeMarkerCommand::Private +{ +public: + QList shapes; ///< the shapes to set marker for + QList> oldMarkers; ///< the old markers, one for each shape + QExplicitlySharedDataPointer marker; ///< the new marker to set + KoFlake::MarkerPosition position; + QList oldAutoFillMarkers; +}; + +KoPathShapeMarkerCommand::KoPathShapeMarkerCommand(const QList &shapes, KoMarker *marker, KoFlake::MarkerPosition position, KUndo2Command *parent) + : KUndo2Command(kundo2_i18n("Set marker"), parent), + m_d(new Private) { - setText(kundo2_i18n("Set marker")); + m_d->shapes = shapes; + m_d->marker = marker; + m_d->position = position; // save old markers - Q_FOREACH (KoPathShape *shape, m_shapes) { - m_oldMarkers.append(shape->marker(position)); + Q_FOREACH (KoPathShape *shape, m_d->shapes) { + m_d->oldMarkers.append(QExplicitlySharedDataPointer(shape->marker(position))); + m_d->oldAutoFillMarkers.append(shape->autoFillMarkers()); } } @@ -45,19 +59,47 @@ void KoPathShapeMarkerCommand::redo() { KUndo2Command::redo(); - Q_FOREACH (KoPathShape *shape, m_shapes) { - shape->setMarker(m_marker, m_position); + Q_FOREACH (KoPathShape *shape, m_d->shapes) { + shape->update(); + shape->setMarker(m_d->marker.data(), m_d->position); + + // we have no GUI for selection auto-filling yet! So just enable it! + shape->setAutoFillMarkers(true); + shape->update(); } } void KoPathShapeMarkerCommand::undo() { KUndo2Command::undo(); - QList::iterator markerIt = m_oldMarkers.begin(); - Q_FOREACH (KoPathShape *shape, m_shapes) { - shape->setMarker(*markerIt, m_position); + auto markerIt = m_d->oldMarkers.begin(); + auto autoFillIt = m_d->oldAutoFillMarkers.begin(); + Q_FOREACH (KoPathShape *shape, m_d->shapes) { + shape->update(); + shape->setMarker((*markerIt).data(), m_d->position); + shape->setAutoFillMarkers(*autoFillIt); shape->update(); ++markerIt; } } + +int KoPathShapeMarkerCommand::id() const +{ + return KisCommandUtils::ChangeShapeMarkersId; +} + +bool KoPathShapeMarkerCommand::mergeWith(const KUndo2Command *command) +{ + const KoPathShapeMarkerCommand *other = dynamic_cast(command); + + if (!other || + other->m_d->shapes != m_d->shapes || + other->m_d->position != m_d->position) { + + return false; + } + + m_d->marker = other->m_d->marker; + return true; +} diff --git a/libs/flake/commands/KoShapeAlignCommand.cpp b/libs/flake/commands/KoShapeAlignCommand.cpp --- a/libs/flake/commands/KoShapeAlignCommand.cpp +++ b/libs/flake/commands/KoShapeAlignCommand.cpp @@ -52,9 +52,9 @@ // debugFlake <<"Found Container"; // else // debugFlake <<"Found shape"; - position = shape->position(); + position = shape->absolutePosition(); previousPositions << position; - bRect = shape->boundingRect(); + bRect = shape->absoluteOutlineRect(); switch (align) { case HorizontalLeftAlignment: delta = QPointF(boundingRect.left(), bRect.y()) - bRect.topLeft(); diff --git a/libs/flake/commands/KoShapeBackgroundCommand.h b/libs/flake/commands/KoShapeBackgroundCommand.h --- a/libs/flake/commands/KoShapeBackgroundCommand.h +++ b/libs/flake/commands/KoShapeBackgroundCommand.h @@ -63,6 +63,10 @@ void redo(); /// revert the actions done in redo void undo(); + + int id() const override; + bool mergeWith(const KUndo2Command *command) override; + private: class Private; Private * const d; diff --git a/libs/flake/commands/KoShapeBackgroundCommand.cpp b/libs/flake/commands/KoShapeBackgroundCommand.cpp --- a/libs/flake/commands/KoShapeBackgroundCommand.cpp +++ b/libs/flake/commands/KoShapeBackgroundCommand.cpp @@ -24,6 +24,9 @@ #include +#include "kis_command_ids.h" + + class Q_DECL_HIDDEN KoShapeBackgroundCommand::Private { public: @@ -111,6 +114,25 @@ } } +int KoShapeBackgroundCommand::id() const +{ + return KisCommandUtils::ChangeShapeBackgroundId; +} + +bool KoShapeBackgroundCommand::mergeWith(const KUndo2Command *command) +{ + const KoShapeBackgroundCommand *other = dynamic_cast(command); + + if (!other || + other->d->shapes != d->shapes) { + + return false; + } + + d->newFills= other->d->newFills; + return true; +} + KoShapeBackgroundCommand::~KoShapeBackgroundCommand() { delete d; diff --git a/libs/flake/commands/KoShapeClipCommand.cpp b/libs/flake/commands/KoShapeClipCommand.cpp --- a/libs/flake/commands/KoShapeClipCommand.cpp +++ b/libs/flake/commands/KoShapeClipCommand.cpp @@ -26,6 +26,8 @@ #include +#include "kis_pointer_utils.h" + class Q_DECL_HIDDEN KoShapeClipCommand::Private { public: @@ -37,7 +39,6 @@ if (executed) { qDeleteAll(oldClipPaths); } else { - clipData->removeClipShapesOwnership(); qDeleteAll(newClipPaths); } } @@ -47,7 +48,6 @@ QList clipPathShapes; QList newClipPaths; QList oldParents; - QExplicitlySharedDataPointer clipData; KoShapeBasedDocumentBase *controller; bool executed; }; @@ -57,10 +57,10 @@ { d->shapesToClip = shapes; d->clipPathShapes = clipPathShapes; - d->clipData = new KoClipData(clipPathShapes); + Q_FOREACH (KoShape *shape, d->shapesToClip) { d->oldClipPaths.append(shape->clipPath()); - d->newClipPaths.append(new KoClipPath(shape, d->clipData.data())); + d->newClipPaths.append(new KoClipPath(implicitCastList(clipPathShapes), KoFlake::UserSpaceOnUse)); } Q_FOREACH (KoPathShape *path, clipPathShapes) { @@ -75,9 +75,8 @@ { d->shapesToClip.append(shape); d->clipPathShapes = clipPathShapes; - d->clipData = new KoClipData(clipPathShapes); d->oldClipPaths.append(shape->clipPath()); - d->newClipPaths.append(new KoClipPath(shape, d->clipData.data())); + d->newClipPaths.append(new KoClipPath(implicitCastList(clipPathShapes), KoFlake::UserSpaceOnUse)); Q_FOREACH (KoPathShape *path, clipPathShapes) { d->oldParents.append(path->parent()); diff --git a/libs/flake/commands/KoShapeCreateCommand.cpp b/libs/flake/commands/KoShapeCreateCommand.cpp --- a/libs/flake/commands/KoShapeCreateCommand.cpp +++ b/libs/flake/commands/KoShapeCreateCommand.cpp @@ -25,11 +25,16 @@ #include +#include "kis_assert.h" +#include +#include + + class Q_DECL_HIDDEN KoShapeCreateCommand::Private { public: Private(KoShapeBasedDocumentBase *c, KoShape *s) - : controller(c), + : shapesDocument(c), shape(s), shapeParent(shape->parent()), deleteShape(true) { @@ -39,10 +44,12 @@ delete shape; } - KoShapeBasedDocumentBase *controller; + KoShapeBasedDocumentBase *shapesDocument; KoShape *shape; KoShapeContainer *shapeParent; bool deleteShape; + + QScopedPointer reorderingCommand; }; KoShapeCreateCommand::KoShapeCreateCommand(KoShapeBasedDocumentBase *controller, KoShape *shape, KUndo2Command *parent) @@ -61,22 +68,40 @@ { KUndo2Command::redo(); Q_ASSERT(d->shape); - Q_ASSERT(d->controller); - if (d->shapeParent) + Q_ASSERT(d->shapesDocument); + + if (d->shapeParent) { d->shapeParent->addShape(d->shape); + } // the parent has to be there when it is added to the KoShapeBasedDocumentBase - d->controller->addShape(d->shape); - d->shapeParent = d->shape->parent(); // update parent if the 'addShape' changed it + d->shapesDocument->addShape(d->shape); + d->shapeParent = d->shape->parent(); // update parent if the 'addShape' changed it d->deleteShape = false; + + KIS_SAFE_ASSERT_RECOVER_NOOP(d->shapeParent || + dynamic_cast(d->shape)); + + if (d->shapeParent) { + d->reorderingCommand.reset(KoShapeReorderCommand::mergeInShape(d->shapeParent->shapes(), d->shape)); + if (d->reorderingCommand) { + d->reorderingCommand->redo(); + } + } } void KoShapeCreateCommand::undo() { KUndo2Command::undo(); Q_ASSERT(d->shape); - Q_ASSERT(d->controller); + Q_ASSERT(d->shapesDocument); + + if (d->reorderingCommand) { + d->reorderingCommand->undo(); + d->reorderingCommand.reset(); + } + // the parent has to be there when it is removed from the KoShapeBasedDocumentBase - d->controller->removeShape(d->shape); + d->shapesDocument->removeShape(d->shape); if (d->shapeParent) d->shapeParent->removeShape(d->shape); d->deleteShape = true; diff --git a/libs/flake/commands/KoShapeDistributeCommand.cpp b/libs/flake/commands/KoShapeDistributeCommand.cpp --- a/libs/flake/commands/KoShapeDistributeCommand.cpp +++ b/libs/flake/commands/KoShapeDistributeCommand.cpp @@ -50,7 +50,7 @@ qreal extent = 0.0; // sort by position and calculate sum of objects widht/height Q_FOREACH (KoShape *shape, shapes) { - bRect = shape->boundingRect(); + bRect = shape->absoluteOutlineRect(); switch (d->distribute) { case HorizontalCenterDistribution: sortedPos[bRect.center().x()] = shape; @@ -90,13 +90,13 @@ QMapIterator it(sortedPos); while (it.hasNext()) { it.next(); - position = it.value()->position(); + position = it.value()->absolutePosition(); previousPositions << position; - bRect = it.value()->boundingRect(); + bRect = it.value()->absoluteOutlineRect(); switch (d->distribute) { case HorizontalCenterDistribution: - delta = QPointF(boundingRect.x() + first->boundingRect().width() / 2 + pos - bRect.width() / 2, bRect.y()) - bRect.topLeft(); + delta = QPointF(boundingRect.x() + first->absoluteOutlineRect().width() / 2 + pos - bRect.width() / 2, bRect.y()) - bRect.topLeft(); break; case HorizontalGapsDistribution: delta = QPointF(boundingRect.left() + pos, bRect.y()) - bRect.topLeft(); @@ -106,17 +106,17 @@ delta = QPointF(boundingRect.left() + pos, bRect.y()) - bRect.topLeft(); break; case HorizontalRightDistribution: - delta = QPointF(boundingRect.left() + first->boundingRect().width() + pos - bRect.width(), bRect.y()) - bRect.topLeft(); + delta = QPointF(boundingRect.left() + first->absoluteOutlineRect().width() + pos - bRect.width(), bRect.y()) - bRect.topLeft(); break; case VerticalCenterDistribution: - delta = QPointF(bRect.x(), boundingRect.y() + first->boundingRect().height() / 2 + pos - bRect.height() / 2) - bRect.topLeft(); + delta = QPointF(bRect.x(), boundingRect.y() + first->absoluteOutlineRect().height() / 2 + pos - bRect.height() / 2) - bRect.topLeft(); break; case VerticalGapsDistribution: delta = QPointF(bRect.x(), boundingRect.top() + pos) - bRect.topLeft(); pos += bRect.height(); break; case VerticalBottomDistribution: - delta = QPointF(bRect.x(), boundingRect.top() + first->boundingRect().height() + pos - bRect.height()) - bRect.topLeft(); + delta = QPointF(bRect.x(), boundingRect.top() + first->absoluteOutlineRect().height() + pos - bRect.height()) - bRect.topLeft(); break; case VerticalTopDistribution: delta = QPointF(bRect.x(), boundingRect.top() + pos) - bRect.topLeft(); @@ -151,28 +151,28 @@ { switch (distribute) { case HorizontalCenterDistribution: - return boundingRect.width() - last->boundingRect().width() / 2 - first->boundingRect().width() / 2; + return boundingRect.width() - last->absoluteOutlineRect().width() / 2 - first->absoluteOutlineRect().width() / 2; break; case HorizontalGapsDistribution: return boundingRect.width() - extent; break; case HorizontalLeftDistribution: - return boundingRect.width() - last->boundingRect().width(); + return boundingRect.width() - last->absoluteOutlineRect().width(); break; case HorizontalRightDistribution: - return boundingRect.width() - first->boundingRect().width(); + return boundingRect.width() - first->absoluteOutlineRect().width(); break; case VerticalCenterDistribution: - return boundingRect.height() - last->boundingRect().height() / 2 - first->boundingRect().height() / 2; + return boundingRect.height() - last->absoluteOutlineRect().height() / 2 - first->absoluteOutlineRect().height() / 2; break; case VerticalGapsDistribution: return boundingRect.height() - extent; break; case VerticalBottomDistribution: - return boundingRect.height() - first->boundingRect().height(); + return boundingRect.height() - first->absoluteOutlineRect().height(); break; case VerticalTopDistribution: - return boundingRect.height() - last->boundingRect().height(); + return boundingRect.height() - last->absoluteOutlineRect().height(); break; } return 0.0; diff --git a/libs/flake/commands/KoShapeGroupCommand.h b/libs/flake/commands/KoShapeGroupCommand.h --- a/libs/flake/commands/KoShapeGroupCommand.h +++ b/libs/flake/commands/KoShapeGroupCommand.h @@ -59,6 +59,20 @@ */ KoShapeGroupCommand(KoShapeContainer *container, const QList &shapes, const QList &clipped, const QList &inheritTransform, KUndo2Command *parent = 0); + + /** + * Command to group a set of shapes into a predefined container. + * @param container the container to group the shapes under. + * @param shapes a list of all the shapes that should be grouped. + * @param clipped shows whether the shapes should be clipped by the container + * See KoShapeContainer::isClipped() + * @param inheritTransform shows whether the shapes should inherit the parent transformation + * See KoShapeContainer::inheritsTransform() + * @param parent the parent command used for macro commands + */ + KoShapeGroupCommand(KoShapeContainer *container, const QList &shapes, + bool clipped, bool inheritTransform, bool shouldNormalize, KUndo2Command *parent = 0); + /** * Command to group a set of shapes into a predefined container. * Convenience constructor since KoShapeGroup does not allow clipping. diff --git a/libs/flake/commands/KoShapeGroupCommand.cpp b/libs/flake/commands/KoShapeGroupCommand.cpp --- a/libs/flake/commands/KoShapeGroupCommand.cpp +++ b/libs/flake/commands/KoShapeGroupCommand.cpp @@ -40,35 +40,49 @@ return new KoShapeGroupCommand(container, orderedShapes, parent); } -KoShapeGroupCommandPrivate::KoShapeGroupCommandPrivate(KoShapeContainer *c, const QList &s, const QList &clip, const QList &it) +KoShapeGroupCommandPrivate::KoShapeGroupCommandPrivate(KoShapeContainer *c, const QList &s, const QList &clip, const QList &it, bool _shouldNormalize) : shapes(s), clipped(clip), inheritTransform(it), + shouldNormalize(_shouldNormalize), container(c) + { } KoShapeGroupCommand::KoShapeGroupCommand(KoShapeContainer *container, const QList &shapes, const QList &clipped, const QList &inheritTransform, KUndo2Command *parent) : KUndo2Command(parent), - d(new KoShapeGroupCommandPrivate(container,shapes, clipped, inheritTransform)) + d(new KoShapeGroupCommandPrivate(container,shapes, clipped, inheritTransform, true)) { Q_ASSERT(d->clipped.count() == d->shapes.count()); Q_ASSERT(d->inheritTransform.count() == d->shapes.count()); d->init(this); } KoShapeGroupCommand::KoShapeGroupCommand(KoShapeGroup *container, const QList &shapes, KUndo2Command *parent) : KUndo2Command(parent), - d(new KoShapeGroupCommandPrivate(container,shapes)) + d(new KoShapeGroupCommandPrivate(container,shapes, QList(), QList(), true)) { for (int i = 0; i < shapes.count(); ++i) { d->clipped.append(false); d->inheritTransform.append(false); } d->init(this); } +KoShapeGroupCommand::KoShapeGroupCommand(KoShapeContainer *container, const QList &shapes, + bool clipped, bool inheritTransform, bool shouldNormalize, KUndo2Command *parent) + : KUndo2Command(parent), + d(new KoShapeGroupCommandPrivate(container,shapes, QList(), QList(), shouldNormalize)) +{ + for (int i = 0; i < shapes.count(); ++i) { + d->clipped.append(clipped); + d->inheritTransform.append(inheritTransform); + } + d->init(this); +} + KoShapeGroupCommand::~KoShapeGroupCommand() { delete d; @@ -100,10 +114,10 @@ { KUndo2Command::redo(); - if (dynamic_cast(d->container)) { + if (d->shouldNormalize && dynamic_cast(d->container)) { QRectF bound = d->containerBoundingRect(); - QPointF oldGroupPosition = d->container->absolutePosition(KoFlake::TopLeftCorner); - d->container->setAbsolutePosition(bound.topLeft(), KoFlake::TopLeftCorner); + QPointF oldGroupPosition = d->container->absolutePosition(KoFlake::TopLeft); + d->container->setAbsolutePosition(bound.topLeft(), KoFlake::TopLeft); d->container->setSize(bound.size()); if (d->container->shapeCount() > 0) { @@ -175,8 +189,8 @@ shape->setZIndex(d->oldZIndex[i]); } - if (dynamic_cast(d->container)) { - QPointF oldGroupPosition = d->container->absolutePosition(KoFlake::TopLeftCorner); + if (d->shouldNormalize && dynamic_cast(d->container)) { + QPointF oldGroupPosition = d->container->absolutePosition(KoFlake::TopLeft); if (d->container->shapeCount() > 0) { bool boundingRectInitialized = false; QRectF bound; @@ -193,28 +207,22 @@ Q_FOREACH (KoShape * child, d->container->shapes()) child->setAbsolutePosition(child->absolutePosition() + positionOffset); - d->container->setAbsolutePosition(bound.topLeft(), KoFlake::TopLeftCorner); + d->container->setAbsolutePosition(bound.topLeft(), KoFlake::TopLeft); d->container->setSize(bound.size()); } } } QRectF KoShapeGroupCommandPrivate::containerBoundingRect() { - bool boundingRectInitialized = true; QRectF bound; - if (container->shapeCount() > 0) - bound = container->boundingRect(); - else - boundingRectInitialized = false; + if (container->shapeCount() > 0) { + bound = container->absoluteTransformation(0).mapRect(container->outlineRect()); + } Q_FOREACH (KoShape *shape, shapes) { - if (boundingRectInitialized) - bound = bound.united(shape->boundingRect()); - else { - bound = shape->boundingRect(); - boundingRectInitialized = true; - } + bound |= shape->absoluteTransformation(0).mapRect(shape->outlineRect()); } + return bound; } diff --git a/libs/flake/commands/KoShapeGroupCommand_p.h b/libs/flake/commands/KoShapeGroupCommand_p.h --- a/libs/flake/commands/KoShapeGroupCommand_p.h +++ b/libs/flake/commands/KoShapeGroupCommand_p.h @@ -31,13 +31,14 @@ class KoShapeGroupCommandPrivate { public: - KoShapeGroupCommandPrivate(KoShapeContainer *container, const QList &shapes, const QList &clipped = QList(), const QList &inheritTransform = QList()); + KoShapeGroupCommandPrivate(KoShapeContainer *container, const QList &shapes, const QList &clipped, const QList &inheritTransform, bool _shouldNormalize); void init(KUndo2Command *q); QRectF containerBoundingRect(); QList shapes; /// clipped; ///< list of booleans to specify the shape of the same index to be clipped QList inheritTransform; ///< list of booleans to specify the shape of the same index to inherit transform + bool shouldNormalize; ///< Adjust the coordinate system of the group to its origin into the topleft of the group KoShapeContainer *container; ///< the container where the grouping should be for. QList oldParents; ///< the old parents of the shapes QList oldClipped; ///< if the shape was clipped in the old parent diff --git a/libs/flake/commands/KoShapeKeepAspectRatioCommand.h b/libs/flake/commands/KoShapeKeepAspectRatioCommand.h --- a/libs/flake/commands/KoShapeKeepAspectRatioCommand.h +++ b/libs/flake/commands/KoShapeKeepAspectRatioCommand.h @@ -20,15 +20,16 @@ #ifndef KOSHAPEKEEPASPECTRATIOCOMMAND_H #define KOSHAPEKEEPASPECTRATIOCOMMAND_H +#include "kritaflake_export.h" #include #include class KoShape; /** * Command that changes the keepAspectRatio property of KoShape */ -class KoShapeKeepAspectRatioCommand : public KUndo2Command +class KRITAFLAKE_EXPORT KoShapeKeepAspectRatioCommand : public KUndo2Command { public: /** diff --git a/libs/flake/commands/KoShapeMoveCommand.h b/libs/flake/commands/KoShapeMoveCommand.h --- a/libs/flake/commands/KoShapeMoveCommand.h +++ b/libs/flake/commands/KoShapeMoveCommand.h @@ -26,6 +26,7 @@ #include #include #include +#include class KoShape; @@ -43,15 +44,18 @@ * @param parent the parent command used for macro commands */ KoShapeMoveCommand(const QList &shapes, QList &previousPositions, QList &newPositions, - KUndo2Command *parent = 0); + KoFlake::AnchorPosition anchor = KoFlake::Center, KUndo2Command *parent = 0); + + KoShapeMoveCommand(const QList &shapes, const QPointF &offset, KUndo2Command *parent = 0); + ~KoShapeMoveCommand(); /// redo the command - void redo(); + void redo() override; /// revert the actions done in redo - void undo(); + void undo() override; - /// update newPositions list with new postions. - void setNewPositions(QList newPositions); + int id() const override; + bool mergeWith(const KUndo2Command *command) override; private: class Private; diff --git a/libs/flake/commands/KoShapeMoveCommand.cpp b/libs/flake/commands/KoShapeMoveCommand.cpp --- a/libs/flake/commands/KoShapeMoveCommand.cpp +++ b/libs/flake/commands/KoShapeMoveCommand.cpp @@ -22,25 +22,41 @@ #include #include +#include "kis_command_ids.h" class Q_DECL_HIDDEN KoShapeMoveCommand::Private { public: QList shapes; QList previousPositions, newPositions; + KoFlake::AnchorPosition anchor; }; -KoShapeMoveCommand::KoShapeMoveCommand(const QList &shapes, QList &previousPositions, QList &newPositions, KUndo2Command *parent) - : KUndo2Command(parent), +KoShapeMoveCommand::KoShapeMoveCommand(const QList &shapes, QList &previousPositions, QList &newPositions, KoFlake::AnchorPosition anchor, KUndo2Command *parent) + : KUndo2Command(kundo2_i18n("Move shapes"), parent), d(new Private()) { d->shapes = shapes; d->previousPositions = previousPositions; d->newPositions = newPositions; + d->anchor = anchor; Q_ASSERT(d->shapes.count() == d->previousPositions.count()); Q_ASSERT(d->shapes.count() == d->newPositions.count()); +} + +KoShapeMoveCommand::KoShapeMoveCommand(const QList &shapes, const QPointF &offset, KUndo2Command *parent) + : KUndo2Command(kundo2_i18n("Move shapes"), parent), + d(new Private()) +{ + d->shapes = shapes; + d->anchor = KoFlake::Center; - setText(kundo2_i18n("Move shapes")); + Q_FOREACH (KoShape *shape, d->shapes) { + const QPointF pos = shape->absolutePosition(); + + d->previousPositions << pos; + d->newPositions << pos + offset; + } } KoShapeMoveCommand::~KoShapeMoveCommand() @@ -53,7 +69,7 @@ KUndo2Command::redo(); for (int i = 0; i < d->shapes.count(); i++) { d->shapes.at(i)->update(); - d->shapes.at(i)->setPosition(d->newPositions.at(i)); + d->shapes.at(i)->setAbsolutePosition(d->newPositions.at(i), d->anchor); d->shapes.at(i)->update(); } } @@ -63,13 +79,26 @@ KUndo2Command::undo(); for (int i = 0; i < d->shapes.count(); i++) { d->shapes.at(i)->update(); - d->shapes.at(i)->setPosition(d->previousPositions.at(i)); + d->shapes.at(i)->setAbsolutePosition(d->previousPositions.at(i), d->anchor); d->shapes.at(i)->update(); } } -/// update newPositions list with new postions. -void KoShapeMoveCommand::setNewPositions(QList newPositions) +int KoShapeMoveCommand::id() const { - d->newPositions = newPositions; + return KisCommandUtils::MoveShapeId; +} + +bool KoShapeMoveCommand::mergeWith(const KUndo2Command *command) +{ + const KoShapeMoveCommand *other = dynamic_cast(command); + + if (other->d->shapes != d->shapes || + other->d->anchor != d->anchor) { + + return false; + } + + d->newPositions = other->d->newPositions; + return true; } diff --git a/libs/flake/commands/KoShapeReorderCommand.h b/libs/flake/commands/KoShapeReorderCommand.h --- a/libs/flake/commands/KoShapeReorderCommand.h +++ b/libs/flake/commands/KoShapeReorderCommand.h @@ -63,6 +63,20 @@ 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 happend + */ + static KoShapeReorderCommand *mergeInShape(QList shapes, KoShape *newShape, + KUndo2Command *parent = 0); + /// redo the command void redo(); /// revert the actions done in redo diff --git a/libs/flake/commands/KoShapeReorderCommand.cpp b/libs/flake/commands/KoShapeReorderCommand.cpp --- a/libs/flake/commands/KoShapeReorderCommand.cpp +++ b/libs/flake/commands/KoShapeReorderCommand.cpp @@ -179,3 +179,42 @@ Q_ASSERT(changedShapes.count() == newIndexes.count()); return changedShapes.isEmpty() ? 0: new KoShapeReorderCommand(changedShapes, newIndexes, parent); } + +KoShapeReorderCommand *KoShapeReorderCommand::mergeInShape(QList shapes, KoShape *newShape, KUndo2Command *parent) +{ + qSort(shapes.begin(), shapes.end(), KoShape::compareShapeZIndex); + + QList reindexedShapes; + QList reindexedIndexes; + + const int originalShapeZIndex = newShape->zIndex(); + int newShapeZIndex = originalShapeZIndex; + int lastOccupiedShapeZIndex = originalShapeZIndex + 1; + + Q_FOREACH (KoShape *shape, shapes) { + if (shape == newShape) continue; + + const int zIndex = shape->zIndex(); + + if (newShapeZIndex == originalShapeZIndex) { + if (zIndex == originalShapeZIndex) { + newShapeZIndex = originalShapeZIndex + 1; + lastOccupiedShapeZIndex = newShapeZIndex; + + reindexedShapes << newShape; + reindexedIndexes << newShapeZIndex; + } + } else { + if (newShapeZIndex != originalShapeZIndex && + zIndex >= newShapeZIndex && + zIndex <= lastOccupiedShapeZIndex) { + + lastOccupiedShapeZIndex = zIndex + 1; + reindexedShapes << shape; + reindexedIndexes << lastOccupiedShapeZIndex; + } + } + } + + return !reindexedShapes.isEmpty() ? new KoShapeReorderCommand(reindexedShapes, reindexedIndexes, parent) : 0; +} diff --git a/libs/flake/commands/KoShapeResizeCommand.h b/libs/flake/commands/KoShapeResizeCommand.h new file mode 100644 --- /dev/null +++ b/libs/flake/commands/KoShapeResizeCommand.h @@ -0,0 +1,57 @@ +/* + * 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 KOSHAPERESIZECOMMAND_H +#define KOSHAPERESIZECOMMAND_H + +#include "kritaflake_export.h" +#include "kundo2command.h" +#include "kis_command_utils.h" + +#include +#include +#include + +#include + +class KoShape; + + +class KRITAFLAKE_EXPORT KoShapeResizeCommand : public KisCommandUtils::SkipFirstRedoBase +{ +public: + KoShapeResizeCommand(const QList &shapes, + qreal scaleX, qreal scaleY, + const QPointF &absoluteStillPoint, bool useGLobalMode, + bool usePostScaling, const QTransform &postScalingCoveringTransform, + KUndo2Command *parent = 0); + + ~KoShapeResizeCommand(); + void redoImpl() override; + void undoImpl() override; + + int id() const override; + bool mergeWith(const KUndo2Command *command) override; + +private: + class Private; + QScopedPointer const m_d; + +}; + +#endif // KOSHAPERESIZECOMMAND_H diff --git a/libs/flake/commands/KoShapeResizeCommand.cpp b/libs/flake/commands/KoShapeResizeCommand.cpp new file mode 100644 --- /dev/null +++ b/libs/flake/commands/KoShapeResizeCommand.cpp @@ -0,0 +1,127 @@ +/* + * 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 "KoShapeResizeCommand.h" + +#include +#include "kis_command_ids.h" + + +struct Q_DECL_HIDDEN KoShapeResizeCommand::Private +{ + QList shapes; + qreal scaleX; + qreal scaleY; + QPointF absoluteStillPoint; + bool useGlobalMode; + bool usePostScaling; + QTransform postScalingCoveringTransform; + + QList oldSizes; + QList oldTransforms; +}; + + +KoShapeResizeCommand::KoShapeResizeCommand(const QList &shapes, + qreal scaleX, qreal scaleY, + const QPointF &absoluteStillPoint, + bool useGLobalMode, + bool usePostScaling, + const QTransform &postScalingCoveringTransform, + KUndo2Command *parent) + : SkipFirstRedoBase(false, kundo2_i18n("Resize"), parent), + m_d(new Private) +{ + m_d->shapes = shapes; + m_d->scaleX = scaleX; + m_d->scaleY = scaleY; + m_d->absoluteStillPoint = absoluteStillPoint; + m_d->useGlobalMode = useGLobalMode; + m_d->usePostScaling = usePostScaling; + m_d->postScalingCoveringTransform = postScalingCoveringTransform; + + Q_FOREACH (KoShape *shape, m_d->shapes) { + m_d->oldSizes << shape->size(); + m_d->oldTransforms << shape->transformation(); + } +} + +KoShapeResizeCommand::~KoShapeResizeCommand() +{ +} + +void KoShapeResizeCommand::redoImpl() +{ + Q_FOREACH (KoShape *shape, m_d->shapes) { + shape->update(); + + KoFlake::resizeShape(shape, + m_d->scaleX, m_d->scaleY, + m_d->absoluteStillPoint, + m_d->useGlobalMode, + m_d->usePostScaling, + m_d->postScalingCoveringTransform); + + shape->update(); + } +} + +void KoShapeResizeCommand::undoImpl() +{ + for (int i = 0; i < m_d->shapes.size(); i++) { + KoShape *shape = m_d->shapes[i]; + + shape->update(); + shape->setSize(m_d->oldSizes[i]); + shape->setTransformation(m_d->oldTransforms[i]); + shape->update(); + } +} + +int KoShapeResizeCommand::id() const +{ + return KisCommandUtils::ResizeShapeId; +} + +bool KoShapeResizeCommand::mergeWith(const KUndo2Command *command) +{ + const KoShapeResizeCommand *other = dynamic_cast(command); + + if (!other || + other->m_d->absoluteStillPoint != m_d->absoluteStillPoint || + other->m_d->shapes != m_d->shapes || + other->m_d->useGlobalMode != m_d->useGlobalMode || + other->m_d->usePostScaling != m_d->usePostScaling) { + + return false; + } + + // check if the significant orientations coincide + if (m_d->useGlobalMode && !m_d->usePostScaling) { + Qt::Orientation our = KoFlake::significantScaleOrientation(m_d->scaleX, m_d->scaleY); + Qt::Orientation their = KoFlake::significantScaleOrientation(other->m_d->scaleX, other->m_d->scaleY); + + if (our != their) { + return false; + } + } + + m_d->scaleX *= other->m_d->scaleX; + m_d->scaleY *= other->m_d->scaleY; + return true; +} diff --git a/libs/flake/commands/KoShapeStrokeCommand.h b/libs/flake/commands/KoShapeStrokeCommand.h --- a/libs/flake/commands/KoShapeStrokeCommand.h +++ b/libs/flake/commands/KoShapeStrokeCommand.h @@ -25,6 +25,7 @@ #include "kritaflake_export.h" +#include #include #include @@ -41,29 +42,33 @@ * @param stroke the new stroke, the same for all given shapes * @param parent the parent command used for macro commands */ - KoShapeStrokeCommand(const QList &shapes, KoShapeStrokeModel *stroke, KUndo2Command *parent = 0); + KoShapeStrokeCommand(const QList &shapes, KoShapeStrokeModelSP stroke, KUndo2Command *parent = 0); /** * Command to set new shape strokes. * @param shapes a set of all the shapes that should get a new stroke. * @param strokes the new strokes, one for each shape * @param parent the parent command used for macro commands */ - KoShapeStrokeCommand(const QList &shapes, const QList &strokes, KUndo2Command *parent = 0); + KoShapeStrokeCommand(const QList &shapes, const QList &strokes, KUndo2Command *parent = 0); /** * Command to set a new shape stroke. * @param shape a single shape that should get the new stroke. * @param stroke the new stroke * @param parent the parent command used for macro commands */ - KoShapeStrokeCommand(KoShape* shape, KoShapeStrokeModel *stroke, KUndo2Command *parent = 0); + KoShapeStrokeCommand(KoShape* shape, KoShapeStrokeModelSP stroke, KUndo2Command *parent = 0); virtual ~KoShapeStrokeCommand(); /// redo the command void redo(); /// revert the actions done in redo void undo(); + + int id() const override; + bool mergeWith(const KUndo2Command *command) override; + private: class Private; Private * const d; diff --git a/libs/flake/commands/KoShapeStrokeCommand.cpp b/libs/flake/commands/KoShapeStrokeCommand.cpp --- a/libs/flake/commands/KoShapeStrokeCommand.cpp +++ b/libs/flake/commands/KoShapeStrokeCommand.cpp @@ -26,38 +26,33 @@ #include +#include "kis_command_ids.h" + + class Q_DECL_HIDDEN KoShapeStrokeCommand::Private { public: Private() {} ~Private() { - Q_FOREACH (KoShapeStrokeModel* stroke, oldStrokes) { - if (stroke && !stroke->deref()) - delete stroke; - } } - void addOldStroke(KoShapeStrokeModel * oldStroke) + void addOldStroke(KoShapeStrokeModelSP oldStroke) { - if (oldStroke) - oldStroke->ref(); oldStrokes.append(oldStroke); } - void addNewStroke(KoShapeStrokeModel * newStroke) + void addNewStroke(KoShapeStrokeModelSP newStroke) { - if (newStroke) - newStroke->ref(); newStrokes.append(newStroke); } QList shapes; ///< the shapes to set stroke for - QList oldStrokes; ///< the old strokes, one for each shape - QList newStrokes; ///< the new strokes to set + QList oldStrokes; ///< the old strokes, one for each shape + QList newStrokes; ///< the new strokes to set }; -KoShapeStrokeCommand::KoShapeStrokeCommand(const QList &shapes, KoShapeStrokeModel *stroke, KUndo2Command *parent) +KoShapeStrokeCommand::KoShapeStrokeCommand(const QList &shapes, KoShapeStrokeModelSP stroke, KUndo2Command *parent) : KUndo2Command(parent) , d(new Private()) { @@ -73,7 +68,7 @@ } KoShapeStrokeCommand::KoShapeStrokeCommand(const QList &shapes, - const QList &strokes, + const QList &strokes, KUndo2Command *parent) : KUndo2Command(parent) , d(new Private()) @@ -85,13 +80,13 @@ // save old strokes Q_FOREACH (KoShape *shape, shapes) d->addOldStroke(shape->stroke()); - foreach (KoShapeStrokeModel * stroke, strokes) + foreach (KoShapeStrokeModelSP stroke, strokes) d->addNewStroke(stroke); setText(kundo2_i18n("Set stroke")); } -KoShapeStrokeCommand::KoShapeStrokeCommand(KoShape* shape, KoShapeStrokeModel *stroke, KUndo2Command *parent) +KoShapeStrokeCommand::KoShapeStrokeCommand(KoShape* shape, KoShapeStrokeModelSP stroke, KUndo2Command *parent) : KUndo2Command(parent) , d(new Private()) { @@ -110,7 +105,7 @@ void KoShapeStrokeCommand::redo() { KUndo2Command::redo(); - QList::iterator strokeIt = d->newStrokes.begin(); + QList::iterator strokeIt = d->newStrokes.begin(); Q_FOREACH (KoShape *shape, d->shapes) { shape->update(); shape->setStroke(*strokeIt); @@ -122,11 +117,30 @@ void KoShapeStrokeCommand::undo() { KUndo2Command::undo(); - QList::iterator strokeIt = d->oldStrokes.begin(); + QList::iterator strokeIt = d->oldStrokes.begin(); Q_FOREACH (KoShape *shape, d->shapes) { shape->update(); shape->setStroke(*strokeIt); shape->update(); ++strokeIt; } } + +int KoShapeStrokeCommand::id() const +{ + return KisCommandUtils::ChangeShapeStrokeId; +} + +bool KoShapeStrokeCommand::mergeWith(const KUndo2Command *command) +{ + const KoShapeStrokeCommand *other = dynamic_cast(command); + + if (!other || + other->d->shapes != d->shapes) { + + return false; + } + + d->newStrokes = other->d->newStrokes; + return true; +} diff --git a/libs/flake/commands/KoShapeTransformCommand.h b/libs/flake/commands/KoShapeTransformCommand.h --- a/libs/flake/commands/KoShapeTransformCommand.h +++ b/libs/flake/commands/KoShapeTransformCommand.h @@ -50,6 +50,9 @@ /// revert the actions done in redo void undo(); + int id() const override; + bool mergeWith(const KUndo2Command *command) override; + private: class Private; Private * const d; diff --git a/libs/flake/commands/KoShapeTransformCommand.cpp b/libs/flake/commands/KoShapeTransformCommand.cpp --- a/libs/flake/commands/KoShapeTransformCommand.cpp +++ b/libs/flake/commands/KoShapeTransformCommand.cpp @@ -18,6 +18,8 @@ * Boston, MA 02110-1301, USA. */ +#include "kis_command_ids.h" + #include "KoShapeTransformCommand.h" #include "KoShape.h" @@ -75,3 +77,23 @@ shape->update(); } } + +int KoShapeTransformCommand::id() const +{ + return KisCommandUtils::TransformShapeId; +} + +bool KoShapeTransformCommand::mergeWith(const KUndo2Command *command) +{ + const KoShapeTransformCommand *other = dynamic_cast(command); + + if (!other || + other->d->shapes != d->shapes || + other->text() != text()) { + + return false; + } + + d->newState = other->d->newState; + return true; +} diff --git a/libs/flake/commands/KoShapeTransparencyCommand.h b/libs/flake/commands/KoShapeTransparencyCommand.h --- a/libs/flake/commands/KoShapeTransparencyCommand.h +++ b/libs/flake/commands/KoShapeTransparencyCommand.h @@ -57,9 +57,13 @@ virtual ~KoShapeTransparencyCommand(); /// redo the command - void redo(); + void redo() override; /// revert the actions done in redo - void undo(); + void undo() override; + + int id() const override; + bool mergeWith(const KUndo2Command *command) override; + private: class Private; Private * const d; diff --git a/libs/flake/commands/KoShapeTransparencyCommand.cpp b/libs/flake/commands/KoShapeTransparencyCommand.cpp --- a/libs/flake/commands/KoShapeTransparencyCommand.cpp +++ b/libs/flake/commands/KoShapeTransparencyCommand.cpp @@ -21,6 +21,7 @@ #include "KoShape.h" #include +#include "kis_command_ids.h" class Q_DECL_HIDDEN KoShapeTransparencyCommand::Private { @@ -98,3 +99,20 @@ ++transparencyIt; } } + +int KoShapeTransparencyCommand::id() const +{ + return KisCommandUtils::ChangeShapeTransparencyId; +} + +bool KoShapeTransparencyCommand::mergeWith(const KUndo2Command *command) +{ + const KoShapeTransparencyCommand *other = dynamic_cast(command); + + if (!other || other->d->shapes != d->shapes) { + return false; + } + + d->newTransparencies = other->d->newTransparencies; + return true; +} diff --git a/libs/flake/commands/KoShapeUnclipCommand.cpp b/libs/flake/commands/KoShapeUnclipCommand.cpp --- a/libs/flake/commands/KoShapeUnclipCommand.cpp +++ b/libs/flake/commands/KoShapeUnclipCommand.cpp @@ -23,25 +23,18 @@ #include "KoShapeContainer.h" #include "KoPathShape.h" #include "KoShapeBasedDocumentBase.h" -#include "KoShapeRegistry.h" -#include "KoShapeLoadingContext.h" -#include "KoShapeOdfSaveHelper.h" -#include "KoDrag.h" -#include -#include -#include -#include +#include #include -class KoShapeUnclipCommand::Private : public KoOdfPaste +class KoShapeUnclipCommand::Private { public: Private(KoShapeBasedDocumentBase *c) : controller(c), executed(false) { } - ~Private() override { + ~Private() { if (executed) { qDeleteAll(oldClipPaths); } else { @@ -58,53 +51,35 @@ KoClipPath *clipPath = shape->clipPath(); if (!clipPath) continue; - QList shapes; + Q_FOREACH (KoShape *clipShape, clipPath->clipPathShapes()) { - shapes.append(clipShape); - } - KoShapeOdfSaveHelper saveHelper(shapes); - KoDrag drag; - drag.setOdf(KoOdf::mimeType(KoOdf::Text), saveHelper); + KoShape *clone = clipShape->cloneShape(); - const int pathShapeCount = clipPathShapes.count(); + KoPathShape *pathShape = dynamic_cast(clone); + KIS_SAFE_ASSERT_RECOVER(pathShape) { + delete clone; + continue; + } - paste(KoOdf::Text, drag.mimeData()); + clipPathShapes.append(pathShape); + } // apply transformations - for (int i = pathShapeCount; i < clipPathShapes.count(); ++i) { - KoPathShape *pathShape = clipPathShapes[i]; + Q_FOREACH (KoPathShape *pathShape, clipPathShapes) { // apply transformation so that it matches the current clipped shapes clip path pathShape->applyAbsoluteTransformation(clipPath->clipDataTransformation(shape)); - pathShape->setZIndex(shape->zIndex()+1); + pathShape->setZIndex(shape->zIndex() + 1); clipPathParents.append(shape->parent()); } } } - /// reimplemented from KoOdfPaste - bool process(const KoXmlElement &body, KoOdfReadStore &odfStore) override { - KoOdfLoadingContext loadingContext(odfStore.styles(), odfStore.store()); - KoShapeLoadingContext context(loadingContext, controller->resourceManager()); - - KoXmlElement element; - forEachElement(element, body) { - KoShape *shape = KoShapeRegistry::instance()->createShapeFromOdf(element, context); - if (!shape) - continue; - KoPathShape *pathShape = dynamic_cast(shape); - if (!pathShape) { - delete shape; - continue; - } - clipPathShapes.append(pathShape); - } - return true; - } - QList shapesToUnclip; QList oldClipPaths; QList clipPathShapes; QList clipPathParents; + + // TODO: damn! this is not a controller, this is a document! Needs refactoring! KoShapeBasedDocumentBase *controller; bool executed; diff --git a/libs/flake/commands/KoShapeUngroupCommand.cpp b/libs/flake/commands/KoShapeUngroupCommand.cpp --- a/libs/flake/commands/KoShapeUngroupCommand.cpp +++ b/libs/flake/commands/KoShapeUngroupCommand.cpp @@ -26,7 +26,11 @@ KoShapeUngroupCommand::KoShapeUngroupCommand(KoShapeContainer *container, const QList &shapes, const QList &topLevelShapes, KUndo2Command *parent) - : KoShapeGroupCommand(*(new KoShapeGroupCommandPrivate(container, shapes)), parent) + : KoShapeGroupCommand(*(new KoShapeGroupCommandPrivate(container, + shapes, + QList(), + QList(), + false)), parent) { QList orderdShapes(shapes); qSort(orderdShapes.begin(), orderdShapes.end(), KoShape::compareShapeZIndex); @@ -49,10 +53,11 @@ d->oldParents.append(d->container->parent()); d->oldClipped.append(d->container->isClipped(shape)); d->oldInheritTransform.append(shape->parent() && shape->parent()->inheritsTransform(shape)); - d->inheritTransform.append(false); + d->inheritTransform.append(d->container->inheritsTransform(shape)); // TODO this might also need to change the children of the parent but that is very problematic if the parent is 0 d->oldZIndex.append(zIndex++); } + d->shouldNormalize = false; setText(kundo2_i18n("Ungroup shapes")); } diff --git a/libs/flake/flake.qrc b/libs/flake/flake.qrc --- a/libs/flake/flake.qrc +++ b/libs/flake/flake.qrc @@ -7,19 +7,6 @@ pics/22-actions-createpath.png pics/22-actions-editpath.png pics/22-actions-hand.png - pics/22-actions-path-break-point.png - pics/22-actions-path-break-segment.png - pics/22-actions-pathpoint-corner.png - pics/22-actions-pathpoint-curve.png - pics/22-actions-pathpoint-insert.png - pics/22-actions-pathpoint-join.png - pics/22-actions-pathpoint-line.png - pics/22-actions-pathpoint-merge.png - pics/22-actions-pathpoint-remove.png - pics/22-actions-pathpoint-smooth.png - pics/22-actions-pathpoint-symmetric.png - pics/22-actions-pathsegment-curve.png - pics/22-actions-pathsegment-line.png pics/22-actions-pathshape.png pics/questionmark.png pics/sc-actions-snap-boundingbox.svg diff --git a/libs/flake/pics/22-actions-path-break-point.png b/libs/flake/pics/22-actions-path-break-point.png deleted file mode 100644 index 0c27563f1477a6f5bbed8cc6154c5c1db150d141..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@LiW-JNv3ubV5b|VeQG4ynC4B@z*oN$19hg3sLM#hyZ7Z@b? z`DT0!_$GfqLiW-JNv3ubV5b|VeQG52(F4B@z*oN$1Bi^L2u1A~sV);3+< zhK~{wGZZKN(D==t(Dk2T(gIs%LiW-JNv3ubV5b|VeQG4*tD4B@z*oY27dn|X&OBfG&A)-r9D3}bcB pU`x?rn3+`Kuq;CI#Nq%3hN&l5KbqS5p8@J&@O1TaS?83{1OR5NC~p7& diff --git a/libs/flake/pics/22-actions-pathpoint-insert.png b/libs/flake/pics/22-actions-pathpoint-insert.png deleted file mode 100644 index 812e6995b0e5f0f0652a8b056c25fe780a13b593..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@LiW-JNv3ubV5b|VeQ(ere14B@z*oZ!H`hR;It+Jyji9LiW-JNv3ubV5b|VeQG4OP84B@z*oN$1Bhtv)!2?>|QQahHh z9jM^pIV0e?X3Af7gQN8fkpfpY{M|o+_b?}edNH@>i4>8`AX7bE{an^LB{Ts5T}Ufs diff --git a/libs/flake/pics/22-actions-pathpoint-line.png b/libs/flake/pics/22-actions-pathpoint-line.png deleted file mode 100644 index d661cb2391ba9f68fdb8d208550ff05640311f9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@LiW-JNv3ubV5b|VeQG4^zE4B@z*oN$1BhFAup=fw@IQ41Lj zLiW-JNv3ubV5b|VeQ(eQL}4B@z*oS-1Lp{>m-Az%sfZ+-_S pZVyA_CXWCB diff --git a/libs/flake/pics/22-actions-pathpoint-smooth.png b/libs/flake/pics/22-actions-pathpoint-smooth.png deleted file mode 100644 index 846889367bae1858b0078369e972716d95642844..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@LiW-JNv3ubV5b|VeQk@0kK4B@z*oUni=!_eT*!>oivj?@I6 dhK~{}m>8U;S-3l|>6`$W;OXk;vd$@?2>=n2B3S?c diff --git a/libs/flake/pics/22-actions-pathpoint-symmetric.png b/libs/flake/pics/22-actions-pathpoint-symmetric.png deleted file mode 100644 index d3ef551b3de9cce093837dd99150f8dc71957da0..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@LiW-JNv3ubV5b|VeQk@a+O4B@z*oUni=!|;QALezmn0;g1^ em>wGjure?>__HwI@T%hn8R6;b=d#Wzp$PyUCLwA7 diff --git a/libs/flake/pics/22-actions-pathsegment-curve.png b/libs/flake/pics/22-actions-pathsegment-curve.png deleted file mode 100644 index e359a90c298c8aa7150091703c3004e6e2de89b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@FVdQ&MBb@09jrm!2kdN diff --git a/libs/flake/styles/CMakeLists.txt b/libs/flake/styles/CMakeLists.txt --- a/libs/flake/styles/CMakeLists.txt +++ b/libs/flake/styles/CMakeLists.txt @@ -1,3 +1,3 @@ install(FILES - markers.xml + markers.svg DESTINATION ${DATA_INSTALL_DIR}/krita/styles) diff --git a/libs/flake/styles/markers.svg b/libs/flake/styles/markers.svg new file mode 100644 --- /dev/null +++ b/libs/flake/styles/markers.svg @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/flake/styles/markers.xml b/libs/flake/styles/markers.xml deleted file mode 100644 --- a/libs/flake/styles/markers.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/libs/flake/svg/KoShapePainter.h b/libs/flake/svg/KoShapePainter.h --- a/libs/flake/svg/KoShapePainter.h +++ b/libs/flake/svg/KoShapePainter.h @@ -23,10 +23,10 @@ #include #include +#include "kritaflake_export.h" class KoShape; class KoViewConverter; -class KoShapeManagerPaintingStrategy; class QPainter; class QImage; @@ -36,10 +36,10 @@ * the shapeManager and all its caching every time. If at all possible use * a shapeManager directly and avoid loosing the cache between usages. */ -class KoShapePainter +class KRITAFLAKE_EXPORT KoShapePainter { public: - explicit KoShapePainter(KoShapeManagerPaintingStrategy *strategy = 0); + explicit KoShapePainter(); ~KoShapePainter(); /** diff --git a/libs/flake/svg/KoShapePainter.cpp b/libs/flake/svg/KoShapePainter.cpp --- a/libs/flake/svg/KoShapePainter.cpp +++ b/libs/flake/svg/KoShapePainter.cpp @@ -22,8 +22,8 @@ #include "KoShapePainter.h" #include "KoCanvasBase.h" +#include "KoSelectedShapesProxySimple.h" #include "KoShapeManager.h" -#include "KoShapeManagerPaintingStrategy.h" #include "KoShape.h" #include "KoViewConverter.h" #include "KoShapeStrokeModel.h" @@ -39,13 +39,14 @@ { public: SimpleCanvas() - : KoCanvasBase(0), m_shapeManager(new KoShapeManager(this)) + : KoCanvasBase(0), + m_shapeManager(new KoShapeManager(this)), + m_selectedShapesProxy(new KoSelectedShapesProxySimple(m_shapeManager.data())) { } ~SimpleCanvas() override { - delete m_shapeManager; } void gridSize(QPointF *offset, QSizeF *spacing) const override @@ -65,7 +66,12 @@ KoShapeManager *shapeManager() const override { - return m_shapeManager; + return m_shapeManager.data(); + } + + KoSelectedShapesProxy *selectedShapesProxy() const override + { + return m_selectedShapesProxy.data(); } void updateCanvas(const QRectF&) override @@ -102,7 +108,8 @@ void setCursor(const QCursor &) override {} private: - KoShapeManager *m_shapeManager; + QScopedPointer m_shapeManager; + QScopedPointer m_selectedShapesProxy; }; class Q_DECL_HIDDEN KoShapePainter::Private @@ -117,13 +124,9 @@ SimpleCanvas * canvas; }; -KoShapePainter::KoShapePainter(KoShapeManagerPaintingStrategy *strategy) +KoShapePainter::KoShapePainter() : d(new Private()) { - if (strategy) { - strategy->setShapeManager(d->canvas->shapeManager()); - d->canvas->shapeManager()->setPaintingStrategy(strategy); - } } KoShapePainter::~KoShapePainter() diff --git a/libs/flake/svg/SvgClipPathHelper.h b/libs/flake/svg/SvgClipPathHelper.h --- a/libs/flake/svg/SvgClipPathHelper.h +++ b/libs/flake/svg/SvgClipPathHelper.h @@ -20,29 +20,30 @@ #ifndef SVGCLIPPATHHELPER_H #define SVGCLIPPATHHELPER_H -#include +#include +#include + +class KoShape; class SvgClipPathHelper { public: - enum Units { UserSpaceOnUse, ObjectBoundingBox }; - SvgClipPathHelper(); ~SvgClipPathHelper(); /// Set the clip path units type - void setClipPathUnits(Units clipPathUnits); + void setClipPathUnits(KoFlake::CoordinateSystem clipPathUnits); /// Returns the clip path units type - Units clipPathUnits() const; + KoFlake::CoordinateSystem clipPathUnits() const; + + QList shapes() const; + void setShapes(const QList &shapes); - /// Sets the dom element containing the clip path - void setContent(const KoXmlElement &content); - /// Return the clip path element - KoXmlElement content() const; + bool isEmpty() const; private: - Units m_clipPathUnits; - KoXmlElement m_content; + KoFlake::CoordinateSystem m_clipPathUnits; + QList m_shapes; }; #endif // SVGCLIPPATHHELPER_H diff --git a/libs/flake/svg/SvgClipPathHelper.cpp b/libs/flake/svg/SvgClipPathHelper.cpp --- a/libs/flake/svg/SvgClipPathHelper.cpp +++ b/libs/flake/svg/SvgClipPathHelper.cpp @@ -20,30 +20,35 @@ #include "SvgClipPathHelper.h" SvgClipPathHelper::SvgClipPathHelper() - : m_clipPathUnits(UserSpaceOnUse) // default as per svg spec + : m_clipPathUnits(KoFlake::UserSpaceOnUse) // default as per svg spec { } SvgClipPathHelper::~SvgClipPathHelper() { } -void SvgClipPathHelper::setClipPathUnits(Units clipPathUnits) +void SvgClipPathHelper::setClipPathUnits(KoFlake::CoordinateSystem clipPathUnits) { m_clipPathUnits = clipPathUnits; } -SvgClipPathHelper::Units SvgClipPathHelper::clipPathUnits() const +KoFlake::CoordinateSystem SvgClipPathHelper::clipPathUnits() const { return m_clipPathUnits; } -void SvgClipPathHelper::setContent(const KoXmlElement &content) +QList SvgClipPathHelper::shapes() const { - m_content = content; + return m_shapes; } -KoXmlElement SvgClipPathHelper::content() const +void SvgClipPathHelper::setShapes(const QList &shapes) { - return m_content; + m_shapes = shapes; +} + +bool SvgClipPathHelper::isEmpty() const +{ + return m_shapes.isEmpty(); } diff --git a/libs/flake/svg/SvgFilterHelper.h b/libs/flake/svg/SvgFilterHelper.h --- a/libs/flake/svg/SvgFilterHelper.h +++ b/libs/flake/svg/SvgFilterHelper.h @@ -23,27 +23,26 @@ #include #include +#include #include class QRectF; class SvgFilterHelper { public: - enum Units { UserSpaceOnUse, ObjectBoundingBox }; - SvgFilterHelper(); ~SvgFilterHelper(); /// Set the filter units type - void setFilterUnits(Units filterUnits); + void setFilterUnits(KoFlake::CoordinateSystem filterUnits); /// Returns the filter units type - Units filterUnits() const; + KoFlake::CoordinateSystem filterUnits() const; /// Set the filter primitive units type - void setPrimitiveUnits(Units primitiveUnits); + void setPrimitiveUnits(KoFlake::CoordinateSystem primitiveUnits); /// Returns the filter primitive units type - Units primitiveUnits() const; + KoFlake::CoordinateSystem primitiveUnits() const; /// Sets filter position void setPosition(const QPointF & position); @@ -63,8 +62,8 @@ static QPointF toUserSpace(const QPointF &position, const QRectF &objectBound); static QSizeF toUserSpace(const QSizeF &size, const QRectF &objectBound); private: - Units m_filterUnits; - Units m_primitiveUnits; + KoFlake::CoordinateSystem m_filterUnits; + KoFlake::CoordinateSystem m_primitiveUnits; QPointF m_position; QSizeF m_size; KoXmlElement m_filterContent; diff --git a/libs/flake/svg/SvgFilterHelper.cpp b/libs/flake/svg/SvgFilterHelper.cpp --- a/libs/flake/svg/SvgFilterHelper.cpp +++ b/libs/flake/svg/SvgFilterHelper.cpp @@ -21,8 +21,8 @@ #include "SvgUtil.h" SvgFilterHelper::SvgFilterHelper() - : m_filterUnits(ObjectBoundingBox) // default as per svg spec - , m_primitiveUnits(UserSpaceOnUse) // default as per svg spec + : m_filterUnits(KoFlake::ObjectBoundingBox) // default as per svg spec + , m_primitiveUnits(KoFlake::UserSpaceOnUse) // default as per svg spec , m_position(-0.1, -0.1) // default as per svg spec , m_size(1.2, 1.2) // default as per svg spec { @@ -32,22 +32,22 @@ { } -void SvgFilterHelper::setFilterUnits(Units filterUnits) +void SvgFilterHelper::setFilterUnits(KoFlake::CoordinateSystem filterUnits) { m_filterUnits = filterUnits; } -SvgFilterHelper::Units SvgFilterHelper::filterUnits() const +KoFlake::CoordinateSystem SvgFilterHelper::filterUnits() const { return m_filterUnits; } -void SvgFilterHelper::setPrimitiveUnits(Units primitiveUnits) +void SvgFilterHelper::setPrimitiveUnits(KoFlake::CoordinateSystem primitiveUnits) { m_primitiveUnits = primitiveUnits; } -SvgFilterHelper::Units SvgFilterHelper::primitiveUnits() const +KoFlake::CoordinateSystem SvgFilterHelper::primitiveUnits() const { return m_primitiveUnits; } @@ -59,7 +59,7 @@ QPointF SvgFilterHelper::position(const QRectF & objectBound) const { - if (m_filterUnits == UserSpaceOnUse) { + if (m_filterUnits == KoFlake::UserSpaceOnUse) { return m_position; } else { return SvgUtil::objectToUserSpace(m_position, objectBound); @@ -73,7 +73,7 @@ QSizeF SvgFilterHelper::size(const QRectF & objectBound) const { - if (m_filterUnits == UserSpaceOnUse) { + if (m_filterUnits == KoFlake::UserSpaceOnUse) { return m_size; } else { return SvgUtil::objectToUserSpace(m_size, objectBound); diff --git a/libs/flake/svg/SvgGradientHelper.h b/libs/flake/svg/SvgGradientHelper.h --- a/libs/flake/svg/SvgGradientHelper.h +++ b/libs/flake/svg/SvgGradientHelper.h @@ -21,35 +21,27 @@ #ifndef SVGGRADIENTHELPER_H #define SVGGRADIENTHELPER_H +#include #include - -class QGradient; +#include class SvgGradientHelper { public: - enum Units { UserSpaceOnUse, ObjectBoundingBox }; - SvgGradientHelper(); ~SvgGradientHelper(); /// Copy constructor SvgGradientHelper(const SvgGradientHelper &other); /// Sets the gradient units type - void setGradientUnits(Units units); + void setGradientUnits(KoFlake::CoordinateSystem units); /// Returns gradient units type - Units gradientUnits() const; + KoFlake::CoordinateSystem gradientUnits() const; /// Sets the gradient void setGradient(QGradient * g); /// Retrurns the gradient - QGradient * gradient(); - - /// Copies the given gradient - void copyGradient(QGradient * g); - - /// Returns fill adjusted to the given bounding box - QBrush adjustedFill(const QRectF &bound); + QGradient * gradient() const; /// Returns the gradient transformation QTransform transform() const; @@ -62,16 +54,17 @@ QGradient * adjustedGradient(const QRectF &bound) const; /// Converts a gradient from LogicalMode to ObjectBoundingMode - static QGradient *convertGradient(const QGradient * originalGradient, const QSizeF &size); + static QGradient *convertGradient(const QGradient * originalGradient, const QTransform &userToRelativeTransform, const QRectF &size); -private: + QGradient::Spread spreadMode() const; + void setSpreadMode(const QGradient::Spread &spreadMode); - /// Duplicates the given gradient and applies the given transformation - static QGradient *duplicateGradient(const QGradient * g, const QTransform &transform); +private: QGradient * m_gradient; - Units m_gradientUnits; + KoFlake::CoordinateSystem m_gradientUnits; QTransform m_gradientTransform; + QGradient::Spread m_spreadMode; }; #endif // SVGGRADIENTHELPER_H diff --git a/libs/flake/svg/SvgGradientHelper.cpp b/libs/flake/svg/SvgGradientHelper.cpp --- a/libs/flake/svg/SvgGradientHelper.cpp +++ b/libs/flake/svg/SvgGradientHelper.cpp @@ -28,7 +28,7 @@ #include SvgGradientHelper::SvgGradientHelper() - : m_gradient(0), m_gradientUnits(ObjectBoundingBox) + : m_gradient(new QGradient()), m_gradientUnits(KoFlake::ObjectBoundingBox) { } @@ -38,11 +38,11 @@ } SvgGradientHelper::SvgGradientHelper(const SvgGradientHelper &other) - : m_gradient(0), m_gradientUnits(ObjectBoundingBox) + : m_gradient(0), m_gradientUnits(KoFlake::ObjectBoundingBox) { m_gradientUnits = other.m_gradientUnits; m_gradientTransform = other.m_gradientTransform; - copyGradient(other.m_gradient); + m_gradient = KoFlake::cloneGradient(other.m_gradient); } SvgGradientHelper & SvgGradientHelper::operator = (const SvgGradientHelper & rhs) @@ -52,22 +52,22 @@ m_gradientUnits = rhs.m_gradientUnits; m_gradientTransform = rhs.m_gradientTransform; - copyGradient(rhs.m_gradient); + m_gradient = KoFlake::cloneGradient(rhs.m_gradient); return *this; } -void SvgGradientHelper::setGradientUnits(Units units) +void SvgGradientHelper::setGradientUnits(KoFlake::CoordinateSystem units) { m_gradientUnits = units; } -SvgGradientHelper::Units SvgGradientHelper::gradientUnits() const +KoFlake::CoordinateSystem SvgGradientHelper::gradientUnits() const { return m_gradientUnits; } -QGradient * SvgGradientHelper::gradient() +QGradient * SvgGradientHelper::gradient() const { return m_gradient; } @@ -78,25 +78,6 @@ m_gradient = g; } -void SvgGradientHelper::copyGradient(QGradient * other) -{ - delete m_gradient; - m_gradient = duplicateGradient(other, QTransform()); -} - -QBrush SvgGradientHelper::adjustedFill(const QRectF &bound) -{ - QBrush brush; - - QGradient * g = adjustedGradient(bound); - if (g) { - brush = QBrush(*g); - delete g; - } - - return brush; -} - QTransform SvgGradientHelper::transform() const { return m_gradientTransform; @@ -107,107 +88,12 @@ m_gradientTransform = transform; } -QGradient * SvgGradientHelper::adjustedGradient(const QRectF &bound) const +QGradient::Spread SvgGradientHelper::spreadMode() const { - QTransform matrix; - matrix.scale(0.01 * bound.width(), 0.01 * bound.height()); - - return duplicateGradient(m_gradient, matrix); + return m_spreadMode; } -QGradient * SvgGradientHelper::duplicateGradient(const QGradient * originalGradient, const QTransform &transform) +void SvgGradientHelper::setSpreadMode(const QGradient::Spread &spreadMode) { - if (! originalGradient) - return 0; - - QGradient * duplicatedGradient = 0; - - switch (originalGradient->type()) { - case QGradient::ConicalGradient: { - const QConicalGradient * o = static_cast(originalGradient); - QConicalGradient * g = new QConicalGradient(); - g->setAngle(o->angle()); - g->setCenter(transform.map(o->center())); - duplicatedGradient = g; - } - break; - case QGradient::LinearGradient: { - const QLinearGradient * o = static_cast(originalGradient); - QLinearGradient * g = new QLinearGradient(); - g->setStart(transform.map(o->start())); - g->setFinalStop(transform.map(o->finalStop())); - duplicatedGradient = g; - } - break; - case QGradient::RadialGradient: { - const QRadialGradient * o = static_cast(originalGradient); - QRadialGradient * g = new QRadialGradient(); - g->setCenter(transform.map(o->center())); - g->setFocalPoint(transform.map(o->focalPoint())); - g->setRadius(transform.map(QPointF(o->radius(), 0.0)).x()); - duplicatedGradient = g; - } - break; - default: - return 0; - } - - duplicatedGradient->setCoordinateMode(originalGradient->coordinateMode()); - duplicatedGradient->setStops(originalGradient->stops()); - duplicatedGradient->setSpread(originalGradient->spread()); - - return duplicatedGradient; + m_spreadMode = spreadMode; } - -QGradient *SvgGradientHelper::convertGradient(const QGradient *originalGradient, const QSizeF &size) -{ - if (! originalGradient) - return 0; - - if (originalGradient->coordinateMode() != QGradient::LogicalMode) { - return duplicateGradient(originalGradient, QTransform()); - } - - QGradient *duplicatedGradient = 0; - - switch (originalGradient->type()) { - case QGradient::ConicalGradient: - { - const QConicalGradient *o = static_cast(originalGradient); - QConicalGradient *g = new QConicalGradient(); - g->setAngle(o->angle()); - g->setCenter(KoFlake::toRelative(o->center(),size)); - duplicatedGradient = g; - } - break; - case QGradient::LinearGradient: - { - const QLinearGradient *o = static_cast(originalGradient); - QLinearGradient *g = new QLinearGradient(); - g->setStart(KoFlake::toRelative(o->start(),size)); - g->setFinalStop(KoFlake::toRelative(o->finalStop(),size)); - duplicatedGradient = g; - } - break; - case QGradient::RadialGradient: - { - const QRadialGradient *o = static_cast(originalGradient); - QRadialGradient *g = new QRadialGradient(); - g->setCenter(KoFlake::toRelative(o->center(),size)); - g->setFocalPoint(KoFlake::toRelative(o->focalPoint(),size)); - g->setRadius(KoFlake::toRelative(QPointF(o->radius(), 0.0), - QSizeF(sqrt(size.width() * size.width() + size.height() * size.height()), 0.0)).x()); - duplicatedGradient = g; - } - break; - default: - return 0; - } - - duplicatedGradient->setCoordinateMode(QGradient::ObjectBoundingMode); - duplicatedGradient->setStops(originalGradient->stops()); - duplicatedGradient->setSpread(originalGradient->spread()); - - return duplicatedGradient; -} - diff --git a/libs/flake/svg/SvgGraphicContext.h b/libs/flake/svg/SvgGraphicContext.h --- a/libs/flake/svg/SvgGraphicContext.h +++ b/libs/flake/svg/SvgGraphicContext.h @@ -37,18 +37,20 @@ }; SvgGraphicsContext(); + void workaroundClearInheritedFillProperties(); StyleType fillType; ///< the current fill type Qt::FillRule fillRule; ///< the current fill rule QColor fillColor; ///< the current fill color QString fillId; ///< the current fill id (used for gradient/pattern fills) StyleType strokeType;///< the current stroke type QString strokeId; ///< the current stroke id (used for gradient strokes) - KoShapeStroke stroke; ///< the current stroke + KoShapeStrokeSP stroke; ///< the current stroke QString filterId; ///< the current filter id QString clipPathId; ///< the current clip path id + QString clipMaskId; ///< the current clip mask id Qt::FillRule clipRule; ///< the current clip rule qreal opacity; ///< the shapes opacity @@ -58,15 +60,23 @@ QString xmlBaseDir; ///< the current base directory (used for loading external content) bool preserveWhitespace;///< preserve whitespace in element text - QRectF currentBoundbox; ///< the current bound box used for bounding box units + QRectF currentBoundingBox; ///< the current bound box used for bounding box units bool forcePercentage; ///< force parsing coordinates/length as percentages of currentBoundbox QTransform viewboxTransform; ///< view box transformation qreal letterSpacing; ///< additional spacing between characters of text elements qreal wordSpacing; ///< additional spacing between words of text elements QString baselineShift; ///< basline shift mode for text elements bool display; ///< controls display of shape + bool visible; ///< controls visibility of the shape (inherited) + qreal pixelsPerInch; ///< controls the resolution of the image raster + + QString markerStartId; + QString markerMidId; + QString markerEndId; + + bool autoFillMarkers; }; #endif // SVGGRAPHICCONTEXT_H diff --git a/libs/flake/svg/SvgGraphicContext.cpp b/libs/flake/svg/SvgGraphicContext.cpp --- a/libs/flake/svg/SvgGraphicContext.cpp +++ b/libs/flake/svg/SvgGraphicContext.cpp @@ -20,13 +20,18 @@ #include "SvgGraphicContext.h" +#include "kis_pointer_utils.h" + + SvgGraphicsContext::SvgGraphicsContext() { strokeType = None; - stroke.setLineStyle(Qt::NoPen, QVector()); // default is no stroke - stroke.setLineWidth(1.0); - stroke.setCapStyle(Qt::FlatCap); - stroke.setJoinStyle(Qt::MiterJoin); + + stroke = toQShared(new KoShapeStroke()); + stroke->setLineStyle(Qt::NoPen, QVector()); // default is no stroke + stroke->setLineWidth(1.0); + stroke->setCapStyle(Qt::FlatCap); + stroke->setJoinStyle(Qt::MiterJoin); fillType = Solid; fillRule = Qt::WindingFill; @@ -38,10 +43,42 @@ forcePercentage = false; display = true; + visible = true; clipRule = Qt::WindingFill; preserveWhitespace = false; letterSpacing = 0.0; wordSpacing = 0.0; + pixelsPerInch = 72.0; + + autoFillMarkers = false; +} + +void SvgGraphicsContext::workaroundClearInheritedFillProperties() +{ + /** + * HACK ALERT: according to SVG patterns, clip paths and clip masks + * must not inherit any properties from the referencing element. + * We still don't support it, therefore we reset only fill/stroke + * properties to avoid cyclic fill inheritance, which may cause + * infinite recursion. + */ + + + strokeType = None; + + stroke = toQShared(new KoShapeStroke()); + stroke->setLineStyle(Qt::NoPen, QVector()); // default is no stroke + stroke->setLineWidth(1.0); + stroke->setCapStyle(Qt::FlatCap); + stroke->setJoinStyle(Qt::MiterJoin); + + fillType = Solid; + fillRule = Qt::WindingFill; + fillColor = QColor(Qt::black); // default is black fill as per svg spec + + opacity = 1.0; + + currentColor = Qt::black; } diff --git a/libs/flake/svg/SvgLoadingContext.h b/libs/flake/svg/SvgLoadingContext.h --- a/libs/flake/svg/SvgLoadingContext.h +++ b/libs/flake/svg/SvgLoadingContext.h @@ -20,6 +20,7 @@ #ifndef SVGLOADINGCONTEXT_H #define SVGLOADINGCONTEXT_H +#include #include #include #include "kritaflake_export.h" @@ -55,6 +56,8 @@ /// Constructs an absolute file path from the given href and current xml base directory QString absoluteFilePath(const QString &href); + QString relativeFilePath(const QString &href); + /// Returns the next z-index int nextZIndex(); @@ -80,11 +83,21 @@ void addStyleSheet(const KoXmlElement &styleSheet); /// Returns list of css styles matching to the specified element - QStringList matchingStyles(const KoXmlElement &element) const; + QStringList matchingCssStyles(const KoXmlElement &element) const; /// Returns a style parser to parse styles SvgStyleParser &styleParser(); + /// parses 'color-profile' tag and saves it in the context + void parseProfile(const KoXmlElement &element); + + bool isRootContext() const; + + typedef std::function FileFetcherFunc; + void setFileFetcher(FileFetcherFunc func); + + QByteArray fetchExternalFile(const QString &url); + private: class Private; Private * const d; diff --git a/libs/flake/svg/SvgLoadingContext.cpp b/libs/flake/svg/SvgLoadingContext.cpp --- a/libs/flake/svg/SvgLoadingContext.cpp +++ b/libs/flake/svg/SvgLoadingContext.cpp @@ -23,14 +23,18 @@ #include #include +#include +#include +#include #include #include #include "SvgGraphicContext.h" #include "SvgUtil.h" #include "SvgCssHelper.h" #include "SvgStyleParser.h" +#include "kis_debug.h" class Q_DECL_HIDDEN SvgLoadingContext::Private @@ -56,8 +60,10 @@ KoDocumentResourceManager *documentResourceManager; QHash loadedShapes; QHash definitions; + QHash profiles; SvgCssHelper cssStyles; SvgStyleParser *styleParser; + FileFetcherFunc fileFetcher; }; SvgLoadingContext::SvgLoadingContext(KoDocumentResourceManager *documentResourceManager) @@ -81,6 +87,8 @@ return d->gcStack.top(); } +#include "parsers/SvgTransformParser.h" + SvgGraphicsContext *SvgLoadingContext::pushGraphicsContext(const KoXmlElement &element, bool inherit) { SvgGraphicsContext *gc = new SvgGraphicsContext; @@ -91,14 +99,18 @@ gc->filterId.clear(); // filters are not inherited gc->clipPathId.clear(); // clip paths are not inherited + gc->clipMaskId.clear(); // clip masks are not inherited gc->display = true; // display is not inherited gc->opacity = 1.0; // opacity is not inherited gc->baselineShift.clear(); // baseline-shift is not inherited if (!element.isNull()) { if (element.hasAttribute("transform")) { - QTransform mat = SvgUtil::parseTransform(element.attribute("transform")); - gc->matrix = mat * gc->matrix; + SvgTransformParser p(element.attribute("transform")); + if (p.isValid()) { + QTransform mat = p.transform(); + gc->matrix = mat * gc->matrix; + } } if (element.hasAttribute("xml:base")) gc->xmlBaseDir = element.attribute("xml:base"); @@ -154,6 +166,22 @@ return absFile; } +QString SvgLoadingContext::relativeFilePath(const QString &href) +{ + const SvgGraphicsContext *gc = currentGC(); + if (!gc) return href; + + QString result = href; + + if (!gc->xmlBaseDir.isEmpty()) { + result = gc->xmlBaseDir + QDir::separator() + href; + } else if (!d->initialXmlBaseDir.isEmpty()) { + result = d->initialXmlBaseDir + QDir::separator() + href; + } + + return QDir::cleanPath(result); +} + int SvgLoadingContext::nextZIndex() { return d->zIndex++; @@ -198,12 +226,74 @@ d->cssStyles.parseStylesheet(styleSheet); } -QStringList SvgLoadingContext::matchingStyles(const KoXmlElement &element) const +QStringList SvgLoadingContext::matchingCssStyles(const KoXmlElement &element) const { return d->cssStyles.matchStyles(element); } SvgStyleParser &SvgLoadingContext::styleParser() { return *d->styleParser; } + +void SvgLoadingContext::parseProfile(const KoXmlElement &element) +{ + const QString href = element.attribute("xlink:href"); + const QByteArray uniqueId = QByteArray::fromHex(element.attribute("local").toLatin1()); + const QString name = element.attribute("name"); + + if (element.attribute("rendering-intent", "auto") != "auto") { + // WARNING: Krita does *not* treat rendering intents attributes of the profile! + qDebug() << "WARNING: we do *not* treat rendering intents attributes of the profile!"; + } + + if (d->profiles.contains(name)) { + qDebug() << "Profile already in the map!" << ppVar(name); + return; + } + + const KoColorProfile *profile = + KoColorSpaceRegistry::instance()->profileByUniqueId(uniqueId); + + if (!profile && d->fileFetcher) { + KoColorSpaceEngine *engine = KoColorSpaceEngineRegistry::instance()->get("icc"); + KIS_ASSERT(engine); + if (engine) { + const QString fileName = relativeFilePath(href); + const QByteArray profileData = d->fileFetcher(fileName); + if (!profileData.isEmpty()) { + profile = engine->addProfile(profileData); + + if (profile->uniqueId() != uniqueId) { + qDebug() << "WARNING: ProfileID of the attached profile doesn't match the one mentioned in SVG element"; + qDebug() << " " << ppVar(profile->uniqueId().toHex()); + qDebug() << " " << ppVar(uniqueId.toHex()); + } + } else { + qDebug() << "WARNING: couldn't fetch the ICCprofile file!" << fileName; + } + } + } + + if (profile) { + d->profiles.insert(name, profile); + } else { + qDebug() << "WARNING: couldn't load SVG profile" << ppVar(name) << ppVar(href) << ppVar(uniqueId); + } +} + +bool SvgLoadingContext::isRootContext() const +{ + KIS_ASSERT(!d->gcStack.isEmpty()); + return d->gcStack.size() == 1; +} + +void SvgLoadingContext::setFileFetcher(SvgLoadingContext::FileFetcherFunc func) +{ + d->fileFetcher = func; +} + +QByteArray SvgLoadingContext::fetchExternalFile(const QString &url) +{ + return d->fileFetcher ? d->fileFetcher(url) : QByteArray(); +} diff --git a/libs/flake/svg/SvgParser.h b/libs/flake/svg/SvgParser.h --- a/libs/flake/svg/SvgParser.h +++ b/libs/flake/svg/SvgParser.h @@ -27,19 +27,24 @@ #include #include #include +#include +#include #include "kritaflake_export.h" #include "SvgGradientHelper.h" -#include "SvgPatternHelper.h" #include "SvgFilterHelper.h" #include "SvgClipPathHelper.h" #include "SvgLoadingContext.h" #include "SvgStyleParser.h" +#include "KoClipMask.h" class KoShape; class KoShapeGroup; class KoDocumentResourceManager; +class KoVectorPatternBackground; +class KoMarker; +class KoPathShape; class KRITAFLAKE_EXPORT SvgParser @@ -54,46 +59,60 @@ /// Sets the initial xml base directory (the directory form where the file is read) void setXmlBaseDir(const QString &baseDir); + void setResolution(const QRectF boundsInPixels, qreal pixelsPerInch); + /// Returns the list of all shapes of the svg document QList shapes() const; + typedef std::function FileFetcherFunc; + void setFileFetcher(FileFetcherFunc func); + + QList> knownMarkers() const; + protected: + /// Parses a group-like element element, saving all its topmost properties + KoShape* parseGroup(const KoXmlElement &e, const KoXmlElement &overrideChildrenFrom = KoXmlElement()); /// Parses a container element, returning a list of child shapes QList parseContainer(const KoXmlElement &); + QList parseSingleElement(const KoXmlElement &b); /// Parses a use element, returning a list of child shapes - QList parseUse(const KoXmlElement &); - /// Parses definitions for later use - void parseDefs(const KoXmlElement &); + KoShape* parseUse(const KoXmlElement &); /// Parses a gradient element - bool parseGradient(const KoXmlElement &, const KoXmlElement &referencedBy = KoXmlElement()); + SvgGradientHelper *parseGradient(const KoXmlElement &); /// Parses a pattern element - void parsePattern(SvgPatternHelper &pattern, const KoXmlElement &); + QSharedPointer parsePattern(const KoXmlElement &e, const KoShape *__shape); /// Parses a filter element bool parseFilter(const KoXmlElement &, const KoXmlElement &referencedBy = KoXmlElement()); /// Parses a clip path element - bool parseClipPath(const KoXmlElement &, const KoXmlElement &referencedBy = KoXmlElement()); + bool parseClipPath(const KoXmlElement &); + bool parseClipMask(const KoXmlElement &e); + bool parseMarker(const KoXmlElement &e); /// parses a length attribute qreal parseUnit(const QString &, bool horiz = false, bool vert = false, const QRectF &bbox = QRectF()); /// parses a length attribute in x-direction qreal parseUnitX(const QString &unit); /// parses a length attribute in y-direction qreal parseUnitY(const QString &unit); /// parses a length attribute in xy-direction qreal parseUnitXY(const QString &unit); + /// parses a angular attribute values, result in radians + qreal parseAngular(const QString &unit); + + KoShape *createObjectDirect(const KoXmlElement &b); /// Creates an object from the given xml element KoShape * createObject(const KoXmlElement &, const SvgStyles &style = SvgStyles()); /// Create path object from the given xml element KoShape * createPath(const KoXmlElement &); /// find gradient with given id in gradient map - SvgGradientHelper* findGradient(const QString &id, const QString &href = QString()); + SvgGradientHelper* findGradient(const QString &id); /// find pattern with given id in pattern map - SvgPatternHelper* findPattern(const QString &id); + QSharedPointer findPattern(const QString &id, const KoShape *shape); /// find filter with given id in filter map SvgFilterHelper* findFilter(const QString &id, const QString &href = QString()); /// find clip path with given id in clip path map - SvgClipPathHelper* findClipPath(const QString &id, const QString &href = QString()); + SvgClipPathHelper* findClipPath(const QString &id); /// Adds list of shapes to the given group shape void addToGroup(QList shapes, KoShapeGroup * group); @@ -106,11 +125,14 @@ /// Builds the document from the given shapes list void buildDocument(QList shapes); + void uploadStyleToContext(const KoXmlElement &e); + void applyCurrentStyle(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates); + /// Applies styles to the given shape - void applyStyle(KoShape *, const KoXmlElement &); + void applyStyle(KoShape *, const KoXmlElement &, const QPointF &shapeToOriginalUserCoordinates); /// Applies styles to the given shape - void applyStyle(KoShape *, const SvgStyles &); + void applyStyle(KoShape *, const SvgStyles &, const QPointF &shapeToOriginalUserCoordinates); /// Applies the current fill style to the object void applyFillStyle(KoShape * shape); @@ -122,18 +144,25 @@ void applyFilter(KoShape * shape); /// Applies the current clip path to the object - void applyClipping(KoShape *shape); + void applyClipping(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates); + void applyMaskClipping(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates); + void applyMarkers(KoPathShape *shape); /// Applies id to specified shape void applyId(const QString &id, KoShape *shape); + /// Applies viewBox transformation to the current graphical context + /// NOTE: after applying the function currectBoundingBox can become null! + void applyViewBoxTransform(const KoXmlElement &element); + private: QSizeF m_documentSize; SvgLoadingContext m_context; QMap m_gradients; - QMap m_patterns; QMap m_filters; QMap m_clipPaths; + QMap> m_clipMasks; + QMap> m_markers; KoDocumentResourceManager *m_documentResourceManager; QList m_shapes; QList m_toplevelShapes; diff --git a/libs/flake/svg/SvgParser.cpp b/libs/flake/svg/SvgParser.cpp --- a/libs/flake/svg/SvgParser.cpp +++ b/libs/flake/svg/SvgParser.cpp @@ -30,6 +30,8 @@ #include #include +#include +#include #include #include @@ -49,15 +51,22 @@ #include "KoFilterEffectStack.h" #include "KoFilterEffectLoadingContext.h" #include +#include #include #include "SvgUtil.h" #include "SvgShape.h" #include "SvgGraphicContext.h" -#include "SvgPatternHelper.h" #include "SvgFilterHelper.h" #include "SvgGradientHelper.h" #include "SvgClipPathHelper.h" +#include "parsers/SvgTransformParser.h" +#include "kis_pointer_utils.h" +#include +#include + +#include "kis_debug.h" +#include "kis_global.h" SvgParser::SvgParser(KoDocumentResourceManager *documentResourceManager) @@ -73,6 +82,27 @@ void SvgParser::setXmlBaseDir(const QString &baseDir) { m_context.setInitialXmlBaseDir(baseDir); + + setFileFetcher( + [this](const QString &name) { + const QString fileName = m_context.xmlBaseDir() + QDir::separator() + name; + QFile file(fileName); + KIS_SAFE_ASSERT_RECOVER_RETURN_VALUE(file.exists(), QByteArray()); + file.open(QIODevice::ReadOnly); + return file.readAll(); + }); +} + +void SvgParser::setResolution(const QRectF boundsInPixels, qreal pixelsPerInch) +{ + KIS_ASSERT(!m_context.currentGC()); + m_context.pushGraphicsContext(); + m_context.currentGC()->pixelsPerInch = pixelsPerInch; + + const qreal scale = 72.0 / pixelsPerInch; + const QTransform t = QTransform::fromScale(scale, scale); + m_context.currentGC()->currentBoundingBox = boundsInPixels; + m_context.currentGC()->matrix = t; } QList SvgParser::shapes() const @@ -83,77 +113,39 @@ // Helper functions // --------------------------------------------------------------------------------------- -SvgGradientHelper* SvgParser::findGradient(const QString &id, const QString &href) +SvgGradientHelper* SvgParser::findGradient(const QString &id) { + SvgGradientHelper *result = 0; + // check if gradient was already parsed, and return it - if (m_gradients.contains(id)) - return &m_gradients[ id ]; + if (m_gradients.contains(id)) { + result = &m_gradients[ id ]; + } // check if gradient was stored for later parsing - if (!m_context.hasDefinition(id)) - return 0; - - const KoXmlElement &e = m_context.definition(id); - if (!e.tagName().contains("Gradient")) - return 0; - - if (e.childNodesCount() == 0) { - QString mhref = e.attribute("xlink:href").mid(1); - - if (m_context.hasDefinition(mhref)) - return findGradient(mhref, id); - else - return 0; - } else { - // ok parse gradient now - if (! parseGradient(m_context.definition(id), m_context.definition(href))) - return 0; + if (!result && m_context.hasDefinition(id)) { + const KoXmlElement &e = m_context.definition(id); + if (e.tagName().contains("Gradient")) { + result = parseGradient(m_context.definition(id)); + } } - // return successfully parsed gradient or 0 - QString n; - if (href.isEmpty()) - n = id; - else - n = href; - - if (m_gradients.contains(n)) - return &m_gradients[ n ]; - else - return 0; + return result; } -SvgPatternHelper* SvgParser::findPattern(const QString &id) +QSharedPointer SvgParser::findPattern(const QString &id, const KoShape *shape) { - // check if pattern was already parsed, and return it - if (m_patterns.contains(id)) - return &m_patterns[ id ]; - - // check if pattern was stored for later parsing - if (!m_context.hasDefinition(id)) - return 0; - - SvgPatternHelper pattern; - - const KoXmlElement &e = m_context.definition(id); - if (e.tagName() != "pattern") - return 0; + QSharedPointer result; - // are we referencing another pattern ? - if (e.hasAttribute("xlink:href")) { - QString mhref = e.attribute("xlink:href").mid(1); - SvgPatternHelper *refPattern = findPattern(mhref); - // inherit attributes of referenced pattern - if (refPattern) - pattern = *refPattern; + // check if gradient was stored for later parsing + if (m_context.hasDefinition(id)) { + const KoXmlElement &e = m_context.definition(id); + if (e.tagName() == "pattern") { + result = parsePattern(m_context.definition(id), shape); + } } - // ok parse pattern now - parsePattern(pattern, m_context.definition(id)); - // add to parsed pattern list - m_patterns.insert(id, pattern); - - return &m_patterns[ id ]; + return result; } SvgFilterHelper* SvgParser::findFilter(const QString &id, const QString &href) @@ -193,36 +185,9 @@ return 0; } -SvgClipPathHelper* SvgParser::findClipPath(const QString &id, const QString &href) +SvgClipPathHelper* SvgParser::findClipPath(const QString &id) { - // check if clip path was already parsed, and return it - if (m_clipPaths.contains(id)) - return &m_clipPaths[ id ]; - - // check if clip path was stored for later parsing - if (!m_context.hasDefinition(id)) - return 0; - - const KoXmlElement &e = m_context.definition(id); - if (e.childNodesCount() == 0) { - QString mhref = e.attribute("xlink:href").mid(1); - - if (m_context.hasDefinition(mhref)) - return findClipPath(mhref, id); - else - return 0; - } else { - // ok clip path filter now - if (! parseClipPath(m_context.definition(id), m_context.definition(href))) - return 0; - } - - // return successfully parsed clip path or 0 - const QString n = href.isEmpty() ? id : href; - if (m_clipPaths.contains(n)) - return &m_clipPaths[ n ]; - else - return 0; + return m_clipPaths.contains(id) ? &m_clipPaths[id] : 0; } // Parsing functions @@ -248,177 +213,280 @@ return SvgUtil::parseUnitXY(m_context.currentGC(), unit); } -bool SvgParser::parseGradient(const KoXmlElement &e, const KoXmlElement &referencedBy) +qreal SvgParser::parseAngular(const QString &unit) +{ + return SvgUtil::parseUnitAngular(m_context.currentGC(), unit); +} + + +SvgGradientHelper* SvgParser::parseGradient(const KoXmlElement &e) { // IMPROVEMENTS: // - Store the parsed colorstops in some sort of a cache so they don't need to be parsed again. // - A gradient inherits attributes it does not have from the referencing gradient. // - Gradients with no color stops have no fill or stroke. // - Gradients with one color stop have a solid color. SvgGraphicsContext *gc = m_context.currentGC(); - if (!gc) - return false; + if (!gc) return 0; + + SvgGradientHelper gradHelper; - SvgGradientHelper gradhelper; + QString gradientId = e.attribute("id"); + if (gradientId.isEmpty()) return 0; + + // check if we have this gradient already parsed + // copy existing gradient if it exists + if (m_gradients.contains(gradientId)) { + return &m_gradients[gradientId]; + } if (e.hasAttribute("xlink:href")) { + // strip the '#' symbol QString href = e.attribute("xlink:href").mid(1); - if (! href.isEmpty()) { + + if (!href.isEmpty()) { // copy the referenced gradient if found SvgGradientHelper *pGrad = findGradient(href); - if (pGrad) - gradhelper = *pGrad; - } else { - //gc->fillType = SvgGraphicsContext::None; // <--- TODO Fill OR Stroke are none - return false; + if (pGrad) { + gradHelper = *pGrad; + } } } - // Use the gradient that is referencing, or if there isn't one, the original gradient. - KoXmlElement b; - if (!referencedBy.isNull()) - b = referencedBy; - else - b = e; - - QString gradientId = b.attribute("id"); + const QGradientStops defaultStops = gradHelper.gradient()->stops(); - if (! gradientId.isEmpty()) { - // check if we have this gradient already parsed - // copy existing gradient if it exists - if (m_gradients.find(gradientId) != m_gradients.end()) - gradhelper.copyGradient(m_gradients[ gradientId ].gradient()); + if (e.attribute("gradientUnits") == "userSpaceOnUse") { + gradHelper.setGradientUnits(KoFlake::UserSpaceOnUse); } - if (b.attribute("gradientUnits") == "userSpaceOnUse") - gradhelper.setGradientUnits(SvgGradientHelper::UserSpaceOnUse); + m_context.pushGraphicsContext(e); + uploadStyleToContext(e); - // parse color prop - QColor c = gc->currentColor; - - if (!b.attribute("color").isEmpty()) { - m_context.styleParser().parseColor(c, b.attribute("color")); - } else { - // try style attr - QString style = b.attribute("style").simplified(); - QStringList substyles = style.split(';', QString::SkipEmptyParts); - for (QStringList::Iterator it = substyles.begin(); it != substyles.end(); ++it) { - QStringList substyle = it->split(':'); - QString command = substyle[0].trimmed(); - QString params = substyle[1].trimmed(); - if (command == "color") - m_context.styleParser().parseColor(c, params); - } - } - gc->currentColor = c; - - if (b.tagName() == "linearGradient") { + if (e.tagName() == "linearGradient") { QLinearGradient *g = new QLinearGradient(); - if (gradhelper.gradientUnits() == SvgGradientHelper::ObjectBoundingBox) { + if (gradHelper.gradientUnits() == KoFlake::ObjectBoundingBox) { g->setCoordinateMode(QGradient::ObjectBoundingMode); - g->setStart(QPointF(SvgUtil::fromPercentage(b.attribute("x1", "0%")), - SvgUtil::fromPercentage(b.attribute("y1", "0%")))); - g->setFinalStop(QPointF(SvgUtil::fromPercentage(b.attribute("x2", "100%")), - SvgUtil::fromPercentage(b.attribute("y2", "0%")))); + g->setStart(QPointF(SvgUtil::fromPercentage(e.attribute("x1", "0%")), + SvgUtil::fromPercentage(e.attribute("y1", "0%")))); + g->setFinalStop(QPointF(SvgUtil::fromPercentage(e.attribute("x2", "100%")), + SvgUtil::fromPercentage(e.attribute("y2", "0%")))); } else { - g->setStart(QPointF(SvgUtil::fromUserSpace(b.attribute("x1").toDouble()), - SvgUtil::fromUserSpace(b.attribute("y1").toDouble()))); - g->setFinalStop(QPointF(SvgUtil::fromUserSpace(b.attribute("x2").toDouble()), - SvgUtil::fromUserSpace(b.attribute("y2").toDouble()))); + g->setStart(QPointF(parseUnitX(e.attribute("x1")), + parseUnitY(e.attribute("y1")))); + g->setFinalStop(QPointF(parseUnitX(e.attribute("x2")), + parseUnitY(e.attribute("y2")))); } - // preserve color stops - if (gradhelper.gradient()) - g->setStops(gradhelper.gradient()->stops()); - gradhelper.setGradient(g); - } else if (b.tagName() == "radialGradient") { + gradHelper.setGradient(g); + + } else if (e.tagName() == "radialGradient") { QRadialGradient *g = new QRadialGradient(); - if (gradhelper.gradientUnits() == SvgGradientHelper::ObjectBoundingBox) { + if (gradHelper.gradientUnits() == KoFlake::ObjectBoundingBox) { g->setCoordinateMode(QGradient::ObjectBoundingMode); - g->setCenter(QPointF(SvgUtil::fromPercentage(b.attribute("cx", "50%")), - SvgUtil::fromPercentage(b.attribute("cy", "50%")))); - g->setRadius(SvgUtil::fromPercentage(b.attribute("r", "50%"))); - g->setFocalPoint(QPointF(SvgUtil::fromPercentage(b.attribute("fx", "50%")), - SvgUtil::fromPercentage(b.attribute("fy", "50%")))); + g->setCenter(QPointF(SvgUtil::fromPercentage(e.attribute("cx", "50%")), + SvgUtil::fromPercentage(e.attribute("cy", "50%")))); + g->setRadius(SvgUtil::fromPercentage(e.attribute("r", "50%"))); + g->setFocalPoint(QPointF(SvgUtil::fromPercentage(e.attribute("fx", "50%")), + SvgUtil::fromPercentage(e.attribute("fy", "50%")))); } else { - g->setCenter(QPointF(SvgUtil::fromUserSpace(b.attribute("cx").toDouble()), - SvgUtil::fromUserSpace(b.attribute("cy").toDouble()))); - g->setFocalPoint(QPointF(SvgUtil::fromUserSpace(b.attribute("fx").toDouble()), - SvgUtil::fromUserSpace(b.attribute("fy").toDouble()))); - g->setRadius(SvgUtil::fromUserSpace(b.attribute("r").toDouble())); + g->setCenter(QPointF(parseUnitX(e.attribute("cx")), + parseUnitY(e.attribute("cy")))); + g->setFocalPoint(QPointF(parseUnitX(e.attribute("fx")), + parseUnitY(e.attribute("fy")))); + g->setRadius(parseUnitXY(e.attribute("r"))); } - // preserve color stops - if (gradhelper.gradient()) - g->setStops(gradhelper.gradient()->stops()); - gradhelper.setGradient(g); + gradHelper.setGradient(g); } else { - return false; + qDebug() << "WARNING: Failed to parse gradient with tag" << e.tagName(); } // handle spread method - QString spreadMethod = b.attribute("spreadMethod"); - if (!spreadMethod.isEmpty()) { - if (spreadMethod == "reflect") - gradhelper.gradient()->setSpread(QGradient::ReflectSpread); - else if (spreadMethod == "repeat") - gradhelper.gradient()->setSpread(QGradient::RepeatSpread); - else - gradhelper.gradient()->setSpread(QGradient::PadSpread); - } else - gradhelper.gradient()->setSpread(QGradient::PadSpread); + QGradient::Spread spreadMethod = QGradient::PadSpread; + QString spreadMethodStr = e.attribute("spreadMethod"); + if (!spreadMethodStr.isEmpty()) { + if (spreadMethodStr == "reflect") { + spreadMethod = QGradient::ReflectSpread; + } else if (spreadMethodStr == "repeat") { + spreadMethod = QGradient::RepeatSpread; + } + } - // Parse the color stops. The referencing gradient does not have colorstops, - // so use the stops from the gradient it references to (e in this case and not b) - m_context.styleParser().parseColorStops(gradhelper.gradient(), e); - gradhelper.setTransform(SvgUtil::parseTransform(b.attribute("gradientTransform"))); - m_gradients.insert(gradientId, gradhelper); + gradHelper.setSpreadMode(spreadMethod); - return true; + // Parse the color stops. + m_context.styleParser().parseColorStops(gradHelper.gradient(), e, gc, defaultStops); + + if (e.hasAttribute("gradientTransform")) { + SvgTransformParser p(e.attribute("gradientTransform")); + if (p.isValid()) { + gradHelper.setTransform(p.transform()); + } + } + + m_context.popGraphicsContext(); + + m_gradients.insert(gradientId, gradHelper); + + return &m_gradients[gradientId]; } -void SvgParser::parsePattern(SvgPatternHelper &pattern, const KoXmlElement &e) +inline QPointF bakeShapeOffset(const QTransform &patternTransform, const QPointF &shapeOffset) { - if (e.attribute("patternUnits") == "userSpaceOnUse") { - pattern.setPatternUnits(SvgPatternHelper::UserSpaceOnUse); + QTransform result = + patternTransform * + QTransform::fromTranslate(-shapeOffset.x(), -shapeOffset.y()) * + patternTransform.inverted(); + KIS_ASSERT_RECOVER_NOOP(result.type() <= QTransform::TxTranslate); + + return QPointF(result.dx(), result.dy()); +} + +QSharedPointer SvgParser::parsePattern(const KoXmlElement &e, const KoShape *shape) +{ + /** + * Unlike the gradient parsing function, this method is called every time we + * *reference* the pattern, not when we define it. Therefore we can already + * use the coordinate system of the destination. + */ + + QSharedPointer pattHelper; + + SvgGraphicsContext *gc = m_context.currentGC(); + if (!gc) return pattHelper; + + const QString patternId = e.attribute("id"); + if (patternId.isEmpty()) return pattHelper; + + pattHelper = toQShared(new KoVectorPatternBackground); + + if (e.hasAttribute("xlink:href")) { + // strip the '#' symbol + QString href = e.attribute("xlink:href").mid(1); + + if (!href.isEmpty() &&href != patternId) { + // copy the referenced pattern if found + QSharedPointer pPatt = findPattern(href, shape); + if (pPatt) { + pattHelper = pPatt; + } + } } - if (e.attribute("patternContentUnits") == "objectBoundingBox") { - pattern.setPatternContentUnits(SvgPatternHelper::ObjectBoundingBox); + + pattHelper->setReferenceCoordinates( + KoFlake::coordinatesFromString(e.attribute("patternUnits"), + pattHelper->referenceCoordinates())); + + pattHelper->setContentCoordinates( + KoFlake::coordinatesFromString(e.attribute("patternContentUnits"), + pattHelper->contentCoordinates())); + + if (e.hasAttribute("patternTransform")) { + SvgTransformParser p(e.attribute("patternTransform")); + if (p.isValid()) { + pattHelper->setPatternTransform(p.transform()); + } } - const QString viewBox = e.attribute("viewBox"); - if (!viewBox.isEmpty()) { - pattern.setPatternContentViewbox(SvgUtil::parseViewBox(viewBox)); + + if (pattHelper->referenceCoordinates() == KoFlake::ObjectBoundingBox) { + QRectF referenceRect( + SvgUtil::fromPercentage(e.attribute("x", "0%")), + SvgUtil::fromPercentage(e.attribute("y", "0%")), + SvgUtil::fromPercentage(e.attribute("width", "0%")), // 0% is according to SVG 1.1, don't ask me why! + SvgUtil::fromPercentage(e.attribute("height", "0%"))); // 0% is according to SVG 1.1, don't ask me why! + + pattHelper->setReferenceRect(referenceRect); + } else { + QRectF referenceRect( + parseUnitX(e.attribute("x", "0")), + parseUnitY(e.attribute("y", "0")), + parseUnitX(e.attribute("width", "0")), // 0 is according to SVG 1.1, don't ask me why! + parseUnitY(e.attribute("height", "0"))); // 0 is according to SVG 1.1, don't ask me why! + + pattHelper->setReferenceRect(referenceRect); } - const QString transform = e.attribute("patternTransform"); - if (!transform.isEmpty()) { - pattern.setTransform(SvgUtil::parseTransform(transform)); + + /** + * In Krita shapes X,Y coordinates are baked into the the shape global transform, but + * the pattern should be painted in "user" coordinates. Therefore, we should handle + * this offfset separately. + * + * TODO: Please also not that this offset is different from extraShapeOffset(), + * because A.inverted() * B != A * B.inverted(). I'm not sure which variant is + * correct (DK) + */ + + const QTransform dstShapeTransform = shape->absoluteTransformation(0); + const QTransform shapeOffsetTransform = dstShapeTransform * gc->matrix.inverted(); + KIS_SAFE_ASSERT_RECOVER_NOOP(shapeOffsetTransform.type() <= QTransform::TxTranslate); + const QPointF extraShapeOffset(shapeOffsetTransform.dx(), shapeOffsetTransform.dy()); + + m_context.pushGraphicsContext(e); + gc = m_context.currentGC(); + gc->workaroundClearInheritedFillProperties(); // HACK! + + // start building shape tree from scratch + gc->matrix = QTransform(); + + const QRectF boundingRect = shape->outline().boundingRect()/*.translated(extraShapeOffset)*/; + const QTransform relativeToShape(boundingRect.width(), 0, 0, boundingRect.height(), + boundingRect.x(), boundingRect.y()); + + + + // WARNING1: OBB and ViewBox transformations are *baked* into the pattern shapes! + // although we expect the pattern be reusable, but it is not so! + // WARNING2: the pattern shapes are stored in *User* coordinate system, although + // the "official" content system might be either OBB or User. It means that + // this baked transform should be stripped before writing the shapes back + // into SVG + if (e.hasAttribute("viewBox")) { + gc->currentBoundingBox = + pattHelper->referenceCoordinates() == KoFlake::ObjectBoundingBox ? + relativeToShape.mapRect(pattHelper->referenceRect()) : + pattHelper->referenceRect(); + + applyViewBoxTransform(e); + pattHelper->setContentCoordinates(pattHelper->referenceCoordinates()); + + } else if (pattHelper->contentCoordinates() == KoFlake::ObjectBoundingBox) { + gc->matrix = relativeToShape * gc->matrix; } - const QString x = e.attribute("x"); - const QString y = e.attribute("y"); - const QString w = e.attribute("width"); - const QString h = e.attribute("height"); - // parse tile reference rectangle - if (pattern.patternUnits() == SvgPatternHelper::UserSpaceOnUse) { - if (!x.isEmpty() && !y.isEmpty()) { - pattern.setPosition(QPointF(parseUnitX(x), parseUnitY(y))); - } - if (!w.isEmpty() && !h.isEmpty()) { - pattern.setSize(QSizeF(parseUnitX(w), parseUnitY(h))); - } - } else { - // x, y, width, height are in percentages of the object referencing the pattern - // so we just parse the percentages - if (!x.isEmpty() && !y.isEmpty()) { - pattern.setPosition(QPointF(SvgUtil::fromPercentage(x), SvgUtil::fromPercentage(y))); - } - if (!w.isEmpty() && !h.isEmpty()) { - pattern.setSize(QSizeF(SvgUtil::fromPercentage(w), SvgUtil::fromPercentage(h))); + // We do *not* apply patternTransform here! Here we only bake the untransformed + // version of the shape. The transformed one will be done in the very end while rendering. + + QList patternShapes = parseContainer(e); + + if (pattHelper->contentCoordinates() == KoFlake::UserSpaceOnUse) { + // In Krita we normalize the shapes, bake this transform into the pattern shapes + + const QPointF offset = bakeShapeOffset(pattHelper->patternTransform(), extraShapeOffset); + + Q_FOREACH (KoShape *shape, patternShapes) { + shape->applyAbsoluteTransformation(QTransform::fromTranslate(offset.x(), offset.y())); } } - if (e.hasChildNodes()) { - pattern.setContent(e); + if (pattHelper->referenceCoordinates() == KoFlake::UserSpaceOnUse) { + // In Krita we normalize the shapes, bake this transform into reference rect + // NOTE: this is possible *only* when pattern transform is not perspective + // (which is always true for SVG) + + const QPointF offset = bakeShapeOffset(pattHelper->patternTransform(), extraShapeOffset); + + QRectF ref = pattHelper->referenceRect(); + ref.translate(offset); + pattHelper->setReferenceRect(ref); + } + + m_context.popGraphicsContext(); + gc = m_context.currentGC(); + + if (!patternShapes.isEmpty()) { + pattHelper->setShapes(patternShapes); } + + return pattHelper; } bool SvgParser::parseFilter(const KoXmlElement &e, const KoXmlElement &referencedBy) @@ -446,12 +514,12 @@ } if (b.attribute("filterUnits") == "userSpaceOnUse") - filter.setFilterUnits(SvgFilterHelper::UserSpaceOnUse); + filter.setFilterUnits(KoFlake::UserSpaceOnUse); if (b.attribute("primitiveUnits") == "objectBoundingBox") - filter.setPrimitiveUnits(SvgFilterHelper::ObjectBoundingBox); + filter.setPrimitiveUnits(KoFlake::ObjectBoundingBox); // parse filter region rectangle - if (filter.filterUnits() == SvgFilterHelper::UserSpaceOnUse) { + if (filter.filterUnits() == KoFlake::UserSpaceOnUse) { filter.setPosition(QPointF(parseUnitX(b.attribute("x")), parseUnitY(b.attribute("y")))); filter.setSize(QSizeF(parseUnitX(b.attribute("width")), @@ -470,45 +538,168 @@ return true; } -bool SvgParser::parseClipPath(const KoXmlElement &e, const KoXmlElement &referencedBy) +bool SvgParser::parseMarker(const KoXmlElement &e) +{ + const QString id = e.attribute("id"); + if (id.isEmpty()) return false; + + QScopedPointer marker(new KoMarker()); + marker->setCoordinateSystem( + KoMarker::coordinateSystemFromString(e.attribute("markerUnits", "strokeWidth"))); + + marker->setReferencePoint(QPointF(parseUnitX(e.attribute("refX")), + parseUnitY(e.attribute("refY")))); + + marker->setReferenceSize(QSizeF(parseUnitX(e.attribute("markerWidth", "3")), + parseUnitY(e.attribute("markerHeight", "3")))); + + const QString orientation = e.attribute("orient", "0"); + + if (orientation == "auto") { + marker->setAutoOrientation(true); + } else { + marker->setExplicitOrientation(parseAngular(orientation)); + } + + // ensure that the clip path is loaded in local coordinates system + m_context.pushGraphicsContext(e, false); + m_context.currentGC()->matrix = QTransform(); + m_context.currentGC()->currentBoundingBox = QRectF(QPointF(0, 0), marker->referenceSize()); + + KoShape *markerShape = parseGroup(e); + + m_context.popGraphicsContext(); + + if (!markerShape) return false; + + marker->setShapes({markerShape}); + + m_markers.insert(id, QExplicitlySharedDataPointer(marker.take())); + + return true; +} + +bool SvgParser::parseClipPath(const KoXmlElement &e) { SvgClipPathHelper clipPath; - // Use the filter that is referencing, or if there isn't one, the original filter - KoXmlElement b; - if (!referencedBy.isNull()) - b = referencedBy; - else - b = e; + const QString id = e.attribute("id"); + if (id.isEmpty()) return false; - // check if we are referencing another clip path - if (e.hasAttribute("xlink:href")) { - QString href = e.attribute("xlink:href").mid(1); - if (! href.isEmpty()) { - // copy the referenced clip path if found - SvgClipPathHelper *refClipPath = findClipPath(href); - if (refClipPath) - clipPath = *refClipPath; - } + clipPath.setClipPathUnits( + KoFlake::coordinatesFromString(e.attribute("clipPathUnits"), KoFlake::UserSpaceOnUse)); + + // ensure that the clip path is loaded in local coordinates system + m_context.pushGraphicsContext(e); + m_context.currentGC()->matrix = QTransform(); + m_context.currentGC()->workaroundClearInheritedFillProperties(); // HACK! + + KoShape *clipShape = parseGroup(e); + + m_context.popGraphicsContext(); + + if (!clipShape) return false; + + clipPath.setShapes({clipShape}); + m_clipPaths.insert(id, clipPath); + + return true; +} + +bool SvgParser::parseClipMask(const KoXmlElement &e) +{ + QSharedPointer clipMask(new KoClipMask); + + const QString id = e.attribute("id"); + if (id.isEmpty()) return false; + + clipMask->setCoordinates(KoFlake::coordinatesFromString(e.attribute("maskUnits"), KoFlake::ObjectBoundingBox)); + clipMask->setContentCoordinates(KoFlake::coordinatesFromString(e.attribute("maskContentUnits"), KoFlake::UserSpaceOnUse)); + + QRectF maskRect; + + if (clipMask->coordinates() == KoFlake::ObjectBoundingBox) { + maskRect.setRect( + SvgUtil::fromPercentage(e.attribute("x", "-10%")), + SvgUtil::fromPercentage(e.attribute("y", "-10%")), + SvgUtil::fromPercentage(e.attribute("width", "120%")), + SvgUtil::fromPercentage(e.attribute("height", "120%"))); } else { - clipPath.setContent(b); + maskRect.setRect( + parseUnitX(e.attribute("x", "-10%")), // yes, percents are insane in this case, + parseUnitY(e.attribute("y", "-10%")), // but this is what SVG 1.1 tells us... + parseUnitX(e.attribute("width", "120%")), + parseUnitY(e.attribute("height", "120%"))); } - if (b.attribute("clipPathUnits") == "objectBoundingBox") - clipPath.setClipPathUnits(SvgClipPathHelper::ObjectBoundingBox); + clipMask->setMaskRect(maskRect); + + + // ensure that the clip mask is loaded in local coordinates system + m_context.pushGraphicsContext(e); + m_context.currentGC()->matrix = QTransform(); + m_context.currentGC()->workaroundClearInheritedFillProperties(); // HACK! + KoShape *clipShape = parseGroup(e); - m_clipPaths.insert(b.attribute("id"), clipPath); + m_context.popGraphicsContext(); + + if (!clipShape) return false; + clipMask->setShapes({clipShape}); + m_clipMasks.insert(id, clipMask); return true; } -void SvgParser::applyStyle(KoShape *obj, const KoXmlElement &e) +void SvgParser::uploadStyleToContext(const KoXmlElement &e) { - applyStyle(obj, m_context.styleParser().collectStyles(e)); + SvgStyles styles = m_context.styleParser().collectStyles(e); + m_context.styleParser().parseFont(styles); + m_context.styleParser().parseStyle(styles); } -void SvgParser::applyStyle(KoShape *obj, const SvgStyles &styles) +void SvgParser::applyCurrentStyle(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates) +{ + if (!shape) return; + + SvgGraphicsContext *gc = m_context.currentGC(); + KIS_ASSERT(gc); + + if (!dynamic_cast(shape)) { + applyFillStyle(shape); + applyStrokeStyle(shape); + } + + if (KoPathShape *pathShape = dynamic_cast(shape)) { + applyMarkers(pathShape); + } + + applyFilter(shape); + applyClipping(shape, shapeToOriginalUserCoordinates); + applyMaskClipping(shape, shapeToOriginalUserCoordinates); + + if (!gc->display || !gc->visible) { + /** + * WARNING: here is a small inconsistency with the standard: + * in the standard, 'display' is not inherited, but in + * flake it is! + * + * NOTE: though the standard says: "A value of 'display:none' indicates + * that the given element and ***its children*** shall not be + * rendered directly". Therefore, using setVisible(false) is fully + * legitimate here (DK 29.11.16). + */ + shape->setVisible(false); + } + shape->setTransparency(1.0 - gc->opacity); +} + +void SvgParser::applyStyle(KoShape *obj, const KoXmlElement &e, const QPointF &shapeToOriginalUserCoordinates) +{ + applyStyle(obj, m_context.styleParser().collectStyles(e), shapeToOriginalUserCoordinates); +} + +void SvgParser::applyStyle(KoShape *obj, const SvgStyles &styles, const QPointF &shapeToOriginalUserCoordinates) { SvgGraphicsContext *gc = m_context.currentGC(); if (!gc) @@ -523,14 +714,70 @@ applyFillStyle(obj); applyStrokeStyle(obj); } + + if (KoPathShape *pathShape = dynamic_cast(obj)) { + applyMarkers(pathShape); + } + applyFilter(obj); - applyClipping(obj); + applyClipping(obj, shapeToOriginalUserCoordinates); + applyMaskClipping(obj, shapeToOriginalUserCoordinates); - if (! gc->display) + if (!gc->display || !gc->visible) { obj->setVisible(false); + } obj->setTransparency(1.0 - gc->opacity); } +QGradient* prepareGradientForShape(const SvgGradientHelper *gradient, + const KoShape *shape, + const SvgGraphicsContext *gc, + QTransform *transform) +{ + QGradient *resultGradient = 0; + KIS_ASSERT(transform); + + if (gradient->gradientUnits() == KoFlake::ObjectBoundingBox) { + resultGradient = KoFlake::cloneGradient(gradient->gradient()); + *transform = gradient->transform(); + } else { + if (gradient->gradient()->type() == QGradient::LinearGradient) { + /** + * Create a converted gradient that looks the same, but linked to the + * bounding rect of the shape, so it would be transformed with the shape + */ + + const QRectF boundingRect = shape->outline().boundingRect(); + const QTransform relativeToShape(boundingRect.width(), 0, 0, boundingRect.height(), + boundingRect.x(), boundingRect.y()); + + const QTransform relativeToUser = + relativeToShape * shape->transformation() * gc->matrix.inverted(); + + const QTransform userToRelative = relativeToUser.inverted(); + + const QLinearGradient *o = static_cast(gradient->gradient()); + QLinearGradient *g = new QLinearGradient(); + g->setStart(userToRelative.map(o->start())); + g->setFinalStop(userToRelative.map(o->finalStop())); + g->setCoordinateMode(QGradient::ObjectBoundingMode); + g->setStops(o->stops()); + g->setSpread(o->spread()); + + resultGradient = g; + *transform = relativeToUser * gradient->transform() * userToRelative; + + } else if (gradient->gradient()->type() == QGradient::RadialGradient) { + // For radial and conical gradients such conversion is not possible + + resultGradient = KoFlake::cloneGradient(gradient->gradient()); + *transform = gradient->transform() * gc->matrix * shape->transformation().inverted(); + } + } + + return resultGradient; +} + void SvgParser::applyFillStyle(KoShape *shape) { SvgGraphicsContext *gc = m_context.currentGC(); @@ -545,91 +792,20 @@ // try to find referenced gradient SvgGradientHelper *gradient = findGradient(gc->fillId); if (gradient) { - // great, we have a gradient fill - QSharedPointer bg; - if (gradient->gradientUnits() == SvgGradientHelper::ObjectBoundingBox) { - bg = QSharedPointer(new KoGradientBackground(*gradient->gradient())); - bg->setTransform(gradient->transform()); - } else { - QGradient *convertedGradient = SvgGradientHelper::convertGradient(gradient->gradient(), shape->size()); - bg = QSharedPointer(new KoGradientBackground(convertedGradient)); - QTransform invShapematrix = shape->transformation().inverted(); - bg->setTransform(gradient->transform() * gc->matrix * invShapematrix); + QTransform transform; + QGradient *result = prepareGradientForShape(gradient, shape, gc, &transform); + if (result) { + QSharedPointer bg; + bg = toQShared(new KoGradientBackground(result)); + bg->setTransform(transform); + shape->setBackground(bg); } - shape->setBackground(bg); } else { - // try to find referenced pattern - SvgPatternHelper *pattern = findPattern(gc->fillId); - KoImageCollection *imageCollection = m_documentResourceManager->imageCollection(); - if (pattern && imageCollection) { - // great we have a pattern fill - QRectF objectBound = QRectF(QPoint(), shape->size()); - QRectF currentBoundbox = gc->currentBoundbox; - - // properties from the object are not inherited - // so we are creating a new context without copying - SvgGraphicsContext *gc = m_context.pushGraphicsContext(pattern->content(), false); - - // the pattern establishes a new coordinate system with its - // origin at the patterns x and y attributes - gc->matrix = QTransform(); - // object bounding box units are relative to the object the pattern is applied - if (pattern->patternContentUnits() == SvgPatternHelper::ObjectBoundingBox) { - gc->currentBoundbox = objectBound; - gc->forcePercentage = true; - } else { - // inherit the current bounding box - gc->currentBoundbox = currentBoundbox; - } - - applyStyle(0, pattern->content()); - - // parse the pattern content elements - QList patternContent = parseContainer(pattern->content()); - - // generate the pattern image from the shapes and the object bounding rect - QImage image = pattern->generateImage(objectBound, patternContent); - - m_context.popGraphicsContext(); - - // delete the shapes created from the pattern content - qDeleteAll(patternContent); + QSharedPointer pattern = + findPattern(gc->fillId, shape); - if (!image.isNull()) { - QSharedPointer bg(new KoPatternBackground(imageCollection)); - bg->setPattern(image); - - QPointF refPoint = shape->documentToShape(pattern->position(objectBound)); - QSizeF tileSize = pattern->size(objectBound); - - bg->setPatternDisplaySize(tileSize); - if (pattern->patternUnits() == SvgPatternHelper::ObjectBoundingBox) { - if (tileSize == objectBound.size()) - bg->setRepeat(KoPatternBackground::Stretched); - } - - // calculate pattern reference point offset in percent of tileSize - // and relative to the topleft corner of the shape - qreal fx = refPoint.x() / tileSize.width(); - qreal fy = refPoint.y() / tileSize.height(); - if (fx < 0.0) - fx = std::floor(fx); - else if (fx > 1.0) - fx = std::ceil(fx); - else - fx = 0.0; - if (fy < 0.0) - fy = std::floor(fy); - else if (fx > 1.0) - fy = std::ceil(fy); - else - fy = 0.0; - qreal offsetX = 100.0 * (refPoint.x() - fx * tileSize.width()) / tileSize.width(); - qreal offsetY = 100.0 * (refPoint.y() - fy * tileSize.height()) / tileSize.height(); - bg->setReferencePointOffset(QPointF(offsetX, offsetY)); - - shape->setBackground(bg); - } + if (pattern) { + shape->setBackground(pattern); } else { // no referenced fill found, use fallback color shape->setBackground(QSharedPointer(new KoColorBackground(gc->fillColor))); @@ -642,55 +818,59 @@ path->setFillRule(gc->fillRule); } +void applyDashes(const KoShapeStrokeSP srcStroke, KoShapeStrokeSP dstStroke) +{ + const double lineWidth = srcStroke->lineWidth(); + QVector dashes = srcStroke->lineDashes(); + + // apply line width to dashes and dash offset + if (dashes.count() && lineWidth > 0.0) { + const double dashOffset = srcStroke->dashOffset(); + QVector dashes = srcStroke->lineDashes(); + + for (int i = 0; i < dashes.count(); ++i) { + dashes[i] /= lineWidth; + } + + dstStroke->setLineStyle(Qt::CustomDashLine, dashes); + dstStroke->setDashOffset(dashOffset / lineWidth); + } else { + dstStroke->setLineStyle(Qt::SolidLine, QVector()); + } +} + void SvgParser::applyStrokeStyle(KoShape *shape) { SvgGraphicsContext *gc = m_context.currentGC(); if (! gc) return; if (gc->strokeType == SvgGraphicsContext::None) { - shape->setStroke(0); + shape->setStroke(KoShapeStrokeModelSP()); } else if (gc->strokeType == SvgGraphicsContext::Solid) { - double lineWidth = gc->stroke.lineWidth(); - QVector dashes = gc->stroke.lineDashes(); - - KoShapeStroke *stroke = new KoShapeStroke(gc->stroke); - - // apply line width to dashes and dash offset - if (dashes.count() && lineWidth > 0.0) { - QVector dashes = stroke->lineDashes(); - for (int i = 0; i < dashes.count(); ++i) - dashes[i] /= lineWidth; - double dashOffset = stroke->dashOffset(); - stroke->setLineStyle(Qt::CustomDashLine, dashes); - stroke->setDashOffset(dashOffset / lineWidth); - } else { - stroke->setLineStyle(Qt::SolidLine, QVector()); - } + KoShapeStrokeSP stroke(new KoShapeStroke(*gc->stroke)); + applyDashes(gc->stroke, stroke); shape->setStroke(stroke); } else if (gc->strokeType == SvgGraphicsContext::Complex) { // try to find referenced gradient SvgGradientHelper *gradient = findGradient(gc->strokeId); if (gradient) { - // great, we have a gradient stroke - QBrush brush; - if (gradient->gradientUnits() == SvgGradientHelper::ObjectBoundingBox) { - brush = *gradient->gradient(); - brush.setTransform(gradient->transform()); - } else { - QGradient *convertedGradient(SvgGradientHelper::convertGradient(gradient->gradient(), shape->size())); - brush = *convertedGradient; - delete convertedGradient; - brush.setTransform(gradient->transform() * gc->matrix * shape->transformation().inverted()); + QTransform transform; + QGradient *result = prepareGradientForShape(gradient, shape, gc, &transform); + if (result) { + QBrush brush = *result; + delete result; + brush.setTransform(transform); + + KoShapeStrokeSP stroke(new KoShapeStroke(*gc->stroke)); + stroke->setLineBrush(brush); + applyDashes(gc->stroke, stroke); + shape->setStroke(stroke); } - KoShapeStroke *stroke = new KoShapeStroke(gc->stroke); - stroke->setLineBrush(brush); - stroke->setLineStyle(Qt::SolidLine, QVector()); - shape->setStroke(stroke); } else { // no referenced stroke found, use fallback color - KoShapeStroke *stroke = new KoShapeStroke(gc->stroke); - stroke->setLineStyle(Qt::SolidLine, QVector()); + KoShapeStrokeSP stroke(new KoShapeStroke(*gc->stroke)); + applyDashes(gc->stroke, stroke); shape->setStroke(stroke); } } @@ -727,8 +907,8 @@ KoFilterEffectLoadingContext context(m_context.xmlBaseDir()); context.setShapeBoundingBox(bound); // enable units conversion - context.enableFilterUnitsConversion(filter->filterUnits() == SvgFilterHelper::UserSpaceOnUse); - context.enableFilterPrimitiveUnitsConversion(filter->primitiveUnits() == SvgFilterHelper::UserSpaceOnUse); + context.enableFilterUnitsConversion(filter->filterUnits() == KoFlake::UserSpaceOnUse); + context.enableFilterPrimitiveUnitsConversion(filter->primitiveUnits() == KoFlake::UserSpaceOnUse); KoFilterEffectRegistry *registry = KoFilterEffectRegistry::instance(); @@ -761,7 +941,7 @@ QRectF subRegion; // parse subregion - if (filter->primitiveUnits() == SvgFilterHelper::UserSpaceOnUse) { + if (filter->primitiveUnits() == KoFlake::UserSpaceOnUse) { const QString xa = primitive.attribute("x"); const QString ya = primitive.attribute("y"); const QString wa = primitive.attribute("width"); @@ -824,7 +1004,28 @@ } } -void SvgParser::applyClipping(KoShape *shape) +void SvgParser::applyMarkers(KoPathShape *shape) +{ + SvgGraphicsContext *gc = m_context.currentGC(); + if (!gc) + return; + + if (!gc->markerStartId.isEmpty() && m_markers.contains(gc->markerStartId)) { + shape->setMarker(m_markers[gc->markerStartId].data(), KoFlake::StartMarker); + } + + if (!gc->markerMidId.isEmpty() && m_markers.contains(gc->markerMidId)) { + shape->setMarker(m_markers[gc->markerMidId].data(), KoFlake::MidMarker); + } + + if (!gc->markerEndId.isEmpty() && m_markers.contains(gc->markerEndId)) { + shape->setMarker(m_markers[gc->markerEndId].data(), KoFlake::EndMarker); + } + + shape->setAutoFillMarkers(gc->autoFillMarkers); +} + +void SvgParser::applyClipping(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates) { SvgGraphicsContext *gc = m_context.currentGC(); if (! gc) @@ -834,193 +1035,205 @@ return; SvgClipPathHelper *clipPath = findClipPath(gc->clipPathId); - if (! clipPath) + if (!clipPath || clipPath->isEmpty()) return; - debugFlake << "applying clip path" << gc->clipPathId << "clip rule" << gc->clipRule; - - const bool boundingBoxUnits = clipPath->clipPathUnits() == SvgClipPathHelper::ObjectBoundingBox; - debugFlake << "using" << (boundingBoxUnits ? "boundingBoxUnits" : "userSpaceOnUse"); + QList shapes; - QTransform shapeMatrix = shape->absoluteTransformation(0); - // TODO: - // clip path element can have a clip-path property - // -> clip-path = intersection of children with referenced clip-path - // any of its children can have a clip-path property - // -> child element is clipped and the ORed with other children - m_context.pushGraphicsContext(); + Q_FOREACH (KoShape *item, clipPath->shapes()) { + KoShape *clonedShape = item->cloneShape(); + KIS_ASSERT_RECOVER(clonedShape) { continue; } - if (boundingBoxUnits) { - SvgGraphicsContext *gc = m_context.currentGC(); - gc->matrix.reset(); - gc->viewboxTransform.reset(); - gc->currentBoundbox = shape->boundingRect(); - gc->forcePercentage = true; + shapes.append(clonedShape); } - QList clipShapes = parseContainer(clipPath->content()); - QList pathShapes; - while (!clipShapes.isEmpty()) { - KoShape *clipShape = clipShapes.first(); - clipShapes.removeFirst(); - // remove clip shape from list of all parsed shapes - m_shapes.removeOne(clipShape); - // check if we have a path shape - KoPathShape *path = dynamic_cast(clipShape); - if (!path) { - // if shape is a group, ungroup and add children to lits of clip shapes - KoShapeGroup *group = dynamic_cast(clipShape); - if (group) { - QList groupedShapes = group->shapes(); - KoShapeUngroupCommand cmd(group, groupedShapes); - cmd.redo(); - clipShapes.append(groupedShapes); - } else { - // shape is not a group shape, use its outline as clip path - QPainterPath outline = clipShape->absoluteTransformation(0).map(clipShape->outline()); - path = KoPathShape::createShapeFromPainterPath(outline); - } - delete clipShape; - } - if (path) { - debugFlake << "using shape" << path->name() << "as clip path"; - pathShapes.append(path); - if (boundingBoxUnits) - path->applyAbsoluteTransformation(shapeMatrix); + if (!shapeToOriginalUserCoordinates.isNull()) { + const QTransform t = + QTransform::fromTranslate(shapeToOriginalUserCoordinates.x(), + shapeToOriginalUserCoordinates.y()); + + Q_FOREACH(KoShape *s, shapes) { + s->applyAbsoluteTransformation(t); } } - m_context.popGraphicsContext(); - - if (pathShapes.count()) { - QTransform transformToShape; - if (!boundingBoxUnits) - transformToShape = shape->absoluteTransformation(0).inverted(); - KoClipData *clipData = new KoClipData(pathShapes); - KoClipPath *clipPath = new KoClipPath(shape, clipData); - clipPath->setClipRule(gc->clipRule); - shape->setClipPath(clipPath); - } + KoClipPath *clipPathObject = new KoClipPath(shapes, + clipPath->clipPathUnits() == KoFlake::ObjectBoundingBox ? + KoFlake::ObjectBoundingBox : KoFlake::UserSpaceOnUse); + shape->setClipPath(clipPathObject); } -QList SvgParser::parseUse(const KoXmlElement &e) +void SvgParser::applyMaskClipping(KoShape *shape, const QPointF &shapeToOriginalUserCoordinates) { - QList shapes; + SvgGraphicsContext *gc = m_context.currentGC(); + if (!gc) + return; - QString id = e.attribute("xlink:href"); - // - if (!id.isEmpty()) { - SvgGraphicsContext *gc = m_context.pushGraphicsContext(e); + if (gc->clipMaskId.isEmpty()) + return; - // TODO: use width and height attributes too - gc->matrix.translate(parseUnitX(e.attribute("x", "0")), parseUnitY(e.attribute("y", "0"))); - QString key = id.mid(1); + QSharedPointer originalClipMask = m_clipMasks.value(gc->clipMaskId); + if (!originalClipMask || originalClipMask->isEmpty()) return; - if (m_context.hasDefinition(key)) { - const KoXmlElement &a = m_context.definition(key); - SvgStyles styles = m_context.styleParser().mergeStyles(e, a); - if (a.tagName() == "g" || a.tagName() == "a" || a.tagName() == "symbol") { - m_context.pushGraphicsContext(a); + KoClipMask *clipMask = originalClipMask->clone(); - KoShapeGroup *group = new KoShapeGroup(); - group->setZIndex(m_context.nextZIndex()); + clipMask->setExtraShapeOffset(shapeToOriginalUserCoordinates); - applyStyle(0, styles); - m_context.styleParser().parseFont(styles); + shape->setClipMask(clipMask); +} - QList childShapes = parseContainer(a); +KoShape* SvgParser::parseUse(const KoXmlElement &e) +{ + KoShape *result = 0; - // handle id - applyId(a.attribute("id"), group); + QString id = e.attribute("xlink:href"); + if (!id.isEmpty()) { - addToGroup(childShapes, group); - applyStyle(group, styles); // apply style to group after size is set + QString key = id.mid(1); + if (m_context.hasDefinition(key)) { - shapes.append(group); + SvgGraphicsContext *gc = m_context.pushGraphicsContext(e); - m_context.popGraphicsContext(); - } else { - // Create the object with the merged styles. - // The object inherits all style attributes from the use tag, but keeps it's own attributes. - // So, not just use the style attributes of the use tag, but merge them first. - KoShape *shape = createObject(a, styles); - if (shape) - shapes.append(shape); - } - } else { - // TODO: any named object can be referenced too + // TODO: parse 'width' and 'hight' as well + gc->matrix.translate(parseUnitX(e.attribute("x", "0")), parseUnitY(e.attribute("y", "0"))); + + const KoXmlElement &referencedElement = m_context.definition(key); + result = parseGroup(e, referencedElement); + + m_context.popGraphicsContext(); } - m_context.popGraphicsContext(); } - return shapes; + return result; } void SvgParser::addToGroup(QList shapes, KoShapeGroup *group) { m_shapes += shapes; - if (! group) + if (!group || shapes.isEmpty()) return; - KoShapeGroupCommand cmd(group, shapes); + // not clipped, but inherit transform + KoShapeGroupCommand cmd(group, shapes, false, true, false); cmd.redo(); } QList SvgParser::parseSvg(const KoXmlElement &e, QSizeF *fragmentSize) { // check if we are the root svg element - const bool isRootSvg = !m_context.currentGC(); - - SvgGraphicsContext *gc = m_context.pushGraphicsContext(); + const bool isRootSvg = m_context.isRootContext(); - applyStyle(0, e); + // parse 'transform' field if preset + SvgGraphicsContext *gc = m_context.pushGraphicsContext(e); - QRectF viewBox; - - const QString viewBoxStr = e.attribute("viewBox"); - if (!viewBoxStr.isEmpty()) { - viewBox = SvgUtil::parseViewBox(viewBoxStr); - } + applyStyle(0, e, QPointF()); const QString w = e.attribute("width"); const QString h = e.attribute("height"); - const qreal width = w.isEmpty() ? 550.0 : parseUnit(w, true, false, viewBox); - const qreal height = h.isEmpty() ? 841.0 : parseUnit(h, false, true, viewBox); + const qreal width = w.isEmpty() ? 666.0 : parseUnitX(w); + const qreal height = h.isEmpty() ? 555.0 : parseUnitY(h); QSizeF svgFragmentSize(QSizeF(width, height)); - if (fragmentSize) + if (fragmentSize) { *fragmentSize = svgFragmentSize; + } - gc->currentBoundbox = QRectF(QPointF(0, 0), svgFragmentSize); + gc->currentBoundingBox = QRectF(QPointF(0, 0), svgFragmentSize); - if (! isRootSvg) { - QTransform move; + if (!isRootSvg) { // x and y attribute has no meaning for outermost svg elements const qreal x = parseUnit(e.attribute("x", "0")); const qreal y = parseUnit(e.attribute("y", "0")); - move.translate(x, y); + + QTransform move = QTransform::fromTranslate(x, y); gc->matrix = move * gc->matrix; - gc->viewboxTransform = move *gc->viewboxTransform; } - if (!viewBoxStr.isEmpty()) { - QTransform viewTransform; - viewTransform.translate(viewBox.x(), viewBox.y()); - viewTransform.scale(width / viewBox.width() , height / viewBox.height()); + applyViewBoxTransform(e); + + QList shapes; + + // SVG 1.1: skip the rendering of the element if it has null viewBox + if (gc->currentBoundingBox.isValid()) { + shapes = parseContainer(e); + } + + m_context.popGraphicsContext(); + + return shapes; +} + +void SvgParser::applyViewBoxTransform(const KoXmlElement &element) +{ + SvgGraphicsContext *gc = m_context.currentGC(); + + QRectF viewRect = gc->currentBoundingBox; + QTransform viewTransform; + + if (SvgUtil::parseViewBox(gc, element, gc->currentBoundingBox, + &viewRect, &viewTransform)) { + gc->matrix = viewTransform * gc->matrix; - gc->viewboxTransform = viewTransform *gc->viewboxTransform; - gc->currentBoundbox.setWidth(gc->currentBoundbox.width() * (viewBox.width() / width)); - gc->currentBoundbox.setHeight(gc->currentBoundbox.height() * (viewBox.height() / height)); + gc->currentBoundingBox = viewRect; + } +} + +QList > SvgParser::knownMarkers() const +{ + return m_markers.values(); +} + +void SvgParser::setFileFetcher(SvgParser::FileFetcherFunc func) +{ + m_context.setFileFetcher(func); +} + +inline QPointF extraShapeOffset(const KoShape *shape, const QTransform coordinateSystemOnLoading) +{ + const QTransform shapeToOriginalUserCoordinates = + shape->absoluteTransformation(0).inverted() * + coordinateSystemOnLoading; + + KIS_SAFE_ASSERT_RECOVER_NOOP(shapeToOriginalUserCoordinates.type() <= QTransform::TxTranslate); + return QPointF(shapeToOriginalUserCoordinates.dx(), shapeToOriginalUserCoordinates.dy()); +} + +KoShape* SvgParser::parseGroup(const KoXmlElement &b, const KoXmlElement &overrideChildrenFrom) +{ + m_context.pushGraphicsContext(b); + + KoShapeGroup *group = new KoShapeGroup(); + group->setZIndex(m_context.nextZIndex()); + + // groups should also have their own coordinate system! + group->applyAbsoluteTransformation(m_context.currentGC()->matrix); + const QPointF extraOffset = extraShapeOffset(group, m_context.currentGC()->matrix); + + uploadStyleToContext(b); + + QList childShapes; + + if (!overrideChildrenFrom.isNull()) { + // we upload styles from both: and + uploadStyleToContext(overrideChildrenFrom); + childShapes = parseSingleElement(overrideChildrenFrom); + } else { + childShapes = parseContainer(b); } - QList shapes = parseContainer(e); + // handle id + applyId(b.attribute("id"), group); + + addToGroup(childShapes, group); + + applyCurrentStyle(group, extraOffset); // apply style to this group after size is set m_context.popGraphicsContext(); - return shapes; + return group; } QList SvgParser::parseContainer(const KoXmlElement &e) @@ -1051,103 +1264,81 @@ } } - if (b.tagName() == "svg") { - shapes += parseSvg(b); - } else if (b.tagName() == "g" || b.tagName() == "a" || b.tagName() == "symbol") { - // treat svg link as group so we don't miss its child elements - m_context.pushGraphicsContext(b); - - KoShapeGroup *group = new KoShapeGroup(); - group->setZIndex(m_context.nextZIndex()); - - SvgStyles styles = m_context.styleParser().collectStyles(b); - m_context.styleParser().parseFont(styles); - applyStyle(0, styles); // parse style for inheritance - - QList childShapes = parseContainer(b); - - // handle id - applyId(b.attribute("id"), group); - - addToGroup(childShapes, group); - - const QString viewBoxStr = b.attribute("viewBox"); - if (!viewBoxStr.isEmpty()) { - QRectF viewBox = SvgUtil::parseViewBox(viewBoxStr); - QTransform viewTransform; - viewTransform.translate(viewBox.x(), viewBox.y()); - viewTransform.scale(group->size().width() / viewBox.width() , group->size().height() / viewBox.height()); - group->applyAbsoluteTransformation(viewTransform); - } - applyStyle(group, styles); // apply style to this group after size is set - - shapes.append(group); - - m_context.popGraphicsContext(); - } else if (b.tagName() == "switch") { - m_context.pushGraphicsContext(b); - shapes += parseContainer(b); - m_context.popGraphicsContext(); - } else if (b.tagName() == "defs") { - parseDefs(b); - } else if (b.tagName() == "linearGradient" || b.tagName() == "radialGradient") { - parseGradient(b); - } else if (b.tagName() == "pattern") { - m_context.addDefinition(b); - } else if (b.tagName() == "filter") { - parseFilter(b); - } else if (b.tagName() == "clipPath") { - parseClipPath(b); - } else if (b.tagName() == "style") { - m_context.addStyleSheet(b); - } else if (b.tagName() == "rect" || - b.tagName() == "ellipse" || - b.tagName() == "circle" || - b.tagName() == "line" || - b.tagName() == "polyline" || - b.tagName() == "polygon" || - b.tagName() == "path" || - b.tagName() == "image" || - b.tagName() == "text") { - KoShape *shape = createObject(b); - if (shape) - shapes.append(shape); - } else if (b.tagName() == "use") { - shapes += parseUse(b); - } else { - // this is an unknown element, so try to load it anyway - // there might be a shape that handles that element - KoShape *shape = createObject(b); - if (shape) { - shapes.append(shape); - } else { - continue; - } - } + QList currentShapes = parseSingleElement(b); + shapes.append(currentShapes); // if we are parsing a switch, stop after the first supported element - if (isSwitch) + if (isSwitch && !currentShapes.isEmpty()) break; } return shapes; } -void SvgParser::parseDefs(const KoXmlElement &e) +QList SvgParser::parseSingleElement(const KoXmlElement &b) { - for (KoXmlNode n = e.firstChild(); !n.isNull(); n = n.nextSibling()) { - KoXmlElement b = n.toElement(); - if (b.isNull()) - continue; + QList shapes; - if (b.tagName() == "style") { - m_context.addStyleSheet(b); - } else if (b.tagName() == "defs") { - parseDefs(b); - } else { - m_context.addDefinition(b); + // save definition for later instantiation with 'use' + m_context.addDefinition(b); + + if (b.tagName() == "svg") { + shapes += parseSvg(b); + } else if (b.tagName() == "g" || b.tagName() == "a" || b.tagName() == "symbol") { + // treat svg link as group so we don't miss its child elements + shapes += parseGroup(b); + } else if (b.tagName() == "switch") { + m_context.pushGraphicsContext(b); + shapes += parseContainer(b); + m_context.popGraphicsContext(); + } else if (b.tagName() == "defs") { + if (b.childNodesCount() > 0) { + /** + * WARNING: 'defs' are basically 'display:none' style, therefore they should not play + * any role in shapes outline calculation. But setVisible(false) shapes do! + * Should be fixed in the future! + */ + KoShape *defsShape = parseGroup(b); + defsShape->setVisible(false); + } + } else if (b.tagName() == "linearGradient" || b.tagName() == "radialGradient") { + } else if (b.tagName() == "pattern") { + } else if (b.tagName() == "filter") { + parseFilter(b); + } else if (b.tagName() == "clipPath") { + parseClipPath(b); + } else if (b.tagName() == "mask") { + parseClipMask(b); + } else if (b.tagName() == "marker") { + parseMarker(b); + } else if (b.tagName() == "style") { + m_context.addStyleSheet(b); + } else if (b.tagName() == "rect" || + b.tagName() == "ellipse" || + b.tagName() == "circle" || + b.tagName() == "line" || + b.tagName() == "polyline" || + b.tagName() == "polygon" || + b.tagName() == "path" || + b.tagName() == "image" || + b.tagName() == "text") { + KoShape *shape = createObjectDirect(b); + if (shape) + shapes.append(shape); + } else if (b.tagName() == "use") { + shapes += parseUse(b); + } else if (b.tagName() == "color-profile") { + m_context.parseProfile(b); + } else { + // this is an unknown element, so try to load it anyway + // there might be a shape that handles that element + KoShape *shape = createObject(b); + if (shape) { + shapes.append(shape); } } + + return shapes; } // Creating functions @@ -1224,17 +1415,40 @@ return obj; } +KoShape * SvgParser::createObjectDirect(const KoXmlElement &b) +{ + m_context.pushGraphicsContext(b); + uploadStyleToContext(b); + + KoShape *obj = createShapeFromElement(b, m_context); + if (obj) { + obj->applyAbsoluteTransformation(m_context.currentGC()->matrix); + const QPointF extraOffset = extraShapeOffset(obj, m_context.currentGC()->matrix); + + applyCurrentStyle(obj, extraOffset); + + // handle id + applyId(b.attribute("id"), obj); + obj->setZIndex(m_context.nextZIndex()); + } + + m_context.popGraphicsContext(); + + return obj; +} + KoShape * SvgParser::createObject(const KoXmlElement &b, const SvgStyles &style) { m_context.pushGraphicsContext(b); KoShape *obj = createShapeFromElement(b, m_context); if (obj) { obj->applyAbsoluteTransformation(m_context.currentGC()->matrix); + const QPointF extraOffset = extraShapeOffset(obj, m_context.currentGC()->matrix); SvgStyles objStyle = style.isEmpty() ? m_context.styleParser().collectStyles(b) : style; m_context.styleParser().parseFont(objStyle); - applyStyle(obj, objStyle); + applyStyle(obj, objStyle, extraOffset); // handle id applyId(b.attribute("id"), obj); @@ -1250,7 +1464,10 @@ { KoShape *object = 0; - QList factories = KoShapeRegistry::instance()->factoriesForElement(KoXmlNS::svg, element.tagName()); + + const QString tagName = SvgUtil::mapExtendedShapeTag(element.tagName(), element); + QList factories = KoShapeRegistry::instance()->factoriesForElement(KoXmlNS::svg, tagName); + foreach (KoShapeFactoryBase *f, factories) { KoShape *shape = f->createDefaultShape(m_documentResourceManager); if (!shape) @@ -1266,9 +1483,8 @@ shape->setTransformation(QTransform()); // reset border - KoShapeStrokeModel *oldStroke = shape->stroke(); - shape->setStroke(0); - delete oldStroke; + KoShapeStrokeModelSP oldStroke = shape->stroke(); + shape->setStroke(KoShapeStrokeModelSP()); // reset fill shape->setBackground(QSharedPointer(0)); @@ -1309,9 +1525,8 @@ shape->setTransformation(QTransform()); // reset border - KoShapeStrokeModel *oldStroke = shape->stroke(); - shape->setStroke(0); - delete oldStroke; + KoShapeStrokeModelSP oldStroke = shape->stroke(); + shape->setStroke(KoShapeStrokeModelSP()); // reset fill shape->setBackground(QSharedPointer(0)); diff --git a/libs/flake/svg/SvgPatternHelper.h b/libs/flake/svg/SvgPatternHelper.h deleted file mode 100644 --- a/libs/flake/svg/SvgPatternHelper.h +++ /dev/null @@ -1,87 +0,0 @@ -/* This file is part of the KDE project - * Copyright (C) 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. - */ - -#ifndef SVGPATTERNHELPER_H -#define SVGPATTERNHELPER_H - -#include -#include -#include - -class KoShape; - -class SvgPatternHelper -{ -public: - enum Units { UserSpaceOnUse, ObjectBoundingBox }; - - SvgPatternHelper(); - ~SvgPatternHelper(); - - /// Set pattern units type (affects pattern x, y, width and height) - void setPatternUnits(Units units); - /// Return pattern units type - Units patternUnits() const; - - /// Set pattern content units type (affects coordinates/length of pattern child shapes) - void setPatternContentUnits(Units units); - /// Returns pattern content units type - Units patternContentUnits() const; - - /// Sets the pattern transformation found in attribute "patternTransform" - void setTransform(const QTransform &transform); - /// Returns the pattern transform - QTransform transform() const; - - /// Sets pattern tile position - void setPosition(const QPointF & position); - /// Returns pattern tile position (objectBound is used when patternUnits == ObjectBoundingBox) - QPointF position(const QRectF & objectBound) const; - - /// Sets pattern tile size - void setSize(const QSizeF & size); - /// Returns pattern tile size (objectBound is used when patternUnits == ObjectBoundingBox) - QSizeF size(const QRectF & objectBound) const; - - /// Sets the dom element containing the pattern content - void setContent(const KoXmlElement &content); - /// Return the pattern content element - KoXmlElement content() const; - - /// copies the content from the given pattern helper - void copyContent(const SvgPatternHelper &other); - - /// Sets the pattern view box (the view box content is fitted into the pattern tile) - void setPatternContentViewbox(const QRectF &viewBox); - - /// generates the pattern image from the given shapes and using the specified bounding box - QImage generateImage(const QRectF &objectBound, const QList content); - -private: - - Units m_patternUnits; - Units m_patternContentUnits; - QTransform m_transform; - QPointF m_position; - QSizeF m_size; - KoXmlElement m_patternContent; - QRectF m_patternContentViewbox; -}; - -#endif // SVGPATTERNHELPER_H diff --git a/libs/flake/svg/SvgPatternHelper.cpp b/libs/flake/svg/SvgPatternHelper.cpp deleted file mode 100644 --- a/libs/flake/svg/SvgPatternHelper.cpp +++ /dev/null @@ -1,153 +0,0 @@ -/* This file is part of the KDE project - * Copyright (C) 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 "SvgPatternHelper.h" -#include "SvgUtil.h" - -#include -#include -#include - -#include - -SvgPatternHelper::SvgPatternHelper() - : m_patternUnits(ObjectBoundingBox), m_patternContentUnits(UserSpaceOnUse) -{ -} - -SvgPatternHelper::~SvgPatternHelper() -{ -} - -void SvgPatternHelper::setPatternUnits(Units units) -{ - m_patternUnits = units; -} - -SvgPatternHelper::Units SvgPatternHelper::patternUnits() const -{ - return m_patternUnits; -} - -void SvgPatternHelper::setPatternContentUnits(Units units) -{ - m_patternContentUnits = units; -} - -SvgPatternHelper::Units SvgPatternHelper::patternContentUnits() const -{ - return m_patternContentUnits; -} - -void SvgPatternHelper::setTransform(const QTransform &transform) -{ - m_transform = transform; -} - -QTransform SvgPatternHelper::transform() const -{ - return m_transform; -} - -void SvgPatternHelper::setPosition(const QPointF & position) -{ - m_position = position; -} - -QPointF SvgPatternHelper::position(const QRectF & objectBound) const -{ - if (m_patternUnits == UserSpaceOnUse) { - return m_position; - } else { - return SvgUtil::objectToUserSpace(m_position, objectBound); - } -} - -void SvgPatternHelper::setSize(const QSizeF & size) -{ - m_size = size; -} - -QSizeF SvgPatternHelper::size(const QRectF & objectBound) const -{ - if (m_patternUnits == UserSpaceOnUse) { - return m_size; - } else { - return SvgUtil::objectToUserSpace(m_size, objectBound); - } -} - -void SvgPatternHelper::setContent(const KoXmlElement &content) -{ - m_patternContent = content; -} - -KoXmlElement SvgPatternHelper::content() const -{ - return m_patternContent; -} - -void SvgPatternHelper::copyContent(const SvgPatternHelper &other) -{ - m_patternContent = other.m_patternContent; -} - -void SvgPatternHelper::setPatternContentViewbox(const QRectF &viewBox) -{ - m_patternContentViewbox = viewBox; -} - -QImage SvgPatternHelper::generateImage(const QRectF &objectBound, const QList content) -{ - KoViewConverter zoomHandler; - - QSizeF patternSize = size(objectBound); - if (patternSize.isEmpty()) - return QImage(); - - QSizeF tileSize = zoomHandler.documentToView(patternSize); - if (tileSize.isEmpty()) - return QImage(); - - QTransform viewMatrix; - - if (! m_patternContentViewbox.isNull()) { - viewMatrix.translate(-m_patternContentViewbox.x(), -m_patternContentViewbox.y()); - const qreal xScale = patternSize.width() / m_patternContentViewbox.width(); - const qreal yScale = patternSize.height() / m_patternContentViewbox.height(); - viewMatrix.scale(xScale, yScale); - } - - // setup the tile image - QImage tile(tileSize.toSize(), QImage::Format_ARGB32); - tile.fill(QColor(Qt::transparent).rgba()); - - // setup the painter to paint the tile content - QPainter tilePainter(&tile); - tilePainter.setClipRect(tile.rect()); - tilePainter.setWorldTransform(viewMatrix); - //tilePainter.setRenderHint(QPainter::Antialiasing); - - // paint the content into the tile image - KoShapePainter shapePainter; - shapePainter.setShapes(content); - shapePainter.paint(tilePainter, zoomHandler); - - return tile; -} diff --git a/libs/flake/svg/SvgStyleParser.h b/libs/flake/svg/SvgStyleParser.h --- a/libs/flake/svg/SvgStyleParser.h +++ b/libs/flake/svg/SvgStyleParser.h @@ -24,6 +24,7 @@ #include "kritaflake_export.h" #include +#include typedef QMap SvgStyles; @@ -33,6 +34,7 @@ class QColor; class QGradient; + class KRITAFLAKE_EXPORT SvgStyleParser { public: @@ -49,7 +51,7 @@ bool parseColor(QColor &, const QString &); /// Parses gradient color stops - void parseColorStops(QGradient *, const KoXmlElement &); + void parseColorStops(QGradient *, const KoXmlElement &, SvgGraphicsContext *context, const QGradientStops &defaultStops); /// Creates style map from given xml element SvgStyles collectStyles(const KoXmlElement &); @@ -60,6 +62,7 @@ /// Merges two style elements, returning the merged style SvgStyles mergeStyles(const KoXmlElement &, const KoXmlElement &); + SvgStyles parseOneCssStyle(const QString &style, const QStringList &interestingAttributes); private: /// Parses a single style attribute diff --git a/libs/flake/svg/SvgStyleParser.cpp b/libs/flake/svg/SvgStyleParser.cpp --- a/libs/flake/svg/SvgStyleParser.cpp +++ b/libs/flake/svg/SvgStyleParser.cpp @@ -42,11 +42,12 @@ fontAttributes << "font-family" << "font-size" << "font-weight"; fontAttributes << "text-decoration" << "letter-spacing" << "word-spacing" << "baseline-shift"; // the order of the style attributes is important, don't change without reason !!! - styleAttributes << "color" << "display"; + styleAttributes << "color" << "display" << "visibility"; styleAttributes << "fill" << "fill-rule" << "fill-opacity"; styleAttributes << "stroke" << "stroke-width" << "stroke-linejoin" << "stroke-linecap"; styleAttributes << "stroke-dasharray" << "stroke-dashoffset" << "stroke-opacity" << "stroke-miterlimit"; - styleAttributes << "opacity" << "filter" << "clip-path" << "clip-rule"; + styleAttributes << "opacity" << "filter" << "clip-path" << "clip-rule" << "mask"; + styleAttributes << "marker" << "marker-start" << "marker-mid" << "marker-end" << "krita:marker-fill-method"; } SvgLoadingContext &context; @@ -98,7 +99,7 @@ void SvgStyleParser::parsePA(SvgGraphicsContext *gc, const QString &command, const QString ¶ms) { QColor fillcolor = gc->fillColor; - QColor strokecolor = gc->stroke.color(); + QColor strokecolor = gc->stroke->color(); if (params == "inherit") return; @@ -139,34 +140,40 @@ parseColor(strokecolor, params); } } else if (command == "stroke-width") { - gc->stroke.setLineWidth(SvgUtil::parseUnitXY(gc, params)); + gc->stroke->setLineWidth(SvgUtil::parseUnitXY(gc, params)); } else if (command == "stroke-linejoin") { if (params == "miter") - gc->stroke.setJoinStyle(Qt::MiterJoin); + gc->stroke->setJoinStyle(Qt::MiterJoin); else if (params == "round") - gc->stroke.setJoinStyle(Qt::RoundJoin); + gc->stroke->setJoinStyle(Qt::RoundJoin); else if (params == "bevel") - gc->stroke.setJoinStyle(Qt::BevelJoin); + gc->stroke->setJoinStyle(Qt::BevelJoin); } else if (command == "stroke-linecap") { if (params == "butt") - gc->stroke.setCapStyle(Qt::FlatCap); + gc->stroke->setCapStyle(Qt::FlatCap); else if (params == "round") - gc->stroke.setCapStyle(Qt::RoundCap); + gc->stroke->setCapStyle(Qt::RoundCap); else if (params == "square") - gc->stroke.setCapStyle(Qt::SquareCap); + gc->stroke->setCapStyle(Qt::SquareCap); } else if (command == "stroke-miterlimit") { - gc->stroke.setMiterLimit(params.toFloat()); + gc->stroke->setMiterLimit(params.toFloat()); } else if (command == "stroke-dasharray") { QVector array; if (params != "none") { QString dashString = params; QStringList dashes = dashString.replace(',', ' ').simplified().split(' '); - for (QStringList::Iterator it = dashes.begin(); it != dashes.end(); ++it) - array.append((*it).toFloat()); + for (QStringList::Iterator it = dashes.begin(); it != dashes.end(); ++it) { + array.append(SvgUtil::parseUnitXY(gc, *it)); + } + + // if the array is odd repeat it according to the standard + if (array.size() & 1) { + array << array; + } } - gc->stroke.setLineStyle(Qt::CustomDashLine, array); + gc->stroke->setLineStyle(Qt::CustomDashLine, array); } else if (command == "stroke-dashoffset") { - gc->stroke.setDashOffset(params.toFloat()); + gc->stroke->setDashOffset(params.toFloat()); } // handle opacity else if (command == "stroke-opacity") @@ -276,6 +283,9 @@ } else if (command == "display") { if (params == "none") gc->display = false; + } else if (command == "visibility") { + // visible is inherited! + gc->visible = params == "visible"; } else if (command == "filter") { if (params != "none" && params.startsWith("url(")) { unsigned int start = params.indexOf('#') + 1; @@ -293,10 +303,44 @@ gc->clipRule = Qt::WindingFill; else if (params == "evenodd") gc->clipRule = Qt::OddEvenFill; + } else if (command == "mask") { + if (params != "none" && params.startsWith("url(")) { + unsigned int start = params.indexOf('#') + 1; + unsigned int end = params.indexOf(')', start); + gc->clipMaskId = params.mid(start, end - start); + } + } else if (command == "marker-start") { + if (params != "none" && params.startsWith("url(")) { + unsigned int start = params.indexOf('#') + 1; + unsigned int end = params.indexOf(')', start); + gc->markerStartId = params.mid(start, end - start); + } + } else if (command == "marker-end") { + if (params != "none" && params.startsWith("url(")) { + unsigned int start = params.indexOf('#') + 1; + unsigned int end = params.indexOf(')', start); + gc->markerEndId = params.mid(start, end - start); + } + } else if (command == "marker-mid") { + if (params != "none" && params.startsWith("url(")) { + unsigned int start = params.indexOf('#') + 1; + unsigned int end = params.indexOf(')', start); + gc->markerMidId = params.mid(start, end - start); + } + } else if (command == "marker") { + if (params != "none" && params.startsWith("url(")) { + unsigned int start = params.indexOf('#') + 1; + unsigned int end = params.indexOf(')', start); + gc->markerStartId = params.mid(start, end - start); + gc->markerMidId = gc->markerStartId; + gc->markerEndId = gc->markerStartId; + } + } else if (command == "krita:marker-fill-method") { + gc->autoFillMarkers = params == "auto"; } gc->fillColor = fillcolor; - gc->stroke.setColor(strokecolor); + gc->stroke->setColor(strokecolor); } bool SvgStyleParser::parseColor(QColor &color, const QString &s) @@ -337,63 +381,101 @@ return true; } -void SvgStyleParser::parseColorStops(QGradient *gradient, const KoXmlElement &e) +void SvgStyleParser::parseColorStops(QGradient *gradient, + const KoXmlElement &e, + SvgGraphicsContext *context, + const QGradientStops &defaultStops) { QGradientStops stops; - QColor c; - for (KoXmlNode n = e.firstChild(); !n.isNull(); n = n.nextSibling()) { - KoXmlElement stop = n.toElement(); + qreal previousOffset = 0.0; + + KoXmlElement stop; + forEachElement(stop, e) { if (stop.tagName() == "stop") { - float offset; - QString temp = stop.attribute("offset"); - if (temp.contains('%')) { - temp = temp.left(temp.length() - 1); - offset = temp.toFloat() / 100.0; - } else - offset = temp.toFloat(); + qreal offset = 0.0; + QString offsetStr = stop.attribute("offset").trimmed(); + if (offsetStr.endsWith('%')) { + offsetStr = offsetStr.left(offsetStr.length() - 1); + offset = offsetStr.toFloat() / 100.0; + } else { + offset = offsetStr.toFloat(); + } + + // according to SVG the value must be within [0; 1] interval + offset = qBound(0.0, offset, 1.0); + + // according to SVG the stops' offset must be non-decreasing + offset = qMax(offset, previousOffset); + previousOffset = offset; + + QColor color; QString stopColorStr = stop.attribute("stop-color"); - if (!stopColorStr.isEmpty()) { - if (stopColorStr == "inherit") { - stopColorStr = inheritedAttribute("stop-color", stop); - } - parseColor(c, stopColorStr); + QString stopOpacityStr = stop.attribute("stop-opacity"); + + const QStringList attributes({"stop-color", "stop-opacity"}); + SvgStyles styles = parseOneCssStyle(stop.attribute("style"), attributes); + + // SVG: CSS values have precedence over presentation attributes! + if (styles.contains("stop-color")) { + stopColorStr = styles.value("stop-color"); } - else { - // try style attr - QString style = stop.attribute("style").simplified(); - QStringList substyles = style.split(';', QString::SkipEmptyParts); - for (QStringList::Iterator it = substyles.begin(); it != substyles.end(); ++it) { - QStringList substyle = it->split(':'); - QString command = substyle[0].trimmed(); - QString params = substyle[1].trimmed(); - if (command == "stop-color") - parseColor(c, params); - if (command == "stop-opacity") - c.setAlphaF(params.toDouble()); - } + if (styles.contains("stop-opacity")) { + stopOpacityStr = styles.value("stop-opacity"); } - QString opacityStr = stop.attribute("stop-opacity"); - if (!opacityStr.isEmpty()) { - if (opacityStr == "inherit") { - opacityStr = inheritedAttribute("stop-opacity", stop); - } - c.setAlphaF(opacityStr.toDouble()); + + if (stopColorStr.isEmpty() && stopColorStr == "inherit") { + color = context->currentColor; + } else { + parseColor(color, stopColorStr); + } + + if (!stopOpacityStr.isEmpty() && stopOpacityStr != "inherit") { + color.setAlphaF(qBound(0.0, stopOpacityStr.toDouble(), 1.0)); } - stops.append(QPair(offset, c)); + + stops.append(QPair(offset, color)); } } - if (stops.count()) + + if (!stops.isEmpty()) { gradient->setStops(stops); + } else { + gradient->setStops(defaultStops); + } +} + +SvgStyles SvgStyleParser::parseOneCssStyle(const QString &style, const QStringList &interestingAttributes) +{ + SvgStyles parsedStyles; + if (style.isEmpty()) return parsedStyles; + + QStringList substyles = style.simplified().split(';', QString::SkipEmptyParts); + if (!substyles.count()) return parsedStyles; + + for (QStringList::Iterator it = substyles.begin(); it != substyles.end(); ++it) { + QStringList substyle = it->split(':'); + if (substyle.count() != 2) + continue; + QString command = substyle[0].trimmed(); + QString params = substyle[1].trimmed(); + + if (interestingAttributes.isEmpty() || interestingAttributes.contains(command)) { + parsedStyles[command] = params; + } + } + + return parsedStyles; } SvgStyles SvgStyleParser::collectStyles(const KoXmlElement &e) { SvgStyles styleMap; // collect individual presentation style attributes which have the priority 0 + // according to SVG standard Q_FOREACH (const QString &command, d->styleAttributes) { const QString attribute = e.attribute(command); if (!attribute.isEmpty()) @@ -406,7 +488,7 @@ } // match css style rules to element - QStringList cssStyles = d->context.matchingStyles(e); + QStringList cssStyles = d->context.matchingCssStyles(e); // collect all css style attributes Q_FOREACH (const QString &style, cssStyles) { @@ -419,12 +501,18 @@ continue; QString command = substyle[0].trimmed(); QString params = substyle[1].trimmed(); + + // toggle the namespace selector into the xml-like one + command.replace("|", ":"); + // only use style and font attributes if (d->styleAttributes.contains(command) || d->fontAttributes.contains(command)) styleMap[command] = params; } } + // FIXME: if 'inherit' we should just remove the property and use the one from the context! + // replace keyword "inherit" for style values QMutableMapIterator it(styleMap); while (it.hasNext()) { diff --git a/libs/flake/svg/SvgStyleWriter.h b/libs/flake/svg/SvgStyleWriter.h --- a/libs/flake/svg/SvgStyleWriter.h +++ b/libs/flake/svg/SvgStyleWriter.h @@ -36,6 +36,7 @@ class SvgSavingContext; class KoShape; class KoPatternBackground; +class KoVectorPatternBackground; class QTransform; class QGradient; @@ -55,12 +56,17 @@ static void saveSvgEffects(KoShape *shape, SvgSavingContext &context); /// Saves clipping of specified shape static void saveSvgClipping(KoShape *shape, SvgSavingContext &context); + /// Saves masking of specified shape + static void saveSvgMasking(KoShape *shape, SvgSavingContext &context); + /// Saves markers of the path shape if present + static void saveSvgMarkers(KoShape *shape, SvgSavingContext &context); /// Saves gradient color stops static void saveSvgColorStops(const QGradientStops &colorStops, SvgSavingContext &context); /// Saves gradient static QString saveSvgGradient(const QGradient *gradient, const QTransform &gradientTransform, SvgSavingContext &context); /// Saves pattern static QString saveSvgPattern(QSharedPointer pattern, KoShape *shape, SvgSavingContext &context); + static QString saveSvgVectorPattern(QSharedPointer pattern, KoShape *shape, SvgSavingContext &context); }; #endif // SVGSTYLEWRITER_H diff --git a/libs/flake/svg/SvgStyleWriter.cpp b/libs/flake/svg/SvgStyleWriter.cpp --- a/libs/flake/svg/SvgStyleWriter.cpp +++ b/libs/flake/svg/SvgStyleWriter.cpp @@ -44,22 +44,32 @@ #include #include #include +#include #include #include +#include +#include #include #include #include #include #include #include +#include "kis_dom_utils.h" +#include "kis_algebra_2d.h" +#include +#include + void SvgStyleWriter::saveSvgStyle(KoShape *shape, SvgSavingContext &context) { saveSvgFill(shape, context); saveSvgStroke(shape, context); saveSvgEffects(shape, context); saveSvgClipping(shape, context); + saveSvgMasking(shape, context); + saveSvgMarkers(shape, context); if (! shape->isVisible()) context.shapeWriter().addAttribute("display", "none"); if (shape->transparency() > 0.0) @@ -89,6 +99,11 @@ const QString patternId = saveSvgPattern(pbg, shape, context); context.shapeWriter().addAttribute("fill", "url(#" + patternId + ")"); } + QSharedPointer vpbg = qSharedPointerDynamicCast(shape->background()); + if (vpbg) { + const QString patternId = saveSvgVectorPattern(vpbg, shape, context); + context.shapeWriter().addAttribute("fill", "url(#" + patternId + ")"); + } KoPathShape * path = dynamic_cast(shape); if (path && shape->background()) { @@ -100,53 +115,54 @@ void SvgStyleWriter::saveSvgStroke(KoShape *shape, SvgSavingContext &context) { - const KoShapeStroke * line = dynamic_cast(shape->stroke()); - if (! line) + const QSharedPointer lineBorder = qSharedPointerDynamicCast(shape->stroke()); + + if (! lineBorder) return; QString strokeStr("none"); - if (line->lineBrush().gradient()) { - QString gradientId = saveSvgGradient(line->lineBrush().gradient(), line->lineBrush().transform(), context); + if (lineBorder->lineBrush().gradient()) { + QString gradientId = saveSvgGradient(lineBorder->lineBrush().gradient(), lineBorder->lineBrush().transform(), context); strokeStr = "url(#" + gradientId + ")"; } else { - strokeStr = line->color().name(); + strokeStr = lineBorder->color().name(); } if (!strokeStr.isEmpty()) context.shapeWriter().addAttribute("stroke", strokeStr); - if (line->color().alphaF() < 1.0) - context.shapeWriter().addAttribute("stroke-opacity", line->color().alphaF()); - context.shapeWriter().addAttribute("stroke-width", SvgUtil::toUserSpace(line->lineWidth())); + if (lineBorder->color().alphaF() < 1.0) + context.shapeWriter().addAttribute("stroke-opacity", lineBorder->color().alphaF()); + context.shapeWriter().addAttribute("stroke-width", SvgUtil::toUserSpace(lineBorder->lineWidth())); - if (line->capStyle() == Qt::FlatCap) + if (lineBorder->capStyle() == Qt::FlatCap) context.shapeWriter().addAttribute("stroke-linecap", "butt"); - else if (line->capStyle() == Qt::RoundCap) + else if (lineBorder->capStyle() == Qt::RoundCap) context.shapeWriter().addAttribute("stroke-linecap", "round"); - else if (line->capStyle() == Qt::SquareCap) + else if (lineBorder->capStyle() == Qt::SquareCap) context.shapeWriter().addAttribute("stroke-linecap", "square"); - if (line->joinStyle() == Qt::MiterJoin) { + if (lineBorder->joinStyle() == Qt::MiterJoin) { context.shapeWriter().addAttribute("stroke-linejoin", "miter"); - context.shapeWriter().addAttribute("stroke-miterlimit", line->miterLimit()); - } else if (line->joinStyle() == Qt::RoundJoin) + context.shapeWriter().addAttribute("stroke-miterlimit", lineBorder->miterLimit()); + } else if (lineBorder->joinStyle() == Qt::RoundJoin) context.shapeWriter().addAttribute("stroke-linejoin", "round"); - else if (line->joinStyle() == Qt::BevelJoin) + else if (lineBorder->joinStyle() == Qt::BevelJoin) context.shapeWriter().addAttribute("stroke-linejoin", "bevel"); // dash - if (line->lineStyle() > Qt::SolidLine) { - qreal dashFactor = line->lineWidth(); + if (lineBorder->lineStyle() > Qt::SolidLine) { + qreal dashFactor = lineBorder->lineWidth(); - if (line->dashOffset() != 0) - context.shapeWriter().addAttribute("stroke-dashoffset", dashFactor * line->dashOffset()); + if (lineBorder->dashOffset() != 0) + context.shapeWriter().addAttribute("stroke-dashoffset", dashFactor * lineBorder->dashOffset()); QString dashStr; - const QVector dashes = line->lineDashes(); + const QVector dashes = lineBorder->lineDashes(); int dashCount = dashes.size(); for (int i = 0; i < dashCount; ++i) { if (i > 0) dashStr += ","; - dashStr += QString("%1").arg(dashes[i] * dashFactor); + dashStr += QString("%1").arg(KisDomUtils::toString(dashes[i] * dashFactor)); } context.shapeWriter().addAttribute("stroke-dasharray", dashStr); } @@ -169,36 +185,134 @@ context.shapeWriter().addAttribute("filter", "url(#" + uid + ")"); } +void embedShapes(const QList &shapes, KoXmlWriter &outWriter) +{ + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + { + SvgWriter shapesWriter(shapes, QSizeF(666, 666)); + shapesWriter.saveDetached(buffer); + } + buffer.close(); + outWriter.addCompleteElement(&buffer); +} + + void SvgStyleWriter::saveSvgClipping(KoShape *shape, SvgSavingContext &context) { KoClipPath *clipPath = shape->clipPath(); if (!clipPath) return; - const QSizeF shapeSize = shape->outlineRect().size(); - KoPathShape *path = KoPathShape::createShapeFromPainterPath(clipPath->pathForSize(shapeSize)); - if (!path) - return; - - path->close(); - const QString uid = context.createUID("clippath"); context.styleWriter().startElement("clipPath"); context.styleWriter().addAttribute("id", uid); - context.styleWriter().addAttribute("clipPathUnits", "userSpaceOnUse"); + context.styleWriter().addAttribute("clipPathUnits", KoFlake::coordinateToString(clipPath->coordinates())); - context.styleWriter().startElement("path"); - context.styleWriter().addAttribute("d", path->toString(path->absoluteTransformation(0)*context.userSpaceTransform())); - context.styleWriter().endElement(); // path + embedShapes(clipPath->clipShapes(), context.styleWriter()); context.styleWriter().endElement(); // clipPath context.shapeWriter().addAttribute("clip-path", "url(#" + uid + ")"); if (clipPath->clipRule() != Qt::WindingFill) context.shapeWriter().addAttribute("clip-rule", "evenodd"); } +void SvgStyleWriter::saveSvgMasking(KoShape *shape, SvgSavingContext &context) +{ + KoClipMask*clipMask = shape->clipMask(); + if (!clipMask) + return; + + const QString uid = context.createUID("clipmask"); + + context.styleWriter().startElement("mask"); + context.styleWriter().addAttribute("id", uid); + context.styleWriter().addAttribute("maskUnits", KoFlake::coordinateToString(clipMask->coordinates())); + context.styleWriter().addAttribute("maskContentUnits", KoFlake::coordinateToString(clipMask->contentCoordinates())); + + const QRectF rect = clipMask->maskRect(); + + // think funny duplication? please note the 'pt' suffix! :) + if (clipMask->coordinates() == KoFlake::ObjectBoundingBox) { + context.styleWriter().addAttribute("x", rect.x()); + context.styleWriter().addAttribute("y", rect.y()); + context.styleWriter().addAttribute("width", rect.width()); + context.styleWriter().addAttribute("height", rect.height()); + } else { + context.styleWriter().addAttributePt("x", rect.x()); + context.styleWriter().addAttributePt("y", rect.y()); + context.styleWriter().addAttributePt("width", rect.width()); + context.styleWriter().addAttributePt("height", rect.height()); + } + + embedShapes(clipMask->shapes(), context.styleWriter()); + + context.styleWriter().endElement(); // clipMask + + context.shapeWriter().addAttribute("mask", "url(#" + uid + ")"); +} + +namespace { +void writeMarkerStyle(KoXmlWriter &styleWriter, const KoMarker *marker, const QString &assignedId) { + + styleWriter.startElement("marker"); + styleWriter.addAttribute("id", assignedId); + styleWriter.addAttribute("markerUnits", KoMarker::coordinateSystemToString(marker->coordinateSystem())); + + const QPointF refPoint = marker->referencePoint(); + styleWriter.addAttribute("refX", refPoint.x()); + styleWriter.addAttribute("refY", refPoint.y()); + + const QSizeF refSize = marker->referenceSize(); + styleWriter.addAttribute("markerWidth", refSize.width()); + styleWriter.addAttribute("markerHeight", refSize.height()); + + + if (marker->hasAutoOtientation()) { + styleWriter.addAttribute("orient", "auto"); + } else { + // no suffix means 'degrees' + styleWriter.addAttribute("orient", kisRadiansToDegrees(marker->explicitOrientation())); + } + + embedShapes(marker->shapes(), styleWriter); + + styleWriter.endElement(); // marker +} + +void tryEmbedMarker(const KoPathShape *pathShape, + const QString &markerTag, + KoFlake::MarkerPosition markerPosition, + SvgSavingContext &context) +{ + KoMarker *marker = pathShape->marker(markerPosition); + + if (marker) { + const QString uid = context.createUID("lineMarker"); + writeMarkerStyle(context.styleWriter(), marker, uid); + context.shapeWriter().addAttribute(markerTag.toLatin1().data(), "url(#" + uid + ")"); + } +} + +} + +void SvgStyleWriter::saveSvgMarkers(KoShape *shape, SvgSavingContext &context) +{ + KoPathShape *pathShape = dynamic_cast(shape); + if (!pathShape || !pathShape->hasMarkers()) return; + + + tryEmbedMarker(pathShape, "marker-start", KoFlake::StartMarker, context); + tryEmbedMarker(pathShape, "marker-mid", KoFlake::MidMarker, context); + tryEmbedMarker(pathShape, "marker-end", KoFlake::EndMarker, context); + + if (pathShape->autoFillMarkers()) { + context.shapeWriter().addAttribute("krita:marker-fill-method", "auto"); + } +} + void SvgStyleWriter::saveSvgColorStops(const QGradientStops &colorStops, SvgSavingContext &context) { Q_FOREACH (const QGradientStop &stop, colorStops) { @@ -210,6 +324,16 @@ } } +inline QString convertGradientMode(QGradient::CoordinateMode mode) { + KIS_ASSERT_RECOVER_NOOP(mode != QGradient::StretchToDeviceMode); + + return + mode == QGradient::ObjectBoundingMode ? + "objectBoundingBox" : + "userSpaceOnUse"; + +} + QString SvgStyleWriter::saveSvgGradient(const QGradient *gradient, const QTransform &gradientTransform, SvgSavingContext &context) { if (! gradient) @@ -230,7 +354,7 @@ context.styleWriter().startElement("linearGradient"); context.styleWriter().addAttribute("id", uid); context.styleWriter().addAttribute("gradientTransform", SvgUtil::transformToString(gradientTransform)); - context.styleWriter().addAttribute("gradientUnits", "objectBoundingBox"); + context.styleWriter().addAttribute("gradientUnits", convertGradientMode(g->coordinateMode())); context.styleWriter().addAttribute("x1", g->start().x()); context.styleWriter().addAttribute("y1", g->start().y()); context.styleWriter().addAttribute("x2", g->finalStop().x()); @@ -244,7 +368,7 @@ context.styleWriter().startElement("radialGradient"); context.styleWriter().addAttribute("id", uid); context.styleWriter().addAttribute("gradientTransform", SvgUtil::transformToString(gradientTransform)); - context.styleWriter().addAttribute("gradientUnits", "objectBoundingBox"); + context.styleWriter().addAttribute("gradientUnits", convertGradientMode(g->coordinateMode())); context.styleWriter().addAttribute("cx", g->center().x()); context.styleWriter().addAttribute("cy", g->center().y()); context.styleWriter().addAttribute("fx", g->focalPoint().x()); @@ -343,14 +467,14 @@ context.styleWriter().addAttribute("patternUnits", "userSpaceOnUse"); } - context.styleWriter().addAttribute("viewBox", QString("0 0 %1 %2").arg(imageSize.width()).arg(imageSize.height())); + context.styleWriter().addAttribute("viewBox", QString("0 0 %1 %2").arg(KisDomUtils::toString(imageSize.width())).arg(KisDomUtils::toString(imageSize.height()))); //*m_defs << " patternContentUnits=\"userSpaceOnUse\""; context.styleWriter().startElement("image"); context.styleWriter().addAttribute("x", "0"); context.styleWriter().addAttribute("y", "0"); - context.styleWriter().addAttribute("width", QString("%1px").arg(imageSize.width())); - context.styleWriter().addAttribute("height", QString("%1px").arg(imageSize.height())); + context.styleWriter().addAttribute("width", QString("%1px").arg(KisDomUtils::toString(imageSize.width()))); + context.styleWriter().addAttribute("height", QString("%1px").arg(KisDomUtils::toString(imageSize.height()))); QByteArray ba; QBuffer buffer(&ba); @@ -365,3 +489,58 @@ return uid; } + +QString SvgStyleWriter::saveSvgVectorPattern(QSharedPointer pattern, KoShape *parentShape, SvgSavingContext &context) +{ + const QString uid = context.createUID("pattern"); + + context.styleWriter().startElement("pattern"); + context.styleWriter().addAttribute("id", uid); + + context.styleWriter().addAttribute("patternUnits", KoFlake::coordinateToString(pattern->referenceCoordinates())); + context.styleWriter().addAttribute("patternContentUnits", KoFlake::coordinateToString(pattern->contentCoordinates())); + + const QRectF rect = pattern->referenceRect(); + + if (pattern->referenceCoordinates() == KoFlake::ObjectBoundingBox) { + context.styleWriter().addAttribute("x", rect.x()); + context.styleWriter().addAttribute("y", rect.y()); + context.styleWriter().addAttribute("width", rect.width()); + context.styleWriter().addAttribute("height", rect.height()); + } else { + context.styleWriter().addAttributePt("x", rect.x()); + context.styleWriter().addAttributePt("y", rect.y()); + context.styleWriter().addAttributePt("width", rect.width()); + context.styleWriter().addAttributePt("height", rect.height()); + } + + context.styleWriter().addAttribute("patternTransform", SvgUtil::transformToString(pattern->patternTransform())); + + if (pattern->contentCoordinates() == KoFlake::ObjectBoundingBox) { + // TODO: move this normalization into the KoVectorPatternBackground itself + + QList shapes = pattern->shapes(); + QList clonedShapes; + + const QRectF dstShapeBoundingRect = parentShape->outlineRect(); + const QTransform relativeToShape = KisAlgebra2D::mapToRect(dstShapeBoundingRect); + const QTransform shapeToRelative = relativeToShape.inverted(); + + Q_FOREACH (KoShape *shape, shapes) { + KoShape *clone = shape->cloneShape(); + clone->applyAbsoluteTransformation(shapeToRelative); + clonedShapes.append(clone); + } + + embedShapes(clonedShapes, context.styleWriter()); + qDeleteAll(clonedShapes); + + } else { + QList shapes = pattern->shapes(); + embedShapes(shapes, context.styleWriter()); + } + + context.styleWriter().endElement(); // pattern + + return uid; +} diff --git a/libs/flake/svg/SvgUtil.h b/libs/flake/svg/SvgUtil.h --- a/libs/flake/svg/SvgUtil.h +++ b/libs/flake/svg/SvgUtil.h @@ -26,20 +26,18 @@ class QString; class SvgGraphicsContext; class QTransform; +class KoXmlElement; class KRITAFLAKE_EXPORT SvgUtil { public: - /** - * Converts given value from userspace units to points. - */ - static double fromUserSpace(double value); - /** - * Converts given value from points to userspace units. - */ + // remove later! pixels *are* user coordinates + static double fromUserSpace(double value); static double toUserSpace(double value); + static double ptToPx(SvgGraphicsContext *gc, double value); + /// Converts given point from points to userspace units. static QPointF toUserSpace(const QPointF &point); @@ -83,18 +81,14 @@ */ static QSizeF userSpaceToObject(const QSizeF &size, const QRectF &objectBound); - /** - * Parses transform attribute value into a matrix. - * @param transform the transform attribute value - * @return the resulting transformation matrix - */ - static QTransform parseTransform(const QString &transform); - /// Converts specified transformation to a string static QString transformToString(const QTransform &transform); /// Parses a viewbox attribute into an rectangle - static QRectF parseViewBox(QString viewbox); + static bool parseViewBox(SvgGraphicsContext *gc, const KoXmlElement &e, const QRectF &elementBounds, QRectF *_viewRect, QTransform *_viewTransform); + + struct PreserveAspectRatioParser; + static void parseAspectRatio(const PreserveAspectRatioParser &p, const QRectF &elementBounds, const QRectF &viewRect, QTransform *_viewTransform); /// Parses a length attribute static qreal parseUnit(SvgGraphicsContext *gc, const QString &, bool horiz = false, bool vert = false, const QRectF &bbox = QRectF()); @@ -108,8 +102,40 @@ /// parses a length attribute in xy-direction static qreal parseUnitXY(SvgGraphicsContext *gc, const QString &unit); + /// parses angle, result in *radians*! + static qreal parseUnitAngular(SvgGraphicsContext *gc, const QString &unit); + /// parses the number into parameter number static const char * parseNumber(const char *ptr, qreal &number); + + static qreal parseNumber(const QString &string); + + static QString mapExtendedShapeTag(const QString &tagName, const KoXmlElement &element); + + struct PreserveAspectRatioParser + { + PreserveAspectRatioParser(const QString &str); + + enum Alignment { + Min, + Middle, + Max + }; + + bool defer = false; + Qt::AspectRatioMode mode = Qt::IgnoreAspectRatio; + Alignment xAlignment = Min; + Alignment yAlignment = Min; + + QPointF rectAnchorPoint(const QRectF &rc) const; + + QString toString() const; + + private: + Alignment alignmentFromString(const QString &str) const; + QString alignmentToString(Alignment alignment) const; + static qreal alignedValue(qreal min, qreal max, Alignment alignment); + }; }; #endif // SVGUTIL_H diff --git a/libs/flake/svg/SvgUtil.cpp b/libs/flake/svg/SvgUtil.cpp --- a/libs/flake/svg/SvgUtil.cpp +++ b/libs/flake/svg/SvgUtil.cpp @@ -21,26 +21,37 @@ #include "SvgGraphicContext.h" #include +#include #include #include #include #include +#include #include +#include "kis_debug.h" +#include "kis_global.h" + +#include "kis_dom_utils.h" #define DPI 72.0 #define DEG2RAD(degree) degree/180.0*M_PI double SvgUtil::fromUserSpace(double value) { - return (value * DPI) / 90.0; + return value; } double SvgUtil::toUserSpace(double value) { - return (value * 90.0) / DPI; + return value; +} + +double SvgUtil::ptToPx(SvgGraphicsContext *gc, double value) +{ + return value * gc->pixelsPerInch / DPI; } QPointF SvgUtil::toUserSpace(const QPointF &point) @@ -106,103 +117,104 @@ return QSizeF(w, h); } -QTransform SvgUtil::parseTransform(const QString &transform) -{ - QTransform result; - - // Split string for handling 1 transform statement at a time - QStringList subtransforms = transform.split(')', QString::SkipEmptyParts); - QStringList::ConstIterator it = subtransforms.constBegin(); - QStringList::ConstIterator end = subtransforms.constEnd(); - for (; it != end; ++it) { - QStringList subtransform = (*it).simplified().split('(', QString::SkipEmptyParts); - if (subtransform.count() < 2) - continue; - - subtransform[0] = subtransform[0].trimmed().toLower(); - subtransform[1] = subtransform[1].simplified(); - QRegExp reg("[,( ]"); - QStringList params = subtransform[1].split(reg, QString::SkipEmptyParts); - - if (subtransform[0].startsWith(';') || subtransform[0].startsWith(',')) - subtransform[0] = subtransform[0].right(subtransform[0].length() - 1); - - if (subtransform[0] == "rotate") { - if (params.count() == 3) { - double x = params[1].toDouble(); - double y = params[2].toDouble(); - - result.translate(x, y); - result.rotate(params[0].toDouble()); - result.translate(-x, -y); - } else { - result.rotate(params[0].toDouble()); - } - } else if (subtransform[0] == "translate") { - if (params.count() == 2) { - result.translate(SvgUtil::fromUserSpace(params[0].toDouble()), - SvgUtil::fromUserSpace(params[1].toDouble())); - } else { // Spec : if only one param given, assume 2nd param to be 0 - result.translate(SvgUtil::fromUserSpace(params[0].toDouble()) , 0); - } - } else if (subtransform[0] == "scale") { - if (params.count() == 2) { - result.scale(params[0].toDouble(), params[1].toDouble()); - } else { // Spec : if only one param given, assume uniform scaling - result.scale(params[0].toDouble(), params[0].toDouble()); - } - } else if (subtransform[0].toLower() == "skewx") { - result.shear(tan(DEG2RAD(params[0].toDouble())), 0.0F); - } else if (subtransform[0].toLower() == "skewy") { - result.shear(0.0F, tan(DEG2RAD(params[0].toDouble()))); - } else if (subtransform[0] == "matrix") { - if (params.count() >= 6) { - result.setMatrix(params[0].toDouble(), params[1].toDouble(), 0, - params[2].toDouble(), params[3].toDouble(), 0, - SvgUtil::fromUserSpace(params[4].toDouble()), - SvgUtil::fromUserSpace(params[5].toDouble()), 1); - } - } - } - - return result; -} - QString SvgUtil::transformToString(const QTransform &transform) { if (transform.isIdentity()) return QString(); if (transform.type() == QTransform::TxTranslate) { return QString("translate(%1, %2)") - .arg(toUserSpace(transform.dx())) - .arg(toUserSpace(transform.dy())); + .arg(KisDomUtils::toString(toUserSpace(transform.dx()))) + .arg(KisDomUtils::toString(toUserSpace(transform.dy()))); } else { return QString("matrix(%1 %2 %3 %4 %5 %6)") - .arg(transform.m11()).arg(transform.m12()) - .arg(transform.m21()).arg(transform.m22()) - .arg(toUserSpace(transform.dx())) - .arg(toUserSpace(transform.dy())); + .arg(KisDomUtils::toString(transform.m11())) + .arg(KisDomUtils::toString(transform.m12())) + .arg(KisDomUtils::toString(transform.m21())) + .arg(KisDomUtils::toString(transform.m22())) + .arg(KisDomUtils::toString(toUserSpace(transform.dx()))) + .arg(KisDomUtils::toString(toUserSpace(transform.dy()))); } } -QRectF SvgUtil::parseViewBox(QString viewbox) +bool SvgUtil::parseViewBox(SvgGraphicsContext *gc, const KoXmlElement &e, + const QRectF &elementBounds, + QRectF *_viewRect, QTransform *_viewTransform) { - QRectF viewboxRect; + Q_UNUSED(gc) + + KIS_ASSERT(_viewRect); + KIS_ASSERT(_viewTransform); + + QString viewBoxStr = e.attribute("viewBox"); + if (viewBoxStr.isEmpty()) return false; + + bool result = false; + + QRectF viewBoxRect; // this is a workaround for bug 260429 for a file generated by blender // who has px in the viewbox which is wrong. // reported as bug http://projects.blender.org/tracker/?group_id=9&atid=498&func=detail&aid=30971 - viewbox.remove("px"); + viewBoxStr.remove("px"); - QStringList points = viewbox.replace(',', ' ').simplified().split(' '); + QStringList points = viewBoxStr.replace(',', ' ').simplified().split(' '); if (points.count() == 4) { - viewboxRect.setX(SvgUtil::fromUserSpace(points[0].toFloat())); - viewboxRect.setY(SvgUtil::fromUserSpace(points[1].toFloat())); - viewboxRect.setWidth(SvgUtil::fromUserSpace(points[2].toFloat())); - viewboxRect.setHeight(SvgUtil::fromUserSpace(points[3].toFloat())); + viewBoxRect.setX(SvgUtil::fromUserSpace(points[0].toFloat())); + viewBoxRect.setY(SvgUtil::fromUserSpace(points[1].toFloat())); + viewBoxRect.setWidth(SvgUtil::fromUserSpace(points[2].toFloat())); + viewBoxRect.setHeight(SvgUtil::fromUserSpace(points[3].toFloat())); + + result = true; + } else { + // TODO: WARNING! + } + + if (!result) return false; + + QTransform viewBoxTransform = + QTransform::fromTranslate(-viewBoxRect.x(), -viewBoxRect.y()) * + QTransform::fromScale(elementBounds.width() / viewBoxRect.width(), + elementBounds.height() / viewBoxRect.height()) * + QTransform::fromTranslate(elementBounds.x(), elementBounds.y()); + + const QString aspectString = e.attribute("preserveAspectRatio"); + if (!aspectString.isEmpty()) { + PreserveAspectRatioParser p(aspectString); + parseAspectRatio(p, elementBounds, viewBoxRect, &viewBoxTransform); } - return viewboxRect; + *_viewRect = viewBoxRect; + *_viewTransform = viewBoxTransform; + + return result; +} + +void SvgUtil::parseAspectRatio(const PreserveAspectRatioParser &p, const QRectF &elementBounds, const QRectF &viewBoxRect, QTransform *_viewTransform) +{ + if (p.mode != Qt::IgnoreAspectRatio) { + QTransform viewBoxTransform = *_viewTransform; + + const qreal tan1 = viewBoxRect.height() / viewBoxRect.width(); + const qreal tan2 = elementBounds.height() / elementBounds.width(); + + const qreal uniformScale = + (p.mode == Qt::KeepAspectRatioByExpanding) ^ (tan1 > tan2) ? + elementBounds.height() / viewBoxRect.height() : + elementBounds.width() / viewBoxRect.width(); + + viewBoxTransform = + QTransform::fromTranslate(-viewBoxRect.x(), -viewBoxRect.y()) * + QTransform::fromScale(uniformScale, uniformScale) * + QTransform::fromTranslate(elementBounds.x(), elementBounds.y()); + + const QPointF viewBoxAnchor = viewBoxTransform.map(p.rectAnchorPoint(viewBoxRect)); + const QPointF elementAnchor = p.rectAnchorPoint(elementBounds); + const QPointF offset = elementAnchor - viewBoxAnchor; + + viewBoxTransform = viewBoxTransform * QTransform::fromTranslate(offset.x(), offset.y()); + + *_viewTransform = viewBoxTransform; + } } qreal SvgUtil::parseUnit(SvgGraphicsContext *gc, const QString &unit, bool horiz, bool vert, const QRectF &bbox) @@ -221,19 +233,21 @@ if (int(end - start) < unit.length()) { if (unit.right(2) == "px") value = SvgUtil::fromUserSpace(value); + else if (unit.right(2) == "pt") + value = ptToPx(gc, value); else if (unit.right(2) == "cm") - value = CM_TO_POINT(value); + value = ptToPx(gc, CM_TO_POINT(value)); else if (unit.right(2) == "pc") - value = PI_TO_POINT(value); + value = ptToPx(gc, PI_TO_POINT(value)); else if (unit.right(2) == "mm") - value = MM_TO_POINT(value); + value = ptToPx(gc, MM_TO_POINT(value)); else if (unit.right(2) == "in") - value = INCH_TO_POINT(value); + value = ptToPx(gc, INCH_TO_POINT(value)); else if (unit.right(2) == "em") - value = value * gc->font.pointSize(); + value = ptToPx(gc, value * gc->font.pointSize()); else if (unit.right(2) == "ex") { QFontMetrics metrics(gc->font); - value = value * metrics.xHeight(); + value = ptToPx(gc, value * metrics.xHeight()); } else if (unit.right(1) == "%") { if (horiz && vert) value = (value / 100.0) * (sqrt(pow(bbox.width(), 2) + pow(bbox.height(), 2)) / sqrt(2.0)); @@ -265,31 +279,77 @@ qreal SvgUtil::parseUnitX(SvgGraphicsContext *gc, const QString &unit) { if (gc->forcePercentage) { - return SvgUtil::fromPercentage(unit) * gc->currentBoundbox.width(); + return SvgUtil::fromPercentage(unit) * gc->currentBoundingBox.width(); } else { - return SvgUtil::parseUnit(gc, unit, true, false, gc->currentBoundbox); + return SvgUtil::parseUnit(gc, unit, true, false, gc->currentBoundingBox); } } qreal SvgUtil::parseUnitY(SvgGraphicsContext *gc, const QString &unit) { if (gc->forcePercentage) { - return SvgUtil::fromPercentage(unit) * gc->currentBoundbox.height(); + return SvgUtil::fromPercentage(unit) * gc->currentBoundingBox.height(); } else { - return SvgUtil::parseUnit(gc, unit, false, true, gc->currentBoundbox); + return SvgUtil::parseUnit(gc, unit, false, true, gc->currentBoundingBox); } } qreal SvgUtil::parseUnitXY(SvgGraphicsContext *gc, const QString &unit) { if (gc->forcePercentage) { const qreal value = SvgUtil::fromPercentage(unit); - return value * sqrt(pow(gc->currentBoundbox.width(), 2) + pow(gc->currentBoundbox.height(), 2)) / sqrt(2.0); + return value * sqrt(pow(gc->currentBoundingBox.width(), 2) + pow(gc->currentBoundingBox.height(), 2)) / sqrt(2.0); } else { - return SvgUtil::parseUnit(gc, unit, true, true, gc->currentBoundbox); + return SvgUtil::parseUnit(gc, unit, true, true, gc->currentBoundingBox); } } +qreal SvgUtil::parseUnitAngular(SvgGraphicsContext *gc, const QString &unit) +{ + Q_UNUSED(gc); + + qreal value = 0.0; + + if (unit.isEmpty()) return value; + QByteArray unitLatin1 = unit.toLower().toLatin1(); + + const char *start = unitLatin1.data(); + if (!start) return value; + + const char *end = parseNumber(start, value); + + if (int(end - start) < unit.length()) { + if (unit.right(3) == "deg") { + value = kisDegreesToRadians(value); + } else if (unit.right(4) == "grad") { + value *= M_PI / 200; + } else if (unit.right(3) == "rad") { + // noop! + } else { + value = kisDegreesToRadians(value); + } + } else { + value = kisDegreesToRadians(value); + } + + return value; +} + +qreal SvgUtil::parseNumber(const QString &string) +{ + qreal value = 0.0; + + if (string.isEmpty()) return value; + QByteArray unitLatin1 = string.toLatin1(); + + const char *start = unitLatin1.data(); + if (!start) return value; + + const char *end = parseNumber(start, value); + KIS_SAFE_ASSERT_RECOVER_NOOP(int(end - start) == string.length()); + return value; +} + const char * SvgUtil::parseNumber(const char *ptr, qreal &number) { int integer, exponent; @@ -343,3 +403,111 @@ return ptr; } + +QString SvgUtil::mapExtendedShapeTag(const QString &tagName, const KoXmlElement &element) +{ + QString result = tagName; + + if (tagName == "path") { + QString kritaType = element.attribute("krita:type", ""); + QString sodipodiType = element.attribute("sodipodi:type", ""); + + if (kritaType == "arc") { + result = "krita:arc"; + } else if (sodipodiType == "arc") { + result = "sodipodi:arc"; + } + } + + return result; +} + +SvgUtil::PreserveAspectRatioParser::PreserveAspectRatioParser(const QString &str) +{ + QRegExp rexp("(defer)?\\s*(none|(x(Min|Max|Mid)Y(Min|Max|Mid)))\\s*(meet|slice)?", Qt::CaseInsensitive); + int index = rexp.indexIn(str.toLower()); + + if (index >= 0) { + if (rexp.cap(1) == "defer") { + defer = true; + } + + if (rexp.cap(2) != "none") { + xAlignment = alignmentFromString(rexp.cap(4)); + yAlignment = alignmentFromString(rexp.cap(5)); + mode = rexp.cap(6) == "slice" ? + Qt::KeepAspectRatioByExpanding : Qt::KeepAspectRatio; + } + } +} + +QPointF SvgUtil::PreserveAspectRatioParser::rectAnchorPoint(const QRectF &rc) const +{ + return QPointF(alignedValue(rc.x(), rc.x() + rc.width(), xAlignment), + alignedValue(rc.y(), rc.y() + rc.height(), yAlignment)); +} + +QString SvgUtil::PreserveAspectRatioParser::toString() const +{ + QString result; + + if (!defer && + xAlignment == Middle && + yAlignment == Middle && + mode == Qt::KeepAspectRatio) { + + return result; + } + + if (defer) { + result += "defer "; + } + + if (mode == Qt::IgnoreAspectRatio) { + result += "none"; + } else { + result += QString("x%1Y%2") + .arg(alignmentToString(xAlignment)) + .arg(alignmentToString(yAlignment)); + + if (mode == Qt::KeepAspectRatioByExpanding) { + result += " slice"; + } + } + + return result; +} + +SvgUtil::PreserveAspectRatioParser::Alignment SvgUtil::PreserveAspectRatioParser::alignmentFromString(const QString &str) const { + return + str == "max" ? Max : + str == "mid" ? Middle : Min; +} + +QString SvgUtil::PreserveAspectRatioParser::alignmentToString(SvgUtil::PreserveAspectRatioParser::Alignment alignment) const +{ + return + alignment == Max ? "Max" : + alignment == Min ? "Min" : + "Mid"; + +} + +qreal SvgUtil::PreserveAspectRatioParser::alignedValue(qreal min, qreal max, SvgUtil::PreserveAspectRatioParser::Alignment alignment) +{ + qreal result = min; + + switch (alignment) { + case Min: + result = min; + break; + case Middle: + result = 0.5 * (min + max); + break; + case Max: + result = max; + break; + } + + return result; +} diff --git a/libs/flake/svg/SvgWriter.h b/libs/flake/svg/SvgWriter.h --- a/libs/flake/svg/SvgWriter.h +++ b/libs/flake/svg/SvgWriter.h @@ -60,7 +60,11 @@ /// Writes svg to the specified file bool save(const QString &filename, bool writeInlineImages); + bool saveDetached(QIODevice &outputDevice); + private: + void saveShapes(const QList shapes, SvgSavingContext &savingContext); + void saveLayer(KoShapeLayer *layer, SvgSavingContext &context); void saveGroup(KoShapeGroup *group, SvgSavingContext &context); void saveShape(KoShape *shape, SvgSavingContext &context); diff --git a/libs/flake/svg/SvgWriter.cpp b/libs/flake/svg/SvgWriter.cpp --- a/libs/flake/svg/SvgWriter.cpp +++ b/libs/flake/svg/SvgWriter.cpp @@ -104,36 +104,58 @@ svgStream << "\"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">" << endl; // add some PR. one line is more than enough. - svgStream << "" << endl; - - svgStream << "" << endl; + svgStream << "" << endl; + + svgStream << "" << endl; { SvgSavingContext savingContext(outputDevice, m_writeInlineImages); - - // top level shapes - Q_FOREACH (KoShape *shape, m_toplevelShapes) { - KoShapeLayer *layer = dynamic_cast(shape); - if(layer) { - saveLayer(layer, savingContext); - } else { - KoShapeGroup *group = dynamic_cast(shape); - if (group) - saveGroup(group, savingContext); - else - saveShape(shape, savingContext); - } - } + saveShapes(m_toplevelShapes, savingContext); } // end tag: svgStream << endl << "" << endl; return true; } +bool SvgWriter::saveDetached(QIODevice &outputDevice) +{ + if (m_toplevelShapes.isEmpty()) + return false; + + SvgSavingContext savingContext(outputDevice, m_writeInlineImages); + saveShapes(m_toplevelShapes, savingContext); + + return true; +} + +void SvgWriter::saveShapes(const QList shapes, SvgSavingContext &savingContext) +{ + // top level shapes + Q_FOREACH (KoShape *shape, shapes) { + KoShapeLayer *layer = dynamic_cast(shape); + if(layer) { + saveLayer(layer, savingContext); + } else { + KoShapeGroup *group = dynamic_cast(shape); + if (group) + saveGroup(group, savingContext); + else + saveShape(shape, savingContext); + } + } +} + void SvgWriter::saveLayer(KoShapeLayer *layer, SvgSavingContext &context) { context.shapeWriter().startElement("g"); diff --git a/libs/image/tests/kis_algebra_2d_test.h b/libs/flake/svg/parsers/SvgTransformParser.h copy from libs/image/tests/kis_algebra_2d_test.h copy to libs/flake/svg/parsers/SvgTransformParser.h --- a/libs/image/tests/kis_algebra_2d_test.h +++ b/libs/flake/svg/parsers/SvgTransformParser.h @@ -16,21 +16,23 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KIS_ALGEBRA_2D_TEST_H -#define __KIS_ALGEBRA_2D_TEST_H +#ifndef SVGTRANSFORMPARSER_H +#define SVGTRANSFORMPARSER_H -#include +#include +#include "kritaflake_export.h" -class KisAlgebra2DTest : public QObject + +class KRITAFLAKE_EXPORT SvgTransformParser { - Q_OBJECT -private Q_SLOTS: - void testHalfPlane(); - void testOuterCircle(); +public: + SvgTransformParser(const QString &str); + bool isValid() const; + QTransform transform() const; - void testQuadraticEquation(); - void testIntersections(); - void testWeirdIntersections(); +private: + bool m_isValid; + QTransform m_transform; }; -#endif /* __KIS_ALGEBRA_2D_TEST_H */ +#endif // SVGTRANSFORMPARSER_H diff --git a/libs/flake/svg/parsers/SvgTransformParser.cpp b/libs/flake/svg/parsers/SvgTransformParser.cpp new file mode 100644 --- /dev/null +++ b/libs/flake/svg/parsers/SvgTransformParser.cpp @@ -0,0 +1,278 @@ +/* + * 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 "SvgTransformParser.h" + +#include + +//#include "kis_debug.h" + +#include +#include +#include +#include +#include + + +namespace Private +{ + +struct matrix +{ + qreal a = 0; + qreal b = 0; + qreal c = 0; + qreal d = 0; + qreal e = 0; + qreal f = 0; +}; + +struct translate +{ + qreal tx = 0.0; + qreal ty = 0.0; +}; + +struct scale +{ + qreal sx = 0; + qreal sy = 0; + bool syPresent = false; +}; + +struct rotate +{ + qreal angle = 0; + qreal cx = 0; + qreal cy = 0; +}; + +struct skewX +{ + qreal angle = 0; +}; + +struct skewY +{ + qreal angle = 0; +}; + +struct transform_unit +{ + transform_unit() {} + + transform_unit(const matrix &m) { + transform = QTransform(m.a, m.b, m.c, m.d, m.e, m.f); + } + + transform_unit(const translate &t) { + transform = QTransform::fromTranslate(t.tx, t.ty); + } + + transform_unit(const scale &sc) { + transform = + QTransform::fromScale(sc.sx, + sc.syPresent ? sc.sy : sc.sx); + } + + transform_unit(const rotate &r) { + transform.rotate(r.angle); + if (r.cx != 0.0 || r.cy != 0.0) { + transform = + QTransform::fromTranslate(-r.cx, -r.cy) * + transform * + QTransform::fromTranslate(r.cx, r.cy); + } + } + + transform_unit(const skewX &sx) { + const qreal deg2rad = qreal(0.017453292519943295769); + const qreal value = tan(deg2rad * sx.angle); + transform.shear(value, 0); + } + + transform_unit(const skewY &sy) { + const qreal deg2rad = qreal(0.017453292519943295769); + const qreal value = tan(deg2rad * sy.angle); + transform.shear(0, value); + } + + QTransform transform; +}; +} + +// We need to tell fusion about our transform_unit struct +// to make it a first-class fusion citizen. This has to +// be in global scope. + +BOOST_FUSION_ADAPT_STRUCT( + Private::matrix, + (qreal, a) + (qreal, b) + (qreal, c) + (qreal, d) + (qreal, e) + (qreal, f) + ) + +BOOST_FUSION_ADAPT_STRUCT( + Private::translate, + (qreal, tx) + (qreal, ty) + ) + +BOOST_FUSION_ADAPT_STRUCT( + Private::scale, + (qreal, sx) + (qreal, sy) + (bool, syPresent) + ) + +BOOST_FUSION_ADAPT_STRUCT( + Private::rotate, + (qreal, angle) + (qreal, cx) + (qreal, cy) + ) + +BOOST_FUSION_ADAPT_STRUCT( + Private::skewX, + (qreal, angle) + ) + +BOOST_FUSION_ADAPT_STRUCT( + Private::skewY, + (qreal, angle) + ) + +#define BOOST_SPIRIT_DEBUG 1 + +namespace Private +{ + // Define our grammar + + namespace qi = boost::spirit::qi; + namespace ascii = boost::spirit::ascii; + + template + struct transform_unit_parser : qi::grammar(), ascii::space_type> + { + transform_unit_parser() : transform_unit_parser::base_type(start) + { + namespace phoenix = boost::phoenix; + using qi::lit; + using qi::double_; + using ascii::char_; + using qi::cntrl; + using phoenix::at_c; + using phoenix::push_back; + using namespace qi::labels; + + + comma %= -char_(','); + + matrix_rule %= + lit("matrix") + >> '(' + >> double_ >> comma + >> double_ >> comma + >> double_ >> comma + >> double_ >> comma + >> double_ >> comma + >> double_ >> comma + >> ')'; + + translate_rule %= + lit("translate") + >> '(' >> double_ >> comma >> -double_ >> ')'; + + scale_rule %= + lit("scale") + >> '(' + >> double_ >> comma + >> -double_ [at_c<2>(_val) = true] + >> ')'; + + + // due to braces "-(...)" we cannot use automated + // semantic actions without relayouting the structure + rotate_rule = + lit("rotate") + >> '(' + >> double_ [at_c<0>(_val) = _1] + >> comma + >> -(double_ [at_c<1>(_val) = _1] + >> comma + >> double_ [at_c<2>(_val) = _1]) + >> ')'; + + skewX_rule %= lit("skewX") >> '(' >> double_ >> ')'; + skewY_rule %= lit("skewY") >> '(' >> double_ >> ')'; + + start %= + (matrix_rule | translate_rule | scale_rule | + rotate_rule | skewX_rule | skewY_rule) % + (cntrl | comma); + } + + qi::rule(), ascii::space_type> start; + qi::rule translate_rule; + qi::rule matrix_rule; + qi::rule scale_rule; + qi::rule rotate_rule; + qi::rule skewX_rule; + qi::rule skewY_rule; + qi::rule comma; + }; +} + + +SvgTransformParser::SvgTransformParser(const QString &_str) + : m_isValid(false) +{ + using boost::spirit::ascii::space; + typedef std::string::const_iterator iterator_type; + typedef Private::transform_unit_parser transform_unit_parser; + + transform_unit_parser g; // Our grammar + const std::string str = _str.toStdString(); + + std::vector transforms; + iterator_type iter = str.begin(); + iterator_type end = str.end(); + bool r = phrase_parse(iter, end, g, space, transforms); + + if (r && iter == end) { + m_isValid = true; + + for (const Private::transform_unit &t : transforms) { + m_transform = t.transform * m_transform; + } + } +} +bool SvgTransformParser::isValid() const +{ + return m_isValid; +} + +QTransform SvgTransformParser::transform() const +{ + return m_transform; +} + + diff --git a/libs/flake/tests/CMakeLists.txt b/libs/flake/tests/CMakeLists.txt --- a/libs/flake/tests/CMakeLists.txt +++ b/libs/flake/tests/CMakeLists.txt @@ -3,6 +3,7 @@ include(ECMAddTests) include(KritaAddBrokenUnitTest) + macro_add_unittest_definitions() ecm_add_tests( @@ -57,3 +58,37 @@ krita_add_broken_unit_test(TestPointMergeCommand.cpp TEST_NAME libs-kritaflake-TestPointMergeCommand LINK_LIBRARIES kritaflake Qt5::Test) + +ecm_add_test( + TestKoDrag.cpp + TEST_NAME libs-kritaflake-TestKoDrag + LINK_LIBRARIES kritaflake Qt5::Test +) + +ecm_add_test( + TestKoMarkerCollection.cpp + TEST_NAME libs-kritaflake-TestKoMarkerCollection + LINK_LIBRARIES kritaflake Qt5::Test +) + +ecm_add_test( + TestSvgParser.cpp + TEST_NAME libs-kritaflake-TestSvgParser + LINK_LIBRARIES kritaflake Qt5::Test +) + +ecm_add_test( + TestSvgParser.cpp + TEST_NAME libs-kritaflake-TestSvgParserCloned + LINK_LIBRARIES kritaflake Qt5::Test +) +set_property(TARGET libs-kritaflake-TestSvgParserCloned + PROPERTY COMPILE_DEFINITIONS USE_CLONED_SHAPES) + +ecm_add_test( + TestSvgParser.cpp + TEST_NAME libs-kritaflake-TestSvgParserRoundTrip + LINK_LIBRARIES kritaflake Qt5::Test +) +set_property(TARGET libs-kritaflake-TestSvgParserRoundTrip + PROPERTY COMPILE_DEFINITIONS USE_ROUND_TRIP) diff --git a/libs/flake/tests/MockShapes.h b/libs/flake/tests/MockShapes.h --- a/libs/flake/tests/MockShapes.h +++ b/libs/flake/tests/MockShapes.h @@ -20,6 +20,7 @@ #ifndef MOCKSHAPES_H #define MOCKSHAPES_H +#include #include #include #include @@ -78,12 +79,56 @@ class KoToolProxy; +class KRITAFLAKE_EXPORT MockShapeController : public KoShapeBasedDocumentBase +{ +public: + void addShape(KoShape* shape) { + m_shapes.insert(shape); + if (m_shapeManager) { + m_shapeManager->addShape(shape); + } + } + void removeShape(KoShape* shape) { + m_shapes.remove(shape); + if (m_shapeManager) { + m_shapeManager->remove(shape); + } + } + bool contains(KoShape* shape) { + return m_shapes.contains(shape); + } + + void setShapeManager(KoShapeManager *shapeManager) { + m_shapeManager = shapeManager; + } + + QRectF documentRectInPixels() const { + return QRectF(0,0,100,100); + } + + qreal pixelsPerInch() const { + return 72.0; + } + +private: + QSet m_shapes; + KoShapeManager *m_shapeManager = 0; +}; + class KRITAFLAKE_EXPORT MockCanvas : public KoCanvasBase { Q_OBJECT public: MockCanvas(KoShapeBasedDocumentBase *aKoShapeBasedDocumentBase =0)//made for TestSnapStrategy.cpp - : KoCanvasBase(aKoShapeBasedDocumentBase), m_shapeManager(new KoShapeManager(this)) {} + : KoCanvasBase(aKoShapeBasedDocumentBase), + m_shapeManager(new KoShapeManager(this)), + m_selectedShapesProxy(new KoSelectedShapesProxySimple(m_shapeManager.data())) + { + if (MockShapeController *controller = dynamic_cast(aKoShapeBasedDocumentBase)) { + controller->setShapeManager(m_shapeManager.data()); + } + } + ~MockCanvas() {} void setHorz(qreal pHorz){ m_horz = pHorz; @@ -102,7 +147,10 @@ } void addCommand(KUndo2Command*) { } KoShapeManager *shapeManager() const { - return m_shapeManager; + return m_shapeManager.data(); + } + KoSelectedShapesProxy *selectedShapesProxy() const { + return m_selectedShapesProxy.data(); } void updateCanvas(const QRectF&) {} KoToolProxy * toolProxy() const { @@ -123,28 +171,13 @@ void updateInputMethodInfo() {} void setCursor(const QCursor &) {} private: - KoShapeManager *m_shapeManager; + QScopedPointer m_shapeManager; + QScopedPointer m_selectedShapesProxy; qreal m_horz; qreal m_vert; }; -class KRITAFLAKE_EXPORT MockShapeController : public KoShapeBasedDocumentBase -{ -public: - void addShape(KoShape* shape) { - m_shapes.insert(shape); - } - void removeShape(KoShape* shape) { - m_shapes.remove(shape); - } - bool contains(KoShape* shape) { - return m_shapes.contains(shape); - } -private: - QSet m_shapes; -}; - -class MockContainerModel : public KoShapeContainerModel +class KRITAFLAKE_EXPORT MockContainerModel : public KoShapeContainerModel { public: MockContainerModel() { diff --git a/libs/image/kis_undo_store.cpp b/libs/flake/tests/TestKoClipMaskPainter.h copy from libs/image/kis_undo_store.cpp copy to libs/flake/tests/TestKoClipMaskPainter.h --- a/libs/image/kis_undo_store.cpp +++ b/libs/flake/tests/TestKoClipMaskPainter.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007 Sven Langkamp + * 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 @@ -16,15 +16,18 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_undo_store.h" +#ifndef TESTKOCLIPMASKPAINTER_H +#define TESTKOCLIPMASKPAINTER_H -#include "kis_debug.h" +#include - -KisUndoStore::KisUndoStore() +class TestKoClipMaskPainter : public QObject { -} + Q_OBJECT +private Q_SLOTS: + void test(); +}; -KisUndoStore::~KisUndoStore() -{ -} + + +#endif // TESTKOCLIPMASKPAINTER_H diff --git a/libs/flake/tests/TestKoClipMaskPainter.cpp b/libs/flake/tests/TestKoClipMaskPainter.cpp new file mode 100644 --- /dev/null +++ b/libs/flake/tests/TestKoClipMaskPainter.cpp @@ -0,0 +1,74 @@ +/* + * 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 "TestKoClipMaskPainter.h" + +#include +#include +#include + +#include + +#include "../../sdk/tests/qimage_test_util.h" + + +void drawRect(QPainter *gc, const QRect &rc, const QColor &color) +{ + gc->save(); + + gc->setBrush(color); + gc->drawRect(rc); + + gc->restore(); +} + + +void TestKoClipMaskPainter::test() +{ + QImage canvas(30, 30, QImage::Format_ARGB32); + canvas.fill(0); + + const QRect rect1(4,4,15,15); + const QRect clipRect1(4,4,13,15); + const QRect rect2(10,10,15,15); + + QTransform t; + t.rotate(90); + t = t * QTransform::fromTranslate(30, 0); + + QPainter painter(&canvas); + painter.setPen(Qt::NoPen); + painter.setTransform(t); + + painter.setClipRect(clipRect1); + + KoClipMaskPainter clipPainter(&painter, painter.transform().mapRect(rect1)); + + // please debug using: + //drawRect(&painter, rect1, Qt::blue); + //drawRect(&painter, rect2, Qt::red); + + drawRect(clipPainter.shapePainter(), rect1, Qt::blue); + drawRect(clipPainter.maskPainter(), rect2, Qt::red); + + clipPainter.renderOnGlobalPainter(); + + QVERIFY(TestUtil::checkQImage(canvas, "", "clip_mask", "render_mask")); +} + +QTEST_MAIN(TestKoClipMaskPainter) diff --git a/libs/image/kis_undo_store.cpp b/libs/flake/tests/TestKoDrag.h copy from libs/image/kis_undo_store.cpp copy to libs/flake/tests/TestKoDrag.h --- a/libs/image/kis_undo_store.cpp +++ b/libs/flake/tests/TestKoDrag.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007 Sven Langkamp + * 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 @@ -16,15 +16,17 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_undo_store.h" +#ifndef TESTKODRAG_H +#define TESTKODRAG_H -#include "kis_debug.h" +#include +#include - -KisUndoStore::KisUndoStore() +class TestKoDrag : public QObject { -} + Q_OBJECT +private Q_SLOTS: + void test(); +}; -KisUndoStore::~KisUndoStore() -{ -} +#endif // TESTKODRAG_H diff --git a/libs/flake/tests/TestKoDrag.cpp b/libs/flake/tests/TestKoDrag.cpp new file mode 100644 --- /dev/null +++ b/libs/flake/tests/TestKoDrag.cpp @@ -0,0 +1,87 @@ +/* + * 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 "TestKoDrag.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include "../../sdk/tests/qimage_test_util.h" + +void TestKoDrag::test() +{ + const QString fileName = TestUtil::fetchDataFileLazy("test_svg_file.svg"); + QVERIFY(!fileName.isEmpty()); + + QFile testShapes(fileName); + testShapes.open(QIODevice::ReadOnly); + + KoXmlDocument doc; + doc.setContent(testShapes.readAll()); + + KoDocumentResourceManager resourceManager; + SvgParser parser(&resourceManager); + parser.setResolution(QRectF(0, 0, 30, 30) /* px */, 72 /* ppi */); + + QSizeF fragmentSize; + QList shapes = parser.parseSvg(doc.documentElement(), &fragmentSize); + QCOMPARE(fragmentSize, QSizeF(30,30)); + + { + QCOMPARE(shapes.size(), 1); + + KoShapeGroup *layer = dynamic_cast(shapes.first()); + QVERIFY(layer); + QCOMPARE(layer->shapeCount(), 2); + + QCOMPARE(KoShape::boundingRect(shapes).toAlignedRect(), QRect(4,3,24,24)); + } + + KoDrag drag; + drag.setSvg(shapes); + drag.addToClipboard(); + + KoSvgPaste paste; + QVERIFY(paste.hasShapes()); + + QList newShapes = paste.fetchShapes(QRectF(0,0,15,15) /* px */, 144 /* ppi */, &fragmentSize); + + { + QCOMPARE(newShapes.size(), 1); + + KoShapeGroup *layer = dynamic_cast(newShapes.first()); + QVERIFY(layer); + QCOMPARE(layer->shapeCount(), 2); + + QCOMPARE(fragmentSize.toSize(), QSize(54, 53)); + QCOMPARE(KoShape::boundingRect(newShapes).toAlignedRect(), QRect(4,3,24,24)); + } + + + qDeleteAll(shapes); + qDeleteAll(newShapes); +} + + +QTEST_MAIN(TestKoDrag) diff --git a/libs/flake/tests/TestShapePainting.h b/libs/flake/tests/TestKoMarkerCollection.h copy from libs/flake/tests/TestShapePainting.h copy to libs/flake/tests/TestKoMarkerCollection.h --- a/libs/flake/tests/TestShapePainting.h +++ b/libs/flake/tests/TestKoMarkerCollection.h @@ -1,7 +1,5 @@ /* - * This file is part of Calligra tests - * - * Copyright (C) 2006-2010 Thomas Zander + * 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 @@ -17,19 +15,19 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef TESTSHAPEPAINT_H -#define TESTSHAPEPAINT_H -#include +#ifndef TESTKOMARKERCOLLECTION_H +#define TESTKOMARKERCOLLECTION_H + +#include -class TestShapePainting : public QObject +class TestKoMarkerCollection : public QObject { Q_OBJECT private Q_SLOTS: - - void testPaintShape(); - void testPaintHiddenShape(); - void testPaintOrder(); + void testLoadMarkersFromFile(); + void testDeduplication(); + void testMarkerBounds(); }; -#endif +#endif // TESTKOMARKERCOLLECTION_H diff --git a/libs/flake/tests/TestKoMarkerCollection.cpp b/libs/flake/tests/TestKoMarkerCollection.cpp new file mode 100644 --- /dev/null +++ b/libs/flake/tests/TestKoMarkerCollection.cpp @@ -0,0 +1,110 @@ +/* + * 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 "TestKoMarkerCollection.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "kis_debug.h" +#include "../../sdk/tests/qimage_test_util.h" + +#include + + +void initMarkerCollection(KoMarkerCollection *collection) +{ + QCOMPARE(collection->markers().size(), 1); + + const QString fileName = TestUtil::fetchDataFileLazy("test_markers.svg"); + QVERIFY(QFileInfo(fileName).exists()); + + collection->loadMarkersFromFile(fileName); + QCOMPARE(collection->markers().size(), 10); +} + +void TestKoMarkerCollection::testLoadMarkersFromFile() +{ + KoMarkerCollection collection; + initMarkerCollection(&collection); +} + +void TestKoMarkerCollection::testDeduplication() +{ + QPainterPath path1; + path1.addRect(QRect(5,5,15,15)); + + KoPathShape *shape1(KoPathShape::createShapeFromPainterPath(path1)); + shape1->setBackground(QSharedPointer(new KoColorBackground(Qt::blue))); + + KoMarker *marker(new KoMarker()); + marker->setAutoOrientation(true); + marker->setShapes({shape1}); + + KoMarkerCollection collection; + QCOMPARE(collection.markers().size(), 1); + + KoMarker *clonedMarker = new KoMarker(*marker); + + collection.addMarker(marker); + QCOMPARE(collection.markers().size(), 2); + + collection.addMarker(marker); + QCOMPARE(collection.markers().size(), 2); + + collection.addMarker(clonedMarker); + QCOMPARE(collection.markers().size(), 2); +} + +void testOneMarkerPosition(KoMarker *marker, KoFlake::MarkerPosition position, const QString &testName) +{ + QImage image(30,30, QImage::Format_ARGB32); + image.fill(0); + QPainter painter(&image); + + QPen pen(Qt::black, 2); + marker->drawPreview(&painter, image.rect(), pen, position); + + QVERIFY(TestUtil::checkQImage(image, "marker_collection", "preview", testName)); +} + +void TestKoMarkerCollection::testMarkerBounds() +{ + KoMarkerCollection collection; + initMarkerCollection(&collection); + + QList allMarkers = collection.markers(); + KoMarker *marker = allMarkers[3]; + + QCOMPARE(marker->boundingRect(1, 0).toAlignedRect(), QRect(-7,-3,9,6)); + QCOMPARE(marker->boundingRect(1, M_PI).toAlignedRect(), QRect(-2,-3,9,6)); + + QCOMPARE(marker->outline(1, 0).boundingRect().toAlignedRect(), QRect(-6,-2,7,4)); + QCOMPARE(marker->outline(1, M_PI).boundingRect().toAlignedRect(), QRect(-1,-2,7,4)); + + testOneMarkerPosition(marker, KoFlake::StartMarker, "start_marker"); + testOneMarkerPosition(marker, KoFlake::MidMarker, "mid_marker"); + testOneMarkerPosition(marker, KoFlake::EndMarker, "end_marker"); +} + +QTEST_MAIN(TestKoMarkerCollection) diff --git a/libs/flake/tests/TestPointMergeCommand.h b/libs/flake/tests/TestPointMergeCommand.h --- a/libs/flake/tests/TestPointMergeCommand.h +++ b/libs/flake/tests/TestPointMergeCommand.h @@ -30,6 +30,24 @@ void closeSingleCurvePath(); void connectLineSubpaths(); void connectCurveSubpaths(); + + void testCombineShapes(); + void testMultipathMergeShapesBothSequential(); + void testMultipathMergeShapesFirstReversed(); + void testMultipathMergeShapesSecondReversed(); + void testMultipathMergeShapesBothReversed(); + + void testMultipathMergeShapesSingleShapeEndToStart(); + void testMultipathMergeShapesSingleShapeStartToEnd(); + + void testMultipathJoinShapesBothSequential(); + void testMultipathJoinShapesFirstReversed(); + void testMultipathJoinShapesSecondReversed(); + void testMultipathJoinShapesBothReversed(); + + void testMultipathJoinShapesSingleShapeEndToStart(); + void testMultipathJoinShapesSingleShapeStartToEnd(); + }; #endif // TESTPOINTMERGECOMMAND_H diff --git a/libs/flake/tests/TestPointMergeCommand.cpp b/libs/flake/tests/TestPointMergeCommand.cpp --- a/libs/flake/tests/TestPointMergeCommand.cpp +++ b/libs/flake/tests/TestPointMergeCommand.cpp @@ -246,4 +246,319 @@ QVERIFY(!path1.pointByIndex(index1)->activeControlPoint2()); } +#include +#include +#include "kis_debug.h" +#include + +void TestPointMergeCommand::testCombineShapes() +{ + MockShapeController mockController; + MockCanvas canvas(&mockController); + + QList shapes; + + for (int i = 0; i < 3; i++) { + const QPointF step(15,15); + const QRectF rect = QRectF(5,5,10,10).translated(step * i); + + QPainterPath p; + p.addRect(rect); + + KoPathShape *shape = KoPathShape::createShapeFromPainterPath(p); + QCOMPARE(shape->absoluteOutlineRect(), rect); + + shapes << shape; + mockController.addShape(shape); + } + + KoPathCombineCommand cmd(&mockController, shapes); + cmd.redo(); + + QCOMPARE(canvas.shapeManager()->shapes().size(), 1); + + KoPathShape *combinedShape = dynamic_cast(canvas.shapeManager()->shapes().first()); + QCOMPARE(combinedShape, cmd.combinedPath()); + QCOMPARE(combinedShape->subpathCount(), 3); + QCOMPARE(combinedShape->absoluteOutlineRect(), QRectF(5,5,40,40)); + + QList tstPoints; + QList expPoints; + + tstPoints << KoPathPointData(shapes[0], KoPathPointIndex(0,1)); + expPoints << KoPathPointData(combinedShape, KoPathPointIndex(0,1)); + + tstPoints << KoPathPointData(shapes[1], KoPathPointIndex(0,2)); + expPoints << KoPathPointData(combinedShape, KoPathPointIndex(1,2)); + + tstPoints << KoPathPointData(shapes[2], KoPathPointIndex(0,3)); + expPoints << KoPathPointData(combinedShape, KoPathPointIndex(2,3)); + + for (int i = 0; i < tstPoints.size(); i++) { + KoPathPointData convertedPoint = cmd.originalToCombined(tstPoints[i]); + QCOMPARE(convertedPoint, expPoints[i]); + } + + qDeleteAll(canvas.shapeManager()->shapes()); +} + +#include +#include +#include +#include "kis_algebra_2d.h" + +inline QPointF fetchPoint(KoPathShape *shape, int subpath, int pointIndex) { + return shape->absoluteTransformation(0).map( + shape->pointByIndex(KoPathPointIndex(subpath, pointIndex))->point()); +} + +void dumpShape(KoPathShape *shape, const QString &fileName) +{ + QImage tmp(50,50, QImage::Format_ARGB32); + tmp.fill(0); + QPainter p(&tmp); + p.drawPath(shape->absoluteTransformation(0).map(shape->outline())); + tmp.save(fileName); +} + +template +void testMultipathMergeShapesImpl(const int srcPointIndex1, + const int srcPointIndex2, + const QList &expectedResultPoints, + bool singleShape = false) +{ + MockShapeController mockController; + MockCanvas canvas(&mockController); + + QList shapes; + + for (int i = 0; i < 3; i++) { + const QPointF step(15,15); + const QRectF rect = QRectF(5,5,10,10).translated(step * i); + + QPainterPath p; + p.moveTo(rect.topLeft()); + p.lineTo(rect.bottomRight()); + p.lineTo(rect.topRight()); + + KoPathShape *shape = KoPathShape::createShapeFromPainterPath(p); + QCOMPARE(shape->absoluteOutlineRect(), rect); + + shapes << shape; + mockController.addShape(shape); + } + + + { + KoPathPointData pd1(shapes[0], KoPathPointIndex(0,srcPointIndex1)); + KoPathPointData pd2(shapes[singleShape ? 0 : 1], KoPathPointIndex(0,srcPointIndex2)); + + MergeCommand cmd(pd1, pd2, &mockController, canvas.shapeManager()->selection()); + + cmd.redo(); + + const int expectedShapesCount = singleShape ? 3 : 2; + QCOMPARE(canvas.shapeManager()->shapes().size(), expectedShapesCount); + + KoPathShape *combinedShape = 0; + + if (!singleShape) { + combinedShape = dynamic_cast(canvas.shapeManager()->shapes()[1]); + QCOMPARE(combinedShape, cmd.testingCombinedPath()); + } else { + combinedShape = dynamic_cast(canvas.shapeManager()->shapes()[0]); + QCOMPARE(combinedShape, shapes[0]); + } + + QCOMPARE(combinedShape->subpathCount(), 1); + + QRectF expectedOutlineRect; + KisAlgebra2D::accumulateBounds(expectedResultPoints, &expectedOutlineRect); + QVERIFY(KisAlgebra2D::fuzzyCompareRects(combinedShape->absoluteOutlineRect(), expectedOutlineRect, 0.01)); + + if (singleShape) { + QCOMPARE(combinedShape->isClosedSubpath(0), true); + } + + QCOMPARE(combinedShape->subpathPointCount(0), expectedResultPoints.size()); + for (int i = 0; i < expectedResultPoints.size(); i++) { + if (fetchPoint(combinedShape, 0, i) != expectedResultPoints[i]) { + qDebug() << ppVar(i); + qDebug() << ppVar(fetchPoint(combinedShape, 0, i)); + qDebug() << ppVar(expectedResultPoints[i]); + + QFAIL("Resulting shape points are different!"); + } + } + + QList shapes = canvas.shapeManager()->selection()->selectedEditableShapes(); + QCOMPARE(shapes.size(), 1); + QCOMPARE(shapes.first(), combinedShape); + + //dumpShape(combinedShape, "tmp_0_seq.png"); + cmd.undo(); + + QCOMPARE(canvas.shapeManager()->shapes().size(), 3); + } +} + + +void TestPointMergeCommand::testMultipathMergeShapesBothSequential() +{ + // both sequential + testMultipathMergeShapesImpl(2, 0, + { + QPointF(5,5), + QPointF(15,15), + QPointF(17.5,12.5), // merged by melding the points! + QPointF(30,30), + QPointF(30,20) + }); +} + +void TestPointMergeCommand::testMultipathMergeShapesFirstReversed() +{ + // first reversed + testMultipathMergeShapesImpl(0, 0, + { + QPointF(15,5), + QPointF(15,15), + QPointF(12.5,12.5), // merged by melding the points! + QPointF(30,30), + QPointF(30,20) + }); +} + +void TestPointMergeCommand::testMultipathMergeShapesSecondReversed() +{ + // second reversed + testMultipathMergeShapesImpl(2, 2, + { + QPointF(5,5), + QPointF(15,15), + QPointF(22.5,12.5), // merged by melding the points! + QPointF(30,30), + QPointF(20,20) + }); +} + +void TestPointMergeCommand::testMultipathMergeShapesBothReversed() +{ + // both reversed + testMultipathMergeShapesImpl(0, 2, + { + QPointF(15,5), + QPointF(15,15), + QPointF(17.5,12.5), // merged by melding the points! + QPointF(30,30), + QPointF(20,20) + }); +} + +void TestPointMergeCommand::testMultipathMergeShapesSingleShapeEndToStart() +{ + // close end->start + testMultipathMergeShapesImpl(2, 0, + { + QPointF(15,15), + QPointF(10,5) + }, true); +} + +void TestPointMergeCommand::testMultipathMergeShapesSingleShapeStartToEnd() +{ + // close start->end + testMultipathMergeShapesImpl(0, 2, + { + QPointF(15,15), + QPointF(10,5) + }, true); +} + +void TestPointMergeCommand::testMultipathJoinShapesBothSequential() +{ + // both sequential + testMultipathMergeShapesImpl + (2, 0, + { + QPointF(5,5), + QPointF(15,15), + QPointF(15,5), + QPointF(20,20), + QPointF(30,30), + QPointF(30,20) + }); +} + +void TestPointMergeCommand::testMultipathJoinShapesFirstReversed() +{ + // first reversed + testMultipathMergeShapesImpl + (0, 0, + { + QPointF(15,5), + QPointF(15,15), + QPointF(5,5), + QPointF(20,20), + QPointF(30,30), + QPointF(30,20) + }); +} + +void TestPointMergeCommand::testMultipathJoinShapesSecondReversed() +{ + // second reversed + testMultipathMergeShapesImpl + (2, 2, + { + QPointF(5,5), + QPointF(15,15), + QPointF(15,5), + QPointF(30,20), + QPointF(30,30), + QPointF(20,20) + }); +} + +void TestPointMergeCommand::testMultipathJoinShapesBothReversed() +{ + // both reversed + testMultipathMergeShapesImpl + (0, 2, + { + QPointF(15,5), + QPointF(15,15), + QPointF(5,5), + QPointF(30,20), + QPointF(30,30), + QPointF(20,20) + }); +} + +void TestPointMergeCommand::testMultipathJoinShapesSingleShapeEndToStart() +{ + // close end->start + testMultipathMergeShapesImpl + (2, 0, + { + QPointF(5,5), + QPointF(15,15), + QPointF(15,5) + }, true); +} + +void TestPointMergeCommand::testMultipathJoinShapesSingleShapeStartToEnd() +{ + // close start->end + testMultipathMergeShapesImpl + (0, 2, + { + QPointF(5,5), + QPointF(15,15), + QPointF(15,5) + }, true); +} + + + QTEST_MAIN(TestPointMergeCommand) diff --git a/libs/flake/tests/TestPosition.cpp b/libs/flake/tests/TestPosition.cpp --- a/libs/flake/tests/TestPosition.cpp +++ b/libs/flake/tests/TestPosition.cpp @@ -96,11 +96,11 @@ shape1->setPosition(QPointF(10, 10)); QCOMPARE(shape1->absolutePosition(), QPointF(10 + 25, 10 + 25)); - QCOMPARE(shape1->absolutePosition(KoFlake::CenteredPosition), QPointF(10 + 25, 10 + 25)); - QCOMPARE(shape1->absolutePosition(KoFlake::TopLeftCorner), QPointF(10 + 50, 10)); - QCOMPARE(shape1->absolutePosition(KoFlake::BottomRightCorner), QPointF(10, 10 + 50)); + QCOMPARE(shape1->absolutePosition(KoFlake::Center), QPointF(10 + 25, 10 + 25)); + QCOMPARE(shape1->absolutePosition(KoFlake::TopLeft), QPointF(10 + 50, 10)); + QCOMPARE(shape1->absolutePosition(KoFlake::BottomRight), QPointF(10, 10 + 50)); - QCOMPARE(container2->absolutePosition(KoFlake::TopLeftCorner), QPointF(200, 200)); + QCOMPARE(container2->absolutePosition(KoFlake::TopLeft), QPointF(200, 200)); } void TestPosition::testSetAbsolutePosition() @@ -135,20 +135,20 @@ shape1->setAbsolutePosition(QPointF(100, 100)); QCOMPARE(shape1->absolutePosition(), QPointF(100, 100)); - shape1->setAbsolutePosition(QPointF(100, 100), KoFlake::TopLeftCorner); - QCOMPARE(shape1->absolutePosition(KoFlake::TopLeftCorner), QPointF(100, 100)); + shape1->setAbsolutePosition(QPointF(100, 100), KoFlake::TopLeft); + QCOMPARE(shape1->absolutePosition(KoFlake::TopLeft), QPointF(100, 100)); - childShape1->setAbsolutePosition(QPointF(0, 0), KoFlake::BottomRightCorner); + childShape1->setAbsolutePosition(QPointF(0, 0), KoFlake::BottomRight); QCOMPARE(childShape1->position(), QPointF(-150, -150)); - childShape1->setAbsolutePosition(QPointF(0, 0), KoFlake::BottomLeftCorner); + childShape1->setAbsolutePosition(QPointF(0, 0), KoFlake::BottomLeft); QCOMPARE(childShape1->position(), QPointF(-100, -150)); - childShape1->setAbsolutePosition(QPointF(0, 0), KoFlake::TopRightCorner); + childShape1->setAbsolutePosition(QPointF(0, 0), KoFlake::TopRight); QCOMPARE(childShape1->position(), QPointF(-150, -100)); container2->setInheritsTransform(childShape2, true); - childShape2->setAbsolutePosition(QPointF(0, 0), KoFlake::TopLeftCorner); + childShape2->setAbsolutePosition(QPointF(0, 0), KoFlake::TopLeft); QCOMPARE(childShape2->position(), QPointF(-200, 200)); } diff --git a/libs/flake/tests/TestResourceManager.cpp b/libs/flake/tests/TestResourceManager.cpp --- a/libs/flake/tests/TestResourceManager.cpp +++ b/libs/flake/tests/TestResourceManager.cpp @@ -52,7 +52,7 @@ QCOMPARE(spy.count(), 2); } -#include "kis_global.h" +#include "kis_pointer_utils.h" struct DerivedResource : public KoDerivedResourceConverter { diff --git a/libs/flake/tests/TestSelection.cpp b/libs/flake/tests/TestSelection.cpp --- a/libs/flake/tests/TestSelection.cpp +++ b/libs/flake/tests/TestSelection.cpp @@ -36,61 +36,41 @@ QCOMPARE(selection.selectedShapes().count(), 0); selection.select(shape1); QCOMPARE(selection.count(), 1); - QCOMPARE(selection.selectedShapes(KoFlake::FullSelection).count(), 1); - QCOMPARE(selection.selectedShapes(KoFlake::StrippedSelection).count(), 1); - QCOMPARE(selection.selectedShapes(KoFlake::TopLevelSelection).count(), 1); + QCOMPARE(selection.selectedShapes().count(), 1); selection.select(shape1); // same one. QCOMPARE(selection.count(), 1); - QCOMPARE(selection.selectedShapes(KoFlake::FullSelection).count(), 1); - QCOMPARE(selection.selectedShapes(KoFlake::StrippedSelection).count(), 1); - QCOMPARE(selection.selectedShapes(KoFlake::TopLevelSelection).count(), 1); + QCOMPARE(selection.selectedShapes().count(), 1); selection.select(shape2); selection.select(shape3); QCOMPARE(selection.count(), 3); - QCOMPARE(selection.selectedShapes(KoFlake::FullSelection).count(), 3); - QCOMPARE(selection.selectedShapes(KoFlake::StrippedSelection).count(), 3); - QCOMPARE(selection.selectedShapes(KoFlake::TopLevelSelection).count(), 3); + QCOMPARE(selection.selectedShapes().count(), 3); + + selection.deselectAll(); MockGroup *group1 = new MockGroup(); group1->addShape(shape1); group1->addShape(shape2); selection.select(group1); - QCOMPARE(selection.count(), 3); // don't return the grouping shape. - // Stripped returns no groups, so simply all 3 shapes - QCOMPARE(selection.selectedShapes(KoFlake::FullSelection).count(), 3); - // stripped returns no groups; so simply all shapes. - QCOMPARE(selection.selectedShapes(KoFlake::StrippedSelection).count(), 3); - // toplevel returns shape3 and group1 - QCOMPARE(selection.selectedShapes(KoFlake::TopLevelSelection).count(), 2); + QCOMPARE(selection.count(), 1); + QCOMPARE(selection.selectedShapes().count(), 1); MockGroup *group2 = new MockGroup(); group2->addShape(shape3); group2->addShape(group1); selection.select(group2); - QCOMPARE(selection.count(), 3); // thats 5 minus 2 grouping shapes. - // Stripped returns no groups, so simply all 3 shapes - QCOMPARE(selection.selectedShapes(KoFlake::FullSelection).count(), 3); - // Stripped returns no groups, so simply all 3 shapes - QCOMPARE(selection.selectedShapes(KoFlake::StrippedSelection).count(), 3); - // toplevel returns only group2 - QCOMPARE(selection.selectedShapes(KoFlake::TopLevelSelection).count(), 1); - + QCOMPARE(selection.count(), 2); + QCOMPARE(selection.selectedShapes().count(), 2); group1->removeShape(shape1); group1->removeShape(shape2); MockContainer *container = new MockContainer(); container->addShape(shape1); container->addShape(shape2); selection.select(container); - QCOMPARE(selection.count(), 4); // thats 6 minus 2 grouping shapes. - // Stripped returns no groups, so simply all 3 shapes + container - QCOMPARE(selection.selectedShapes(KoFlake::FullSelection).count(), 4); - // Stripped returns no groups, and no children of a container. So; container + shape3 - QCOMPARE(selection.selectedShapes(KoFlake::StrippedSelection).count(), 2); - // toplevel returns only group2 + container - QCOMPARE(selection.selectedShapes(KoFlake::TopLevelSelection).count(), 2); + QCOMPARE(selection.count(), 3); + QCOMPARE(selection.selectedShapes().count(), 3); delete group2; delete container; diff --git a/libs/flake/tests/TestShapeAt.cpp b/libs/flake/tests/TestShapeAt.cpp --- a/libs/flake/tests/TestShapeAt.cpp +++ b/libs/flake/tests/TestShapeAt.cpp @@ -113,7 +113,7 @@ shape.setSize(bbox.size()); QCOMPARE(shape.boundingRect(), bbox); - KoShapeStroke *stroke = new KoShapeStroke(); + KoShapeStrokeSP stroke(new KoShapeStroke()); stroke->setLineWidth(20); // which means the shape grows 10 in all directions. shape.setStroke(stroke); KoInsets strokeInsets; diff --git a/libs/flake/tests/TestShapeContainer.cpp b/libs/flake/tests/TestShapeContainer.cpp --- a/libs/flake/tests/TestShapeContainer.cpp +++ b/libs/flake/tests/TestShapeContainer.cpp @@ -142,23 +142,23 @@ QList oldPositions; for(int i=0; i< transformShapes.size(); i++) { - oldPositions.append(transformShapes.at(i)->absolutePosition(KoFlake::TopLeftCorner)); + oldPositions.append(transformShapes.at(i)->absolutePosition(KoFlake::TopLeft)); } KoShapeTransformCommand* transformCommand; transformCommand = new KoShapeTransformCommand(transformShapes, oldTransformations, newTransformations); transformCommand->redo(); for(int i=0; i< transformShapes.size(); i++) { - QCOMPARE(transformShapes.at(i)->absolutePosition(KoFlake::TopLeftCorner), oldPositions.at(i)*0.5); + QCOMPARE(transformShapes.at(i)->absolutePosition(KoFlake::TopLeft), oldPositions.at(i)*0.5); } transformShapes.takeLast(); KoShapeUngroupCommand* ungroupCmd = new KoShapeUngroupCommand(group, transformShapes); ungroupCmd->redo(); for(int i=0; i< transformShapes.size(); i++) { - QCOMPARE(transformShapes.at(i)->absolutePosition(KoFlake::TopLeftCorner), oldPositions.at(i)*0.5); + QCOMPARE(transformShapes.at(i)->absolutePosition(KoFlake::TopLeft), oldPositions.at(i)*0.5); } } @@ -182,7 +182,7 @@ groupCommand->redo(); KoSelection* selection = new KoSelection(); - selection->select(shape1, true); + selection->select(shape1); QList transformShapes; transformShapes.append(selection->selectedShapes()); @@ -200,19 +200,19 @@ QList oldPositions; for(int i=0; i< transformShapes.size(); i++) { - oldPositions.append(transformShapes.at(i)->absolutePosition(KoFlake::TopLeftCorner)); + oldPositions.append(transformShapes.at(i)->absolutePosition(KoFlake::TopLeft)); } KoShapeTransformCommand* transformCommand; transformCommand = new KoShapeTransformCommand(transformShapes, oldTransformations, newTransformations); transformCommand->redo(); - QRectF r1(shape1->absolutePosition(KoFlake::TopLeftCorner), shape1->absolutePosition(KoFlake::BottomRightCorner)); - QRectF r2(shape2->absolutePosition(KoFlake::TopLeftCorner), shape2->absolutePosition(KoFlake::BottomRightCorner)); + QRectF r1(shape1->absolutePosition(KoFlake::TopLeft), shape1->absolutePosition(KoFlake::BottomRight)); + QRectF r2(shape2->absolutePosition(KoFlake::TopLeft), shape2->absolutePosition(KoFlake::BottomRight)); QSizeF shapeSize=r1.united(r2).size(); selection = new KoSelection(); - selection->select(shape1, true); + selection->select(shape1); QSizeF selecSize = selection->size(); bool works=false; diff --git a/libs/flake/tests/TestShapeGroupCommand.cpp b/libs/flake/tests/TestShapeGroupCommand.cpp --- a/libs/flake/tests/TestShapeGroupCommand.cpp +++ b/libs/flake/tests/TestShapeGroupCommand.cpp @@ -25,6 +25,8 @@ #include #include +#include "kis_pointer_utils.h" + #include TestShapeGroupCommand::TestShapeGroupCommand() @@ -79,7 +81,7 @@ strokeShape2->setPosition( QPointF(25,25) ); strokeGroup = new KoShapeGroup(); - strokeGroup->setStroke( new KoShapeStroke( 2.0f ) ); + strokeGroup->setStroke( toQShared(new KoShapeStroke( 2.0f )) ); strokeGroup->setShadow( new KoShapeShadow() ); } @@ -143,19 +145,19 @@ cmd1->redo(); QCOMPARE(toplevelShape1->parent(), toplevelGroup); - QCOMPARE(toplevelShape1->absolutePosition(KoFlake::TopLeftCorner), QPointF(50, 50)); + QCOMPARE(toplevelShape1->absolutePosition(KoFlake::TopLeft), QPointF(50, 50)); QCOMPARE(toplevelShape1->position(), QPointF(0, 0)); QCOMPARE(toplevelShape2->parent(), toplevelGroup); - QCOMPARE(toplevelShape2->absolutePosition(KoFlake::TopLeftCorner), QPointF(50, 150)); + QCOMPARE(toplevelShape2->absolutePosition(KoFlake::TopLeft), QPointF(50, 150)); QCOMPARE(toplevelShape2->position(), QPointF(0, 100)); QCOMPARE(toplevelGroup->position(), QPointF(50, 50)); cmd1->undo(); QVERIFY(toplevelShape1->parent() == 0); - QCOMPARE(toplevelShape1->absolutePosition(KoFlake::TopLeftCorner), QPointF(50, 50)); + QCOMPARE(toplevelShape1->absolutePosition(KoFlake::TopLeft), QPointF(50, 50)); QCOMPARE(toplevelShape1->position(), QPointF(50, 50)); QVERIFY(toplevelShape2->parent() == 0); - QCOMPARE(toplevelShape2->absolutePosition(KoFlake::TopLeftCorner), QPointF(50, 150)); + QCOMPARE(toplevelShape2->absolutePosition(KoFlake::TopLeft), QPointF(50, 150)); QCOMPARE(toplevelShape2->position(), QPointF(50, 150)); } @@ -178,20 +180,20 @@ cmd1->redo(); QCOMPARE(toplevelShape1->parent(), toplevelGroup); - QCOMPARE(toplevelShape1->absolutePosition(KoFlake::TopLeftCorner), QPointF(50, 50)); + QCOMPARE(toplevelShape1->absolutePosition(KoFlake::TopLeft), QPointF(50, 50)); QCOMPARE(toplevelShape1->position(), QPointF(0, 0)); QCOMPARE(toplevelShape2->parent(), toplevelGroup); - QCOMPARE(toplevelShape2->absolutePosition(KoFlake::TopLeftCorner), QPointF(50, 150)); + QCOMPARE(toplevelShape2->absolutePosition(KoFlake::TopLeft), QPointF(50, 150)); QCOMPARE(toplevelShape2->position(), QPointF(0, 100)); QCOMPARE(toplevelGroup->position(), QPointF(50, 50)); QCOMPARE(sublevelShape1->parent(), sublevelGroup); - QCOMPARE(sublevelShape1->absolutePosition(KoFlake::TopLeftCorner), QPointF(150, 150)); + QCOMPARE(sublevelShape1->absolutePosition(KoFlake::TopLeft), QPointF(150, 150)); QCOMPARE(sublevelShape1->position(), QPointF(0, 0)); QCOMPARE(sublevelShape2->parent(), sublevelGroup); - QCOMPARE(sublevelShape2->absolutePosition(KoFlake::TopLeftCorner), QPointF(250, 150)); + QCOMPARE(sublevelShape2->absolutePosition(KoFlake::TopLeft), QPointF(250, 150)); QCOMPARE(sublevelShape2->position(), QPointF(100, 0)); - QCOMPARE(sublevelGroup->absolutePosition(KoFlake::TopLeftCorner), QPointF(150, 150)); + QCOMPARE(sublevelGroup->absolutePosition(KoFlake::TopLeft), QPointF(150, 150)); QCOMPARE(sublevelGroup->position(), QPointF(100, 100)); // check that the shapes are added in the correct order @@ -216,14 +218,14 @@ cmd2->redo(); QVERIFY(extraShape1->parent() == toplevelGroup); - QCOMPARE(extraShape1->absolutePosition(KoFlake::TopLeftCorner), QPointF(150, 50)); + QCOMPARE(extraShape1->absolutePosition(KoFlake::TopLeft), QPointF(150, 50)); QCOMPARE(extraShape1->position(), QPointF(100, 0)); QCOMPARE(toplevelGroup->position(), QPointF(50, 50)); cmd2->undo(); QVERIFY(extraShape1->parent() == 0); - QCOMPARE(extraShape1->absolutePosition(KoFlake::TopLeftCorner), QPointF(150, 50)); + QCOMPARE(extraShape1->absolutePosition(KoFlake::TopLeft), QPointF(150, 50)); QCOMPARE(extraShape1->position(), QPointF(150, 50)); QCOMPARE(toplevelGroup->position(), QPointF(50, 50)); } @@ -244,25 +246,25 @@ cmd2->redo(); QVERIFY(extraShape2->parent() == sublevelGroup); - QCOMPARE(extraShape2->absolutePosition(KoFlake::TopLeftCorner), QPointF(250, 50)); + QCOMPARE(extraShape2->absolutePosition(KoFlake::TopLeft), QPointF(250, 50)); QCOMPARE(extraShape2->position(), QPointF(100, 0)); - QCOMPARE(sublevelShape1->absolutePosition(KoFlake::TopLeftCorner), QPointF(150, 150)); + QCOMPARE(sublevelShape1->absolutePosition(KoFlake::TopLeft), QPointF(150, 150)); QCOMPARE(sublevelShape1->position(), QPointF(0, 100)); - QCOMPARE(sublevelShape2->absolutePosition(KoFlake::TopLeftCorner), QPointF(250, 150)); + QCOMPARE(sublevelShape2->absolutePosition(KoFlake::TopLeft), QPointF(250, 150)); QCOMPARE(sublevelShape2->position(), QPointF(100, 100)); - QCOMPARE(sublevelGroup->absolutePosition(KoFlake::TopLeftCorner), QPointF(150, 50)); + QCOMPARE(sublevelGroup->absolutePosition(KoFlake::TopLeft), QPointF(150, 50)); QCOMPARE(sublevelGroup->position(), QPointF(100, 0)); cmd2->undo(); QVERIFY(extraShape2->parent() == 0); - QCOMPARE(extraShape2->absolutePosition(KoFlake::TopLeftCorner), QPointF(250, 50)); + QCOMPARE(extraShape2->absolutePosition(KoFlake::TopLeft), QPointF(250, 50)); QCOMPARE(extraShape2->position(), QPointF(250, 50)); - QCOMPARE(sublevelShape1->absolutePosition(KoFlake::TopLeftCorner), QPointF(150, 150)); + QCOMPARE(sublevelShape1->absolutePosition(KoFlake::TopLeft), QPointF(150, 150)); QCOMPARE(sublevelShape1->position(), QPointF(0, 0)); - QCOMPARE(sublevelShape2->absolutePosition(KoFlake::TopLeftCorner), QPointF(250, 150)); + QCOMPARE(sublevelShape2->absolutePosition(KoFlake::TopLeft), QPointF(250, 150)); QCOMPARE(sublevelShape2->position(), QPointF(100, 0)); - QCOMPARE(sublevelGroup->absolutePosition(KoFlake::TopLeftCorner), QPointF(150, 150)); + QCOMPARE(sublevelGroup->absolutePosition(KoFlake::TopLeft), QPointF(150, 150)); QCOMPARE(sublevelGroup->position(), QPointF(100, 100)); } diff --git a/libs/flake/tests/TestShapePainting.h b/libs/flake/tests/TestShapePainting.h --- a/libs/flake/tests/TestShapePainting.h +++ b/libs/flake/tests/TestShapePainting.h @@ -30,6 +30,7 @@ void testPaintShape(); void testPaintHiddenShape(); void testPaintOrder(); + void testGroupUngroup(); }; #endif diff --git a/libs/flake/tests/TestShapePainting.cpp b/libs/flake/tests/TestShapePainting.cpp --- a/libs/flake/tests/TestShapePainting.cpp +++ b/libs/flake/tests/TestShapePainting.cpp @@ -252,5 +252,74 @@ delete bottom; delete root; } +#include +#include +#include +#include +#include "kis_debug.h" +void TestShapePainting::testGroupUngroup() +{ + MockShape *shape1 = new MockShape(); + MockShape *shape2 = new MockShape(); + shape1->setName("shape1"); + shape2->setName("shape2"); + + + QList groupedShapes = {shape1, shape2}; + + + MockShapeController controller; + MockCanvas canvas(&controller); + KoShapeManager *manager = canvas.shapeManager(); + + controller.addShape(shape1); + controller.addShape(shape2); + + QImage image(100, 100, QImage::Format_Mono); + QPainter painter(&image); + painter.setClipRect(image.rect()); + KoViewConverter vc; + + KoShapeGroup *group = 0; + + + for (int i = 0; i < 3; i++) { + { + group = new KoShapeGroup(); + group->setName("group"); + + KUndo2Command groupingCommand; + canvas.shapeController()->addShapeDirect(group, &groupingCommand); + new KoShapeGroupCommand(group, groupedShapes, false, true, true, &groupingCommand); + + groupingCommand.redo(); + + manager->paint(painter, vc, false); + + QCOMPARE(shape1->paintedCount, 2 * i + 1); + QCOMPARE(shape2->paintedCount, 2 * i + 1); + QCOMPARE(manager->shapes().size(), 3); + } + + { + KUndo2Command ungroupingCommand; + + new KoShapeUngroupCommand(group, group->shapes(), QList(), &ungroupingCommand); + canvas.shapeController()->removeShape(group, &ungroupingCommand); + + ungroupingCommand.redo(); + + manager->paint(painter, vc, false); + + QCOMPARE(shape1->paintedCount, 2 * i + 2); + QCOMPARE(shape2->paintedCount, 2 * i + 2); + QCOMPARE(manager->shapes().size(), 2); + + group = 0; + } + + } +} + QTEST_MAIN(TestShapePainting) diff --git a/libs/flake/tests/TestShapeReorderCommand.h b/libs/flake/tests/TestShapeReorderCommand.h --- a/libs/flake/tests/TestShapeReorderCommand.h +++ b/libs/flake/tests/TestShapeReorderCommand.h @@ -41,6 +41,8 @@ void testMoveDownOverlapping(); void testSendToBackChildren(); void testNoCommand(); + void testMergeInShape(); + void testMergeInShapeDistant(); }; #endif // TESTSHAPEREORDERCOMMAND_H diff --git a/libs/flake/tests/TestShapeReorderCommand.cpp b/libs/flake/tests/TestShapeReorderCommand.cpp --- a/libs/flake/tests/TestShapeReorderCommand.cpp +++ b/libs/flake/tests/TestShapeReorderCommand.cpp @@ -558,5 +558,51 @@ cmd = KoShapeReorderCommand::createCommand(selectedShapes, &manager, KoShapeReorderCommand::LowerShape); QVERIFY(cmd == 0); } +#include +#include +void testMergeInShapeImpl(const QVector indexesProfile, + int newShapeIndex, + const QVector expectedIndexes) +{ + KIS_ASSERT(indexesProfile.size() == expectedIndexes.size()); + + QVector shapesStore(indexesProfile.size()); + + QList managedShapes; + + for (int i = 0; i < shapesStore.size(); i++) { + shapesStore[i].setSize(QSizeF(100,100)); + shapesStore[i].setZIndex(indexesProfile[i]); + + managedShapes << &shapesStore[i]; + } + + QScopedPointer cmd( + KoShapeReorderCommand::mergeInShape(managedShapes, &shapesStore[newShapeIndex])); + cmd->redo(); + + for (int i = 0; i < shapesStore.size(); i++) { + //qDebug() << ppVar(i) << ppVar(shapesStore[i].zIndex()); + QCOMPARE(shapesStore[i].zIndex(), expectedIndexes[i]); + } +} + +void TestShapeReorderCommand::testMergeInShape() +{ + QVector indexesProfile({1,1,2,2,2,3,3,4,5,6}); + int newShapeIndex = 3; + QVector expectedIndexes({1,1,2,3,2,4,4,5,6,7}); + + testMergeInShapeImpl(indexesProfile, newShapeIndex, expectedIndexes); +} + +void TestShapeReorderCommand::testMergeInShapeDistant() +{ + QVector indexesProfile({1,1,2,2,2,4,4,5,6,7}); + int newShapeIndex = 3; + QVector expectedIndexes({1,1,2,3,2,4,4,5,6,7}); + + testMergeInShapeImpl(indexesProfile, newShapeIndex, expectedIndexes); +} QTEST_MAIN(TestShapeReorderCommand) diff --git a/libs/flake/tests/TestShapeStrokeCommand.cpp b/libs/flake/tests/TestShapeStrokeCommand.cpp --- a/libs/flake/tests/TestShapeStrokeCommand.cpp +++ b/libs/flake/tests/TestShapeStrokeCommand.cpp @@ -29,9 +29,9 @@ void TestShapeStrokeCommand::refCounting() { MockShape * shape1 = new MockShape(); - KoShapeStrokeModel *whiteStroke = new KoShapeStroke(1.0, QColor(Qt::white)); - KoShapeStrokeModel *blackStroke = new KoShapeStroke(1.0, QColor(Qt::black)); - KoShapeStrokeModel *redStroke = new KoShapeStroke(1.0, QColor(Qt::red)); + KoShapeStrokeModelSP whiteStroke(new KoShapeStroke(1.0, QColor(Qt::white))); + KoShapeStrokeModelSP blackStroke(new KoShapeStroke(1.0, QColor(Qt::black))); + KoShapeStrokeModelSP redStroke(new KoShapeStroke(1.0, QColor(Qt::red))); shape1->setStroke(whiteStroke); QVERIFY(shape1->stroke() == whiteStroke); @@ -63,10 +63,7 @@ whiteStroke->strokeInsets(shape1, insets); delete cmd2; - delete shape1; // This deletes whiteStroke - - delete blackStroke; - delete redStroke; + delete shape1; } QTEST_MAIN(TestShapeStrokeCommand) diff --git a/libs/flake/tests/TestSvgParser.h b/libs/flake/tests/TestSvgParser.h new file mode 100644 --- /dev/null +++ b/libs/flake/tests/TestSvgParser.h @@ -0,0 +1,173 @@ +/* + * 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 testMarkersFillAsShape(); + + void testGradientRecoveringTrasnform(); + void testMarkersOnClosedPath(); + void testMarkersAngularUnits(); + + void testSodipodiArcShape(); + void testSodipodiArcShapeOpen(); + void testKritaChordShape(); + void testSodipodiChordShape(); +}; + +#endif // TESTSVGPARSER_H diff --git a/libs/flake/tests/TestSvgParser.cpp b/libs/flake/tests/TestSvgParser.cpp new file mode 100644 --- /dev/null +++ b/libs/flake/tests/TestSvgParser.cpp @@ -0,0 +1,3386 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "kis_algebra_2d.h" + +struct SvgTester +{ + SvgTester (const QString &data) + : parser(&resourceManager) + { + QVERIFY(doc.setContent(data.toLatin1())); + root = doc.documentElement(); + + parser.setXmlBaseDir("./"); + + + savedData = data; + //printf("%s", savedData.toLatin1().data()); + + } + + ~SvgTester () + { + qDeleteAll(shapes); + } + + void run() { + shapes = parser.parseSvg(root, &fragmentSize); + } + + KoShape* findShape(const QString &name, KoShape *parent = 0) { + if (parent && parent->name() == name) { + return parent; + } + + QList children; + + if (!parent) { + children = shapes; + } else { + KoShapeContainer *cont = dynamic_cast(parent); + if (cont) { + children = cont->shapes(); + } + } + + Q_FOREACH (KoShape *shape, children) { + KoShape *result = findShape(name, shape); + if (result) { + return result; + } + } + + return 0; + } + + KoShapeGroup* findGroup(const QString &name) { + KoShapeGroup *group = 0; + KoShape *shape = findShape(name); + if (shape) { + group = dynamic_cast(shape); + } + return group; + } + + + + KoDocumentResourceManager resourceManager; + SvgParser parser; + KoXmlDocument doc; + KoXmlElement root; + QSizeF fragmentSize; + QList shapes; + QString savedData; +}; + +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 decendants! + */ + + 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)); +} + +#include "../../sdk/tests/qimage_test_util.h" + +#ifdef USE_ROUND_TRIP +#include "SvgWriter.h" +#include +#include +#endif + +struct SvgRenderTester : public SvgTester +{ + SvgRenderTester(const QString &data) + : SvgTester(data), + m_fuzzyThreshold(0) + { + } + + void setFuzzyThreshold(int fuzzyThreshold) { + m_fuzzyThreshold = fuzzyThreshold; + } + + static void testRender(KoShape *shape, const QString &prefix, const QString &testName, const QSize canvasSize, int fuzzyThreshold = 0) { + QImage canvas(canvasSize, QImage::Format_ARGB32); + canvas.fill(0); + KoViewConverter converter; + QPainter painter(&canvas); + + KoShapePainter p; + p.setShapes({shape}); + painter.setClipRect(canvas.rect()); + p.paint(painter, converter); + + QVERIFY(TestUtil::checkQImage(canvas, "svg_render", prefix, testName, fuzzyThreshold)); + } + + void test_standard_30px_72ppi(const QString &testName, bool verifyGeometry = true, const QSize &canvasSize = QSize(30,30)) { + parser.setResolution(QRectF(0, 0, 30, 30) /* px */, 72 /* ppi */); + run(); + +#ifdef USE_CLONED_SHAPES + { + QList newShapes; + Q_FOREACH (KoShape *shape, shapes) { + KoShape *clonedShape = shape->cloneShape(); + KIS_ASSERT(clonedShape); + + newShapes << clonedShape; + } + + qDeleteAll(shapes); + shapes = newShapes; + } + +#endif /* USE_CLONED_SHAPES */ + +#ifdef USE_ROUND_TRIP + + const QSizeF sizeInPt(30,30); + QBuffer writeBuf; + writeBuf.open(QIODevice::WriteOnly); + + { + SvgWriter writer(shapes, sizeInPt); + writer.save(writeBuf); + } + + QDomDocument prettyDoc; + prettyDoc.setContent(savedData); + + + qDebug(); + printf("\n=== Original: ===\n\n%s\n", prettyDoc.toByteArray(4).data()); + printf("\n=== Saved: ===\n\n%s\n", writeBuf.data().data()); + qDebug(); + + QVERIFY(doc.setContent(writeBuf.data())); + root = doc.documentElement(); + run(); +#endif /* USE_ROUND_TRIP */ + + KoShape *shape = findShape("testRect"); + KIS_ASSERT(shape); + + if (verifyGeometry) { + QCOMPARE(shape->absolutePosition(KoFlake::TopLeft), QPointF(5,5)); + + const QPointF bottomRight= shape->absolutePosition(KoFlake::BottomRight); + const QPointF expectedBottomRight(15,25); + + if (KisAlgebra2D::norm(bottomRight - expectedBottomRight) > 0.0001 ) { + QCOMPARE(bottomRight, expectedBottomRight); + } + } + + testRender(shape, "load", testName, canvasSize, m_fuzzyThreshold); + } + +private: + int m_fuzzyThreshold; +}; + +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 heirarchy!", 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.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, true, 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, true, 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.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() +{ + // 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); + + { + QBrush brush(gradient); + brush.setTransform(gradientTrasnform); + 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()); + 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/data/clip_mask/clip_mask_render_mask.png b/libs/flake/tests/data/clip_mask/clip_mask_render_mask.png new file mode 100644 index 0000000000000000000000000000000000000000..43be2e802178a86e813b27698c115936b1306b97 GIT binary patch literal 129 zc%17D@N?(olHy`uVBq!ia0vp^av;pX1|+Qw)-3{3oCO|{#S9F5M?jcysy3fAP|(=Z z#WBRA^X=J#oD2#)EC(|zU+x#XeNjwEz;Q;U{B}Wx{gQis#O=}ZT6g^ENl{O+&7cN}PB8p0r##W)!jZKg$6c=eKt-6_+OeTZ##k{Fia4G1r z;6fC^uZ90Ww=P_`5d=XIME`;)x{)%T`zAF_X~Fk+^A7jjbMD8y2NciycA$ziAPCc> zIbS!HS1ubP-!O(zlrWAN(^m0`(7S zlN^uiF4+bn)o>E0F+qH;7sbFL&mx2xAHy1Vi!nV~9n8`u+>~0gnB)w3BED@dm+B6u zB15u{$gVQC2mw9Mkn0ecWcXTXvoiHB;@SV!J5FSv#cVvD8@Rv<8{A`ZB6M^NsUSb~ zb(b6}E3Iu{l~`gV#3olm$1<6ThwGZz&~XBtnP|-Ex}|>>+2SWB751x3p0MY1oNJmr z&HQTIm*^AIGtvE%$O{9xNRKx=s(L-Ox8-6FWKz{?F8v0pQnXn7!*WAR;yi;%-3ONz zU+YNC(mM`$bKBpN>P%1NH>gwx_I%B8}WgE}(yZ<`u^P?xg%rDy4 z`?m69VE=94<(t0Ex(~d%104Dsn~AAS!(4?<2l(*<*tY^4cn^$T$=^xu9M3-ity{pt OJK(`X;HXROtA7D(&cQwa literal 0 Hc$@(GmG&HW{vG--`p=C| u2OK&t3Yuoym8Nd_Qn+;AWFszy{%F%QPIjhUCHz1$89ZJ6T-G@yGywp~J}?mg literal 0 Hc$@#yB&6VO-N70m43F^lm)_O7fA>W7uT9&dX7VeY@6$FJV3 xRJAb(Sag@eba%}wuN7YkJ@*Bxi7?oiGnkrkvilzHz7I5(!PC{xWt~$(698DuF=GG# literal 0 Hc$@b%Mr-TRyj0^1n-ud#Eka4W3^8pz=3>gTe~DWM4fYKky7 literal 0 Hc$@mdKI;Vst0AvA58UO$Q literal 0 Hc$@pwntta%dGi7cP8NpiO%y}CIRTmJB_%`*}7_RyOrsyRnc ztejIgN%~Mx_@<=nwI9wj+3e8>yDQ%2(9d7_`s#A+ReSQ@%h#JZ$bB~uVo>U1Y3NF4 mj1#P8v@x9XPSXE`uJpS3k}}oTG+zL%XYh3Ob6Mw<&;$V9BtGQ; literal 0 Hc$@Heb%6We;=!!(zg;jpy>>ru6{1-oD!M< D9P%>+ literal 0 Hc$@7 O4}+(xpUXO@geCxKUpH_7 literal 0 Hc$@CBH7KIxalBM32)m zZkr05gyPi>!FdI(EpK{f2rsQ&`TK9g+4>!f3sWTo?G)32j$&Z&boFyt=akR{0DDwL A+yDRo literal 0 Hc$@oTRdG9R2!nKs$Cj%`W*kry}1tr)T!XI&6_ zRd#>RKZhfRcg{!5mdKI;Vst0DaRk AzyJUM literal 0 Hc$@Kl)X;FFcgJ9JC2h&fi_(#G4gOc6f+a=z{0`7rKvHAPzQ5_uI;?I-&z~c6a>vUnL<&|5 zn7hAnehI#Rg10SP6;QTty@HE0FnGOzhvy^6B`}|P1M6?U+&xO&R`6cJwv@VS0)y9^ zuLNlPj7W#{Tq1Fq+t32S1nCr37kqn00000NkvXXu0mjfBW|Os literal 0 Hc$@Klsk^XFc3groH&~V!AfXZU{?sGK-`Rj>@lb*IR$s2 zgklvGJ_(^1D#TAD783Y1w#Obl+wvGJ7DG7af<-YZ?n-vXXY=ntJ(^gSr-{9*vy&Mt z66m*J(1Af0{Oc*@#eY+nFQuPuA>YHkfZLKXk(p_#)9QqKq;X}iE2c8S{K;*T+pco+jhCst= zBjZp*Ba#gm6)OjRo3PE~acK{{>dO9hT~t)Jr@Y^nX~kCjdSZ*k7ze?jD-u`iqXhr} N002ovPDHLkV1oaKfRX?J literal 0 Hc$@g(k$o4pA9N6|64^hNQ>BP{GAOnlfl!~&t;ucLK6V|ATs6v literal 0 Hc$@zopr07K6(QUCw| literal 0 Hc$@Vl^tIVx9Z9n$P3FJ57}N}()Tj$W5;L1i>anR6P|>2GpBvj dkYF`oW@wRRS3bAAqzh;*gQu&X%Q~loCIEt(K4Jg> literal 0 Hc$@EE=W`ukyCWwbnq389ZJ6T-G@yGywqo2SkYg literal 0 Hc$@$y$_IqpaT46=(*7r>mdKI;Vst02Nv!KL7v# literal 0 Hc$@3*0YTYX9JRobh_=^Se*| zPpsbCZ_VHAf2{tNq`-QNVS`?0HdF?0$!Ff`PLt^H&Cd!6sC*IQczR2y6>IW4c) zJ-YR>!3C(CiGyLo`BhWv)9!Eetlm3KU=hm%4SmJ-TkbK|vJ*5Ifo3o=e2d)r>HM4B zW|`}+3pq_;5PGWmB;a>P_Ht$+Rgf7B2kcgd{hL{PUG44HD5nk~hedl@WU}N7-O%Sx0dXU^X3R-@|ZGxg7aJDb#wVVfTpQ7;PfOaF!;=ts-+UW{`>7C OAg-sYpUXO@geCxe(tK|K literal 0 Hc$@Op^Uqi$7+2^zW zw_p8QxBs)j4{b)3SL%Nm|2ur`$X?DYq}t%Zslaex-pZ-}BX*muTz6f_X$phTQ`IMN egGrt^U~^vI^Sw~Cd|?I$i0kR<=d#Wzp$Pzy7kz60 literal 0 Hc$@O=u<5X=vX$A(y zcuyC{kcv5P?^^RUF^IT28oaAN9T6}iLgMlBA56y5A4KO}5O}wVfda7m>wWeQced$7 zf3$u*`#grevOob{63m1dmoVqz z{pkt!ew|kS{&vmt0}fafJ6$X0d-mqF$^1?8nAw%^J8vf=N3T%d8h4Qb5ZBYy&t;uc GLK6VDkAWHh literal 0 Hc$@O=u<5X=vX$A(y z5KkA!kcv5P?*?);8;CF*jM(#k>RV2&B`2hA&-`yAcj!_02?+)YfqZVc{s))i635@n zeETGNy1p-1c zEi5>hjL0^BV2KO)4rb1L=43BboFyt=akR{0IQ63djJ3c literal 0 Hc$@67u*{YdH6@F(>0oM|R^`dn4wz i91-%?*?2^0GgG9XkU8_gt-?T?89ZJ6T-G@yGywnxIzI9M literal 0 Hc$@QGFNZecCIpvw&4t@Vv>o%f$b4hg9OA_wmL5?UW4vO95`Rqn1O(^co~vC}@HEq+ua*yrQ_BOf#5 XjM(=Hh$XKATF2n&>gTe~DWM4ft8eB^^FVj7ZmBtjhSz_G-!Fb$>e(<7+)uzI57RKq0AYVHMaG?0#+1kmD|c3 Xa$B3Rscdy7(18q|u6{1-oD!M<#D7>i literal 0 Hc$@F8P7cSFU=dt{r#K4uWg$?iJt#`?CH)UmnTQQ3q5S}s7P|A`1E&v z-Y=*9zH}hS%W}`pw-2Nvc)!;cyB^N7u-GmA^1mgcyNl&}i>;>qK*un6y85}Sb4q9e E0Fj(hv;Y7A literal 0 Hc$@%k>P`yV&%W7QS#YTdbT<7#c*6+epqJP?y=$^Uaaxiw7J@sEuh(`r6bHTeY$ cU+hbn&lqZT%B*A%0y>ky)78&qol`;+02P{0g#Z8m literal 0 Hc$@N1R$Hbc4+)Bq2F@F~_8it6qL;9il#k z?uYhj+?F*JnjUj;r_$a<>g``5#Qd5cpPdxCzTfubKdUx z8!s=5U0ai?HaKpd6*=|Hx8Qe1zi)at-!nKMY8$Vd?yvlD{u{#&=hw;|nLEE_jmCrw j(@54>kEhI&9{pnI>a$XaIx%Y#&~pr)u6{1-oD!M<1d?>4 literal 0 Hc$@KmZ3`oK@`TncYC+P-USsD1cMlCHk-}l-yjHr!X=Bv zWVmE78{|Ym5EP7Nv)ODi2nIomMGrp1x_h&_d;8{?)v_N9JIl_z-W_goQ!l}E%hSfnpwgL%6`-mPXrE0Y)MTptU}o8=V9`?X>JWu5pO0Ay~p$~fr^ zy;@>Y&EgiuPt!^-4e6X`?AnDRamcB1@ocg9)CO_zenM@SEp@1QG+&-7%|L5T2j=0d zIzPQtF7~F>&~#40?c@ZmN~K`wy*i)#vQ$!m0XY2xNO-Cms}8m7JeC@)ta8cLL!n`r cyLQ@=FS@#1(+G%vng9R*07*qoM6N<$g75~wO8@`> literal 0 Hc$@9+juKn;r!+_tF_5E3%0kT#zuV;$z%-dnP t^>~ee%Y|(cKRwQP#V@+N@52Arth!6JdA``M`~`G1gQu&X%Q~loCID**U^)N* literal 0 Hc$@vFcb z?%DcZw{g}bl)rg1Mf5TcpQKO6&GnC~iXtc7sfc}`F0N#7_E%|)zn)mwp*s%_6uiD( h=(uyzek*5tzN9iKi_XxuNkEGkJYD@<);T3K0RY$jLn#0N literal 0 Hc$@-&SK*7aGyM$*8LId59$0h(o>th$9t1d>B0RwJ6it!HCm@WU3`5@wY2)Q l=>0w2C%68bZT#g4V^Nuu#hx9^5kR{cJYD@<);T3K0RSY6M(_Xt literal 0 Hc$@!A>s6 z(RXb^TXb{c`4il)0=wOHmq|+K8ogw5dz~!P@XL38paE9~8^fO^d`iL7g1mrsFnGH9 KxvX;M*p&Q0=xrEart9$OqGx#saKrfjB=(*ljAh%d|2l9o)_`RZu$%175H zw4Zj6S*QHN?00pH)Q@n-;^!jp>)+ktj7c)I$ztaD0e0st-pN09&k literal 0 Hc$@)#7WXJ)_T-M40*g8$o=c~9Kd$9GmD0r=SZYu% zaCGT{V-wzrRh`W<4%^uEyZd5&_+Ez_SEJ-AVpUeGjZ8YWJmi|f=N9#sp9(n?71W~j Wx#jv^D=Y+B$KdJe=d#Wzp$PzdZ9Gf> literal 0 Hc$@ct+=;g~X#m~9;OXk;vd$@?2>=;)OdtRN literal 0 Hc$@{F?hQAxvX#hw3lDN>tTLu7rPM7mfe9Msx1GtoG3SFL2t$*mO=eoC+v9JM%U z*u}|q=J;Z<{wSNQk1LLX89#K6#mfgTymyopD5bo9swLM8HipSx*~Y!{F)a K=d#Wzp$PzKCR+DKIf#oLDkL$Lo^awzF50_pM0~ zGx5H{!JTh(?LyCjw018Kqulehksh`W0%kaz4yk_@QSP&Kv%Am#!>-@##LQdoHy(b- z)F6^P_t@HhvkUe_$EIC#oP5>ny84pm_BG5KH!?79P}I=4JShR_7zR&QKbLh*2~7YF CJWX-{ literal 0 Hc$@Px-0ywpiG7Q(mF! zLR&5tg}R+`OTrdT{c=*@KZC7n#+ILIWEES-XxXXB*~>+3YweP7?&tP$)L8&M^=e|7b&jU6j*|2mW_aUo{srSuIy z=iSoxQP?!M%1HcNXvQj;`!OkRm*$J)HOEE-{QaHW(xdb5XpPL~Su$$ps}qj{9mU}3 L>gTe~DWM4f_j^}f literal 0 Hc$@Yva~g m^}SxhdTv%)VYBJZos3_miRw5lNw@{Ho59o7&t;ucLK6T)uub&< literal 0 Hc$@02pZIL-se?2{`zU%qy z9rC-QKCD}N?n$?lVbYNfLFF*Z`;Xbn!Vy&vzvyUOshvmG7%J3nsLgPhhau-#E8jY1Nunn_t&_yU80^ zBK3N5W%0w_`HSO{ADcz--CnzQ)!9J(S3Z4-f7Y)1+1eGDUd&T_{&n5Lq$`rL&F_EC ui(es88g^XZ=5DOcw%8G zvw-7!Y5BLmcH(SP7CQU-ZA*9}86a`tnbV&6#$B8NQWqY1)WrV$TrE`8TX{=@^Ibvv z+#?n%lAd-3tJNNH<9;rgQQpmFbUTjMBkZqeQ|M%7#smesx9T2or8;q51{oea@lrnkL_zAF%W3`E*7wr VNpAgybN)c>44$rjF6*2UngF9CB>MmW literal 0 Hc$@lD5vMt^S*P6kF(yj&AVFTAFHjJI4RP@RQP4c|GeUh%X9x# iyIj~D<-a3S?lHrvqg>l%rFuZFV(@hJb6Mw<&;$UMu}1I! literal 0 Hc$@)Xy9y1?Q1V2-BZblq(?rZ9lO1MA8<7tQ_?ck1eY-+wM; zX?L-y?fr{$%S?IxbjbFd<70kvhR;2{osof|p+ZY6Sw;BlYV*S&jh?Q4F6*2UngD+7 BH9`OY literal 0 Hc$@9GH7}c>e!aE|ip1M<@gH4iO;+2A$a~jIX~+T?Z;- N@O1TaS?83{1OUVB8;Jk_ literal 0 Hc$@{MS!Te8DS`bZurI(~h&E(X0#%XEHvASJ&tFtXj3r;B@iv zKL34BV(OD}(sF9P?K%G5OsBxA@$$=uB~=!454>dYTi$;8Wg-I@JSbyS*d%9^E*4k` O;(EIJxvX=gKs5nxZ+^RXz31aJv!~W&JsU6Y z{I=tx+j031Uw^$R*VieqYP|gNVM&$6+ygIJ{Fb+0ewoMs0u8tI<|xV;eL3o|5ybU$ L^>bP0l+XkKE#p8l literal 0 Hc$@Eal|F*7+O;m7~^jG_yGCWOCX{`b$PFB2%s;OXk;vd$@?2>^Bi7To{< literal 0 Hc$@O=u<5X=vX`rCE vr;B3<$IRrEgdhLsGx8@m8c0fR)MQ|&-NwYWqE9UXsD#1O)z4*}Q$iB}==m1; literal 0 Hc$@H2@^8NqkwX?2!_h(nb)Zz;5Mn;B3DmDDOz4KKIN;#`5p83BPY+U_$dFb2& a3^QZbP2@0jf9=!&GS1W0&t;ucLK6U^FhBGF literal 0 Hc$@)Sr$zD97_rc<#=_%>{}~KsAe=oQjt2@qYHSb~k&(+H;w^ v|4*u)w|he?qu;a3yXN}8E)xUU0t9kb8JRm+&S3j3^P6f4aTa()7BevL9RXp+soH$fKtT;p z7sn8ZsmTco%o1D-{{KI|uw`N+5Oj8_IWb&R+a!>vnVGl5j)5T}kA=6EwLlrDhr!d; K&t;ucLK6TQA0D0n literal 0 Hc$@ylDb9R1w_=bW7=NE|*#ab3;1LNF zt~?5`O%j�Y{azw9BenY@vfs_L-D;b^n85s22n7EWw S>$d_GGkCiCxvXMiCTc=_dF2@`Y4GXCYl7I*oM WN(dRbL>+MlndRx~=d#Wzp$PzWMnJ3p literal 0 Hc$@-4t3S(wY-V7nc*7!HETlJ0 Ra^rIl*VEO{Wt~$(698awJyQSx literal 0 Hc$@`lc9PR)B literal 0 Hc$@`lc9PR)B literal 0 Hc$@O=u<5X=vX$A&H zTTd6qkcv5P?-=qOa^PWbJpBJZPrOBtPTQF)nrl2(PoDE4je#I2tE#DPxc{-L_S+xj zy6v~0C#OC8`Ihm49}BzUK}U}Q0U?O=u<5X=vX$A(y zKu;IPkcv5P?;7$o8!)gqcKrB1eeNVaMQGc^aoH!Rw;M~8uQaDMwe+Xm(2lJvF0 z_nT^W7yq^kKhIFs)I8yVf{FzPC!Z58=7qBF4C@Svr!M_+ul8SofRIcJ3%lY$M-SXg z6np@{r>KAJ7H%|c!1U04bOit Xd2ZrqSL)H+2I70V`njxgN@xNA&@*r& literal 0 Hc$@O=u<5X=vX`rCJ zr;B5VN9WrsihK+P94rUp{{K(c+92a8+7at{$x|R?x$dHQ-tUgSJ@}u2kbP0l+XkKC50_2 literal 0 Hc$@O=u<5X=vX$A(y zKu;IPkcv5PZ!vNmHsE1CQ1<43{JVVuCjw+kzBdH&w`@F7%E&+noGID=m$CBv=ll0} zxBqX?{e13Q_}WXYmHX>}=87&*u7OGC4A_gHYV8oMPpT=h}eEnF;zD8%UTWf9Hrw^f9>??jWwG LtDnm{r-UW|@(F<} literal 0 Hc$@O=u<5X=vX$A(y z7*7|+kcv5P?;7$oF$lOin%?_=Dk2~vA|pUvddAL%=pRnbKc;gtkPkNOdRNQvUdQ{h z_4VHWtl!t#?SB95y>Wy3K}U}Q0U?>skD@1IyP=eM1|1qUaeQ&aPV z2MQ|4Oqj_Hg>CimO_{It*WcN`X&&=q4s41!BF|gSDPDiM=6n^OOdGl+h{+5yPa^%t zchwo&{(U-CtNq&2pbwkk<_TZt9Jc&!b@uz3=LZt8+K6zT+_xN|zM@$Y1|Y7dtDnm{ Hr-UW|NSJ;w literal 0 Hc$@O=u<5X=vX`rB) zr;B5VM`v=1g4Cb?{&TF{+>$~J1@jF{53V@6SnM*7z|=2_4L9Dn$VgN=+Gc&!kYHV$ b@Pd&+zLZ70(C_sbpa~3~u6{1-oD!MO=u<5X=vX`rB) zr;B5VM`v=1g4Cb?{&TF{+>$~J1@jF{53V@6SnM*7z|=2_4L9Dn$VgN=+Gc&!kYHV$ b@Pd&+zLZ70(C_sbpa~3~u6{1-oD!M + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/libs/flake/tests/data/test_svg_file.svg b/libs/flake/tests/data/test_svg_file.svg new file mode 100644 --- /dev/null +++ b/libs/flake/tests/data/test_svg_file.svg @@ -0,0 +1,76 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/libs/flake/tools/KoInteractionStrategy.h b/libs/flake/tools/KoInteractionStrategy.h --- a/libs/flake/tools/KoInteractionStrategy.h +++ b/libs/flake/tools/KoInteractionStrategy.h @@ -69,13 +69,6 @@ virtual void handleMouseMove(const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers) = 0; /** - * Extending classes should implement this method to update the selectedShapes - * based on the new pointer event. The default implementations does nothing. - * @param event the new pointer event - */ - virtual void handleCustomEvent(KoPointerEvent *event); - - /** * For interactions that are undo-able this method should be implemented to return such * a command. Implementations should return 0 otherwise. * @return a command, or 0. diff --git a/libs/flake/tools/KoInteractionStrategy.cpp b/libs/flake/tools/KoInteractionStrategy.cpp --- a/libs/flake/tools/KoInteractionStrategy.cpp +++ b/libs/flake/tools/KoInteractionStrategy.cpp @@ -51,11 +51,6 @@ delete d_ptr; } -void KoInteractionStrategy::handleCustomEvent(KoPointerEvent *event) -{ - Q_UNUSED(event); -} - void KoInteractionStrategy::paint(QPainter &, const KoViewConverter &) { } diff --git a/libs/flake/tools/KoInteractionStrategyFactory.h b/libs/flake/tools/KoInteractionStrategyFactory.h new file mode 100644 --- /dev/null +++ b/libs/flake/tools/KoInteractionStrategyFactory.h @@ -0,0 +1,58 @@ +/* + * 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 KOINTERACTIONSTRATEGYFACTORY_H +#define KOINTERACTIONSTRATEGYFACTORY_H + +#include +#include +#include "kritaflake_export.h" + +class QString; +class QPainter; +class KoInteractionStrategy; +class KoPointerEvent; +class KoViewConverter; + +class KoInteractionStrategyFactory; +typedef QSharedPointer KoInteractionStrategyFactorySP; + +class KRITAFLAKE_EXPORT KoInteractionStrategyFactory +{ +public: + KoInteractionStrategyFactory(int priority, const QString &id); + virtual ~KoInteractionStrategyFactory(); + + QString id() const; + int priority() const; + + virtual KoInteractionStrategy* createStrategy(KoPointerEvent *ev) = 0; + virtual bool hoverEvent(KoPointerEvent *ev) = 0; + virtual bool paintOnHover(QPainter &painter, const KoViewConverter &converter) = 0; + virtual bool tryUseCustomCursor() = 0; + + static bool compareLess(KoInteractionStrategyFactorySP f1, KoInteractionStrategyFactorySP f2); + +private: + struct Private; + QScopedPointer m_d; +}; + + + +#endif // KOINTERACTIONSTRATEGYFACTORY_H diff --git a/libs/flake/tools/KoInteractionStrategyFactory.cpp b/libs/flake/tools/KoInteractionStrategyFactory.cpp new file mode 100644 --- /dev/null +++ b/libs/flake/tools/KoInteractionStrategyFactory.cpp @@ -0,0 +1,54 @@ +/* + * 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 "KoInteractionStrategyFactory.h" + +#include + +struct KoInteractionStrategyFactory::Private +{ + int priority = 0; + QString id; +}; + +KoInteractionStrategyFactory::KoInteractionStrategyFactory(int priority, const QString &id) + : m_d(new Private) +{ + m_d->priority = priority; + m_d->id = id; +} + +KoInteractionStrategyFactory::~KoInteractionStrategyFactory() +{ +} + +QString KoInteractionStrategyFactory::id() const +{ + return m_d->id; +} + +int KoInteractionStrategyFactory::priority() const +{ + return m_d->priority; +} + +bool KoInteractionStrategyFactory::compareLess(KoInteractionStrategyFactorySP f1, KoInteractionStrategyFactorySP f2) +{ + return f1->priority() < f2->priority(); +} + diff --git a/libs/flake/tools/KoInteractionTool.h b/libs/flake/tools/KoInteractionTool.h --- a/libs/flake/tools/KoInteractionTool.h +++ b/libs/flake/tools/KoInteractionTool.h @@ -26,6 +26,7 @@ #include "kritaflake_export.h" class KoInteractionStrategy; +class KoInteractionStrategyFactory; class KoInteractionToolPrivate; #define KoInteractionTool_ID "InteractionTool" @@ -86,8 +87,15 @@ * Reimplement this factory method to create your strategy to be used for mouse interaction. * @returns a new strategy, or 0 when there is nothing to do. */ + KoInteractionStrategy *createStrategyBase(KoPointerEvent *event); virtual KoInteractionStrategy *createStrategy(KoPointerEvent *event) = 0; + void addInteractionFactory(KoInteractionStrategyFactory *factory); + void removeInteractionFactory(const QString &id); + bool hasInteractioFactory(const QString &id); + + bool tryUseCustomCursor(); + private: KoInteractionTool(const KoInteractionTool&); KoInteractionTool& operator=(const KoInteractionTool&); diff --git a/libs/flake/tools/KoInteractionTool.cpp b/libs/flake/tools/KoInteractionTool.cpp --- a/libs/flake/tools/KoInteractionTool.cpp +++ b/libs/flake/tools/KoInteractionTool.cpp @@ -23,7 +23,10 @@ #include "KoToolBase_p.h" #include "KoPointerEvent.h" #include "KoCanvasBase.h" -#include "KoPanTool.h" + +#include "kis_global.h" +#include "kis_assert.h" + KoInteractionTool::KoInteractionTool(KoCanvasBase *canvas) : KoToolBase(*(new KoInteractionToolPrivate(this, canvas))) @@ -37,8 +40,15 @@ void KoInteractionTool::paint(QPainter &painter, const KoViewConverter &converter) { Q_D(KoInteractionTool); - if (d->currentStrategy) + + if (d->currentStrategy) { d->currentStrategy->paint(painter, converter); + } else { + Q_FOREACH (KoInteractionStrategyFactorySP factory, d->interactionFactories) { + // skip the rest of rendering if the factory asks for it + if (factory->paintOnHover(painter, converter)) break; + } + } } void KoInteractionTool::mousePressEvent(KoPointerEvent *event) @@ -48,19 +58,26 @@ cancelCurrentStrategy(); return; } - d->currentStrategy = createStrategy(event); + d->currentStrategy = createStrategyBase(event); if (d->currentStrategy == 0) event->ignore(); } void KoInteractionTool::mouseMoveEvent(KoPointerEvent *event) { Q_D(KoInteractionTool); d->lastPoint = event->point; + if (d->currentStrategy) d->currentStrategy->handleMouseMove(d->lastPoint, event->modifiers()); - else + else { + Q_FOREACH (KoInteractionStrategyFactorySP factory, d->interactionFactories) { + // skip the rest of rendering if the factory asks for it + if (factory->hoverEvent(event)) return; + } + event->ignore(); + } } void KoInteractionTool::mouseReleaseEvent(KoPointerEvent *event) @@ -94,10 +111,13 @@ void KoInteractionTool::keyReleaseEvent(QKeyEvent *event) { Q_D(KoInteractionTool); - if (d->currentStrategy == 0) { // catch all cases where no current strategy is needed - if (event->key() == Qt::Key_Space) - emit activateTemporary(KoPanTool_ID); - } else if (event->key() == Qt::Key_Escape) { + + if (!d->currentStrategy) { + KoToolBase::keyReleaseEvent(event); + return; + } + + if (event->key() == Qt::Key_Escape) { cancelCurrentStrategy(); event->accept(); } else if (event->key() == Qt::Key_Control || @@ -123,6 +143,75 @@ } } +KoInteractionStrategy *KoInteractionTool::createStrategyBase(KoPointerEvent *event) +{ + Q_D(KoInteractionTool); + + Q_FOREACH (KoInteractionStrategyFactorySP factory, d->interactionFactories) { + KoInteractionStrategy *strategy = factory->createStrategy(event); + if (strategy) { + return strategy; + } + } + + return createStrategy(event); +} + +void KoInteractionTool::addInteractionFactory(KoInteractionStrategyFactory *factory) +{ + Q_D(KoInteractionTool); + + Q_FOREACH (auto f, d->interactionFactories) { + KIS_SAFE_ASSERT_RECOVER_RETURN(f->id() != factory->id()); + } + + d->interactionFactories.append(toQShared(factory)); + qSort(d->interactionFactories.begin(), + d->interactionFactories.end(), + KoInteractionStrategyFactory::compareLess); +} + +void KoInteractionTool::removeInteractionFactory(const QString &id) +{ + Q_D(KoInteractionTool); + QList::iterator it = + d->interactionFactories.begin(); + + while (it != d->interactionFactories.end()) { + if ((*it)->id() == id) { + it = d->interactionFactories.erase(it); + } else { + ++it; + } + } +} + +bool KoInteractionTool::hasInteractioFactory(const QString &id) +{ + Q_D(KoInteractionTool); + + Q_FOREACH (auto f, d->interactionFactories) { + if (f->id() == id) { + return true; + } + } + + return false; +} + +bool KoInteractionTool::tryUseCustomCursor() +{ + Q_D(KoInteractionTool); + + Q_FOREACH (auto f, d->interactionFactories) { + if (f->tryUseCustomCursor()) { + return true; + } + } + + return false; +} + KoInteractionTool::KoInteractionTool(KoInteractionToolPrivate &dd) : KoToolBase(dd) { diff --git a/libs/flake/tools/KoInteractionTool_p.h b/libs/flake/tools/KoInteractionTool_p.h --- a/libs/flake/tools/KoInteractionTool_p.h +++ b/libs/flake/tools/KoInteractionTool_p.h @@ -23,6 +23,7 @@ #include "KoToolBase_p.h" #include "KoInteractionStrategy.h" +#include "KoInteractionStrategyFactory.h" class KoInteractionToolPrivate : public KoToolBasePrivate { @@ -39,6 +40,7 @@ QPointF lastPoint; KoInteractionStrategy *currentStrategy; + QList> interactionFactories; }; #endif diff --git a/libs/flake/tools/KoPanTool.h b/libs/flake/tools/KoPanTool.h --- a/libs/flake/tools/KoPanTool.h +++ b/libs/flake/tools/KoPanTool.h @@ -55,8 +55,6 @@ /// reimplemented from superclass virtual void activate(ToolActivation toolActivation, const QSet &shapes); /// reimplemented method - virtual void customMoveEvent(KoPointerEvent *event); - /// reimplemented method virtual void mouseDoubleClickEvent(KoPointerEvent *event); @@ -69,7 +67,6 @@ QPointF documentToViewport(const QPointF &p); KoCanvasController *m_controller; QPointF m_lastPosition; - bool m_temporary; Q_DECLARE_PRIVATE(KoToolBase) }; diff --git a/libs/flake/tools/KoPanTool.cpp b/libs/flake/tools/KoPanTool.cpp --- a/libs/flake/tools/KoPanTool.cpp +++ b/libs/flake/tools/KoPanTool.cpp @@ -31,10 +31,12 @@ #include #include +#include "kis_assert.h" + + KoPanTool::KoPanTool(KoCanvasBase *canvas) : KoToolBase(canvas), - m_controller(0), - m_temporary(false) + m_controller(0) { } @@ -68,8 +70,6 @@ { event->accept(); useCursor(QCursor(Qt::OpenHandCursor)); - if (m_temporary) - emit done(); } void KoPanTool::keyPressEvent(QKeyEvent *event) @@ -98,18 +98,10 @@ void KoPanTool::activate(ToolActivation toolActivation, const QSet &) { - if (m_controller == 0) { - emit done(); - return; - } - m_temporary = toolActivation == TemporaryActivation; - useCursor(QCursor(Qt::OpenHandCursor)); -} + Q_UNUSED(toolActivation); + KIS_ASSERT_RECOVER_NOOP(m_controller); -void KoPanTool::customMoveEvent(KoPointerEvent * event) -{ - m_controller->pan(QPoint(-event->x(), -event->y())); - event->accept(); + useCursor(QCursor(Qt::OpenHandCursor)); } QPointF KoPanTool::documentToViewport(const QPointF &p) diff --git a/libs/flake/tools/KoParameterChangeStrategy.cpp b/libs/flake/tools/KoParameterChangeStrategy.cpp --- a/libs/flake/tools/KoParameterChangeStrategy.cpp +++ b/libs/flake/tools/KoParameterChangeStrategy.cpp @@ -22,9 +22,15 @@ #include "KoParameterShape.h" #include "commands/KoParameterHandleMoveCommand.h" +#include +#include "KoSnapGuide.h" + + KoParameterChangeStrategy::KoParameterChangeStrategy(KoToolBase *tool, KoParameterShape *parameterShape, int handleId) : KoInteractionStrategy(*(new KoParameterChangeStrategyPrivate(tool, parameterShape, handleId))) { + Q_D(KoParameterChangeStrategy); + d->tool->canvas()->snapGuide()->setIgnoredShapes({parameterShape}); } KoParameterChangeStrategy::KoParameterChangeStrategy(KoParameterChangeStrategyPrivate& dd) @@ -40,14 +46,22 @@ void KoParameterChangeStrategy::handleMouseMove(const QPointF &mouseLocation, Qt::KeyboardModifiers modifiers) { Q_D(KoParameterChangeStrategy); - d->parameterShape->moveHandle(d->handleId, mouseLocation, modifiers); + + d->tool->canvas()->updateCanvas(d->tool->canvas()->snapGuide()->boundingRect()); + QPointF snappedPosition = d->tool->canvas()->snapGuide()->snap(mouseLocation, modifiers); + d->tool->canvas()->updateCanvas(d->tool->canvas()->snapGuide()->boundingRect()); + + d->parameterShape->moveHandle(d->handleId, snappedPosition, modifiers); d->lastModifierUsed = modifiers; - d->releasePoint = mouseLocation; + d->releasePoint = snappedPosition; } KUndo2Command* KoParameterChangeStrategy::createCommand() { Q_D(KoParameterChangeStrategy); + + d->tool->canvas()->snapGuide()->reset(); + KoParameterHandleMoveCommand *cmd = 0; // check if handle position changed if (d->startPoint != QPointF(0, 0) && d->startPoint != d->releasePoint) { diff --git a/libs/flake/tools/KoPathPointRubberSelectStrategy.h b/libs/flake/tools/KoPathPointRubberSelectStrategy.h --- a/libs/flake/tools/KoPathPointRubberSelectStrategy.h +++ b/libs/flake/tools/KoPathPointRubberSelectStrategy.h @@ -33,7 +33,9 @@ public: KoPathPointRubberSelectStrategy(KoPathTool *tool, const QPointF &clicked); virtual ~KoPathPointRubberSelectStrategy() {} - virtual void finishInteraction(Qt::KeyboardModifiers modifiers); + + void handleMouseMove(const QPointF &p, Qt::KeyboardModifiers modifiers) override; + void finishInteraction(Qt::KeyboardModifiers modifiers) override; private: /// pointer to the path tool diff --git a/libs/flake/tools/KoPathPointRubberSelectStrategy.cpp b/libs/flake/tools/KoPathPointRubberSelectStrategy.cpp --- a/libs/flake/tools/KoPathPointRubberSelectStrategy.cpp +++ b/libs/flake/tools/KoPathPointRubberSelectStrategy.cpp @@ -31,14 +31,25 @@ { } +void KoPathPointRubberSelectStrategy::handleMouseMove(const QPointF &p, Qt::KeyboardModifiers modifiers) +{ + KoPathToolSelection * selection = dynamic_cast(m_tool->selection()); + if (selection && !(modifiers & Qt::ShiftModifier)) { + selection->clear(); + } + + KoShapeRubberSelectStrategy::handleMouseMove(p, modifiers); +} + void KoPathPointRubberSelectStrategy::finishInteraction(Qt::KeyboardModifiers modifiers) { Q_D(KoShapeRubberSelectStrategy); KoPathToolSelection * selection = dynamic_cast(m_tool->selection()); - if (! selection) + if (!selection) { return; + } - selection->selectPoints(d->selectedRect(), !(modifiers & Qt::ControlModifier)); + selection->selectPoints(d->selectedRect(), !(modifiers & Qt::ShiftModifier)); m_tool->canvas()->updateCanvas(d->selectedRect().normalized()); selection->repaint(); } diff --git a/libs/flake/tools/KoPathSegmentChangeStrategy.cpp b/libs/flake/tools/KoPathSegmentChangeStrategy.cpp --- a/libs/flake/tools/KoPathSegmentChangeStrategy.cpp +++ b/libs/flake/tools/KoPathSegmentChangeStrategy.cpp @@ -128,7 +128,7 @@ m_ctrlPoint2Move += move2; // save last mouse position - m_lastPosition = mouseLocation; + m_lastPosition = snappedPosition; } void KoPathSegmentChangeStrategy::finishInteraction(Qt::KeyboardModifiers modifiers) diff --git a/libs/flake/tools/KoPathTool.h b/libs/flake/tools/KoPathTool.h --- a/libs/flake/tools/KoPathTool.h +++ b/libs/flake/tools/KoPathTool.h @@ -26,16 +26,20 @@ #include "KoPathShape.h" #include "KoToolBase.h" #include "KoPathToolSelection.h" +#include "kis_signal_auto_connection.h" #include #include class QButtonGroup; class KoCanvasBase; class KoInteractionStrategy; class KoPathToolHandle; class KoParameterShape; +class KUndo2Command; class QAction; +class QMenu; + /// The tool for editing a KoPathShape or a KoParameterShape. /// See KoCreatePathTool for code handling the initial path creation. @@ -46,44 +50,35 @@ explicit KoPathTool(KoCanvasBase *canvas); ~KoPathTool(); - /// reimplemented - virtual void paint(QPainter &painter, const KoViewConverter &converter); - - /// reimplemented - virtual void repaintDecorations(); - - /// reimplemented - virtual void mousePressEvent(KoPointerEvent *event); - /// reimplemented - virtual void mouseMoveEvent(KoPointerEvent *event); - /// reimplemented - virtual void mouseReleaseEvent(KoPointerEvent *event); - /// reimplemented - virtual void keyPressEvent(QKeyEvent *event); - /// reimplemented - virtual void keyReleaseEvent(QKeyEvent *event); - /// reimplemented - virtual void mouseDoubleClickEvent(KoPointerEvent *event); - /// reimplemented - virtual void activate(ToolActivation toolActivation, const QSet &shapes); - /// reimplemented - virtual void deactivate(); - - /// reimplemented - virtual void deleteSelection(); - - /// reimplemented - virtual KoToolSelection* selection(); + void paint(QPainter &painter, const KoViewConverter &converter) override; + void repaintDecorations() override; + void mousePressEvent(KoPointerEvent *event) override; + void mouseMoveEvent(KoPointerEvent *event) override; + void mouseReleaseEvent(KoPointerEvent *event) override; + void keyPressEvent(QKeyEvent *event) override; + void keyReleaseEvent(QKeyEvent *event) override; + void mouseDoubleClickEvent(KoPointerEvent *event) override; + void activate(ToolActivation activation, const QSet &shapes) override; + void deactivate() override; + void deleteSelection() override; + KoToolSelection* selection() override; + void requestUndoDuringStroke(); + void requestStrokeCancellation() override; + void requestStrokeEnd() override; + void explicitUserStrokeEndRequest() override; /// repaints the specified rect void repaint(const QRectF &repaintRect); + QMenu* popupActionsMenu() override; + public Q_SLOTS: void documentResourceChanged(int key, const QVariant & res); Q_SIGNALS: void typeChanged(int types); - void pathChanged(KoPathShape* path); // TODO this is unused, can we remove this one? + void singleShapeChanged(KoPathShape* path); + protected: /// reimplemented virtual QList > createOptionWidgets(); @@ -109,14 +104,20 @@ void updateActions(); void pointToLine(); void pointToCurve(); - void activate(); + void slotSelectionChanged(); + +private: + void clearActivePointSelectionReferences(); + void initializeWithShapes(const QList shapes); + KUndo2Command* createPointToCurveCommand(const QList &points); + void repaintSegment(PathSegment *pathSegment); + void mergePointsImpl(bool doJoin); protected: KoPathToolSelection m_pointSelection; ///< the point selection QCursor m_selectCursor; private: - KoPathToolHandle * m_activeHandle; ///< the currently active handle int m_handleRadius; ///< the radius of the control point handles uint m_grabSensitivity; ///< the grab sensitivity @@ -145,6 +146,9 @@ QAction *m_actionMergePoints; QAction *m_actionConvertToPath; QCursor m_moveCursor; + bool m_activatedTemporarily; + QScopedPointer m_contextMenu; + KisSignalAutoConnectionsStore m_canvasConnections; Q_DECLARE_PRIVATE(KoToolBase) }; diff --git a/libs/flake/tools/KoPathTool.cpp b/libs/flake/tools/KoPathTool.cpp --- a/libs/flake/tools/KoPathTool.cpp +++ b/libs/flake/tools/KoPathTool.cpp @@ -26,6 +26,7 @@ #include "KoPathToolHandle.h" #include "KoCanvasBase.h" #include "KoShapeManager.h" +#include "KoSelectedShapesProxy.h" #include "KoDocumentResourceManager.h" #include "KoViewConverter.h" #include "KoSelection.h" @@ -38,7 +39,8 @@ #include "commands/KoPathSegmentBreakCommand.h" #include "commands/KoParameterToPathCommand.h" #include "commands/KoSubpathJoinCommand.h" -#include "commands/KoPathPointMergeCommand.h" +#include +#include #include "KoParameterShape.h" #include "KoPathPoint.h" #include "KoPathPointRubberSelectStrategy.h" @@ -50,9 +52,14 @@ #include "KoSnapGuide.h" #include "KoShapeController.h" #include "kis_action_registry.h" +#include +#include +#include "kis_command_utils.h" + #include +#include #include #include #include @@ -104,6 +111,7 @@ , m_handleRadius(3) , m_activeSegment(0) , m_currentStrategy(0) + , m_activatedTemporarily(false) { QActionGroup *points = new QActionGroup(this); // m_pointTypeGroup->setExclusive(true); @@ -167,6 +175,8 @@ addAction("convert-to-path", m_actionConvertToPath); connect(m_actionConvertToPath, SIGNAL(triggered()), this, SLOT(convertToPath())); + m_contextMenu.reset(new QMenu()); + connect(points, SIGNAL(triggered(QAction*)), this, SLOT(pointTypeChanged(QAction*))); connect(&m_pointSelection, SIGNAL(selectionChanged()), this, SLOT(pointSelectionChanged())); @@ -195,8 +205,10 @@ PathToolOptionWidget * toolOptions = new PathToolOptionWidget(this); connect(this, SIGNAL(typeChanged(int)), toolOptions, SLOT(setSelectionType(int))); + connect(this, SIGNAL(singleShapeChanged(KoPathShape*)), toolOptions, SLOT(setCurrentShape(KoPathShape*))); + connect(toolOptions, SIGNAL(sigRequestUpdateActions()), this, SLOT(updateActions())); updateOptionsWidget(); - toolOptions->setWindowTitle(i18n("Line/Curve")); + toolOptions->setWindowTitle(i18n("Edit Shape")); list.append(toolOptions); return list; @@ -207,40 +219,50 @@ Q_D(KoToolBase); if (m_pointSelection.hasSelection()) { QList selectedPoints = m_pointSelection.selectedPointsData(); - QList pointToChange; - QList::const_iterator it(selectedPoints.constBegin()); - for (; it != selectedPoints.constEnd(); ++it) { - KoPathPoint *point = it->pathShape->pointByIndex(it->pointIndex); - if (point) { - if (point->activeControlPoint1() && point->activeControlPoint2()) { - pointToChange.append(*it); - } - } + KUndo2Command *initialConversionCommand = createPointToCurveCommand(selectedPoints); + + // conversion should happen before the c-tor + // of KoPathPointTypeCommand is executed! + if (initialConversionCommand) { + initialConversionCommand->redo(); } - if (!pointToChange.isEmpty()) { - KoPathPointTypeCommand *cmd = new KoPathPointTypeCommand(pointToChange, - static_cast(type->data().toInt())); - d->canvas->addCommand(cmd); - updateActions(); + KUndo2Command *command = + new KoPathPointTypeCommand(selectedPoints, + static_cast(type->data().toInt())); + + if (initialConversionCommand) { + using namespace KisCommandUtils; + CompositeCommand *parent = new CompositeCommand(); + parent->setText(command->text()); + parent->addCommand(new SkipFirstRedoWrapper(initialConversionCommand)); + parent->addCommand(command); + command = parent; } + + d->canvas->addCommand(command); } } void KoPathTool::insertPoints() { Q_D(KoToolBase); - if (m_pointSelection.size() > 1) { - QList segments(m_pointSelection.selectedSegmentsData()); - if (!segments.isEmpty()) { - KoPathPointInsertCommand *cmd = new KoPathPointInsertCommand(segments, 0.5); - d->canvas->addCommand(cmd); + QList segments(m_pointSelection.selectedSegmentsData()); + if (segments.size() == 1) { + qreal positionInSegment = 0.5; + if (m_activeSegment && m_activeSegment->isValid()) { + positionInSegment = m_activeSegment->positionOnSegment; + } - foreach (KoPathPoint * p, cmd->insertedPoints()) { - m_pointSelection.add(p, false); - } - updateActions(); + KoPathPointInsertCommand *cmd = new KoPathPointInsertCommand(segments, positionInSegment); + d->canvas->addCommand(cmd); + + // TODO: this construction is dangerous. The canvas can remove the command right after + // it has been added to it! + m_pointSelection.clear(); + foreach (KoPathPoint * p, cmd->insertedPoints()) { + m_pointSelection.add(p, false); } } } @@ -255,7 +277,7 @@ delete m_activeHandle; m_activeHandle = 0; } - m_pointSelection.clear(); + clearActivePointSelectionReferences(); d->canvas->addCommand(cmd); } } @@ -276,7 +298,6 @@ if (! pointToChange.isEmpty()) { d->canvas->addCommand(new KoPathPointTypeCommand(pointToChange, KoPathPointTypeCommand::Line)); - updateActions(); } } } @@ -286,30 +307,41 @@ Q_D(KoToolBase); if (m_pointSelection.hasSelection()) { QList selectedPoints = m_pointSelection.selectedPointsData(); - QList pointToChange; - QList::const_iterator it(selectedPoints.constBegin()); - for (; it != selectedPoints.constEnd(); ++it) { - KoPathPoint *point = it->pathShape->pointByIndex(it->pointIndex); - if (point && (! point->activeControlPoint1() || ! point->activeControlPoint2())) - pointToChange.append(*it); - } + KUndo2Command *command = createPointToCurveCommand(selectedPoints); - if (! pointToChange.isEmpty()) { - d->canvas->addCommand(new KoPathPointTypeCommand(pointToChange, KoPathPointTypeCommand::Curve)); - updateActions(); + if (command) { + d->canvas->addCommand(command); } } } +KUndo2Command* KoPathTool::createPointToCurveCommand(const QList &points) +{ + KUndo2Command *command = 0; + QList pointToChange; + + QList::const_iterator it(points.constBegin()); + for (; it != points.constEnd(); ++it) { + KoPathPoint *point = it->pathShape->pointByIndex(it->pointIndex); + if (point && (! point->activeControlPoint1() || ! point->activeControlPoint2())) + pointToChange.append(*it); + } + + if (!pointToChange.isEmpty()) { + command = new KoPathPointTypeCommand(pointToChange, KoPathPointTypeCommand::Curve); + } + + return command; +} + void KoPathTool::segmentToLine() { Q_D(KoToolBase); if (m_pointSelection.size() > 1) { QList segments(m_pointSelection.selectedSegmentsData()); if (segments.size() > 0) { d->canvas->addCommand(new KoPathSegmentTypeCommand(segments, KoPathSegmentTypeCommand::Line)); - updateActions(); } } } @@ -321,7 +353,6 @@ QList segments(m_pointSelection.selectedSegmentsData()); if (segments.size() > 0) { d->canvas->addCommand(new KoPathSegmentTypeCommand(segments, KoPathSegmentTypeCommand::Curve)); - updateActions(); } } } @@ -340,63 +371,75 @@ updateOptionsWidget(); } -void KoPathTool::joinPoints() +namespace { +bool checkCanJoinToPoints(const KoPathPointData & pd1, const KoPathPointData & pd2) { - Q_D(KoToolBase); - if (m_pointSelection.objectCount() == 1 && m_pointSelection.size() == 2) { - QList pd(m_pointSelection.selectedPointsData()); - const KoPathPointData & pd1 = pd.at(0); - const KoPathPointData & pd2 = pd.at(1); - KoPathShape * pathShape = pd1.pathShape; - if (!pathShape->isClosedSubpath(pd1.pointIndex.first) && - (pd1.pointIndex.second == 0 || - pd1.pointIndex.second == pathShape->subpathPointCount(pd1.pointIndex.first) - 1) && - !pathShape->isClosedSubpath(pd2.pointIndex.first) && - (pd2.pointIndex.second == 0 || - pd2.pointIndex.second == pathShape->subpathPointCount(pd2.pointIndex.first) - 1)) { - KoSubpathJoinCommand *cmd = new KoSubpathJoinCommand(pd1, pd2); - d->canvas->addCommand(cmd); - } - updateActions(); - } + const KoPathPointIndex & index1 = pd1.pointIndex; + const KoPathPointIndex & index2 = pd2.pointIndex; + + KoPathShape *path1 = pd1.pathShape; + KoPathShape *path2 = pd2.pathShape; + + // check if subpaths are already closed + if (path1->isClosedSubpath(index1.first) || path2->isClosedSubpath(index2.first)) + return false; + + // check if first point is an endpoint + if (index1.second != 0 && index1.second != path1->subpathPointCount(index1.first)-1) + return false; + + // check if second point is an endpoint + if (index2.second != 0 && index2.second != path2->subpathPointCount(index2.first)-1) + return false; + + return true; +} } -void KoPathTool::mergePoints() +void KoPathTool::mergePointsImpl(bool doJoin) { Q_D(KoToolBase); - if (m_pointSelection.objectCount() != 1 || m_pointSelection.size() != 2) + + if (m_pointSelection.size() != 2) return; QList pointData = m_pointSelection.selectedPointsData(); + if (pointData.size() != 2) return; + const KoPathPointData & pd1 = pointData.at(0); const KoPathPointData & pd2 = pointData.at(1); - const KoPathPointIndex & index1 = pd1.pointIndex; - const KoPathPointIndex & index2 = pd2.pointIndex; - - KoPathShape * path = pd1.pathShape; - // check if subpaths are already closed - if (path->isClosedSubpath(index1.first) || path->isClosedSubpath(index2.first)) - return; - // check if first point is an endpoint - if (index1.second != 0 && index1.second != path->subpathPointCount(index1.first)-1) - return; - // check if second point is an endpoint - if (index2.second != 0 && index2.second != path->subpathPointCount(index2.first)-1) + if (!checkCanJoinToPoints(pd1, pd2)) { return; + } + + clearActivePointSelectionReferences(); - // now we can start merging the endpoints - KoPathPointMergeCommand *cmd = new KoPathPointMergeCommand(pd1, pd2); + KUndo2Command *cmd = 0; + + if (doJoin) { + cmd = new KoMultiPathPointJoinCommand(pd1, pd2, d->canvas->shapeController()->documentBase(), d->canvas->shapeManager()->selection()); + } else { + cmd = new KoMultiPathPointMergeCommand(pd1, pd2, d->canvas->shapeController()->documentBase(), d->canvas->shapeManager()->selection()); + } d->canvas->addCommand(cmd); - updateActions(); +} + +void KoPathTool::joinPoints() +{ + mergePointsImpl(true); +} + +void KoPathTool::mergePoints() +{ + mergePointsImpl(false); } void KoPathTool::breakAtPoint() { Q_D(KoToolBase); if (m_pointSelection.hasSelection()) { d->canvas->addCommand(new KoPathBreakAtPointCommand(m_pointSelection.selectedPointsData())); - updateActions(); } } @@ -408,56 +451,72 @@ QList segments(m_pointSelection.selectedSegmentsData()); if (segments.size() == 1) { d->canvas->addCommand(new KoPathSegmentBreakCommand(segments.at(0))); - updateActions(); } } } void KoPathTool::paint(QPainter &painter, const KoViewConverter &converter) { Q_D(KoToolBase); - painter.setRenderHint(QPainter::Antialiasing, true); - // use different colors so that it is also visible on a background of the same color - painter.setBrush(Qt::white); - painter.setPen(Qt::blue); Q_FOREACH (KoPathShape *shape, m_pointSelection.selectedShapes()) { - painter.save(); - painter.setTransform(shape->absoluteTransformation(&converter) * painter.transform()); + KisHandlePainterHelper helper = + KoShape::createHandlePainterHelper(&painter, shape, converter, m_handleRadius); + helper.setHandleStyle(KisHandleStyle::primarySelection()); KoParameterShape * parameterShape = dynamic_cast(shape); if (parameterShape && parameterShape->isParametricShape()) { - parameterShape->paintHandles(painter, converter, m_handleRadius); + parameterShape->paintHandles(helper); } else { - shape->paintPoints(painter, converter, m_handleRadius); + shape->paintPoints(helper); } - painter.restore(); + if (!shape->stroke() || !shape->stroke()->isVisible()) { + helper.setHandleStyle(KisHandleStyle::secondarySelection()); + helper.drawPath(shape->outline()); + } } if (m_currentStrategy) { painter.save(); m_currentStrategy->paint(painter, converter); painter.restore(); } - painter.setBrush(Qt::green); - painter.setPen(Qt::blue); - - m_pointSelection.paint(painter, converter); - - painter.setBrush(Qt::red); - painter.setPen(Qt::blue); + m_pointSelection.paint(painter, converter, m_handleRadius); if (m_activeHandle) { if (m_activeHandle->check(m_pointSelection.selectedShapes())) { - m_activeHandle->paint(painter, converter); + m_activeHandle->paint(painter, converter, m_handleRadius); } else { delete m_activeHandle; m_activeHandle = 0; } + } else if (m_activeSegment && m_activeSegment->isValid()) { + + KoPathShape *shape = m_activeSegment->path; + + // if the stroke is invisible, then we already painted the outline of the shape! + if (shape->stroke() && shape->stroke()->isVisible()) { + KoPathPointIndex index = shape->pathPointIndex(m_activeSegment->segmentStart); + KoPathSegment segment = shape->segmentByIndex(index).toCubic(); + + KisHandlePainterHelper helper = + KoShape::createHandlePainterHelper(&painter, shape, converter, m_handleRadius); + helper.setHandleStyle(KisHandleStyle::secondarySelection()); + + QPainterPath path; + path.moveTo(segment.first()->point()); + path.cubicTo(segment.first()->controlPoint2(), + segment.second()->controlPoint1(), + segment.second()->point()); + + helper.drawPath(path); + } } + + if (m_currentStrategy) { painter.save(); KoShape::applyConversion(painter, converter); @@ -488,20 +547,35 @@ // check if we hit a path segment if (m_activeSegment && m_activeSegment->isValid()) { - KoPathPointIndex index = m_activeSegment->path->pathPointIndex(m_activeSegment->segmentStart); - KoPathPointData data(m_activeSegment->path, index); + + KoPathShape *shape = m_activeSegment->path; + KoPathPointIndex index = shape->pathPointIndex(m_activeSegment->segmentStart); + KoPathSegment segment = shape->segmentByIndex(index); + + m_pointSelection.add(segment.first(), !(event->modifiers() & Qt::ShiftModifier)); + m_pointSelection.add(segment.second(), false); + + KoPathPointData data(shape, index); m_currentStrategy = new KoPathSegmentChangeStrategy(this, event->point, data, m_activeSegment->positionOnSegment); event->accept(); - delete m_activeSegment; - m_activeSegment = 0; } else { - if ((event->modifiers() & Qt::ControlModifier) == 0) { - m_pointSelection.clear(); + + KoShapeManager *shapeManager = canvas()->shapeManager(); + KoSelection *selection = shapeManager->selection(); + + KoShape *shape = shapeManager->shapeAt(event->point, KoFlake::ShapeOnTop); + if (shape && !selection->isSelected(shape)) { + + if (!(event->modifiers() & Qt::ShiftModifier)) { + selection->deselectAll(); + } + + selection->select(shape); + } else { + KIS_ASSERT_RECOVER_RETURN(m_currentStrategy == 0); + m_currentStrategy = new KoPathPointRubberSelectStrategy(this, event->point); + event->accept(); } - // start rubberband selection - Q_ASSERT(m_currentStrategy == 0); - m_currentStrategy = new KoPathPointRubberSelectStrategy(this, event->point); - event->accept(); } } } @@ -518,13 +592,25 @@ // repaint new handle positions m_pointSelection.repaint(); - if (m_activeHandle) + if (m_activeHandle) { m_activeHandle->repaint(); + } + + if (m_activeSegment) { + repaintSegment(m_activeSegment); + } + return; } - delete m_activeSegment; - m_activeSegment = 0; + if (m_activeSegment) { + KoPathPointIndex index = m_activeSegment->path->pathPointIndex(m_activeSegment->segmentStart); + KoPathSegment segment = m_activeSegment->path->segmentByIndex(index); + repaint(segment.boundingRect()); + + delete m_activeSegment; + m_activeSegment = 0; + } Q_FOREACH (KoPathShape *shape, m_pointSelection.selectedShapes()) { QRectF roi = handleGrabRect(shape->documentToShape(event->point)); @@ -616,16 +702,20 @@ } useCursor(m_selectCursor); - if (m_activeHandle) + + if (m_activeHandle) { m_activeHandle->repaint(); + } + delete m_activeHandle; m_activeHandle = 0; PathSegment *hoveredSegment = segmentAtPoint(event->point); if(hoveredSegment) { useCursor(Qt::PointingHandCursor); emit statusTextChanged(i18n("Drag to change curve directly. Double click to insert new path point.")); m_activeSegment = hoveredSegment; + repaintSegment(m_activeSegment); } else { uint selectedPointCount = m_pointSelection.size(); if (selectedPointCount == 0) @@ -637,6 +727,15 @@ } } +void KoPathTool::repaintSegment(PathSegment *pathSegment) +{ + if (!pathSegment || !pathSegment->isValid()) return; + + KoPathPointIndex index = pathSegment->path->pathPointIndex(pathSegment->segmentStart); + KoPathSegment segment = pathSegment->path->segmentByIndex(index); + repaint(segment.boundingRect()); +} + void KoPathTool::mouseReleaseEvent(KoPointerEvent *event) { Q_D(KoToolBase); @@ -653,11 +752,6 @@ } delete m_currentStrategy; m_currentStrategy = 0; - - if (m_pointSelection.selectedShapes().count() == 1) - emit pathChanged(m_pointSelection.selectedShapes().first()); - else - emit pathChanged(0); } } @@ -685,17 +779,6 @@ } } else { switch (event->key()) { -// TODO move these to the actions in the constructor. - case Qt::Key_I: { - KoDocumentResourceManager *rm = d->canvas->shapeController()->resourceManager(); - int handleRadius = rm->handleRadius(); - if (event->modifiers() & Qt::ControlModifier) - handleRadius--; - else - handleRadius++; - rm->setHandleRadius(handleRadius); - break; - } #ifndef NDEBUG case Qt::Key_D: if (m_pointSelection.objectCount() == 1) { @@ -744,14 +827,11 @@ event->ignore(); // check if we are doing something else at the moment - if (m_currentStrategy) - return; + if (m_currentStrategy) return; - PathSegment *s = segmentAtPoint(event->point); - if (!s) - return; + QScopedPointer s(segmentAtPoint(event->point)); - if (s->isValid()) { + if (s && s->isValid()) { QList segments; segments.append(KoPathPointData(s->path, s->path->pathPointIndex(s->segmentStart))); KoPathPointInsertCommand *cmd = new KoPathPointInsertCommand(segments, s->positionOnSegment); @@ -762,8 +842,9 @@ } updateActions(); event->accept(); + } else if (!m_activeHandle && !m_activeSegment && m_activatedTemporarily) { + emit done(); } - delete s; } KoPathTool::PathSegment* KoPathTool::segmentAtPoint(const QPointF &point) @@ -776,7 +857,7 @@ // the max allowed distance from a segment const qreal maxSquaredDistance = clickOffset.x()*clickOffset.x(); - PathSegment *segment = new PathSegment; + QScopedPointer segment(new PathSegment); Q_FOREACH (KoPathShape *shape, m_pointSelection.selectedShapes()) { KoParameterShape * parameterShape = dynamic_cast(shape); @@ -809,58 +890,76 @@ } if (!segment->isValid()) { - delete segment; - segment = 0; + segment.reset(); } - return segment; + return segment.take(); } -void KoPathTool::activate(ToolActivation toolActivation, const QSet &shapes) +void KoPathTool::activate(ToolActivation activation, const QSet &shapes) { + KoToolBase::activate(activation, shapes); + Q_D(KoToolBase); - Q_UNUSED(toolActivation); + + m_activatedTemporarily = activation == TemporaryActivation; + // retrieve the actual global handle radius m_handleRadius = handleRadius(); d->canvas->snapGuide()->reset(); - repaintDecorations(); + useCursor(m_selectCursor); + m_canvasConnections.addConnection(d->canvas->selectedShapesProxy(), SIGNAL(selectionChanged()), this, SLOT(slotSelectionChanged())); + m_canvasConnections.addConnection(d->canvas->selectedShapesProxy(), SIGNAL(selectionContentChanged()), this, SLOT(updateActions())); + + initializeWithShapes(shapes.toList()); +} + +void KoPathTool::slotSelectionChanged() +{ + Q_D(KoToolBase); + QList shapes = + d->canvas->selectedShapesProxy()->selection()->selectedEditableShapesAndDelegates(); + + initializeWithShapes(shapes); +} + +void KoPathTool::clearActivePointSelectionReferences() +{ + delete m_activeHandle; + m_activeHandle = 0; + delete m_activeSegment; + m_activeSegment = 0; + + m_pointSelection.clear(); +} + +void KoPathTool::initializeWithShapes(const QList shapes) +{ QList selectedShapes; Q_FOREACH (KoShape *shape, shapes) { KoPathShape *pathShape = dynamic_cast(shape); - if (shape->isEditable() && pathShape) { - // as the tool is just in activation repaintDecorations does not yet get called - // so we need to use repaint of the tool and it is only needed to repaint the - // current canvas - repaint(pathShape->boundingRect()); + if (pathShape && pathShape->isEditable()) { selectedShapes.append(pathShape); } } - if (selectedShapes.isEmpty()) { - emit done(); - return; + + if (selectedShapes != m_pointSelection.selectedShapes()) { + clearActivePointSelectionReferences(); + m_pointSelection.setSelectedShapes(selectedShapes); + repaintDecorations(); } - m_pointSelection.setSelectedShapes(selectedShapes); - useCursor(m_selectCursor); - connect(d->canvas->shapeManager()->selection(), SIGNAL(selectionChanged()), this, SLOT(activate())); - updateOptionsWidget(); - updateActions(); -} -void KoPathTool::activate() -{ - Q_D(KoToolBase); - QSet shapes; - Q_FOREACH (KoShape *shape, d->canvas->shapeManager()->selection()->selectedShapes()) { - QSet delegates = shape->toolDelegates(); - if (delegates.isEmpty()) { - shapes << shape; - } else { - shapes += delegates; - } + Q_FOREACH (KoPathShape *shape, selectedShapes) { + // as the tool is just in activation repaintDecorations does not yet get called + // so we need to use repaint of the tool and it is only needed to repaint the + // current canvas + repaint(shape->boundingRect()); } - activate(DefaultActivation, shapes); + + updateOptionsWidget(); + updateActions(); } void KoPathTool::updateOptionsWidget() @@ -872,42 +971,106 @@ type |= parameterShape && parameterShape->isParametricShape() ? PathToolOptionWidget::ParametricShape : PathToolOptionWidget::PlainPath; } - if (selectedShapes.count() == 1) - emit pathChanged(selectedShapes.first()); - else - emit pathChanged(0); + + emit singleShapeChanged(selectedShapes.size() == 1 ? selectedShapes.first() : 0); emit typeChanged(type); } void KoPathTool::updateActions() { - const bool hasPointsSelected = m_pointSelection.hasSelection(); - m_actionPathPointCorner->setEnabled(hasPointsSelected); - m_actionPathPointSmooth->setEnabled(hasPointsSelected); - m_actionPathPointSymmetric->setEnabled(hasPointsSelected); - m_actionRemovePoint->setEnabled(hasPointsSelected); - m_actionBreakPoint->setEnabled(hasPointsSelected); - m_actionCurvePoint->setEnabled(hasPointsSelected); - m_actionLinePoint->setEnabled(hasPointsSelected); - - bool hasSegmentsSelected = false; - if (hasPointsSelected && m_pointSelection.size() > 1) - hasSegmentsSelected = !m_pointSelection.selectedSegmentsData().isEmpty(); - m_actionAddPoint->setEnabled(hasSegmentsSelected); - m_actionLineSegment->setEnabled(hasSegmentsSelected); - m_actionCurveSegment->setEnabled(hasSegmentsSelected); - - const uint objectCount = m_pointSelection.objectCount(); - const uint pointCount = m_pointSelection.size(); - m_actionBreakSegment->setEnabled(objectCount == 1 && pointCount == 2); - m_actionJoinSegment->setEnabled(objectCount == 1 && pointCount == 2); - m_actionMergePoints->setEnabled(objectCount == 1 && pointCount == 2); + QList pointData = m_pointSelection.selectedPointsData(); + + bool canBreakAtPoint = false; + + bool hasNonSmoothPoints = false; + bool hasNonSymmetricPoints = false; + bool hasNonSplitPoints = false; + + bool hasNonLinePoints = false; + bool hasNonCurvePoints = false; + + bool canJoinSubpaths = false; + + if (!pointData.isEmpty()) { + Q_FOREACH (const KoPathPointData &pd, pointData) { + const int subpathIndex = pd.pointIndex.first; + const int pointIndex = pd.pointIndex.second; + + canBreakAtPoint |= pd.pathShape->isClosedSubpath(subpathIndex) || + (pointIndex > 0 && pointIndex < pd.pathShape->subpathPointCount(subpathIndex) - 1); + + KoPathPoint *point = pd.pathShape->pointByIndex(pd.pointIndex); + + hasNonSmoothPoints |= !(point->properties() & KoPathPoint::IsSmooth); + hasNonSymmetricPoints |= !(point->properties() & KoPathPoint::IsSymmetric); + hasNonSplitPoints |= + point->properties() & KoPathPoint::IsSymmetric || + point->properties() & KoPathPoint::IsSmooth; + + hasNonLinePoints |= point->activeControlPoint1() || point->activeControlPoint2(); + hasNonCurvePoints |= !point->activeControlPoint1() && !point->activeControlPoint2(); + } + + if (pointData.size() == 2) { + const KoPathPointData & pd1 = pointData.at(0); + const KoPathPointData & pd2 = pointData.at(1); + + canJoinSubpaths = checkCanJoinToPoints(pd1, pd2); + } + } + + m_actionPathPointCorner->setEnabled(hasNonSplitPoints); + m_actionPathPointSmooth->setEnabled(hasNonSmoothPoints); + m_actionPathPointSymmetric->setEnabled(hasNonSymmetricPoints); + + m_actionRemovePoint->setEnabled(!pointData.isEmpty()); + + m_actionBreakPoint->setEnabled(canBreakAtPoint); + + m_actionCurvePoint->setEnabled(hasNonCurvePoints); + m_actionLinePoint->setEnabled(hasNonLinePoints); + + m_actionJoinSegment->setEnabled(canJoinSubpaths); + m_actionMergePoints->setEnabled(canJoinSubpaths); + + QList segments(m_pointSelection.selectedSegmentsData()); + + + bool canSplitAtSegment = false; + bool canConvertSegmentToLine = false; + bool canConvertSegmentToCurve= false; + + if (!segments.isEmpty()) { + + canSplitAtSegment = segments.size() == 1; + + bool hasLines = false; + bool hasCurves = false; + + Q_FOREACH (const KoPathPointData &pd, segments) { + KoPathSegment segment = pd.pathShape->segmentByIndex(pd.pointIndex); + hasLines |= segment.degree() == 1; + hasCurves |= segment.degree() > 1; + } + + canConvertSegmentToLine = !segments.isEmpty() && hasCurves; + canConvertSegmentToCurve= !segments.isEmpty() && hasLines; + } + + m_actionAddPoint->setEnabled(canSplitAtSegment); + + m_actionLineSegment->setEnabled(canConvertSegmentToLine); + m_actionCurveSegment->setEnabled(canConvertSegmentToCurve); + + m_actionBreakSegment->setEnabled(canSplitAtSegment); + } void KoPathTool::deactivate() { Q_D(KoToolBase); - disconnect(d->canvas->shapeManager()->selection(), SIGNAL(selectionChanged()), this, SLOT(activate())); + + m_canvasConnections.clear(); m_pointSelection.clear(); m_pointSelection.setSelectedShapes(QList()); delete m_activeHandle; @@ -917,6 +1080,8 @@ delete m_currentStrategy; m_currentStrategy = 0; d->canvas->snapGuide()->reset(); + + KoToolBase::deactivate(); } void KoPathTool::documentResourceChanged(int key, const QVariant & res) @@ -952,6 +1117,77 @@ d->canvas->updateCanvas(repaintRect.adjusted(-radius, -radius, radius, radius)); } +namespace { +void addActionsGroupIfEnabled(QMenu *menu, QAction *a1, QAction *a2) +{ + if (a1->isEnabled() || a2->isEnabled()) { + menu->addAction(a1); + menu->addAction(a2); + menu->addSeparator(); + } +} + +void addActionsGroupIfEnabled(QMenu *menu, QAction *a1, QAction *a2, QAction *a3) +{ + if (a1->isEnabled() || a2->isEnabled()) { + menu->addAction(a1); + menu->addAction(a2); + menu->addAction(a3); + menu->addSeparator(); + } +} +} + +QMenu *KoPathTool::popupActionsMenu() +{ + if (m_activeHandle) { + m_activeHandle->trySelectHandle(); + } + + if (m_activeSegment && m_activeSegment->isValid()) { + KoPathShape *shape = m_activeSegment->path; + KoPathSegment segment = shape->segmentByIndex(shape->pathPointIndex(m_activeSegment->segmentStart)); + + m_pointSelection.add(segment.first(), true); + m_pointSelection.add(segment.second(), false); + } + + if (m_contextMenu) { + m_contextMenu->clear(); + + addActionsGroupIfEnabled(m_contextMenu.data(), + m_actionPathPointCorner, + m_actionPathPointSmooth, + m_actionPathPointSymmetric); + + addActionsGroupIfEnabled(m_contextMenu.data(), + m_actionCurvePoint, + m_actionLinePoint); + + addActionsGroupIfEnabled(m_contextMenu.data(), + m_actionAddPoint, + m_actionRemovePoint); + + addActionsGroupIfEnabled(m_contextMenu.data(), + m_actionLineSegment, + m_actionCurveSegment); + + addActionsGroupIfEnabled(m_contextMenu.data(), + m_actionBreakPoint, + m_actionBreakSegment); + + addActionsGroupIfEnabled(m_contextMenu.data(), + m_actionJoinSegment, + m_actionMergePoints); + + m_contextMenu->addAction(m_actionConvertToPath); + + m_contextMenu->addSeparator(); + } + + return m_contextMenu.data(); +} + void KoPathTool::deleteSelection() { removePoints(); @@ -961,3 +1197,25 @@ { return &m_pointSelection; } + +void KoPathTool::requestUndoDuringStroke() +{ + // noop! +} + +void KoPathTool::requestStrokeCancellation() +{ + explicitUserStrokeEndRequest(); +} + +void KoPathTool::requestStrokeEnd() +{ + // noop! +} + +void KoPathTool::explicitUserStrokeEndRequest() +{ + if (m_activatedTemporarily) { + emit done(); + } +} diff --git a/libs/flake/tools/KoPathToolFactory.cpp b/libs/flake/tools/KoPathToolFactory.cpp --- a/libs/flake/tools/KoPathToolFactory.cpp +++ b/libs/flake/tools/KoPathToolFactory.cpp @@ -27,11 +27,11 @@ KoPathToolFactory::KoPathToolFactory() : KoToolFactoryBase("PathTool") { - setToolTip(i18n("Path editing")); + setToolTip(i18n("Edit Shapes Tool")); setSection(mainToolType()); setIconName(koIconNameCStr("shape_handling")); - setPriority(2); - setActivationShapeId(KoPathShapeId); + setPriority(1); + setActivationShapeId("flake/always,KoPathShape"); } KoPathToolFactory::~KoPathToolFactory() diff --git a/libs/flake/tools/KoPathToolHandle.h b/libs/flake/tools/KoPathToolHandle.h --- a/libs/flake/tools/KoPathToolHandle.h +++ b/libs/flake/tools/KoPathToolHandle.h @@ -35,18 +35,22 @@ class KoPointerEvent; class QPainter; class KoPathShape; +class KisHandlePainterHelper; + class KoPathToolHandle { public: explicit KoPathToolHandle(KoPathTool *tool); virtual ~KoPathToolHandle(); - virtual void paint(QPainter &painter, const KoViewConverter &converter) = 0; + virtual void paint(QPainter &painter, const KoViewConverter &converter, qreal handleRadius) = 0; virtual void repaint() const = 0; virtual KoInteractionStrategy * handleMousePress(KoPointerEvent *event) = 0; // test if handle is still valid virtual bool check(const QList &selectedShapes) = 0; + virtual void trySelectHandle() {}; + protected: uint handleRadius() const; KoPathTool *m_tool; @@ -56,12 +60,13 @@ { public: PointHandle(KoPathTool *tool, KoPathPoint *activePoint, KoPathPoint::PointType activePointType); - void paint(QPainter &painter, const KoViewConverter &converter); + void paint(QPainter &painter, const KoViewConverter &converter, qreal handleRadius); void repaint() const; KoInteractionStrategy *handleMousePress(KoPointerEvent *event); virtual bool check(const QList &selectedShapes); KoPathPoint *activePoint() const; KoPathPoint::PointType activePointType() const; + void trySelectHandle() override; private: KoPathPoint *m_activePoint; KoPathPoint::PointType m_activePointType; @@ -72,10 +77,10 @@ { public: ParameterHandle(KoPathTool *tool, KoParameterShape *parameterShape, int handleId); - void paint(QPainter &painter, const KoViewConverter &converter); + void paint(QPainter &painter, const KoViewConverter &converter, qreal handleRadius); void repaint() const; KoInteractionStrategy *handleMousePress(KoPointerEvent *event); - virtual bool check(const QList &selectedShapes); + bool check(const QList &selectedShapes); protected: KoParameterShape *m_parameterShape; int m_handleId; diff --git a/libs/flake/tools/KoPathToolHandle.cpp b/libs/flake/tools/KoPathToolHandle.cpp --- a/libs/flake/tools/KoPathToolHandle.cpp +++ b/libs/flake/tools/KoPathToolHandle.cpp @@ -36,6 +36,8 @@ #include "KoPointerEvent.h" #include "KoShapeController.h" #include +#include + KoPathToolHandle::KoPathToolHandle(KoPathTool *tool) : m_tool(tool) @@ -58,19 +60,18 @@ { } -void PointHandle::paint(QPainter &painter, const KoViewConverter &converter) +void PointHandle::paint(QPainter &painter, const KoViewConverter &converter, qreal handleRadius) { - painter.save(); - painter.setTransform(m_activePoint->parent()->absoluteTransformation(&converter) * painter.transform()); - KoShape::applyConversion(painter, converter); - KoPathToolSelection * selection = dynamic_cast(m_tool->selection()); KoPathPoint::PointType type = KoPathPoint::Node; - if (selection && selection->contains(m_activePoint)) + if (selection && selection->contains(m_activePoint)) { type = KoPathPoint::All; - m_activePoint->paint(painter, handleRadius(), type); - painter.restore(); + } + + KisHandlePainterHelper helper = KoShape::createHandlePainterHelper(&painter, m_activePoint->parent(), converter, handleRadius); + helper.setHandleStyle(KisHandleStyle::highlightedPrimaryHandles()); + m_activePoint->paint(helper, type); } void PointHandle::repaint() const @@ -88,11 +89,11 @@ { if ((event->button() & Qt::LeftButton) == 0) return 0; - if ((event->modifiers() & Qt::ShiftModifier) == 0) { // no shift pressed. + if ((event->modifiers() & Qt::ControlModifier) == 0) { // no shift pressed. KoPathToolSelection * selection = dynamic_cast(m_tool->selection()); // control select adds/removes points to/from the selection - if (event->modifiers() & Qt::ControlModifier) { + if (event->modifiers() & Qt::ShiftModifier) { if (selection->contains(m_activePoint)) { selection->remove(m_activePoint); } else { @@ -152,20 +153,28 @@ return m_activePointType; } +void PointHandle::trySelectHandle() +{ + KoPathToolSelection * selection = dynamic_cast(m_tool->selection()); + + if (!selection->contains(m_activePoint) && m_activePointType == KoPathPoint::Node) { + selection->clear(); + selection->add(m_activePoint, false); + } +} + ParameterHandle::ParameterHandle(KoPathTool *tool, KoParameterShape *parameterShape, int handleId) : KoPathToolHandle(tool) , m_parameterShape(parameterShape) , m_handleId(handleId) { } -void ParameterHandle::paint(QPainter &painter, const KoViewConverter &converter) +void ParameterHandle::paint(QPainter &painter, const KoViewConverter &converter, qreal handleRadius) { - painter.save(); - painter.setTransform(m_parameterShape->absoluteTransformation(&converter) * painter.transform()); - - m_parameterShape->paintHandle(painter, converter, m_handleId, handleRadius()); - painter.restore(); + KisHandlePainterHelper helper = KoShape::createHandlePainterHelper(&painter, m_parameterShape, converter, handleRadius); + helper.setHandleStyle(KisHandleStyle::highlightedPrimaryHandles()); + m_parameterShape->paintHandle(helper, m_handleId); } void ParameterHandle::repaint() const diff --git a/libs/flake/tools/KoPathToolSelection.h b/libs/flake/tools/KoPathToolSelection.h --- a/libs/flake/tools/KoPathToolSelection.h +++ b/libs/flake/tools/KoPathToolSelection.h @@ -48,7 +48,7 @@ ~KoPathToolSelection(); /// @brief Draw the selected points - void paint(QPainter &painter, const KoViewConverter &converter); + void paint(QPainter &painter, const KoViewConverter &converter, qreal handleRadius); /** * @brief Add a point to the selection diff --git a/libs/flake/tools/KoPathToolSelection.cpp b/libs/flake/tools/KoPathToolSelection.cpp --- a/libs/flake/tools/KoPathToolSelection.cpp +++ b/libs/flake/tools/KoPathToolSelection.cpp @@ -30,6 +30,7 @@ #include #include #include +#include KoPathToolSelection::KoPathToolSelection(KoPathTool * tool) : m_tool(tool) @@ -40,21 +41,17 @@ { } -void KoPathToolSelection::paint(QPainter &painter, const KoViewConverter &converter) +void KoPathToolSelection::paint(QPainter &painter, const KoViewConverter &converter, qreal handleRadius) { - int handleRadius = m_tool->canvas()->shapeController()->resourceManager()->handleRadius(); - PathShapePointMap::iterator it(m_shapePointMap.begin()); for (; it != m_shapePointMap.end(); ++it) { - painter.save(); - - painter.setTransform(it.key()->absoluteTransformation(&converter) * painter.transform()); - KoShape::applyConversion(painter, converter); + KisHandlePainterHelper helper = + KoShape::createHandlePainterHelper(&painter, it.key(), converter, handleRadius); + helper.setHandleStyle(KisHandleStyle::selectedPrimaryHandles()); - Q_FOREACH (KoPathPoint *p, it.value()) - p->paint(painter, handleRadius, KoPathPoint::All); - - painter.restore(); + Q_FOREACH (KoPathPoint *p, it.value()) { + p->paint(helper, KoPathPoint::All); + } } } diff --git a/libs/flake/tools/KoShapeRubberSelectStrategy.h b/libs/flake/tools/KoShapeRubberSelectStrategy.h --- a/libs/flake/tools/KoShapeRubberSelectStrategy.h +++ b/libs/flake/tools/KoShapeRubberSelectStrategy.h @@ -33,6 +33,9 @@ /** * Implement the rubber band selection of flake objects. + * + * 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 { @@ -57,6 +60,13 @@ /// constructor KoShapeRubberSelectStrategy(KoShapeRubberSelectStrategyPrivate &); + enum SelectionMode { + CrossingSelection, + CoveringSelection + }; + + virtual SelectionMode currentMode() const; + private: Q_DECLARE_PRIVATE(KoShapeRubberSelectStrategy) }; diff --git a/libs/flake/tools/KoShapeRubberSelectStrategy.cpp b/libs/flake/tools/KoShapeRubberSelectStrategy.cpp --- a/libs/flake/tools/KoShapeRubberSelectStrategy.cpp +++ b/libs/flake/tools/KoShapeRubberSelectStrategy.cpp @@ -29,6 +29,7 @@ #include "KoSelection.h" #include "KoCanvasBase.h" + KoShapeRubberSelectStrategy::KoShapeRubberSelectStrategy(KoToolBase *tool, const QPointF &clicked, bool useSnapToGrid) : KoInteractionStrategy(*(new KoShapeRubberSelectStrategyPrivate(tool))) { @@ -44,22 +45,31 @@ Q_D(KoShapeRubberSelectStrategy); painter.setRenderHint(QPainter::Antialiasing, false); - QColor selectColor(Qt::blue); // TODO make configurable - selectColor.setAlphaF(0.5); - QBrush sb(selectColor, Qt::SolidPattern); - painter.setPen(QPen(sb, 0)); - painter.setBrush(sb); + const QColor crossingColor(80,130,8); + const QColor coveringColor(8,60,167); + + QColor selectColor( + currentMode() == CrossingSelection ? + crossingColor : coveringColor); + + selectColor.setAlphaF(0.8); + painter.setPen(QPen(selectColor, 0)); + + selectColor.setAlphaF(0.4); + const QBrush fillBrush(selectColor); + painter.setBrush(fillBrush); + QRectF paintRect = converter.documentToView(d->selectedRect()); paintRect = paintRect.normalized(); - paintRect.adjust(0., -0.5, 0.5, 0.); + painter.drawRect(paintRect); } void KoShapeRubberSelectStrategy::handleMouseMove(const QPointF &p, Qt::KeyboardModifiers modifiers) { Q_D(KoShapeRubberSelectStrategy); QPointF point = d->snapGuide->snap(p, modifiers); - if ((modifiers & Qt::AltModifier) != 0) { + if (modifiers & Qt::ControlModifier) { d->tool->canvas()->updateCanvas(d->selectedRect()); d->selectRect.moveTopLeft(d->selectRect.topLeft() - (d->lastPos - point)); d->lastPos = point; @@ -101,16 +111,29 @@ Q_D(KoShapeRubberSelectStrategy); Q_UNUSED(modifiers); KoSelection * selection = d->tool->canvas()->shapeManager()->selection(); - QList shapes(d->tool->canvas()->shapeManager()->shapesAt(d->selectRect)); + + const bool useContainedMode = currentMode() == CoveringSelection; + + QList shapes = + d->tool->canvas()->shapeManager()-> + shapesAt(d->selectedRect(), true, useContainedMode); + Q_FOREACH (KoShape * shape, shapes) { - if (!(shape->isSelectable() && shape->isVisible())) - continue; + if (!shape->isSelectable()) continue; + selection->select(shape); } + d->tool->repaintDecorations(); d->tool->canvas()->updateCanvas(d->selectedRect()); } +KoShapeRubberSelectStrategy::SelectionMode KoShapeRubberSelectStrategy::currentMode() const +{ + Q_D(const KoShapeRubberSelectStrategy); + return d->selectRect.left() < d->selectRect.right() ? CoveringSelection : CrossingSelection; +} + KUndo2Command *KoShapeRubberSelectStrategy::createCommand() { return 0; diff --git a/libs/flake/tools/KoZoomStrategy.h b/libs/flake/tools/KoZoomStrategy.h --- a/libs/flake/tools/KoZoomStrategy.h +++ b/libs/flake/tools/KoZoomStrategy.h @@ -46,6 +46,9 @@ /// Execute the zoom virtual void finishInteraction(Qt::KeyboardModifiers modifiers); virtual void cancelInteraction(); + +protected: + SelectionMode currentMode() const override; private: KoCanvasController *m_controller; diff --git a/libs/flake/tools/KoZoomStrategy.cpp b/libs/flake/tools/KoZoomStrategy.cpp --- a/libs/flake/tools/KoZoomStrategy.cpp +++ b/libs/flake/tools/KoZoomStrategy.cpp @@ -60,6 +60,11 @@ d->tool->canvas()->updateCanvas(d->selectedRect().toRect().normalized()); } +KoShapeRubberSelectStrategy::SelectionMode KoZoomStrategy::currentMode() const +{ + return CoveringSelection; +} + void KoZoomStrategy::forceZoomOut() { m_forceZoomOut = true; diff --git a/libs/flake/tools/PathToolOptionWidget.h b/libs/flake/tools/PathToolOptionWidget.h --- a/libs/flake/tools/PathToolOptionWidget.h +++ b/libs/flake/tools/PathToolOptionWidget.h @@ -25,6 +25,10 @@ #include class KoPathTool; +class KoPathShape; +class KoShapeConfigWidgetBase; +class KoCanvasBase; + class PathToolOptionWidget : public QWidget { @@ -41,9 +45,23 @@ public Q_SLOTS: void setSelectionType(int type); + void setCurrentShape(KoPathShape *pathShape); + +private Q_SLOTS: + void slotShapePropertyChanged(); + +Q_SIGNALS: + void sigRequestUpdateActions(); + +protected: + void showEvent(QShowEvent *event); private: Ui::PathToolOptionWidgetBase widget; + + KoPathShape *m_currentShape; + KoShapeConfigWidgetBase *m_currentPanel; + KoCanvasBase *m_canvas; }; Q_DECLARE_OPERATORS_FOR_FLAGS(PathToolOptionWidget::Types) diff --git a/libs/flake/tools/PathToolOptionWidget.cpp b/libs/flake/tools/PathToolOptionWidget.cpp --- a/libs/flake/tools/PathToolOptionWidget.cpp +++ b/libs/flake/tools/PathToolOptionWidget.cpp @@ -21,8 +21,22 @@ #include "KoPathTool.h" #include +#include +#include +#include +#include +#include +#include +#include +#include +#include "kis_assert.h" + PathToolOptionWidget::PathToolOptionWidget(KoPathTool *tool, QWidget *parent) - : QWidget(parent) + : QWidget(parent), + m_currentShape(0), + m_currentPanel(0), + m_canvas(tool->canvas()) + { widget.setupUi(this); widget.corner->setDefaultAction(tool->action("pathpoint-corner")); @@ -39,6 +53,9 @@ widget.joinSegment->setDefaultAction(tool->action("pathpoint-join")); widget.mergePoints->setDefaultAction(tool->action("pathpoint-merge")); + widget.wdgShapeProperties->setVisible(false); + widget.lineShapeProperties->setVisible(false); + connect(widget.convertToPath, SIGNAL(released()), tool->action("convert-to-path"), SLOT(trigger())); } @@ -54,3 +71,84 @@ else widget.stackedWidget->setCurrentIndex(1); } + +void PathToolOptionWidget::setCurrentShape(KoPathShape *pathShape) +{ + if (pathShape == m_currentShape) return; + + if (m_currentShape) { + m_currentShape = 0; + if (m_currentPanel) { + m_currentPanel->deleteLater(); + m_currentPanel = 0; + } + } + + if (pathShape) { + m_currentShape = pathShape; + QString shapeId = m_currentShape->pathShapeId(); + + // check if we have an edited parametric shape, then we use the path shape id + KoParameterShape *paramShape = dynamic_cast(m_currentShape); + if (paramShape && !paramShape->isParametricShape()) { + shapeId = paramShape->shapeId(); + } + + KoShapeFactoryBase *factory = KoShapeRegistry::instance()->value(shapeId); + KIS_SAFE_ASSERT_RECOVER_RETURN(factory); + + QList panels = factory->createShapeOptionPanels(); + if (!panels.isEmpty()) { + KoShapeConfigWidgetBase *activePanel = 0; + + Q_FOREACH (KoShapeConfigWidgetBase *panel, panels) { + if (!activePanel && panel->showOnShapeSelect()) { + activePanel = panel; + } else { + delete panel; + } + } + + if (activePanel) { + KIS_ASSERT_RECOVER_RETURN(m_canvas); + m_currentPanel = activePanel; + m_currentPanel->setUnit(m_canvas->unit()); + + QLayout *baseLayout = widget.wdgShapeProperties->layout(); + QVBoxLayout *layout = dynamic_cast(widget.wdgShapeProperties->layout()); + + if (!layout) { + KIS_SAFE_ASSERT_RECOVER(!baseLayout) { + delete baseLayout; + } + widget.wdgShapeProperties->setLayout(new QVBoxLayout()); + } + + + KIS_ASSERT_RECOVER_RETURN(widget.wdgShapeProperties->layout()); + widget.wdgShapeProperties->layout()->addWidget(m_currentPanel); + connect(m_currentPanel, SIGNAL(propertyChanged()), SLOT(slotShapePropertyChanged())); + m_currentPanel->open(m_currentShape); + } + } + } + + widget.wdgShapeProperties->setVisible(m_currentPanel); + widget.lineShapeProperties->setVisible(m_currentPanel); +} + +void PathToolOptionWidget::slotShapePropertyChanged() +{ + KIS_SAFE_ASSERT_RECOVER_RETURN(m_currentPanel); + + KUndo2Command *command = m_currentPanel->createCommand(); + if (command) { + m_canvas->addCommand(command); + } +} + +void PathToolOptionWidget::showEvent(QShowEvent *event) +{ + emit sigRequestUpdateActions(); + QWidget::showEvent(event); +} diff --git a/libs/flake/tools/PathToolOptionWidgetBase.ui b/libs/flake/tools/PathToolOptionWidgetBase.ui --- a/libs/flake/tools/PathToolOptionWidgetBase.ui +++ b/libs/flake/tools/PathToolOptionWidgetBase.ui @@ -6,17 +6,33 @@ 0 0 - 280 - 60 + 321 + 208 - - + + 0 - + 0 + + 0 + + + 0 + + + + + + + + Qt::Horizontal + + + @@ -30,9 +46,21 @@ - + 0 + + 0 + + + 0 + + + 0 + + + 3 + @@ -176,40 +204,62 @@ 1 - - + + + 1 + + 0 - - - - Convert To Path + + 0 + + + 0 + + + 0 + + + + + 0 - + + + + Convert To Path + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + - - + + - Qt::Horizontal + Qt::Vertical - 40 - 20 + 20 + 150 - - - - - 0 - 0 - - - - diff --git a/libs/global/CMakeLists.txt b/libs/global/CMakeLists.txt --- a/libs/global/CMakeLists.txt +++ b/libs/global/CMakeLists.txt @@ -9,10 +9,17 @@ set(kritaglobal_LIB_SRCS kis_assert.cpp kis_debug.cpp + kis_algebra_2d.cpp kis_memory_leak_tracker.cpp kis_shared.cpp kis_dom_utils.cpp kis_painting_tweaks.cpp + KisHandlePainterHelper.cpp + KisHandleStyle.cpp + kis_signal_compressor.cpp + kis_signal_compressor_with_param.cpp + kis_acyclic_signal_connector.cpp + KisQPainterStateSaver.cpp ) add_library(kritaglobal SHARED ${kritaglobal_LIB_SRCS} ) diff --git a/libs/global/KisHandlePainterHelper.h b/libs/global/KisHandlePainterHelper.h new file mode 100644 --- /dev/null +++ b/libs/global/KisHandlePainterHelper.h @@ -0,0 +1,169 @@ +/* + * 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 KISHANDLEPAINTERHELPER_H +#define KISHANDLEPAINTERHELPER_H + +#include "kritaglobal_export.h" +#include "kis_algebra_2d.h" + +#include +#include +class QPainter; +class KoShape; +class KoViewConverter; + +/** + * @brief The KisHandlePainterHelper class is a special helper for + * painting handles around objects. It ensures the handlesare painted + * with the same size and line width whatever transformation is setup + * in the painter. The handles will also be rotated/skewed if the object + * itself has these transformations. + * + * On construction it resets QPainter transformation and on destruction + * recovers it back. + * + * Please consider using KoShape::createHandlePainterHelper instead of direct + * construction of the helper. This factory method will also apply the + * transformations needed for a shape. + */ + +class KRITAGLOBAL_EXPORT KisHandlePainterHelper +{ +public: + + /** + * Creates the helper, initializes all the internal transformations and + * *resets* the transformation of the painter. + */ + KisHandlePainterHelper(QPainter *_painter, qreal handleRadius = 0.0); + + /** + * Creates the helper, initializes all the internal transformations and + * *resets* the transformation of the painter. This override also adjusts the + * transformation of the painter into the coordinate system of the shape + */ + KisHandlePainterHelper(QPainter *_painter, const QTransform &originalPainterTransform, qreal handleRadius); + + /** + * Move c-tor. Used to create and return the helper from functions by-value. + */ + KisHandlePainterHelper(KisHandlePainterHelper &&rhs); + KisHandlePainterHelper(KisHandlePainterHelper &rhs) = delete; + + /** + * Restores the transformation of the painter + */ + ~KisHandlePainterHelper(); + + /** + * Sets style used for painting the handles. Please use static methods of + * KisHandleStyle to select predefined styles. + */ + void setHandleStyle(const KisHandleStyle &style); + + /** + * Draws a handle rect with a custom \p radius at position \p center + */ + void drawHandleRect(const QPointF ¢er, qreal radius); + + /** + * Draws a handle circle with a custom \p radius at position \p center + */ + void drawHandleCircle(const QPointF ¢er, qreal radius); + + /** + * Optimized version of the drawing method for drawing handles of + * predefined size + */ + void drawHandleRect(const QPointF ¢er); + + /** + * Optimized version of the drawing method for drawing handles of + * predefined size + */ + void drawHandleCircle(const QPointF ¢er); + + /** + * Optimized version of the drawing method for drawing handles of + * predefined size + */ + void drawHandleSmallCircle(const QPointF ¢er); + + /** + * Draw a rotated handle representing the gradient handle + */ + void drawGradientHandle(const QPointF ¢er, qreal radius); + + /** + * Draw a rotated handle representing the gradient handle + */ + void drawGradientHandle(const QPointF ¢er); + + /** + * Draw a special handle representing the center of the gradient + */ + void drawGradientCrossHandle(const QPointF ¢er, qreal radius); + + /** + * Draw an arrow representing gradient position + */ + void drawGradientArrow(const QPointF &start, const QPointF &end, qreal radius); + + /** + * Draw a line showing the bounding box of the selection + */ + void drawRubberLine(const QPolygonF &poly); + + /** + * Draw a line connecting two points + */ + void drawConnectionLine(const QLineF &line); + + /** + * Draw a line connecting two points + */ + void drawConnectionLine(const QPointF &p1, const QPointF &p2); + + /** + * Draw an arbitrary path + */ + void drawPath(const QPainterPath &path); + +private: + + /** + * Draw a single arrow with the tip at position \p pos, directed from \p from, + * of size \p radius. + */ + void drawArrow(const QPointF &pos, const QPointF &from, qreal radius); + + void init(); + +private: + QPainter *m_painter; + QTransform m_originalPainterTransform; + QTransform m_painterTransform; + qreal m_handleRadius; + KisAlgebra2D::DecomposedMatix m_decomposedMatrix; + QTransform m_handleTransform; + QPolygonF m_handlePolygon; + KisHandleStyle m_handleStyle; +}; + +#endif // KISHANDLEPAINTERHELPER_H diff --git a/libs/global/KisHandlePainterHelper.cpp b/libs/global/KisHandlePainterHelper.cpp new file mode 100644 --- /dev/null +++ b/libs/global/KisHandlePainterHelper.cpp @@ -0,0 +1,280 @@ +/* + * 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 "KisHandlePainterHelper.h" + +#include +#include "kis_algebra_2d.h" +#include "kis_painting_tweaks.h" + +using KisPaintingTweaks::PenBrushSaver; + +KisHandlePainterHelper::KisHandlePainterHelper(QPainter *_painter, qreal handleRadius) + : m_painter(_painter), + m_originalPainterTransform(m_painter->transform()), + m_painterTransform(m_painter->transform()), + m_handleRadius(handleRadius), + m_decomposedMatrix(m_painterTransform) +{ + init(); +} + +KisHandlePainterHelper::KisHandlePainterHelper(QPainter *_painter, const QTransform &originalPainterTransform, qreal handleRadius) + : m_painter(_painter), + m_originalPainterTransform(originalPainterTransform), + m_painterTransform(m_painter->transform()), + m_handleRadius(handleRadius), + m_decomposedMatrix(m_painterTransform) +{ + init(); +} + +KisHandlePainterHelper::KisHandlePainterHelper(KisHandlePainterHelper &&rhs) + : m_painter(rhs.m_painter), + m_originalPainterTransform(rhs.m_originalPainterTransform), + m_painterTransform(rhs.m_painterTransform), + m_handleRadius(rhs.m_handleRadius), + m_decomposedMatrix(rhs.m_decomposedMatrix), + m_handleTransform(rhs.m_handleTransform), + m_handlePolygon(rhs.m_handlePolygon), + m_handleStyle(rhs.m_handleStyle) +{ + // disable the source helper + rhs.m_painter = 0; +} + +void KisHandlePainterHelper::init() +{ + m_handleStyle = KisHandleStyle::inheritStyle(); + + m_painter->setTransform(QTransform()); + m_handleTransform = m_decomposedMatrix.shearTransform() * m_decomposedMatrix.rotateTransform(); + + if (m_handleRadius > 0.0) { + const QRectF handleRect(-m_handleRadius, -m_handleRadius, 2 * m_handleRadius, 2 * m_handleRadius); + m_handlePolygon = m_handleTransform.map(QPolygonF(handleRect)); + } +} + +KisHandlePainterHelper::~KisHandlePainterHelper() { + if (m_painter) { + m_painter->setTransform(m_originalPainterTransform); + } +} + +void KisHandlePainterHelper::setHandleStyle(const KisHandleStyle &style) +{ + m_handleStyle = style; +} + +void KisHandlePainterHelper::drawHandleRect(const QPointF ¢er, qreal radius) { + KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); + + QRectF handleRect(-radius, -radius, 2 * radius, 2 * radius); + QPolygonF handlePolygon = m_handleTransform.map(QPolygonF(handleRect)); + handlePolygon.translate(m_painterTransform.map(center)); + + Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.handleIterations) { + PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); + m_painter->drawPolygon(handlePolygon); + } +} + +void KisHandlePainterHelper::drawHandleCircle(const QPointF ¢er, qreal radius) { + KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); + + QRectF handleRect(-radius, -radius, 2 * radius, 2 * radius); + handleRect.translate(m_painterTransform.map(center)); + + Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.handleIterations) { + PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); + m_painter->drawEllipse(handleRect); + } +} + +void KisHandlePainterHelper::drawHandleCircle(const QPointF ¢er) +{ + drawHandleCircle(center, m_handleRadius); +} + +void KisHandlePainterHelper::drawHandleSmallCircle(const QPointF ¢er) +{ + drawHandleCircle(center, 0.7 * m_handleRadius); +} + +void KisHandlePainterHelper::drawHandleRect(const QPointF ¢er) { + KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); + QPolygonF paintingPolygon = m_handlePolygon.translated(m_painterTransform.map(center)); + + Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.handleIterations) { + PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); + m_painter->drawPolygon(paintingPolygon); + } +} + +void KisHandlePainterHelper::drawGradientHandle(const QPointF ¢er, qreal radius) { + KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); + + QPolygonF handlePolygon; + + handlePolygon << QPointF(-radius, 0); + handlePolygon << QPointF(0, radius); + handlePolygon << QPointF(radius, 0); + handlePolygon << QPointF(0, -radius); + + handlePolygon = m_handleTransform.map(handlePolygon); + handlePolygon.translate(m_painterTransform.map(center)); + + Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.handleIterations) { + PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); + m_painter->drawPolygon(handlePolygon); + } +} + +void KisHandlePainterHelper::drawGradientHandle(const QPointF ¢er) +{ + drawGradientHandle(center, 1.41 * m_handleRadius); +} + +void KisHandlePainterHelper::drawGradientCrossHandle(const QPointF ¢er, qreal radius) { + KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); + + { // Draw a cross + QPainterPath p; + p.moveTo(-radius, -radius); + p.lineTo(radius, radius); + p.moveTo(radius, -radius); + p.lineTo(-radius, radius); + + p = m_handleTransform.map(p); + p.translate(m_painterTransform.map(center)); + + Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.handleIterations) { + PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); + m_painter->drawPath(p); + } + } + + { // Draw a square + const qreal halfRadius = 0.5 * radius; + + QPolygonF handlePolygon; + handlePolygon << QPointF(-halfRadius, 0); + handlePolygon << QPointF(0, halfRadius); + handlePolygon << QPointF(halfRadius, 0); + handlePolygon << QPointF(0, -halfRadius); + + handlePolygon = m_handleTransform.map(handlePolygon); + handlePolygon.translate(m_painterTransform.map(center)); + + Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.handleIterations) { + PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); + m_painter->drawPolygon(handlePolygon); + } + } +} + +void KisHandlePainterHelper::drawArrow(const QPointF &pos, const QPointF &from, qreal radius) +{ + KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); + + QPainterPath p; + + QLineF line(pos, from); + line.setLength(radius); + + QPointF norm = KisAlgebra2D::leftUnitNormal(pos - from); + norm *= 0.34 * radius; + + p.moveTo(line.p2() + norm); + p.lineTo(line.p1()); + p.lineTo(line.p2() - norm); + + p.translate(-pos); + + p = m_handleTransform.map(p).translated(m_painterTransform.map(pos)); + + Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.handleIterations) { + PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); + m_painter->drawPath(p); + } +} + +void KisHandlePainterHelper::drawGradientArrow(const QPointF &start, const QPointF &end, qreal radius) +{ + KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); + + QPainterPath p; + p.moveTo(start); + p.lineTo(end); + p = m_painterTransform.map(p); + + Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.lineIterations) { + PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); + m_painter->drawPath(p); + } + + const qreal length = kisDistance(start, end); + const QPointF diff = end - start; + + if (length > 5 * radius) { + drawArrow(start + 0.33 * diff, start, radius); + drawArrow(start + 0.66 * diff, start, radius); + } else if (length > 3 * radius) { + drawArrow(start + 0.5 * diff, start, radius); + } +} + +void KisHandlePainterHelper::drawRubberLine(const QPolygonF &poly) { + KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); + + QPolygonF paintingPolygon = m_painterTransform.map(poly); + + Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.lineIterations) { + PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); + m_painter->drawPolygon(paintingPolygon); + } +} + +void KisHandlePainterHelper::drawConnectionLine(const QLineF &line) +{ + drawConnectionLine(line.p1(), line.p2()); +} + +void KisHandlePainterHelper::drawConnectionLine(const QPointF &p1, const QPointF &p2) +{ + KIS_SAFE_ASSERT_RECOVER_RETURN(m_painter); + + QPointF realP1 = m_painterTransform.map(p1); + QPointF realP2 = m_painterTransform.map(p2); + + Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.lineIterations) { + PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); + m_painter->drawLine(realP1, realP2); + } +} + +void KisHandlePainterHelper::drawPath(const QPainterPath &path) +{ + const QPainterPath realPath = m_painterTransform.map(path); + + Q_FOREACH (KisHandleStyle::IterationStyle it, m_handleStyle.lineIterations) { + PenBrushSaver saver(it.isValid ? m_painter : 0, it.stylePair, PenBrushSaver::allow_noop); + m_painter->drawPath(realPath); + } +} diff --git a/libs/global/KisHandleStyle.h b/libs/global/KisHandleStyle.h new file mode 100644 --- /dev/null +++ b/libs/global/KisHandleStyle.h @@ -0,0 +1,93 @@ +/* + * 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 KISHANDLESTYLE_H +#define KISHANDLESTYLE_H + +#include +#include +#include + +#include "kritaglobal_export.h" + + +/** + * A special class that defines a set of predefined styles for painting handles. + * Please use static methods for requesting standard krita styles. + */ +class KRITAGLOBAL_EXPORT KisHandleStyle +{ +public: + + /** + * Default style that does *no* change to the painter. That is, the painter + * will paint with its current pen and brush. + */ + static KisHandleStyle& inheritStyle(); + + /** + * Main style. Used for showing a single selection or a boundary box of + * multiple selected objects. + */ + static KisHandleStyle& primarySelection(); + + /** + * Secondary style. Used for highlighting objects inside a mupltiple + * selection. + */ + static KisHandleStyle& secondarySelection(); + + /** + * Style for painting gradient handles + */ + static KisHandleStyle& gradientHandles(); + + /** + * Style for painting linear gradient arrows + */ + static KisHandleStyle& gradientArrows(); + + /** + * Same as primary style, but the handles are filled with red color to show + * that the user is hovering them. + */ + static KisHandleStyle& highlightedPrimaryHandles(); + + /** + * Same as primary style, but the handles are filled with green color to show + * that they are selected. + */ + static KisHandleStyle& selectedPrimaryHandles(); + + struct IterationStyle { + IterationStyle() : isValid(false) {} + IterationStyle(const QPen &pen, const QBrush &brush) + : isValid(true), + stylePair(pen, brush) + { + } + + bool isValid; + QPair stylePair; + }; + + QVector handleIterations; + QVector lineIterations; +}; + +#endif // KISHANDLESTYLE_H diff --git a/libs/global/KisHandleStyle.cpp b/libs/global/KisHandleStyle.cpp new file mode 100644 --- /dev/null +++ b/libs/global/KisHandleStyle.cpp @@ -0,0 +1,127 @@ +/* + * 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 "KisHandleStyle.h" +#include "kis_painting_tweaks.h" + +namespace { +void initDashedStyle(const QColor &baseColor, const QColor &handleFill, KisHandleStyle *style) { + QPen ants; + QPen outline; + KisPaintingTweaks::initAntsPen(&ants, &outline); + + ants.setColor(baseColor); + + style->lineIterations << KisHandleStyle::IterationStyle(outline, Qt::NoBrush); + style->lineIterations << KisHandleStyle::IterationStyle(ants, Qt::NoBrush); + + QPen handlePen(baseColor); + handlePen.setWidth(2); + handlePen.setJoinStyle(Qt::RoundJoin); + + style->handleIterations << KisHandleStyle::IterationStyle(handlePen, handleFill); +} + +static const QColor primaryColor(0, 0, 90, 180); +static const QColor secondaryColor(0, 0, 255, 127); +static const QColor gradientFillColor(255, 197, 39); +static const QColor highlightColor(255, 100, 100); +static const QColor selectionColor(66, 255, 0); + +} + + +KisHandleStyle &KisHandleStyle::inheritStyle() +{ + static QScopedPointer style; + + if (!style) { + style.reset(new KisHandleStyle()); + style->lineIterations << KisHandleStyle::IterationStyle(); + style->handleIterations << KisHandleStyle::IterationStyle(); + } + + return *style; +} + +KisHandleStyle &KisHandleStyle::primarySelection() +{ + static QScopedPointer style; + + if (!style) { + style.reset(new KisHandleStyle()); + initDashedStyle(primaryColor, Qt::white, style.data()); + } + + return *style; +} + +KisHandleStyle &KisHandleStyle::secondarySelection() +{ + static QScopedPointer style; + + if (!style) { + style.reset(new KisHandleStyle()); + initDashedStyle(secondaryColor, Qt::white, style.data()); + } + + return *style; +} + +KisHandleStyle &KisHandleStyle::gradientHandles() +{ + static QScopedPointer style; + + if (!style) { + style.reset(new KisHandleStyle()); + initDashedStyle(primaryColor, gradientFillColor, style.data()); + } + + return *style; +} + +KisHandleStyle &KisHandleStyle::gradientArrows() +{ + return primarySelection(); +} + + +KisHandleStyle &KisHandleStyle::highlightedPrimaryHandles() +{ + static QScopedPointer style; + + if (!style) { + style.reset(new KisHandleStyle()); + initDashedStyle(primaryColor, highlightColor, style.data()); + } + + return *style; +} + +KisHandleStyle &KisHandleStyle::selectedPrimaryHandles() +{ + static QScopedPointer style; + + if (!style) { + style.reset(new KisHandleStyle()); + initDashedStyle(primaryColor, selectionColor, style.data()); + } + + return *style; +} + diff --git a/libs/kundo2/kundo2commandextradata.h b/libs/global/KisQPainterStateSaver.h rename from libs/kundo2/kundo2commandextradata.h rename to libs/global/KisQPainterStateSaver.h --- a/libs/kundo2/kundo2commandextradata.h +++ b/libs/global/KisQPainterStateSaver.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Dmitry Kazakov + * Copyright (c) 2017 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -16,16 +16,22 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#ifndef __KUNDO2COMMANDEXTRADATA_H -#define __KUNDO2COMMANDEXTRADATA_H +#ifndef KISQPAINTERSTATESAVER_H +#define KISQPAINTERSTATESAVER_H -#include "kritaundo2_export.h" +#include "kritaglobal_export.h" +class QPainter; -class KRITAUNDO2_EXPORT KUndo2CommandExtraData +class KRITAGLOBAL_EXPORT KisQPainterStateSaver { public: - virtual ~KUndo2CommandExtraData(); + KisQPainterStateSaver(QPainter *painter); + ~KisQPainterStateSaver(); + +private: + KisQPainterStateSaver(const KisQPainterStateSaver &rhs); + QPainter *m_painter; }; -#endif /* __KUNDO2COMMANDEXTRADATA_H */ +#endif // KISQPAINTERSTATESAVER_H diff --git a/libs/image/kis_undo_store.cpp b/libs/global/KisQPainterStateSaver.cpp copy from libs/image/kis_undo_store.cpp copy to libs/global/KisQPainterStateSaver.cpp --- a/libs/image/kis_undo_store.cpp +++ b/libs/global/KisQPainterStateSaver.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007 Sven Langkamp + * 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 @@ -16,15 +16,18 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -#include "kis_undo_store.h" +#include "KisQPainterStateSaver.h" -#include "kis_debug.h" +#include - -KisUndoStore::KisUndoStore() +KisQPainterStateSaver::KisQPainterStateSaver(QPainter *painter) + : m_painter(painter) { + m_painter->save(); } -KisUndoStore::~KisUndoStore() +KisQPainterStateSaver::~KisQPainterStateSaver() { + m_painter->restore(); } + diff --git a/libs/image/kis_acyclic_signal_connector.h b/libs/global/kis_acyclic_signal_connector.h rename from libs/image/kis_acyclic_signal_connector.h rename to libs/global/kis_acyclic_signal_connector.h --- a/libs/image/kis_acyclic_signal_connector.h +++ b/libs/global/kis_acyclic_signal_connector.h @@ -20,7 +20,8 @@ #define __KIS_ACYCLIC_SIGNAL_CONNECTOR_H #include -#include "kritaimage_export.h" +#include "kritaglobal_export.h" +#include /** @@ -43,11 +44,15 @@ * See an example in KisToolCropConfigWidget. */ -class KRITAIMAGE_EXPORT KisAcyclicSignalConnector : public QObject +class KRITAGLOBAL_EXPORT KisAcyclicSignalConnector : public QObject { Q_OBJECT public: - KisAcyclicSignalConnector(QObject *parent); + typedef std::unique_lock Blocker; + +public: + + KisAcyclicSignalConnector(QObject *parent = 0); void connectForwardDouble(QObject *sender, const char *signal, QObject *receiver, const char *method); @@ -79,6 +84,15 @@ void connectBackwardVariant(QObject *sender, const char *signal, QObject *receiver, const char *method); + void connectForwardResourcePair(QObject *sender, const char *signal, + QObject *receiver, const char *method); + + void connectBackwardResourcePair(QObject *sender, const char *signal, + QObject *receiver, const char *method); + + void lock(); + void unlock(); + private Q_SLOTS: void forwardSlotDouble(double value); void backwardSlotDouble(double value); @@ -95,6 +109,9 @@ void forwardSlotVariant(const QVariant &value); void backwardSlotVariant(const QVariant &value); + void forwardSlotResourcePair(int key, const QVariant &resource); + void backwardSlotResourcePair(int key, const QVariant &resource); + Q_SIGNALS: void forwardSignalDouble(double value); void backwardSignalDouble(double value); @@ -111,6 +128,9 @@ void forwardSignalVariant(const QVariant &value); void backwardSignalVariant(const QVariant &value); + void forwardSignalResourcePair(int key, const QVariant &value); + void backwardSignalResourcePair(int key, const QVariant &value); + private: int m_signalsBlocked; }; diff --git a/libs/image/kis_acyclic_signal_connector.cpp b/libs/global/kis_acyclic_signal_connector.cpp rename from libs/image/kis_acyclic_signal_connector.cpp rename to libs/global/kis_acyclic_signal_connector.cpp --- a/libs/image/kis_acyclic_signal_connector.cpp +++ b/libs/global/kis_acyclic_signal_connector.cpp @@ -106,6 +106,28 @@ connect(this, SIGNAL(backwardSignalVariant(const QVariant&)), receiver, method); } +void KisAcyclicSignalConnector::connectForwardResourcePair(QObject *sender, const char *signal, QObject *receiver, const char *method) +{ + connect(sender, signal, this, SLOT(forwardSlotResourcePair(int,QVariant))); + connect(this, SIGNAL(forwardSignalResourcePair(int,QVariant)), receiver, method); +} + +void KisAcyclicSignalConnector::connectBackwardResourcePair(QObject *sender, const char *signal, QObject *receiver, const char *method) +{ + connect(sender, signal, this, SLOT(backwardSlotResourcePair(int,QVariant))); + connect(this, SIGNAL(backwardSignalResourcePair(int,QVariant)), receiver, method); +} + +void KisAcyclicSignalConnector::lock() +{ + m_signalsBlocked++; +} + +void KisAcyclicSignalConnector::unlock() +{ + m_signalsBlocked--; +} + void KisAcyclicSignalConnector::forwardSlotDouble(double value) { if (m_signalsBlocked) return; @@ -195,3 +217,21 @@ emit backwardSignalVariant(value); m_signalsBlocked--; } + +void KisAcyclicSignalConnector::forwardSlotResourcePair(int key, const QVariant &resource) +{ + if (m_signalsBlocked) return; + + m_signalsBlocked++; + emit forwardSignalResourcePair(key, resource); + m_signalsBlocked--; +} + +void KisAcyclicSignalConnector::backwardSlotResourcePair(int key, const QVariant &resource) +{ + if (m_signalsBlocked) return; + + m_signalsBlocked++; + emit backwardSignalResourcePair(key, resource); + m_signalsBlocked--; +} diff --git a/libs/image/kis_algebra_2d.h b/libs/global/kis_algebra_2d.h rename from libs/image/kis_algebra_2d.h rename to libs/global/kis_algebra_2d.h --- a/libs/image/kis_algebra_2d.h +++ b/libs/global/kis_algebra_2d.h @@ -23,11 +23,14 @@ #include #include #include +#include #include #include -#include +#include +#include class QPainterPath; +class QTransform; namespace KisAlgebra2D { @@ -156,17 +159,17 @@ return qAbs(x - a) <= length && qAbs(x - b) <= length; } -void KRITAIMAGE_EXPORT adjustIfOnPolygonBoundary(const QPolygonF &poly, int polygonDirection, QPointF *pt); +void KRITAGLOBAL_EXPORT adjustIfOnPolygonBoundary(const QPolygonF &poly, int polygonDirection, QPointF *pt); /** * Let \p pt, \p base1 are two vectors. \p base1 is uniformly scaled * and then rotated into \p base2 using transformation matrix S * * R. The function applies the same transformation to \pt and returns * the result. **/ -QPointF KRITAIMAGE_EXPORT transformAsBase(const QPointF &pt, const QPointF &base1, const QPointF &base2); +QPointF KRITAGLOBAL_EXPORT transformAsBase(const QPointF &pt, const QPointF &base1, const QPointF &base2); -qreal KRITAIMAGE_EXPORT angleBetweenVectors(const QPointF &v1, const QPointF &v2); +qreal KRITAGLOBAL_EXPORT angleBetweenVectors(const QPointF &v1, const QPointF &v2); namespace Private { inline void resetEmptyRectangle(const QPoint &pt, QRect *rc) { @@ -268,10 +271,49 @@ return rect.adjusted(-w, -h, w, h); } -QPoint KRITAIMAGE_EXPORT ensureInRect(QPoint pt, const QRect &bounds); -QPointF KRITAIMAGE_EXPORT ensureInRect(QPointF pt, const QRectF &bounds); +QPoint KRITAGLOBAL_EXPORT ensureInRect(QPoint pt, const QRect &bounds); +QPointF KRITAGLOBAL_EXPORT ensureInRect(QPointF pt, const QRectF &bounds); + +template +Rect ensureRectNotSmaller(Rect rc, const decltype(Rect().size()) &size) +{ + typedef decltype(Rect().size()) Size; + typedef decltype(Rect().top()) ValueType; + + if (rc.width() < size.width() || + rc.height() < size.height()) { + + ValueType width = qMax(rc.width(), size.width()); + ValueType height = qMax(rc.height(), size.height()); + + rc = Rect(rc.topLeft(), Size(width, height)); + } + + return rc; +} + +template +Size ensureSizeNotSmaller(const Size &size, const Size &bounds) +{ + Size result = size; + + const auto widthBound = qAbs(bounds.width()); + auto width = result.width(); + if (qAbs(width) < widthBound) { + width = copysign(widthBound, width); + result.setWidth(width); + } + + const auto heightBound = qAbs(bounds.height()); + auto height = result.height(); + if (qAbs(height) < heightBound) { + height = copysign(heightBound, height); + result.setHeight(height); + } + + return result; +} -QRect KRITAIMAGE_EXPORT ensureRectNotSmaller(QRect rc, const QSize &size); /** * Attempt to intersect a line to the area of the a rectangle. @@ -283,7 +325,7 @@ * @param area * @return true if successful */ -bool KRITAIMAGE_EXPORT intersectLineRect(QLineF &line, const QRect rect); +bool KRITAGLOBAL_EXPORT intersectLineRect(QLineF &line, const QRect rect); template @@ -365,12 +407,20 @@ const qreal m_fadeCoeff; }; +QVector KRITAGLOBAL_EXPORT sampleRectWithPoints(const QRect &rect); +QVector KRITAGLOBAL_EXPORT sampleRectWithPoints(const QRectF &rect); + +QRect KRITAGLOBAL_EXPORT approximateRectFromPoints(const QVector &points); +QRectF KRITAGLOBAL_EXPORT approximateRectFromPoints(const QVector &points); + +QRect KRITAGLOBAL_EXPORT approximateRectWithPointTransform(const QRect &rect, std::function func); + /** * Cuts off a portion of a rect \p rc defined by a half-plane \p p * \return the bounding rect of the resulting polygon */ -KRITAIMAGE_EXPORT +KRITAGLOBAL_EXPORT QRectF cutOffRect(const QRectF &rc, const KisAlgebra2D::RightHalfPlane &p); @@ -390,17 +440,19 @@ * solution --- x1 is filled with the result, 2 * solutions --- x1 and x2 are filled. */ -KRITAIMAGE_EXPORT +KRITAGLOBAL_EXPORT int quadraticEquation(qreal a, qreal b, qreal c, qreal *x1, qreal *x2); /** * Finds the points of intersections between two circles * \return the found circles, the result can have 0, 1 or 2 points */ -KRITAIMAGE_EXPORT +KRITAGLOBAL_EXPORT QVector intersectTwoCircles(const QPointF &c1, qreal r1, const QPointF &c2, qreal r2); +KRITAGLOBAL_EXPORT +QTransform mapToRect(const QRectF &rect); /** * Scale the relative point \pt into the bounds of \p rc. The point might be @@ -422,6 +474,89 @@ } +/** + * Compare the matrices with tolerance \p delta + */ +bool KRITAGLOBAL_EXPORT fuzzyMatrixCompare(const QTransform &t1, const QTransform &t2, qreal delta); + +/** + * Compare two rectangles with tolerance \p tolerance. The tolerance means that the + * coordinates of top left and bottom right corners should not differ more than \p tolerance + * pixels. + */ +template +bool fuzzyCompareRects(const Rect &r1, const Rect &r2, Difference tolerance) { + typedef decltype(r1.topLeft()) Point; + + const Point d1 = abs(r1.topLeft() - r2.topLeft()); + const Point d2 = abs(r1.bottomRight() - r2.bottomRight()); + + const Difference maxError = std::max({d1.x(), d1.y(), d2.x(), d2.y()}); + return maxError < tolerance; +} + +struct KRITAGLOBAL_EXPORT DecomposedMatix { + DecomposedMatix(); + + DecomposedMatix(const QTransform &t0); + + inline QTransform scaleTransform() const + { + return QTransform::fromScale(scaleX, scaleY); + } + + inline QTransform shearTransform() const + { + QTransform t; + t.shear(shearXY, 0); + return t; + } + + inline QTransform rotateTransform() const + { + QTransform t; + t.rotate(angle); + return t; + } + + inline QTransform translateTransform() const + { + return QTransform::fromTranslate(dx, dy); + } + + inline QTransform projectTransform() const + { + return + QTransform( + 1,0,proj[0], + 0,1,proj[1], + 0,0,proj[2]); + } + + inline QTransform transform() const { + return + scaleTransform() * + shearTransform() * + rotateTransform() * + translateTransform() * + projectTransform(); + } + + inline bool isValid() const { + return valid; + } + + qreal scaleX = 1.0; + qreal scaleY = 1.0; + qreal shearXY = 0.0; + qreal angle = 0.0; + qreal dx = 0.0; + qreal dy = 0.0; + qreal proj[3] = {0.0, 0.0, 1.0}; + +private: + bool valid = true; +}; } diff --git a/libs/image/kis_algebra_2d.cpp b/libs/global/kis_algebra_2d.cpp rename from libs/image/kis_algebra_2d.cpp rename to libs/global/kis_algebra_2d.cpp --- a/libs/image/kis_algebra_2d.cpp +++ b/libs/global/kis_algebra_2d.cpp @@ -18,9 +18,17 @@ #include "kis_algebra_2d.h" +#include #include #include -#include "krita_utils.h" + +#include +#include +#include +#include + +#include +#include #define SANITY_CHECKS @@ -152,20 +160,6 @@ return ensureInRectImpl(pt, bounds); } -QRect ensureRectNotSmaller(QRect rc, const QSize &size) -{ - if (rc.width() < size.width() || - rc.height() < size.height()) { - - int width = qMax(rc.width(), size.width()); - int height = qMax(rc.height(), size.height()); - - rc = QRect(rc.topLeft(), QSize(width, height)); - } - - return rc; -} - bool intersectLineRect(QLineF &line, const QRect rect) { QPointF pt1 = QPointF(), pt2 = QPointF(); @@ -211,6 +205,98 @@ return true; } + template + QVector sampleRectWithPoints(const Rect &rect) + { + QVector points; + + Point m1 = 0.5 * (rect.topLeft() + rect.topRight()); + Point m2 = 0.5 * (rect.bottomLeft() + rect.bottomRight()); + + points << rect.topLeft(); + points << m1; + points << rect.topRight(); + + points << 0.5 * (rect.topLeft() + rect.bottomLeft()); + points << 0.5 * (m1 + m2); + points << 0.5 * (rect.topRight() + rect.bottomRight()); + + points << rect.bottomLeft(); + points << m2; + points << rect.bottomRight(); + + return points; + } + + QVector sampleRectWithPoints(const QRect &rect) + { + return sampleRectWithPoints(rect); + } + + QVector sampleRectWithPoints(const QRectF &rect) + { + return sampleRectWithPoints(rect); + } + + + template + Rect approximateRectFromPointsImpl(const QVector &points) + { + using namespace boost::accumulators; + accumulator_set > accX; + accumulator_set > accY; + + Q_FOREACH (const Point &pt, points) { + accX(pt.x()); + accY(pt.y()); + } + + Rect resultRect; + + if (alignPixels) { + resultRect.setCoords(std::floor(min(accX)), std::floor(min(accY)), + std::ceil(max(accX)), std::ceil(max(accY))); + } else { + resultRect.setCoords(min(accX), min(accY), + max(accX), max(accY)); + } + + return resultRect; + } + + QRect approximateRectFromPoints(const QVector &points) + { + return approximateRectFromPointsImpl(points); + } + + QRectF approximateRectFromPoints(const QVector &points) + { + return approximateRectFromPointsImpl(points); + } + + QRect approximateRectWithPointTransform(const QRect &rect, std::function func) + { + QVector points = sampleRectWithPoints(rect); + + using namespace boost::accumulators; + accumulator_set > accX; + accumulator_set > accY; + + Q_FOREACH (const QPoint &pt, points) { + QPointF dstPt = func(pt); + + accX(dstPt.x()); + accY(dstPt.y()); + } + + QRect resultRect; + resultRect.setCoords(std::floor(min(accX)), std::floor(min(accY)), + std::ceil(max(accX)), std::ceil(max(accY))); + + return resultRect; + } + + QRectF cutOffRect(const QRectF &rc, const KisAlgebra2D::RightHalfPlane &p) { QVector points; @@ -245,7 +331,7 @@ p1Valid = p2Valid; } - return KritaUtils::approximateRectFromPoints(resultPoints); + return approximateRectFromPoints(resultPoints); } int quadraticEquation(qreal a, qreal b, qreal c, qreal *x1, qreal *x2) @@ -359,4 +445,126 @@ return points; } +QTransform mapToRect(const QRectF &rect) +{ + return + QTransform(rect.width(), 0, 0, rect.height(), + rect.x(), rect.y()); +} + +bool fuzzyMatrixCompare(const QTransform &t1, const QTransform &t2, qreal delta) { + return + qAbs(t1.m11() - t2.m11()) < delta && + qAbs(t1.m12() - t2.m12()) < delta && + qAbs(t1.m13() - t2.m13()) < delta && + qAbs(t1.m21() - t2.m21()) < delta && + qAbs(t1.m22() - t2.m22()) < delta && + qAbs(t1.m23() - t2.m23()) < delta && + qAbs(t1.m31() - t2.m31()) < delta && + qAbs(t1.m32() - t2.m32()) < delta && + qAbs(t1.m33() - t2.m33()) < delta; +} + + +/********************************************************/ +/* DecomposedMatix */ +/********************************************************/ + +DecomposedMatix::DecomposedMatix() +{ +} + +DecomposedMatix::DecomposedMatix(const QTransform &t0) +{ + QTransform t(t0); + + QTransform projMatrix; + + if (t.m33() == 0.0 || t0.determinant() == 0.0) { + qWarning() << "Cannot decompose matrix!" << t; + valid = false; + return; + } + + if (t.type() == QTransform::TxProject) { + QTransform affineTransform(t.toAffine()); + projMatrix = affineTransform.inverted() * t; + + t = affineTransform; + proj[0] = projMatrix.m13(); + proj[1] = projMatrix.m23(); + proj[2] = projMatrix.m33(); + } + + + std::array rows; + + rows[0] = QVector3D(t.m11(), t.m12(), t.m13()); + rows[1] = QVector3D(t.m21(), t.m22(), t.m23()); + rows[2] = QVector3D(t.m31(), t.m32(), t.m33()); + + if (!qFuzzyCompare(t.m33(), 1.0)) { + const qreal invM33 = 1.0 / t.m33(); + + for (auto row : rows) { + row *= invM33; + } + } + + dx = rows[2].x(); + dy = rows[2].y(); + + rows[2] = QVector3D(0,0,1); + + scaleX = rows[0].length(); + rows[0] *= 1.0 / scaleX; + + shearXY = QVector3D::dotProduct(rows[0], rows[1]); + rows[1] = rows[1] - shearXY * rows[0]; + + scaleY = rows[1].length(); + rows[1] *= 1.0 / scaleY; + shearXY *= 1.0 / scaleY; + + // If determinant is negative, one axis was flipped. + qreal determinant = rows[0].x() * rows[1].y() - rows[0].y() * rows[1].x(); + if (determinant < 0) { + // Flip axis with minimum unit vector dot product. + if (rows[0].x() < rows[1].y()) { + scaleX = -scaleX; + rows[0] = -rows[0]; + } else { + scaleY = -scaleY; + rows[1] = -rows[1]; + } + shearXY = - shearXY; + } + + angle = kisRadiansToDegrees(std::atan2(rows[0].y(), rows[0].x())); + + if (angle != 0.0) { + // Rotate(-angle) = [cos(angle), sin(angle), -sin(angle), cos(angle)] + // = [row0x, -row0y, row0y, row0x] + // Thanks to the normalization above. + + qreal sn = -rows[0].y(); + qreal cs = rows[0].x(); + qreal m11 = rows[0].x(); + qreal m12 = rows[0].y(); + qreal m21 = rows[1].x(); + qreal m22 = rows[1].y(); + rows[0].setX(cs * m11 + sn * m21); + rows[0].setY(cs * m12 + sn * m22); + rows[1].setX(-sn * m11 + cs * m21); + rows[1].setY(-sn * m12 + cs * m22); + } + + QTransform leftOver( + rows[0].x(), rows[0].y(), rows[0].z(), + rows[1].x(), rows[1].y(), rows[1].z(), + rows[2].x(), rows[2].y(), rows[2].z()); + + KIS_SAFE_ASSERT_RECOVER_NOOP(fuzzyMatrixCompare(leftOver, QTransform(), 1e-4)); +} + } diff --git a/libs/global/kis_dom_utils.h b/libs/global/kis_dom_utils.h --- a/libs/global/kis_dom_utils.h +++ b/libs/global/kis_dom_utils.h @@ -19,6 +19,8 @@ #ifndef __KIS_DOM_UTILS_H #define __KIS_DOM_UTILS_H +#include + #include #include #include @@ -41,6 +43,24 @@ return QString::number(value); } + inline QString toString(float value) { + QString str; + QTextStream stream; + stream.setString(&str, QIODevice::WriteOnly); + stream.setRealNumberPrecision(FLT_DIG); + stream << value; + return str; + } + + inline QString toString(double value) { + QString str; + QTextStream stream; + stream.setString(&str, QIODevice::WriteOnly); + stream.setRealNumberPrecision(11); + stream << value; + return str; + } + inline int toInt(const QString &str) { bool ok = false; int value = 0; diff --git a/libs/global/kis_global.h b/libs/global/kis_global.h --- a/libs/global/kis_global.h +++ b/libs/global/kis_global.h @@ -112,16 +112,16 @@ a = 2 * M_PI + fmod(a, 2 * M_PI); } - return a > 2 * M_PI ? fmod(a, 2 * M_PI) : a; + return a >= 2 * M_PI ? fmod(a, 2 * M_PI) : a; } // converts \p a to [0, 360.0) range inline qreal normalizeAngleDegrees(qreal a) { if (a < 0.0) { a = 360.0 + fmod(a, 360.0); } - return a > 360.0 ? fmod(a, 360.0) : a; + return a >= 360.0 ? fmod(a, 360.0) : a; } inline qreal shortestAngularDistance(qreal a, qreal b) { @@ -141,6 +141,11 @@ return d1 < d2 ? b1 : b2; } +inline qreal bisectorAngle(qreal a, qreal b) { + const qreal diff = shortestAngularDistance(a, b); + return incrementInDirection(a, 0.5 * diff, b); +} + template inline PointType snapToClosestAxis(PointType P) { if (qAbs(P.x()) < qAbs(P.y())) { @@ -235,62 +240,7 @@ return rc; } -#include - -template -inline QSharedPointer toQShared(T* ptr) { - return QSharedPointer(ptr); -} - -template class List> -List> listToQShared(const List list) { - List> newList; - Q_FOREACH(A* value, list) { - newList.append(toQShared(value)); - } - return newList; -} - - -/** - * Convert a list of strong pointers into a list of weak pointers - */ -template